csswg-drafts icon indicating copy to clipboard operation
csswg-drafts copied to clipboard

[css-page] Expose unprintable areas via CSS

Open mstensho opened this issue 1 year ago • 54 comments

Most printers have a small region along each edge of the page sheet which is unprintable, due to the printer's paper handling mechanism. See https://drafts.csswg.org/css-page-3/#page-terms

The size of these unprintable areas are available to applications (such as browsers) in most OSes, but currently it's not web-exposed.

This means that authors have no means of confidently setting page margins to prevent content (inside a page margin box, or even inside the page area) from getting clipped.

Maybe we could use env() variables for this? https://drafts.csswg.org/css-env-1/#safe-area-insets seems to be exactly what we need, in a way, although the spec currently says that "[f]or rectangular displays, these must all be zero". Furthermore, some printers have non-uniform unprintable area widths. For instance, my old HP deskjet had an unprintable area of about 1.5cm at the bottom (which is huge, and more than a typical margin), but more reasonable widths on the other three sides (3 millimeters or less). The problem with this non-uniformity is that the values are relative to the direction the sheet of paper travels through the printer, as far as I understand, and the application has no way of telling whether the printer is going to rotate what it receives from the application. E.g. if the page has landscape orientation (e.g. @page { size: landscape; }, and the printer is fed with the short edge first (aka portrait), it will have to rotate it. But will it be 90 or 270 degrees? Up to the printer. If the printer can print on both sides, it may also be rotated.

So the only safe thing seems to be: use the larger of all those unprintable area widths - i.e. just one env() variable for this?

Let's pretend it's called env(unprintable-area-width) for now:

<!DOCTYPE html>
<style>
  @page {
    margin: calc(env(unprintable-area-width) + 50px);
    margin-bottom: calc(env(unprintable-area-width) + 50px);
    margin-left: calc(env(unprintable-area-width) + 50px);
    margin-right: calc(env(unprintable-area-width) + 50px);

    @top-center {
      content: "";
      border: solid;
      margin-top: env(unprintable-area-width);
    }
    @bottom-center {
      content: "";
      border: solid;
      margin-bottom: env(unprintable-area-width);
    }
    @right-middle {
      content: "";
      border: solid;
      margin-right: env(unprintable-area-width);
    }
    @left-middle {
      content: "";
      border: solid;
      margin-left: env(unprintable-area-width);
    }
    @top-left-corner {
      content: "";
      width: 30px;
      height: 30px;
      border: solid;
      margin-bottom: auto;
      margin-right: auto;
    }
    @top-right-corner {
      content: "";
      width: 30px;
      height: 30px;
      border: solid;
      margin-bottom: auto;
      margin-left: auto;
    }
    @bottom-right-corner {
      content: "";
      width: 30px;
      height: 30px;
      border: solid;
      margin-top: auto;
      margin-left: auto;
    }
    @bottom-left-corner {
      content: "";
      width: 30px;
      height: 30px;
      border: solid;
      margin-top: auto;
      margin-right: auto;
    }
  }
</style>

Content.

If there's no unprintable area (when creating a PDF, for example), it would look like this:

image

If the unprintable area width is 16px, on the other hand, it would result in a print layout like this:

image

