Grid - how display picture on 2 columns programmatically

Hello everyone,

I’m stuck and I’m begging for help.
I want to being able to display images in a grid, and for landscape pictures, I want the picture to take 2 columns of the grid, nothing too hard.
I don’t know which picture is landscape and which picture is portrait.

So, I have the following function:

#let display_portrait_landscape(img_url, content) = {
    if measure(img_url).width > measure(img_url).height {
        grid.cell(
            colspan: 2,
            content)
    } else {
        grid.cell(
        colspan: 1,
        content)
    }
}

and I use it with context in the grid:

#grid(
    columns:3,
    rows:auto,
    gutter:32pt,
    align: top,
    grid.cell(
        colspan: 1, 
        image(
            "pic1.jpeg", 
            width:100%, 
            height:200pt, 
            fit: "contain"
        )
    ),
   grid.cell(
        colspan: 2, 
        image(
            "pic1.jpeg", 
            width:100%, 
            height:200pt, 
            fit: "contain"
        )
    ),
    context {
        display_portrait_landscape[pic2.jpeg][
            #image(
                 "pic2.jpeg", 
                 width:100%, 
                 height:200pt, 
                 fit: "contain"
             )
         ]
     }
)

I tried on the typst simulator (no error reported): when I put directly colspan:2 inside the grid (the 2 firsts pictures), it works, but when I use the function, it never draw pictures on 2 columns, event by forcing colspan:2 in every possible branches, it’s like the colspan is ignored and the picture is displayed without it. I tried using directly image() instead of content into the function, but still the same issue.
I tried to figure out the picture ratio, the first branch of the function should be triggered, no matter the picture used (which is weird IMHO).

I’ve read everything I can read on the documentation about grid, image and context, I think I’m missing something on the context thing, but I’m dont understand why even the grid doesn’t take into account the colspan from the function.

I’m stuck, some help will be really appreciated.

Thank you.

1 Like

You measure img_url which is pic2.jpeg instead of the image content!

A few things to note (unfortunately I don’t have working answer yet):

  • You define display_portrait_landscape to take the arguments img_url and content, but you pass img_url as content instead of a string. That line should instead be the following instead:

    #{
    display_portrait_landscape("pic2.jpeg")[
      #image(
        "pic2.jpeg", 
        width:100%, 
        height:200pt, 
        fit: "contain"
      )
    ]
    
    // or equivalently
    
    display_portrait_landscape(
      "pic2.jpeg",
      image(
        "pic2.jpeg", 
        width:100%, 
        height:200pt, 
        fit: "contain"
      )
    )
    }
    
  • You actually don’t need to pass the image path to the function if you are passing the image itself. Try the following code:

    #let temp = image(
         "pic2.jpeg", 
         width:100%, 
         height:200pt, 
         fit: "contain"
     )
    #temp.fields()
    

    You’ll find that the image has a source field that has the file name you were looking for.

  • However, it is not the string that you want to measure. In fact, the measure function needs to be called on a specific image. The reason that you need context here is because Typst doesn’t know the size of the image as you defined it until it is placed. Note that you defined the image to take the full width (100%, a ratio) but gave it a fixed height (200pt). Because you passed fit: "contain", the image’s aspect ratio will be preserved as you know, but you’re attempting to ask Typst to do a task it cannot do: to get the size of an image before it is placed, and then change its size based on that output. Hopefully the circularity of that makes sense.

    However, there is a way around this (which I imagine aligns with how you’ve been thinking about this): you just need the aspect ratio of the raw image, before it is placed, without need for context. Unfortunately, I don’t know how to do that, maybe someone else can help us out with that. I thought you might be able to use some hack where you can use a combination of place and hide to place a “dummy” picture in its own context and measure it, but that information cannot be used outside of the context.

    I’ll keep thinking on this…

Thank you for the feedback :slight_smile:

Thank you for the explanation and the fix for the string/content.

Indeed I thought it will measure the size of the raw image, with the infinite context thing, but it seems not.

I will continue searching.

Got it! This is a goofy solution but it might be the best available with Typst at the moment. You can read in files as raw bytes, and then can surf those bytes for the width and height data. I adapted this from some python code I found elsewhere. I don’t have much experience in this topic so use this function at your own risk, but it worked great with two test cases I tried. Here’s the function (no context needed):

#let raw_jpg_size(filename) = {
  let data = read(filename, encoding: none)
  for i in range(1, data.len() + 1) {
    if data.at(i) == 0xFF {
      let marker = data.at(i + 1)
      if marker >= 0xC0 and marker <= 0xCF and marker != 0xC4 and marker != 0xC8 and marker != 0xCC {
        // SOF marker. Segment starts at i+2
        let block_len = 256 * data.at(i + 2) + data.at(i + 3)
        let height = 256 * data.at(i + 5) + data.at(i + 6)
        let width = 256 * data.at(i + 7) + data.at(i + 8)
        return (width:width, height:height)
      }
    }
  }
  return none
}

Used for your situation:

#let display_portrait_landscape(img) = {
  let (width, height) = raw_jpg_size(img.source)
  grid.cell(
    colspan: if width > height {2} else {1},
    img
  )
}

#grid(
    columns:3,
    rows:auto,
    // gutter:32pt,
    stroke: black, // added this for clarity during testing
    align: top,
    ..for i in range(1,4) {([content for clarity],)}, // delete this
    grid.cell(
        colspan: 1, 
        image(
            "pic1.jpeg", 
            width:100%, 
            height:200pt, 
            fit: "contain"
        )
    ),
   grid.cell(
        colspan: 2, 
        image(
            "pic1.jpeg", 
            width:100%, 
            height:200pt, 
            fit: "contain"
        )
    ),
    display_portrait_landscape(image(
       "pic2.jpeg", 
       width:100%, 
       height:200pt, 
       fit: "contain"
    )),
)

Thanks a lot, it works !

There is no need to parse the bytes, measuring the image works fine.

The issue (apart from measuring the image and not the filename), is that the context needs to be placed outside of the grid – otherwise, the context {} creates an implicit grid cell which contains the grid.cell(colspan: 2, ...), effectively rendering the colspan argument useless.

When measuring, we set a width to measure against (fixing how much “100%” are) – and we do not set a height on the image elements, as this makes measuring useless (if we set width and height on the image, we set its aspect ratio that way)

#let portrait_landscape(content) = {
  // we set an explicit width to make 100% width = 100pt
  let dimensions = measure(content, width: 100pt)
  if dimensions.width > dimensions.height {
    grid.cell(colspan: 2, content)
  } else {
    grid.cell(colspan: 1, content)
  }
}

#set image(fit: "contain", width: 100%) // you must not set the height here, as this will make all images to have the same aspect ratio, as it is measured _after_ it is fit to the set width/height
#context grid(
  columns: 3,
  rows: 200pt,
  gutter: 32pt,
  // align: top,
  fill: red, // to show cell sizes
  portrait_landscape(image("figure.png",)),
  portrait_landscape(image("figure.png",)),
  portrait_landscape(image("figure.png",)),
  portrait_landscape(image("flag-turtle.jpeg",)),
  portrait_landscape(image("figure.png",)),
  portrait_landscape(image("figure.png",)),
  portrait_landscape(image("Screenshot from 2021-02-22 11-12-17.png")),
)

3 Likes

Thank you, works great and is elegant.

Now my main issue is that as I don’t know the order of the pictures, if I have 2 landscapes pictures, it panic “exceed the available columns”, so I think I will need to go to a plugin like biceps.

Thank you for the help.