How to use a pattern image as the page background?

I am currently creating some worksheets for exciting, context-orientated lessons.

I have an image that I would like to use as the background for these documents. The image is a seamless pattern, so I would like to repeat it in the background instead of stretching it across the entire width. Is there a good way to repeat a background like a cache?

So far I am stretching the background to the full height and width of the document. However, as the document gets longer, the quality gets worse and worse.

#set page(background: image("assets/paper.jpg", width: 100%, height: 100%))

The result so far looks like this

Thanks for your help.

1 Like

You may be interested in this function

2 Likes

Here you go :slightly_smiling_face:

#set page(
  background: {
    let pat = pattern(
      size: (30pt, 30pt),
      place(circle(radius: 15pt, fill: luma(240))) // replace the `#circle` command with your `#image`
    )
    rect(fill: pat, width: 100%, height: 100%)
  }
)

#lorem(300)

#lorem(100)

#lorem(200)
1 Like

Thank you very much!
That’s a clever solution.

1 Like

I’m glad it helps you :slightly_smiling_face:

This is a clear case of the power through composability design principle.

1 Like

I would like to simplify the answer by providing some “alternatives”:

#let img = circle(radius: 15pt, fill: green.transparentize(70%))

// Option 1
#set page(fill: pattern(img))

// Option2
// #set page(background: box(
//   width: 100%,
//   height: 100%,
//   fill: pattern(img)
// ))

#lorem(300)

#lorem(100)

#lorem(200)
Output

Note that semantically, you would choose box() over rect() in this case. And by default, rect() is visually visible (draw a rectangle), while box() is not.

If you want to add, for example, a monotone background color, then for the image pattern you would have to use background: box(fill: pattern(img)) approach. But if the default white background color is fine (background is drawn on top of the fill, BTW), then you can use a more compact fill: pattern(img) approach.

You can see that the additional box wrapping is just a “workaround” to use fill: pattern() when the page.fill is busy/not available.

Additionally, you might find useful the image.fit parameter.

2 Likes

Cool! I didn’t notice the fill parameter of page, so I made a workaround using background. Your solution is more elegant.

BTW, we don’t need the size: (30pt, 30pt) part at all. This works:

#set page(
  fill: pattern(
    circle(radius: 15pt, fill: luma(240))
  )
)

#lorem(300)

#lorem(100)

#lorem(200)

That’s great! I didn’t touch this because I’ve used the pattern() only a few times and trusted your solution (so I blindly copied that part). The less code, the better.

2 Likes

Yeah, I don’t think it’s possible to make it more minimal than that…

The size part was needed, but only because of placeÂą, which was indeed superfluous.


Âą If you have place without size there, you get this error message:

error: pattern tile size must be non-zero
[…]
  = hint: try setting the size manually

Note that semantically, you would choose box() over rect() in this case. And by default, rect() is visually visible (draw a rectangle), while box() is not.

Just wanted to make a small addition: technically, a block() would be closer to a “brother” of rect() than box(). While both are similar, a box() is always inline and often used to insert otherwise block-level elements (such as image(), circle() and so on) within a text line without breaking the paragraph. That is useful to insert things like icons into text, for instance, since images are normally block-level and thus interrupt the paragraph. Meanwhile, a block() is always block-level (just like rect) and has several useful parameters to customize its appearance and layout behavior, so it’s generally preferable when inserting in a paragraph isn’t important. Otherwise, your remark is correct.

Though one could also argue that it might make more sense to use rect() rather than block() in this particular example for the reason you mention (it’d be visible, and is only there to display a fill). It doesn’t really matter in the end though :slight_smile:

3 Likes