diff --git a/tex2pdf-service/tex2pdf/fonts/LICENSE b/tex2pdf-service/tex2pdf/fonts/LICENSE new file mode 100644 index 0000000..be2a63e --- /dev/null +++ b/tex2pdf-service/tex2pdf/fonts/LICENSE @@ -0,0 +1,9 @@ +The font and related files in this directory are distributed under the +GNU AFFERO GENERAL PUBLIC LICENSE Version 3 (see the file COPYING), with +the following exemption: + +As a special exception, permission is granted to include these font +programs in a Postscript or PDF file that consists of a document that +contains text to be displayed or printed using this font, regardless +of the conditions or license applying to the document itself. + diff --git a/tex2pdf-service/tex2pdf/fonts/NimbusRoman-Regular.otf b/tex2pdf-service/tex2pdf/fonts/NimbusRoman-Regular.otf new file mode 100644 index 0000000..4588aff Binary files /dev/null and b/tex2pdf-service/tex2pdf/fonts/NimbusRoman-Regular.otf differ diff --git a/tex2pdf-service/tex2pdf/pdf_watermark.py b/tex2pdf-service/tex2pdf/pdf_watermark.py index 0527405..65f2fe5 100644 --- a/tex2pdf-service/tex2pdf/pdf_watermark.py +++ b/tex2pdf-service/tex2pdf/pdf_watermark.py @@ -43,6 +43,16 @@ # Resolution used to measure the true glyph extent of the rendered watermark. _INK_DPI = 150 +# Fallback font embedded when the caller passes no custom font. A *self-contained* +# face is mandatory for reproducible output (see _build_watermark_overlay): the +# base-14 "Times-Roman" has no font program, so pdf_oxide would rasterize the +# glyph-extent measurement with whatever font it finds on the host, making the +# stamp placement differ from machine to machine. Nimbus Roman is URW's +# metrically-Times-compatible face (the Ghostscript default Times substitute), +# licensed AGPL-3.0-only WITH PS-or-PDF-font-exception-20170817, which permits +# embedding and redistribution. See tex2pdf/fonts/LICENSE. +_BUNDLED_FONT = Path(__file__).resolve().parent / "fonts" / "NimbusRoman-Regular.otf" + class WatermarkError(Exception): """Custom exception for watermark errors.""" @@ -126,14 +136,21 @@ def _build_watermark_overlay( page_height = fsize * 1.8 baseline = 0.4 * fsize + # Without a caller-supplied font, embed the bundled Nimbus Roman rather than + # the base-14 "Times-Roman": the latter has no font program, so the ink + # measurement below would be rasterized with an arbitrary host font and the + # stamp would land in a slightly different place on every machine. Always + # embedding a self-contained face keeps the output byte-reproducible and + # makes the stamp render identically in every PDF viewer. + if not font_file: + font_file = str(_BUNDLED_FONT) + builder = pdf_oxide.DocumentBuilder() - font_name = "Times-Roman" - if font_file: - # Use the font file stem as the recorded PostScript name (pdf_oxide does - # not reliably parse it from the face) and subset on build(). - embedded = pdf_oxide.EmbeddedFont.from_bytes(Path(font_file).read_bytes(), Path(font_file).stem) - font_name = _WATERMARK_FONT_NAME - builder.register_embedded_font(font_name, embedded) + # Use the font file stem as the recorded PostScript name (pdf_oxide does not + # reliably parse it from the face) and subset on build(). + embedded = pdf_oxide.EmbeddedFont.from_bytes(Path(font_file).read_bytes(), Path(font_file).stem) + font_name = _WATERMARK_FONT_NAME + builder.register_embedded_font(font_name, embedded) page = builder.page(page_width, page_height) # type: ignore[call-arg, arg-type] page.font(font_name, fsize).at(0.0, baseline).inline_color(rgb[0], rgb[1], rgb[2], text)