On paper it would look like this (if the printer doesn't lie too much about its capabilities):

image

Thoughts? @fantasai @tabatkins @rachelandrew

One concern is printers with large unprintable areas (such as my old HP deskjet with 1.5cm at the bottom), resulting in unreasonably large page margins if they are affected by env(unprintable-area-width).

mstensho avatar Dec 19 '24 13:12 mstensho

One variable representing the largest makes sense, I did a quick search but didn't find anything immediately, but I wonder if we can get the most used sizes from anywhere to see if your printer was an outlier or not.

rachelandrew avatar Dec 19 '24 18:12 rachelandrew

An insightful common on why a single size makes sense here: https://issues.chromium.org/issues/41495735#comment20

bfgeek avatar Dec 19 '24 18:12 bfgeek

@rachelandrew It may very well be an outlier. I mean, no browser was able to print default UA footers on that printer.

mstensho avatar Dec 19 '24 21:12 mstensho

So is env(unprintable-area-width) something we could add to css-env? And then mention that in https://drafts.csswg.org/css-page-3/#page-terms (Printable and non-printable areas)?

mstensho avatar Jan 09 '25 09:01 mstensho

@emilio Any thoughts?

mstensho avatar Jan 21 '25 09:01 mstensho

No objection, but coordinate systems might get weird in some cases (think multiple pages per sheet). What values would you get then?

emilio avatar Jan 21 '25 09:01 emilio

I guess in that case you could get zero (and the browser would just position / lay out things accounting for the unprintable margin).

This means that authors have no means of confidently setting page margins to prevent content (inside a page margin box, or even inside the page area) from getting clipped.

Firefox at least has a mechanism to ensure the page margins are at least the unprintable margin (unwriteable in Gecko terminology) here, so I haven't seen that come up too much in practice, fwiw.

emilio avatar Jan 21 '25 10:01 emilio

I guess in that case you could get zero (and the browser would just position / lay out things accounting for the unprintable margin).

Thanks for mentioning this! Yes, that's my thinking as well. If there are multiple pages per sheet surface, the author no longer has any control over the sheet, so the browser should report nothing unprintable to the document, and just apply whatever margin it wants at the actual sheet edges, depending on user settings, maybe? It doesn't make a whole lot of sense to honor specified page margins at sheet edges when there are multiple pages there, although Chrome currently does that, actually.

This means that authors have no means of confidently setting page margins to prevent content (inside a page margin box, or even inside the page area) from getting clipped.

Firefox at least has a mechanism to ensure the page margins are at least the unprintable margin (unwriteable in Gecko terminology) here, so I haven't seen that come up too much in practice, fwiw.

That setting can be used to avoid clipping in the page area, but not in the page margin boxes, right?

mstensho avatar Jan 21 '25 10:01 mstensho

Yeah that's right

emilio avatar Jan 21 '25 12:01 emilio

Maybe add a section to https://drafts.csswg.org/css-env-1/#environment , like this:

2.x Unprintable areas

Name Value Number of dimensions
unprintable-area-max-length <length> 0 (scalar)

Most printers have a small region along each edge of the page sheet which is unprintable, due to the printer's paper handling mechanism. See https://drafts.csswg.org/css-page-3/#page-terms

Some printers don't have a uniform unprintable area width along each of the four paper egdes, and may rotate the print output at their own discretion. The user agent cannot make assumptions about which edge will be fed first into the printer, or what orientation the sheet of paper has. Therefore, only one value can be reliably provided here: The larger of these four values.

?

I hope you'll find time to discuss this soon.

mstensho avatar Mar 25 '25 11:03 mstensho

I think this makes sense. I'd call it env(unprintable-margin) though. :)

fantasai avatar Mar 28 '25 15:03 fantasai

"Margin" is a very overloaded term in printing, but works for me. :)

mstensho avatar Mar 31 '25 06:03 mstensho

Maybe we should avoid "margin" in the name, though.

How about env(unprintable-area) or env(unprintable-area-width)?

mstensho avatar May 26 '25 13:05 mstensho

Or env(safe-printable-inset)?

mstensho avatar May 27 '25 10:05 mstensho

The inset naming feels a bit more consistent with the safe-area-inset-* vars

emilio avatar May 27 '25 12:05 emilio

Drive-by: do I understand correctly that this corresponds to the actual printer's unprintable area? If so, is this not a major fingerprinting vector? Or is this somehow not able to be exfiltrated?

bakkot avatar Jun 24 '25 18:06 bakkot

Drive-by: do I understand correctly that this corresponds to the actual printer's unprintable area? If so, is this not a major fingerprinting vector? Or is this somehow not able to be exfiltrated?

If I understand correctly, this env variable would only be usefully exposed at the time that the user chooses to print (and selects a printer in their print UI, and the browser lays out the document using the selected page size etc. for that printer).

It wouldn't just generally be exposed (or it would be exposed as 0) for web pages viewed in a browser window. So: it does technically add some fingerprinting surface, but only for content that is printed (and I think there's already quite a bit of fingerprintable surface that's exposed at that point).

dholbert avatar Jun 24 '25 18:06 dholbert

Correct. I pointed this out here: https://github.com/mstensho/unprintable-areas?tab=readme-ov-file#are-there-any-privacy-security-and-accessibility-considerations

mstensho avatar Jun 24 '25 19:06 mstensho

Ah, thanks, sorry I missed that.

bakkot avatar Jun 24 '25 19:06 bakkot

Oh, no worries. It's a jungle. Are you still concerned, though?

mstensho avatar Jun 24 '25 20:06 mstensho

Given that it's only exposed after triggering the print dialogue, which is a highly visible action, I'm not personally worried about it. There might technically still be some risk because the print dialogue can be triggered automatically, although I'm not sure if that triggers layout in a way that would expose this information, but in any case I don't expect anyone to actually start triggering the print dialogue just to collect this information.

