diff --git a/Makefile.am b/Makefile.am index 8fdf8fc72..f298ba31a 100644 --- a/Makefile.am +++ b/Makefile.am @@ -95,7 +95,8 @@ check_SCRIPTS = \ cupsfilters/testfilters.sh \ cupsfilters/test-pclm-overflow.sh \ cupsfilters/test-pwgtopdf-bit-row.sh \ - cupsfilters/test-pdftoraster-copy-height.sh + cupsfilters/test-pdftoraster-copy-height.sh \ + cupsfilters/test-pdftopdf-inherited-mediabox.sh check_PROGRAMS = \ testcmyk \ @@ -122,7 +123,8 @@ TESTS = \ cupsfilters/testfilters.sh \ cupsfilters/test-pclm-overflow.sh \ cupsfilters/test-pwgtopdf-bit-row.sh \ - cupsfilters/test-pdftoraster-copy-height.sh + cupsfilters/test-pdftoraster-copy-height.sh \ + cupsfilters/test-pdftopdf-inherited-mediabox.sh # testcmyk # fails as it opens some image.ppm which is nowerhe to be found. # testimage # requires also some ppm file as argument @@ -358,6 +360,7 @@ EXTRA_DIST += cupsfilters/gen-lorem-text.c # (not a check_PROGRAM, so it can skip when ASan is absent); named here so it # ships in "make dist". EXTRA_DIST += cupsfilters/test-pdftoraster-copy-height.c +EXTRA_DIST += cupsfilters/test-pdftopdf-inherited-mediabox.c # Generated deterministic lorem text for texttopdf tests BUILT_SOURCES = cupsfilters/test_files/test_text_lorem.txt CLEANFILES = cupsfilters/test_files/test_text_lorem.txt @@ -375,6 +378,7 @@ EXTRA_DIST += \ cupsfilters/test-filter-cases.txt \ cupsfilters/test_files/filled-2.pdf \ cupsfilters/test_files/form_english.pdf \ + cupsfilters/test_files/inherited_mediabox.pdf \ cupsfilters/test_files/malformed.pdf \ cupsfilters/test_files/onepage-a4-adobe-rgb-8-150dpi.pwg \ cupsfilters/test_files/test_file_1pg.pdf \ diff --git a/cupsfilters/pdftopdf.c b/cupsfilters/pdftopdf.c index f5397f479..679edfd54 100644 --- a/cupsfilters/pdftopdf.c +++ b/cupsfilters/pdftopdf.c @@ -250,6 +250,80 @@ media_to_rect(cups_media_t *size, // I - CUPS media (size) information crop->y2 = 72.0 * (size->length - size->top) / 2540.0; } +// +// 'page_get_rect()' - Look up an inheritable rectangle attribute ("MediaBox" +// or "CropBox") for a page. +// +// Page attributes may be inherited from ancestor /Pages nodes (ISO 32000-1 +// section 7.7.3.4), so walk up the /Parent chain. Also resolve an indirect +// reference to the rectangle array, which pdfioDictGetRect() does not do. +// + +static bool // O - `true` if found, `false` otherwise +page_get_rect(pdfio_obj_t *page, // I - Page object + const char *key, // I - Key ("MediaBox" or "CropBox") + pdfio_rect_t *rect) // O - Rectangle +{ + pdfio_obj_t *obj = page; // Current page/pages node + pdfio_dict_t *dict; // Current dictionary + pdfio_obj_t *ref; // Indirect rectangle array object + pdfio_array_t *array; // Rectangle array + int depth; // Loop guard against cyclic /Parent + + + for (depth = 0; obj && depth < 64; depth ++) + { + if ((dict = pdfioObjGetDict(obj)) == NULL) + break; + + if (pdfioDictGetRect(dict, key, rect)) + return (true); + + if ((ref = pdfioDictGetObj(dict, key)) != NULL && (array = pdfioObjGetArray(ref)) != NULL && pdfioArrayGetSize(array) == 4) + { + rect->x1 = pdfioArrayGetNumber(array, 0); + rect->y1 = pdfioArrayGetNumber(array, 1); + rect->x2 = pdfioArrayGetNumber(array, 2); + rect->y2 = pdfioArrayGetNumber(array, 3); + return (true); + } + + obj = pdfioDictGetObj(dict, "Parent"); + } + + memset(rect, 0, sizeof(pdfio_rect_t)); + return (false); +} + + +// +// 'page_get_rotate()' - Look up the inheritable /Rotate attribute for a page, +// walking up the /Parent chain. +// + +static int // O - Rotation in degrees (0 if unset) +page_get_rotate(pdfio_obj_t *page) // I - Page object +{ + pdfio_obj_t *obj = page; // Current page/pages node + pdfio_dict_t *dict; // Current dictionary + int depth; // Loop guard against cyclic /Parent + + + for (depth = 0; obj && depth < 64; depth ++) + { + if ((dict = pdfioObjGetDict(obj)) == NULL) + break; + + if (pdfioDictGetType(dict, "Rotate") == PDFIO_VALTYPE_NUMBER) + return ((int)pdfioDictGetNumber(dict, "Rotate")); + + obj = pdfioDictGetObj(dict, "Parent"); + } + + return (0); +} + + // // 'prepare_log()' - Log an informational or error message while preparing // documents for printing. @@ -1518,7 +1592,7 @@ flatten_pdf(xform_prepare_t *p, // I - Preparation data idict = pdfioObjGetDict(outpage->input[pg]); annotsArray = pdfioDictGetArray(idict, "Annots"); - rotate_val = (int)pdfioDictGetNumber(idict, "Rotate"); + rotate_val = page_get_rotate(outpage->input[pg]); count = pdfioArrayGetSize(annotsArray); p->annotation_contents = (char**)malloc(count * sizeof(char*)); @@ -2044,10 +2118,10 @@ copy_page(xform_prepare_t *p, // I - Preparation data // Transform input page to output cell... idict = pdfioObjGetDict(outpage->input[layout]); - if (!pdfioDictGetRect(idict, "CropBox", &irect)) + if (!page_get_rect(outpage->input[layout], "CropBox", &irect)) { // No crop box, use media box... - if (!pdfioDictGetRect(idict, "MediaBox", &irect)) + if (!page_get_rect(outpage->input[layout], "MediaBox", &irect)) { // No media box, use output page size... irect = p->media; diff --git a/cupsfilters/test-pdftopdf-inherited-mediabox.c b/cupsfilters/test-pdftopdf-inherited-mediabox.c new file mode 100644 index 000000000..4de83969f --- /dev/null +++ b/cupsfilters/test-pdftopdf-inherited-mediabox.c @@ -0,0 +1,127 @@ +// +// Checker for test-pdftopdf-inherited-mediabox.sh: read back the PDF that +// cfFilterPDFToPDF() produced from test_files/inherited_mediabox.pdf and +// inspect the transformation applied to the input page. +// +// The input page is 2970x2100 pt with its MediaBox inherited from the +// /Pages node; printed on Letter with print-scaling=auto it has to be +// scaled down (by about 0.26) and rotated by 90 degrees. When the +// inherited MediaBox is ignored, pdftopdf assumes the input page is already +// Letter-sized and emits a ~0.97 scale without rotation. +// +// Copyright © 2026 by OpenPrinting +// +// Licensed under Apache License v2.0. See the file "LICENSE" for more +// information. +// + +#include +#include +#include +#include +#include + + +// +// 'main()' - Check the first "cm" operator of page 1. +// + +int // O - 0 on success, 1 on failure +main(int argc, // I - Number of command-line arguments + char *argv[]) // I - Command-line arguments +{ + pdfio_file_t *pdf; // Output PDF from pdftopdf + pdfio_obj_t *page; // First page + pdfio_stream_t *st; // Content stream + size_t i, // Looping var + num_streams; // Number of content streams + char token[256], // Current token + *end; // End of numeric token + double value, // Numeric token value + matrix[6]; // Last six numbers seen + int num_values = 0, // Number of numbers in the window + found = 0; // Found a "cm" operator? + double scale; // Scale factor of the matrix + + + if (argc != 2) + { + fputs("Usage: test-pdftopdf-inherited-mediabox OUTPUT.pdf\n", stderr); + return (1); + } + + if ((pdf = pdfioFileOpen(argv[1], NULL, NULL, NULL, NULL)) == NULL) + { + fprintf(stderr, "Unable to open \"%s\".\n", argv[1]); + return (1); + } + + if (pdfioFileGetNumPages(pdf) != 1) + { + fprintf(stderr, "Expected 1 output page, got %u.\n", (unsigned)pdfioFileGetNumPages(pdf)); + return (1); + } + + page = pdfioFileGetPage(pdf, 0); + num_streams = pdfioPageGetNumStreams(page); + + for (i = 0; i < num_streams && !found; i ++) + { + if ((st = pdfioPageOpenStream(page, i, true)) == NULL) + { + fprintf(stderr, "Unable to open content stream %u.\n", (unsigned)i); + return (1); + } + + while (pdfioStreamGetToken(st, token, sizeof(token))) + { + value = strtod(token, &end); + + if (end > token && !*end) + { + // Number: keep the last six... + if (num_values == 6) + memmove(matrix, matrix + 1, 5 * sizeof(double)); + else + num_values ++; + + matrix[num_values - 1] = value; + } + else if (!strcmp(token, "cm") && num_values == 6) + { + found = 1; + break; + } + else + num_values = 0; + } + + pdfioStreamClose(st); + } + + pdfioFileClose(pdf); + + if (!found) + { + fputs("No \"cm\" operator found on page 1.\n", stderr); + return (1); + } + + scale = hypot(matrix[0], matrix[1]); + + printf("cm = [%g %g %g %g %g %g], scale = %g\n", matrix[0], matrix[1], matrix[2], matrix[3], matrix[4], matrix[5], scale); + + if (scale < 0.1 || scale > 0.5) + { + fputs("Input page was not scaled to fit: inherited MediaBox ignored?\n", stderr); + return (1); + } + + if (fabs(matrix[0]) > 1e-6 || fabs(matrix[3]) > 1e-6) + { + fputs("Input page was not rotated to fit: inherited MediaBox ignored?\n", stderr); + return (1); + } + + return (0); +} diff --git a/cupsfilters/test-pdftopdf-inherited-mediabox.sh b/cupsfilters/test-pdftopdf-inherited-mediabox.sh new file mode 100755 index 000000000..992ab22ad --- /dev/null +++ b/cupsfilters/test-pdftopdf-inherited-mediabox.sh @@ -0,0 +1,71 @@ +#!/usr/bin/env bash +# +# Regression test: cfFilterPDFToPDF() must honor inheritable page attributes. +# +# /MediaBox, /CropBox and /Rotate may be defined on an ancestor /Pages node +# instead of on the /Page itself (ISO 32000-1, 7.7.3.4), and the value may +# be an indirect reference. The PDFio-based pdftopdf used to read these +# keys only from the page dictionary, so a page with an inherited MediaBox +# was taken to be the output page size already: no scaling, no +# auto-rotation, and an oversized page came out cropped to its lower-left +# corner. +# +# test_files/inherited_mediabox.pdf is a single 2970x2100 pt page whose +# MediaBox is only defined, as an indirect array, on the /Pages node. We +# run it through pdftopdf for Letter with print-scaling=auto via the +# testfilters harness and then read the output back with PDFio (see +# test-pdftopdf-inherited-mediabox.c): the page transform must scale by +# roughly 0.26 and rotate by 90 degrees. +# +# Like testfilters.sh, this runs from the top-level build directory and +# passes file names relative to it: testfilters copies the case file names +# into 100-byte buffers, so long absolute paths must be avoided. +# +set -euo pipefail + +CC="${CC:-cc}" + +TESTFILTERS="./testfilters" +if [[ ! -x "${TESTFILTERS}" ]]; then + echo "testfilters harness not found at ${TESTFILTERS}" >&2 + exit 99 +fi + +FIXTURE="cupsfilters/test_files/inherited_mediabox.pdf" +CHECKER_SRC="cupsfilters/test-pdftopdf-inherited-mediabox.c" +for f in "${FIXTURE}" "${CHECKER_SRC}"; do + if [[ ! -f "${f}" ]]; then + echo "test file not found: ${f}" >&2 + exit 99 + fi +done + +PKG_CFLAGS="$(pkg-config --cflags pdfio 2>/dev/null || true)" +PKG_LIBS="$(pkg-config --libs pdfio 2>/dev/null || true)" +if [[ -z "${PKG_LIBS}" ]]; then + echo "pkg-config cannot find pdfio; skipping." >&2 + exit 77 +fi + +WORKDIR="$(mktemp -d ./inherited-mediabox.XXXXXX)" +cleanup() { rm -rf "${WORKDIR}"; } +trap cleanup EXIT + +CASES="${WORKDIR}/cases.txt" +OUTPUT="${WORKDIR}/output.pdf" +CHECKER="${WORKDIR}/check" + +# One testfilters case: fixture -> pdftopdf -> Letter PDF. +printf '%s\tapplication/pdf\t%s\tapplication/pdf\tGeneric\tPDF Color 2\t1\t1\tapplication/pdf\t42\tinherited-user\tinherited-mediabox\t1\tmedia-size=letter print-scaling=auto\tpdftopdf\n' \ + "${FIXTURE}" "${OUTPUT}" > "${CASES}" + +"${TESTFILTERS}" "${CASES}" + +if [[ ! -s "${OUTPUT}" ]]; then + echo "pdftopdf produced no output" >&2 + exit 1 +fi + +"${CC}" -std=gnu11 -O0 ${PKG_CFLAGS} "${CHECKER_SRC}" ${PKG_LIBS} -lm -o "${CHECKER}" + +"${CHECKER}" "${OUTPUT}" diff --git a/cupsfilters/test_files/inherited_mediabox.pdf b/cupsfilters/test_files/inherited_mediabox.pdf new file mode 100644 index 000000000..df0b4cdf4 Binary files /dev/null and b/cupsfilters/test_files/inherited_mediabox.pdf differ