From f01f35034ea6ada63203e232eee6aead0e84ef34 Mon Sep 17 00:00:00 2001 From: David Reed Date: Tue, 22 Sep 2026 16:22:13 -0400 Subject: [PATCH] pdftopdf: keep annotations without an appearance from aborting the job Flattening a link or a form field that has no /N appearance registered a Form XObject under a heap-allocated name and then freed that name. pdfioDictSetObj() keeps the key pointer, so the page /XObject dictionary was corrupt by the time the flattened file was written. Reopening it failed and cfFilterPDFToPDF() exited 1. Chrome's direct-to-CUPS PDF hits this for every hyperlink. Skip annotations that have nothing to paint. Text and choice fields that still carry a value are synthesized under a PDFio-owned name. Fixes #246 --- Makefile.am | 8 +- cupsfilters/pdftopdf.c | 257 ++++++++++-------- cupsfilters/test-pdftopdf-no-appearance.c | 41 +++ cupsfilters/test-pdftopdf-no-appearance.sh | 62 +++++ .../test_files/link-annots-no-appearance.pdf | Bin 0 -> 3271 bytes 5 files changed, 252 insertions(+), 116 deletions(-) create mode 100644 cupsfilters/test-pdftopdf-no-appearance.c create mode 100755 cupsfilters/test-pdftopdf-no-appearance.sh create mode 100644 cupsfilters/test_files/link-annots-no-appearance.pdf diff --git a/Makefile.am b/Makefile.am index f298ba31a..d8be2b26f 100644 --- a/Makefile.am +++ b/Makefile.am @@ -96,7 +96,8 @@ check_SCRIPTS = \ cupsfilters/test-pclm-overflow.sh \ cupsfilters/test-pwgtopdf-bit-row.sh \ cupsfilters/test-pdftoraster-copy-height.sh \ - cupsfilters/test-pdftopdf-inherited-mediabox.sh + cupsfilters/test-pdftopdf-inherited-mediabox.sh \ + cupsfilters/test-pdftopdf-no-appearance.sh check_PROGRAMS = \ testcmyk \ @@ -124,7 +125,8 @@ TESTS = \ cupsfilters/test-pclm-overflow.sh \ cupsfilters/test-pwgtopdf-bit-row.sh \ cupsfilters/test-pdftoraster-copy-height.sh \ - cupsfilters/test-pdftopdf-inherited-mediabox.sh + cupsfilters/test-pdftopdf-inherited-mediabox.sh \ + cupsfilters/test-pdftopdf-no-appearance.sh # testcmyk # fails as it opens some image.ppm which is nowerhe to be found. # testimage # requires also some ppm file as argument @@ -361,6 +363,7 @@ EXTRA_DIST += cupsfilters/gen-lorem-text.c # ships in "make dist". EXTRA_DIST += cupsfilters/test-pdftoraster-copy-height.c EXTRA_DIST += cupsfilters/test-pdftopdf-inherited-mediabox.c +EXTRA_DIST += cupsfilters/test-pdftopdf-no-appearance.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 @@ -379,6 +382,7 @@ EXTRA_DIST += \ cupsfilters/test_files/filled-2.pdf \ cupsfilters/test_files/form_english.pdf \ cupsfilters/test_files/inherited_mediabox.pdf \ + cupsfilters/test_files/link-annots-no-appearance.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 679edfd54..a6b7f8852 100644 --- a/cupsfilters/pdftopdf.c +++ b/cupsfilters/pdftopdf.c @@ -1595,7 +1595,7 @@ flatten_pdf(xform_prepare_t *p, // I - Preparation data rotate_val = page_get_rotate(outpage->input[pg]); count = pdfioArrayGetSize(annotsArray); - p->annotation_contents = (char**)malloc(count * sizeof(char*)); + p->annotation_contents = (char**)calloc(count, sizeof(char*)); int* noAppearanceobjectIndex = (int *)malloc(count * sizeof(int)); noAppearanceobjectCount = 0; @@ -1840,148 +1840,177 @@ flatten_pdf(xform_prepare_t *p, // I - Preparation data fprintf(stderr, "ignore annotation with no appearance\n"); noAppearanceobjectCount++; } - else - { - char *name = (char *)malloc(sizeof(char) * 32); - snprintf(name, 32, "Fxo%d", next_fx); - - pdfio_dict_t *page_resources = pdfioDictGetDict(outpage->pagedict, "Resources"); - if (!page_resources) + else + { + // No selected appearance stream (/N). Link annotations and form + // fields with no appearance have nothing to paint. This branch used + // to build a Form XObject anyway and register it under a + // heap-allocated name, then free that name. pdfioDictSetObj() keeps + // the caller's key pointer, so by the time the flattened file was + // written every /XObject key was a dangling pointer. Reopening the + // file failed and pdftopdf exited 1. Issue #246. + // + // Text and choice fields that still carry a value are synthesized + // below. The XObject name is a PDFio-owned string. + const char *field_type, + *field_value = NULL, + *da_string = NULL; + + field_type = pdfioDictGetName(Annot_dict, "FT"); + if (field_type && (strcmp(field_type, "Tx") == 0 || + strcmp(field_type, "Ch") == 0)) { - page_resources = pdfioDictCreate(outpage->pdf); - pdfioDictSetDict(outpage->pagedict, "Resources", page_resources); - } + field_value = pdfioDictGetName(Annot_dict, "V"); + if (!field_value) + field_value = pdfioDictGetString(Annot_dict, "V"); - pdfio_dict_t *xobj_dict = pdfioDictGetDict(page_resources, "XObject"); - if (!xobj_dict) - { - xobj_dict = pdfioDictCreate(outpage->pdf); - pdfioDictSetDict(page_resources, "XObject", xobj_dict); + da_string = pdfioDictGetName(Annot_dict, "DA"); + if (!da_string) + da_string = pdfioDictGetString(Annot_dict, "DA"); } - - pdfio_array_t *procset = pdfioArrayCreate(outpage->pdf); - pdfioArrayAppendName(procset, "PDF"); // adds /PDF - pdfioArrayAppendName(procset, "Text"); // adds /Text - pdfio_dict_t *resources = pdfioDictCreate(outpage->pdf); - pdfioDictSetArray(resources, "ProcSet", procset); + if (!field_value || !da_string) + { + fprintf(stderr, "DEBUG: special case ignore annotation with no appearance\n"); + noAppearanceobjectIndex[noAppearanceobjectCount] = (int)i; + noAppearanceobjectCount++; + } + else + { + const char *name; + char *content; + pdfio_dict_t *page_resources, + *xobj_dict, + *form_xobj_dict, + *resources; + pdfio_array_t *procset; + pdfio_obj_t *form_xobj; + pdfio_rect_t rect, + bbox; + char font_key[64]; + double font_size = 10.0; + + name = pdfioStringCreatef(outpage->pdf, "Fxo%d", next_fx); + content = name ? special_pdfio_annotation_get_content(Annot_obj, name, + rotate_val, forbidden_flags, required_flags) : NULL; + if (!name || !content || content[0] == '\0') + { + free(content); + fprintf(stderr, "DEBUG: special case ignore annotation with no appearance\n"); + noAppearanceobjectIndex[noAppearanceobjectCount] = (int)i; + noAppearanceobjectCount++; + } + else + { + page_resources = pdfioDictGetDict(outpage->pagedict, "Resources"); + if (!page_resources) + { + page_resources = pdfioDictCreate(outpage->pdf); + pdfioDictSetDict(outpage->pagedict, "Resources", page_resources); + } + + xobj_dict = pdfioDictGetDict(page_resources, "XObject"); + if (!xobj_dict) + { + xobj_dict = pdfioDictCreate(outpage->pdf); + pdfioDictSetDict(page_resources, "XObject", xobj_dict); + } - pdfio_dict_t *form_xobj_dict = pdfioDictCreate(outpage->pdf); - pdfioDictSetName(form_xobj_dict, "Type", "XObject"); - pdfioDictSetName(form_xobj_dict, "Subtype", "Form"); + procset = pdfioArrayCreate(outpage->pdf); + pdfioArrayAppendName(procset, "PDF"); + pdfioArrayAppendName(procset, "Text"); - char *content = special_pdfio_annotation_get_content(Annot_obj, name, rotate_val, forbidden_flags, required_flags); - p->annotation_contents[i-noAppearanceobjectCount] = content; + resources = pdfioDictCreate(outpage->pdf); + pdfioDictSetArray(resources, "ProcSet", procset); - // The addition to page xobject should be made only if the content stream is not NULL. - if (content && content[0] != '\0') - { - pdfio_array_t *bbox = pdfioDictGetArray(Annot_dict, "BBox"); - if (bbox) - { - pdfioDictSetArray(form_xobj_dict, "BBox", pdfioArrayCopy(outpage->pdf, bbox)); - } - else - { - fprintf(stderr, "WARNING: Appearance stream is missing required /BBox.\n"); - pdfio_rect_t rect; - - if (pdfioDictGetRect(Annot_dict, "Rect", &rect)) - { - pdfio_rect_t Bbox; - Bbox.x1 = 0; - Bbox.y1 = 0; - Bbox.x2 = rect.x2 - rect.x1; - Bbox.y2 = rect.y2 - rect.y1; - pdfioDictSetRect(form_xobj_dict, "BBox", &Bbox); - } - } - - pdfio_obj_t *form_xobj = pdfioFileCreateObj(outpage->pdf, form_xobj_dict); - - const char *field_type = pdfioDictGetName(Annot_dict, "FT"); - if (field_type && (strcmp(field_type, "Tx") == 0 || strcmp(field_type, "Ch") == 0)) - { - const char *field_value = pdfioDictGetName(Annot_dict, "V"); - if (!field_value) field_value = pdfioDictGetString(Annot_dict, "V"); + form_xobj_dict = pdfioDictCreate(outpage->pdf); + pdfioDictSetName(form_xobj_dict, "Type", "XObject"); + pdfioDictSetName(form_xobj_dict, "Subtype", "Form"); + + if (!pdfioDictGetRect(Annot_dict, "Rect", &rect)) + { + rect.x1 = 0.0; + rect.y1 = 0.0; + rect.x2 = 0.0; + rect.y2 = 0.0; + } + bbox.x1 = 0.0; + bbox.y1 = 0.0; + bbox.x2 = rect.x2 - rect.x1; + bbox.y2 = rect.y2 - rect.y1; + pdfioDictSetRect(form_xobj_dict, "BBox", &bbox); - const char *da_string = pdfioDictGetName(Annot_dict, "DA"); - if (!da_string) da_string = pdfioDictGetString(Annot_dict, "DA"); + form_xobj = pdfioFileCreateObj(outpage->pdf, form_xobj_dict); - char font_key[64]; - double font_size = 10.0; if (extractFontDetails(da_string, font_key, sizeof(font_key), &font_size)) - { - pdfio_obj_t *font_obj = NULL; - pdfio_dict_t *page_resource_dict = pdfioDictGetDict(outpage->pagedict, "Resources"); - pdfio_dict_t *font_dict = page_resource_dict ? pdfioDictGetDict(page_resource_dict, "Font") : NULL; - + { + pdfio_obj_t *font_obj = NULL; + pdfio_dict_t *page_resource_dict, + *font_dict; + + page_resource_dict = pdfioDictGetDict(outpage->pagedict, "Resources"); + font_dict = page_resource_dict ? pdfioDictGetDict(page_resource_dict, "Font") : NULL; if (font_dict) + font_obj = pdfioDictGetObj(font_dict, font_key); + + if (!font_obj) { - font_obj = pdfioDictGetObj(font_dict, font_key); - } - - // If font_obj is not found in the page's resources, check the AcroForm's /DR. - if (!font_obj) - { - pdfio_dict_t *catalog = pdfioFileGetCatalog(p->inpdf); - pdfio_dict_t *acroform = catalog ? pdfioDictGetDict(catalog, "AcroForm") : NULL; - pdfio_dict_t *acroform_dr = acroform ? pdfioDictGetDict(acroform, "DR") : NULL; - pdfio_dict_t *acroform_font_dict = acroform_dr ? pdfioDictGetDict(acroform_dr, "Font") : NULL; + pdfio_dict_t *catalog, + *acroform, + *acroform_dr, + *acroform_font_dict; + + catalog = pdfioFileGetCatalog(p->inpdf); + acroform = catalog ? pdfioDictGetDict(catalog, "AcroForm") : NULL; + acroform_dr = acroform ? pdfioDictGetDict(acroform, "DR") : NULL; + acroform_font_dict = acroform_dr ? pdfioDictGetDict(acroform_dr, "Font") : NULL; if (acroform_font_dict) - { - font_obj = pdfioDictGetObj(acroform_font_dict, font_key); - } + font_obj = pdfioDictGetObj(acroform_font_dict, font_key); } if (font_obj) { - // 1. Create the dedicated sub-dictionary for fonts. pdfio_dict_t *sub_font_dict = pdfioDictCreate(outpage->pdf); - pdfioDictSetObj(sub_font_dict, font_key, font_obj); + + pdfioDictSetObj(sub_font_dict, pdfioStringCreate(outpage->pdf, font_key), font_obj); pdfioDictSetDict(resources, "Font", sub_font_dict); pdfioDictSetDict(form_xobj_dict, "Resources", resources); - - fprintf(stderr, "SUCCESS: Font /%s correctly nested in /Resources /Font dictionary.\\n", font_key); - } - else - { - fprintf(stderr, "ERROR: Font %s not found in page or AcroForm resources.\\n", font_key); } - } + else + fprintf(stderr, "ERROR: Font %s not found in page or AcroForm resources.\n", font_key); + } - if (field_value && da_string) - { + if (form_xobj) + { pdfio_stream_t *dst_stream = pdfioObjCreateStream(form_xobj, PDFIO_FILTER_NONE); + if (dst_stream) { - pdfio_rect_t form_bbox; - if (pdfioDictGetRect(form_xobj_dict, "BBox", &form_bbox)) - { - double field_height = form_bbox.y2 - form_bbox.y1; - double x = 2.0; - double y = (field_height / 2.0) - (font_size * 0.35); - - pdfioStreamPuts(dst_stream, "BT\n"); - pdfioStreamPrintf(dst_stream, "%s\n", da_string); // Assuming da_string is safe - pdfioStreamPrintf(dst_stream, "%.2f %.2f Td\n", x, y); - pdfioStreamPrintf(dst_stream, "(%s) Tj\n", field_value); // The critical fix - pdfioStreamPuts(dst_stream, "ET\n"); + pdfio_rect_t form_bbox; + double field_height, + x, + y; + if (pdfioDictGetRect(form_xobj_dict, "BBox", &form_bbox)) + { + field_height = form_bbox.y2 - form_bbox.y1; + x = 2.0; + y = (field_height / 2.0) - (font_size * 0.35); + pdfioStreamPuts(dst_stream, "BT\n"); + pdfioStreamPrintf(dst_stream, "%s\n", da_string); + pdfioStreamPrintf(dst_stream, "%.2f %.2f Td\n", x, y); + pdfioStreamPrintf(dst_stream, "(%s) Tj\n", field_value); + pdfioStreamPuts(dst_stream, "ET\n"); } pdfioStreamClose(dst_stream); } - } - } - - pdfioDictSetObj(xobj_dict, name, form_xobj); - next_fx++; + } + + pdfioDictSetObj(xobj_dict, name, form_xobj); + p->annotation_contents[i - noAppearanceobjectCount] = content; + next_fx++; + } } - - //Annot_dict - free(name); - fprintf(stderr, "DEBUG: special case ignore annotation with no appearance\n"); - noAppearanceobjectIndex[noAppearanceobjectCount] = i; - } + } if(N_stream) pdfioStreamClose(N_stream); } diff --git a/cupsfilters/test-pdftopdf-no-appearance.c b/cupsfilters/test-pdftopdf-no-appearance.c new file mode 100644 index 000000000..fceea6de2 --- /dev/null +++ b/cupsfilters/test-pdftopdf-no-appearance.c @@ -0,0 +1,41 @@ +// +// Regression check for issue #246: a PDF whose only annotations are links +// with no appearance stream must come back out of pdftopdf as a readable +// one-page PDF. +// + +#include +#include + +int +main(int argc, + char *argv[]) +{ + pdfio_file_t *pdf; + size_t pages; + + + if (argc != 2) + { + fprintf(stderr, "Usage: %s file.pdf\n", argv[0]); + return (1); + } + + pdf = pdfioFileOpen(argv[1], NULL, NULL, NULL, NULL); + if (!pdf) + { + fprintf(stderr, "pdftopdf output could not be opened\n"); + return (1); + } + + pages = pdfioFileGetNumPages(pdf); + pdfioFileClose(pdf); + + if (pages < 1) + { + fprintf(stderr, "pdftopdf output has no pages\n"); + return (1); + } + + return (0); +} diff --git a/cupsfilters/test-pdftopdf-no-appearance.sh b/cupsfilters/test-pdftopdf-no-appearance.sh new file mode 100755 index 000000000..dc707ebb0 --- /dev/null +++ b/cupsfilters/test-pdftopdf-no-appearance.sh @@ -0,0 +1,62 @@ +#!/usr/bin/env bash +# +# Regression test: link annotations with no appearance stream must not +# make pdftopdf fail. +# +# Chrome's print PDF gives every hyperlink an annotation and no /AP +# stream. pdftopdf flattened those by registering a Form XObject under +# a heap-allocated name and then freeing the name. pdfioDictSetObj() +# keeps that pointer, so the flattened file's /XObject dictionary was +# corrupt, reopening it failed, and the filter exited 1. A page of +# ordinary content with several such links is enough to reproduce it +# (issue #246). +# +# Like testfilters.sh, this runs from the top-level build directory. +# +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/link-annots-no-appearance.pdf" +CHECKER_SRC="cupsfilters/test-pdftopdf-no-appearance.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 ./no-appearance.XXXXXX)" +cleanup() { rm -rf "${WORKDIR}"; } +trap cleanup EXIT + +CASES="${WORKDIR}/cases.txt" +OUTPUT="${WORKDIR}/output.pdf" +CHECKER="${WORKDIR}/check" + +printf '%s\tapplication/pdf\t%s\tapplication/pdf\tGeneric\tPDF Color 2\t1\t1\tapplication/pdf\t42\tno-appearance-user\tlink-annots\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/link-annots-no-appearance.pdf b/cupsfilters/test_files/link-annots-no-appearance.pdf new file mode 100644 index 0000000000000000000000000000000000000000..8379019e79e7f91ee99053e5a834ea9958dcd99e GIT binary patch literal 3271 zcmb`KQBT`25Xay1DeeUc9!7Fb?4%Q_1RK?+F%6aOsoFzm4V0AxC1GNG`rV!BXq{;+ z@~X;huIKa5_WAGY-ue5<8$8S+gaqo#TR1v`c=q$&Lj11m%5{AW@p*Y|TTtXMh4C0{ zwIs)gpLaVrzxdN?*}^tx1BcO|fE&{p`=whG?f-Z`Z zCYc&t z3UZn&4K-WB{s+5W*9S1Wh0|F?v7Gy63z&El)$N(A2E-Q+m!0h4*{b@^xQhENt@c^^y7$T%GU_>T)6m2wdvdP zKa5dieU?#w9tsuJdb?vNWqRwUGQQ?LKTU_-x0%t_T6ImiT2nd7)%3;c$^&w|Dww68!?vmRJt} literal 0 HcmV?d00001