It's maybe worth considering whether there's some subset of often-printed pages where this would be adding additional fingerprinting risk, but off the top of my head I wouldn't expect it to really come up.

bakkot avatar Jun 24 '25 20:06 bakkot

The feature makes sense to me, and I'd be partial to the naming using "inset". It might be useful to consider potential extension path, allowing user agents to expose different values on different sides in the cases where they do know the difference, but as discussed that's not reliably knowable in the general case, so the basic version reporting the largest value make sense.

But the more specific version does seem useful too: it might be detectable on some OS/printer combination, but this is also possibly something a user who knows the behavior of the target printer could enter through some settings UI. This might be more likely in the case of specialized HTMl+CSS->PDF printers than in the browser's print dialog, but it's theoretically possible in either. The challenge there though is that these value may be different per page, as some differently oriented/dimentionned page could be oriented differently in the printer (or, as mentioned, due to recto-verso printing). In which case, it's not enough to have 4 env variables to describe each side, but we'd somehow need to be able to convey a per page info. Not sure how to do that. Maybe env() could evaluate differently in different @page sub-rules, but that feels dirty somehow.

As far as fingerprinting, is there any spec that describes the particularities of the browser's print-preview window? Is it normal expect for how it's invoked? Are there (specified) limitations on what happens to the scripting environment, or to the ability to conditionally load remote resources? I don't think I'm aware of any such spec.

frivoal avatar Jun 25 '25 09:06 frivoal

cc: @MurakamiShinyu

frivoal avatar Jun 25 '25 09:06 frivoal

I have some concerns (whose strength I'm going back and forth on) about how useful this is, in the face of @page { size :...} mismatches potentially causing promised-to-be-"safe" margins to be scaled down and made unsafe. (Some discussion in second half of https://github.com/mozilla/standards-positions/issues/1258#issuecomment-3003424374 - I proposed there that we magically correct for this by doing a @page-size-based adjustment to the value of the variable itself, but @mstensho correctly pointed out that this creates circular dependencies, so we can't really correct for this in that way.)

It occurs to me that one way to avoid those circular dependencies would be to just make this a flag instead of an env, e.g.

@page {
  margin: 0.5;
/* This will increase the `margin` on any side(s) where the "safe margin"
is larger than what the author specified above. This could even take different
values like `pad` vs `clamp` to control whether it gets added to the `margin`
vs. gets treated as a lower-bound for the `margin`, if we want that level of
expressiveness. */
  enforce-safe-margins: auto;
}

@mstensho what do you think? Notably, this is less expressive than the proposal here in several ways -- but I think that reduced-expressiveness might not be useful/necessary anyway? In particular:

  • This alternate proposal doesn't allow you to do arithmetic (e.g. adding or subtracting a bit from the safe margin). (Though you can still get a similar effect by e.g. adding CSS margins in your content if your really want to; and/or we could have enforce-safe-margins: pad to specify that they "stack" on top of the specified margins rather than serving as a lower-bound.)
  • This alternate proposal doesn't let you toggle the safe margin separately per-side (though of course we could extend it to do so, e.g. enforce-safe-margins: auto auto auto none or something). I'm not sure that it's useful to do that, though? I don't know that authors are going to want to say "Please enforce safe margins for these 3 sides but allow clipping on this one".

dholbert avatar Jul 02 '25 15:07 dholbert

