Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions Makefile.am
Original file line number Diff line number Diff line change
Expand Up @@ -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 \
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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 \
Expand Down
80 changes: 77 additions & 3 deletions cupsfilters/pdftopdf.c
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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*));
Expand Down Expand Up @@ -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;
Expand Down
127 changes: 127 additions & 0 deletions cupsfilters/test-pdftopdf-inherited-mediabox.c
Original file line number Diff line number Diff line change
@@ -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 <pdfio.h>
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>


//
// '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);
}
71 changes: 71 additions & 0 deletions cupsfilters/test-pdftopdf-inherited-mediabox.sh
Original file line number Diff line number Diff line change
@@ -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}"
Binary file added cupsfilters/test_files/inherited_mediabox.pdf
Binary file not shown.
Loading