Following up on my previous comment & its alternate approach with a enforce-safe-margins descriptor -- I can imagine it having several useful values:

  • none: default, what browsers do right now (don't apply any lower-bound on the author-specified margins)
  • minimum: for each side, use that side's safe margin as a lower-bound on the author-specified margin: ... values.
  • balanced: use the largest of the safe insets (essentially the safe-printable-inset) as a lower-bound for all 4 sides of the author-specified margins. (So e.g. if an author has specified margin: 0.1in, and the user's printer has its largest safe-inset of 0.2in on the bottom edge, then the printout will end up with 0.2in on all sides.)
  • additive: This is an optional modifier that says whether the safe-insets are to be treated as a lower-bound vs. something to be added to the author-specified margin values (if that's useful to do).

That lets you achieve the same outcome as calc(env(unprintable-area-width) + 50px) in the first comment here, via @page { margin: 50px; enforce-safe-margins: balanced additive} for example.

Compared to the env approach, this^ design has the benefit that the actual determination of the safe-margin-amount can happen at used-value-time, when the UA has knowledge of both the author's @page { size: ... value and the output paper-size (which might be wildly different). This gives the UA the flexibility to apply a safe margin that's actually safe even-in-the-face-of-possible-downscaling (if the content has a large @page { size: ...} value) instead of being forced to promise in advance that a particular margin (e.g. 0.2in) will be safe only to have it downscaled to a smaller not-so-safe amount (e.g. if the author specifies a @page size that's twice as large as the output medium).

(The names are all examples right now & open to bikeshedding if we decide this is a good route to pursue.)

dholbert avatar Jul 02 '25 20:07 dholbert

Interesting approach. I too am not sure if we would need ways to specify this individually for each side, if we go down this path.

True, this is less expressive than the env() approach, which would allow for (presumably) silly things like flex-basis: env(safe-printable-inset). Using env(safe-printable-inset) IS silly as long as we're not dealing with something that's flush with the page area edges. Using it for widths and height is always silly. But I guess that's environment variables for you. As if e.g. width: env(safe-area-inset-top) makes a lot of sense...

Not sure about minimum vs balanced. Since we have no control over what the printer does regarding rotating the output, and whether it's the short or the long edge that's fed first, there's no meaningful individual safe inset.

mstensho avatar Jul 07 '25 07:07 mstensho

Not sure about minimum vs balanced. Since we have no control over what the printer does regarding rotating the output, and whether it's the short or the long edge that's fed first, there's no meaningful individual safe inset.

Yeah, I don't know for sure if we need those exact keyword values -- I'm just providing a handful of values as examples that might be useful.

The idea with minimum would be to just use whatever "safe" margin-values that today's browsers already use when a user prints a simple web page and chooses "Margins: Minimum" in the print dialog. (This option is available in both Firefox and Chrome at least, and it uses the "safe margins" that we get from the printer.)

If these safe-values end up being wrong in certain cases (e.g. due to the printer feeding half of the pages in reverse for duplex printing), then that's presumably already a problem that any software printing with "Margins: Minimum" would be encountering, and I don't see a reason to shield CSS from that problem in this sort-of-edge-case. And if print drivers are (or become) expressive enough to communicate this sort of situation to desktop print applications (including browsers), then we can account for it when honoring enforce-safe-margins: minimum (e.g. swapping the top/bottom safe-margin clamps that we use, on alternating pages).

dholbert avatar Jul 07 '25 21:07 dholbert

I don't think we should provide both minimum and balanced. True, Firefox and Chrome already support "minimum" margins in the print preview UI, and unless the unprintable insets are the same on all four sides, it's buggy, since we don't know what the printer is going to do regarding rotations. I don't think we want to expose this to the web. A related issue is the "scale: fit to printable area" option when printing PDFs (Chrome has this). See https://issues.chromium.org/u/1/issues/41495735#comment20 . My inglourious printer (used in that bug report) that had a solid half-inch unprintable inset at the bottom, and more reasonable insets along the other edges, is unfortunately no longer with us.

So I believe that leaves us with three options: do nothing, clamp to safe inset (the larger of the four), add to safe inset. The latter is especially useful when making room for page margin boxes (i.e. custom headers / footers / whatever).

Keyword value names could be: none (current behavior, initial value), clamped, additive.

It may be that we don't need both additive and clamped, though. clamped is essentially the same as additive and margin:0, isn't it? Otherwise, if we want both values, that's also a strong argument for allowing for specifying this per paper edge. Maybe one edge wants to clamp, and another edge wants to add? Maybe we want it per edge regardless, if someone wants safety at some edges, and no safety at some other edges? I have no idea who would want such a thing, though. Sounds weird, unless it's a one-pager.

Also, how should the additive value work with auto margins? I suppose it will just behave like clamped then.

  @page {
    width: 200px;
    margin-left: auto;
  }

Hmm. So additive wouldn't always be additive then. If we land on only two options, maybe reasonable values would be none and safe. Property / descriptor name? page-margin-inset? page-margin-safety? And, if we must, make it a shorthand for bikeshed-top, bikeshed-right, bikeshed-bottom, bikeshed-left, bikeshed-block-start, bikeshed-block-end, bikeshed-inline-start, bikeshed-inline-end longhands.

mstensho avatar Jul 16 '25 08:07 mstensho

This feature would be incredibly useful for print use cases, but do we know definitively that UAs have access to this information?

LeaVerou avatar Jul 16 '25 14:07 LeaVerou

Yes, both Firefox and Chrome support the concept of "minimal" margins in the print preview UI, which is information provided by the system (print drivers etc.). I cannot test Safari.

mstensho avatar Jul 16 '25 15:07 mstensho