From bd785691c3eedcf999ce942ae4f480977c584e43 Mon Sep 17 00:00:00 2001 From: oubayashi <214973957@qq.com> Date: Mon, 28 Sep 2026 18:00:59 +0800 Subject: [PATCH 1/2] bannertopdf: support Unicode text with embedded fallback fonts Decode banner fields as UTF-8 instead of writing raw input bytes to Courier strings. Use Fontconfig to find outline fonts covering non-ASCII characters, cache matching fonts, and embed them as Unicode CID fonts through PDFio. Switch font resources as needed when a line contains characters from multiple writing systems while keeping the existing Courier path for ASCII text. Validate UTF-8 sequences, escape special and control characters in PDF literal strings, and gracefully replace characters when no embeddable font is available. Supplementary-plane characters are currently replaced because PDFio 1.6 uses a BMP-sized CIDToGID mapping. This fixes garbled printer names, locations, job titles, user names, and other banner fields containing non-ASCII text. --- Makefile.am | 7 +- cupsfilters/bannertopdf.c | 348 ++++++++++++++++++++++-- cupsfilters/test-bannertopdf-unicode.c | 199 ++++++++++++++ cupsfilters/test-bannertopdf-unicode.sh | 68 +++++ cupsfilters/testfilters.c | 8 + 5 files changed, 607 insertions(+), 23 deletions(-) create mode 100644 cupsfilters/test-bannertopdf-unicode.c create mode 100755 cupsfilters/test-bannertopdf-unicode.sh diff --git a/Makefile.am b/Makefile.am index d8be2b26f..91ab48c62 100644 --- a/Makefile.am +++ b/Makefile.am @@ -97,7 +97,8 @@ check_SCRIPTS = \ cupsfilters/test-pwgtopdf-bit-row.sh \ cupsfilters/test-pdftoraster-copy-height.sh \ cupsfilters/test-pdftopdf-inherited-mediabox.sh \ - cupsfilters/test-pdftopdf-no-appearance.sh + cupsfilters/test-pdftopdf-no-appearance.sh \ + cupsfilters/test-bannertopdf-unicode.sh check_PROGRAMS = \ testcmyk \ @@ -126,7 +127,8 @@ TESTS = \ cupsfilters/test-pwgtopdf-bit-row.sh \ cupsfilters/test-pdftoraster-copy-height.sh \ cupsfilters/test-pdftopdf-inherited-mediabox.sh \ - cupsfilters/test-pdftopdf-no-appearance.sh + cupsfilters/test-pdftopdf-no-appearance.sh \ + cupsfilters/test-bannertopdf-unicode.sh # testcmyk # fails as it opens some image.ppm which is nowerhe to be found. # testimage # requires also some ppm file as argument @@ -364,6 +366,7 @@ EXTRA_DIST += cupsfilters/gen-lorem-text.c EXTRA_DIST += cupsfilters/test-pdftoraster-copy-height.c EXTRA_DIST += cupsfilters/test-pdftopdf-inherited-mediabox.c EXTRA_DIST += cupsfilters/test-pdftopdf-no-appearance.c +EXTRA_DIST += cupsfilters/test-bannertopdf-unicode.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 diff --git a/cupsfilters/bannertopdf.c b/cupsfilters/bannertopdf.c index 7212f865d..db0810605 100644 --- a/cupsfilters/bannertopdf.c +++ b/cupsfilters/bannertopdf.c @@ -19,6 +19,10 @@ #include #include +#ifdef HAVE_FONTCONFIG +#include +#endif + #ifndef HAVE_OPEN_MEMSTREAM #include #include @@ -27,6 +31,7 @@ #include #include +#include // Banner information flags. typedef enum banner_info_e @@ -58,6 +63,33 @@ typedef struct banner_s // Banner data structure unsigned infos; // I - Banner information flags } banner_t; +typedef struct banner_font_s +{ +#ifdef HAVE_FONTCONFIG + FcPattern *pattern; +#endif + char resource[32]; +} banner_font_t; + +#ifdef HAVE_FONTCONFIG +# define BANNER_MAX_FONTS 32 +#endif + +typedef struct banner_fonts_s +{ + iterate_data_t *iterate_helper; + cf_logfunc_t log; + void *log_data; + int warned_missing_font; + int warned_supplementary; +#ifdef HAVE_FONTCONFIG + banner_font_t fonts[BANNER_MAX_FONTS]; +#else + banner_font_t fonts[1]; +#endif + size_t num_fonts; +} banner_fonts_t; + static void banner_free(banner_t *banner) { @@ -344,30 +376,299 @@ duplex_marked(cf_filter_data_t *data) return (0); } +static unsigned int +utf8_next(const unsigned char **s) +{ + const unsigned char *p = *s; + + if (*p < 0x80) + { + *s = p + 1; + return *p; + } + if ((*p & 0xe0) == 0xc0 && *p >= 0xc2 && + (p[1] & 0xc0) == 0x80) + { + *s = p + 2; + return ((*p & 0x1f) << 6) | (p[1] & 0x3f); + } + if ((*p & 0xf0) == 0xe0 && + (p[1] & 0xc0) == 0x80 && + (p[2] & 0xc0) == 0x80 && + !(*p == 0xe0 && p[1] < 0xa0) && + !(*p == 0xed && p[1] >= 0xa0)) + { + *s = p + 3; + return ((*p & 0x0f) << 12) | + ((p[1] & 0x3f) << 6) | + (p[2] & 0x3f); + } + if ((*p & 0xf8) == 0xf0 && *p <= 0xf4 && + (p[1] & 0xc0) == 0x80 && + (p[2] & 0xc0) == 0x80 && + (p[3] & 0xc0) == 0x80 && + !(*p == 0xf0 && p[1] < 0x90) && + !(*p == 0xf4 && p[1] >= 0x90)) + { + *s = p + 4; + return ((*p & 0x07) << 18) | + ((p[1] & 0x3f) << 12) | + ((p[2] & 0x3f) << 6) | + (p[3] & 0x3f); + } + + *s = p + 1; + return 0xfffd; +} + +#ifdef HAVE_FONTCONFIG +static int +banner_add_font_file(banner_fonts_t *font_context, + const char *resource_name, + const char *filename) +{ + pdfio_obj_t *font; + + font = pdfioFileCreateFontObjFromFile( + (pdfio_file_t *)font_context->iterate_helper->pdf, filename, true); + if (!font) + return 1; + + return pdfioPageDictAddFont(font_context->iterate_helper->page_dict, + resource_name, font) ? 0 : 1; +} + +static int +banner_font_for_char(banner_fonts_t *font_context, unsigned int ch) +{ + FcCharSet *charset = NULL; + FcFontSet *matches; + FcPattern *query; + FcResult result; + size_t i; + int match_num; + + for (i = 0; i < font_context->num_fonts; i ++) + if (FcPatternGetCharSet(font_context->fonts[i].pattern, FC_CHARSET, 0, + &charset) == FcResultMatch && + FcCharSetHasChar(charset, ch)) + return (int)i; + + if (!FcInit() || font_context->num_fonts >= BANNER_MAX_FONTS || + (charset = FcCharSetCreate()) == NULL) + return -1; + if ((query = FcPatternCreate()) == NULL) + { + FcCharSetDestroy(charset); + return -1; + } + + FcCharSetAddChar(charset, ch); + FcPatternAddCharSet(query, FC_CHARSET, charset); + FcPatternAddBool(query, FC_SCALABLE, FcTrue); + FcPatternAddBool(query, FC_OUTLINE, FcTrue); + FcCharSetDestroy(charset); + FcConfigSubstitute(NULL, query, FcMatchPattern); + FcDefaultSubstitute(query); + matches = FcFontSort(NULL, query, FcFalse, NULL, &result); + FcPatternDestroy(query); + if (!matches) + return -1; + + for (match_num = 0; match_num < matches->nfont; match_num ++) + { + FcChar8 *filename; + int index = 0; + banner_font_t *font; + + if (FcPatternGetCharSet(matches->fonts[match_num], FC_CHARSET, 0, + &charset) != + FcResultMatch || !FcCharSetHasChar(charset, ch) || + FcPatternGetString(matches->fonts[match_num], FC_FILE, 0, &filename) != + FcResultMatch) + continue; + + // PDFio opens the first face in a TrueType collection. + FcPatternGetInteger(matches->fonts[match_num], FC_INDEX, 0, &index); + if (index != 0) + continue; + + font = font_context->fonts + font_context->num_fonts; + snprintf(font->resource, sizeof(font->resource), + "bannertopdf-unicode-%u", (unsigned)font_context->num_fonts); + font->pattern = FcPatternDuplicate(matches->fonts[match_num]); + if (!font->pattern) + continue; + if (banner_add_font_file(font_context, font->resource, + (const char *)filename) != 0) + { + FcPatternDestroy(font->pattern); + font->pattern = NULL; + continue; + } + + font_context->num_fonts ++; + FcFontSetDestroy(matches); + return (int)(font_context->num_fonts - 1); + } + + FcFontSetDestroy(matches); + return -1; +} + +static void +banner_fonts_free(banner_fonts_t *font_context) +{ + size_t i; + + for (i = 0; i < font_context->num_fonts; i ++) + FcPatternDestroy(font_context->fonts[i].pattern); +} +#else +static int +banner_font_for_char(banner_fonts_t *font_context, unsigned int ch) +{ + (void)font_context; + (void)ch; + return -1; +} + +static void +banner_fonts_free(banner_fonts_t *font_context) +{ + (void)font_context; +} +#endif + +static void +pdf_write_ascii_char(FILE *s, unsigned int ch) +{ + switch (ch) + { + case '\n' : fputs("\\n", s); break; + case '\r' : fputs("\\r", s); break; + case '\t' : fputs("\\t", s); break; + case '\b' : fputs("\\b", s); break; + case '\f' : fputs("\\f", s); break; + case '(' : fputs("\\(", s); break; + case ')' : fputs("\\)", s); break; + case '\\' : fputs("\\\\", s); break; + default : + if (ch < 0x20 || ch == 0x7f) + fprintf(s, "\\%03o", ch); + else + fputc((int)ch, s); + break; + } +} + static void info_linef(FILE *s, + banner_fonts_t *font_context, const char *key, const char *valuefmt, ...) { - va_list ap; + va_list ap, ap_copy; + char *line; + size_t keylen; + int valuelen, current_font = -2; + const unsigned char *p; va_start(ap, valuefmt); - fprintf(s, "(%s: ", key); - vfprintf(s, valuefmt, ap); - fprintf(s, ") Tj T*\n"); + va_copy(ap_copy, ap); + valuelen = vsnprintf(NULL, 0, valuefmt, ap_copy); + va_end(ap_copy); + keylen = strlen(key); + if (valuelen < 0 || + (line = malloc(keylen + (size_t)valuelen + 3)) == NULL) + { + va_end(ap); + return; + } + + memcpy(line, key, keylen); + line[keylen] = ':'; + line[keylen + 1] = ' '; + vsnprintf(line + keylen + 2, (size_t)valuelen + 1, valuefmt, ap); va_end(ap); + + for (p = (const unsigned char *)line; *p;) + { + unsigned int ch = utf8_next(&p); + int font_num; + + if (ch < 0x80) + { + if (current_font != -1) + { + fputs("/bannertopdf-font 14 Tf\n", s); + current_font = -1; + } + fputc('(', s); + pdf_write_ascii_char(s, ch); + while (*p && *p < 0x80) + pdf_write_ascii_char(s, *p ++); + fputs(") Tj\n", s); + continue; + } + + if (ch > 0xffff) + { + if (!font_context->warned_supplementary && font_context->log) + { + font_context->log(font_context->log_data, CF_LOGLEVEL_WARN, + "cfFilterBannerToPDF: PDFio 1.6 cannot map " + "supplementary Unicode character U+%06X", ch); + font_context->warned_supplementary = 1; + } + ch = 0xfffd; + } + + font_num = banner_font_for_char(font_context, ch); + if (font_num < 0) + { + if (!font_context->warned_missing_font && font_context->log) + { + font_context->log(font_context->log_data, CF_LOGLEVEL_WARN, + "cfFilterBannerToPDF: No embeddable font found " + "for Unicode character U+%04X", ch); + font_context->warned_missing_font = 1; + } + if (current_font != -1) + { + fputs("/bannertopdf-font 14 Tf\n", s); + current_font = -1; + } + fputs("(?) Tj\n", s); + continue; + } + + if (current_font != font_num) + { + fprintf(s, "/%s 14 Tf\n", font_context->fonts[font_num].resource); + current_font = font_num; + } + fprintf(s, "<%04X> Tj\n", ch); + } + + if (current_font != -1) + fputs("/bannertopdf-font 14 Tf\n", s); + fputs("T*\n", s); + free(line); } static void info_line(FILE *s, + banner_fonts_t *font_context, const char *key, const char *value) { - info_linef(s, key, "%s", value); + info_linef(s, font_context, key, "%s", value); } static void info_line_time(FILE *s, + banner_fonts_t *font_context, const char *key, const char *timestamp) { @@ -378,10 +679,10 @@ info_line_time(FILE *s, { time = (time_t)atoll(timestamp); strftime(buf, sizeof buf, "%c", localtime(&time)); - info_line(s, key, buf); + info_line(s, font_context, key, buf); } else - info_line(s, key, "unknown"); + info_line(s, font_context, key, "unknown"); } static const char * @@ -604,6 +905,7 @@ generate_banner_pdf(banner_t *banner, ipp_attribute_t *ipp_attr; char buf2[1024]; const char *value; + banner_fonts_t font_context = { 0 }; #ifndef HAVE_OPEN_MEMSTREAM struct stat st; #endif @@ -618,6 +920,9 @@ generate_banner_pdf(banner_t *banner, iterate_data_t *iterate_helper = (iterate_data_t *)malloc(sizeof(iterate_data_t)); output_doc = cfCopyPDFdoc(input_doc, outputfp, iterate_helper); + font_context.iterate_helper = iterate_helper; + font_context.log = log; + font_context.log_data = ld; for (i = 0; i < 4; i ++) media_limits[i] = -1.0; @@ -692,57 +997,57 @@ generate_banner_pdf(banner_t *banner, if ((banner->infos & INFO_PRINTER_NAME) && data->printer && data->printer[0] && data->printer[0] != '/') - info_line(s, "Printer", data->printer); + info_line(s, &font_context, "Printer", data->printer); if ((banner->infos & INFO_PRINTER_INFO) && (((value = cupsGetOption("printer-info", num_options, options)) != NULL && value[0]) || ((value = getenv("PRINTER_INFO")) != NULL && value[0]))) - info_line(s, "Description", value); + info_line(s, &font_context, "Description", value); if ((banner->infos & INFO_PRINTER_LOCATION) && (((value = cupsGetOption("printer-location", num_options, options)) != NULL && value[0]) || ((value = getenv("PRINTER_LOCATION")) != NULL && value[0]))) - info_line(s, "Location", value); + info_line(s, &font_context, "Location", value); if ((banner->infos & INFO_JOB_ID) && data->printer && data->printer[0] && data->printer[0] != '/' && jobid && jobid[0]) - info_linef(s, "Job ID", "%s-%s", data->printer, jobid); + info_linef(s, &font_context, "Job ID", "%s-%s", data->printer, jobid); if ((banner->infos & INFO_JOB_NAME) && jobtitle && jobtitle[0]) - info_line(s, "Job Title", jobtitle); + info_line(s, &font_context, "Job Title", jobtitle); if ((banner->infos & INFO_JOB_ORIGINATING_HOST_NAME) && (value = cupsGetOption("job-originating-host-name", num_options, options)) != NULL && value[0]) - info_line(s, "Printed from", value); + info_line(s, &font_context, "Printed from", value); if ((banner->infos & INFO_JOB_ORIGINATING_USER_NAME) && user && user[0]) - info_line(s, "Printed by", user); + info_line(s, &font_context, "Printed by", user); if ((banner->infos & INFO_TIME_AT_CREATION) && (value = cupsGetOption("time-at-creation", num_options, options)) != NULL && value[0]) - info_line_time(s, "Created at", value); + info_line_time(s, &font_context, "Created at", value); if ((banner->infos & INFO_TIME_AT_PROCESSING) && (value = cupsGetOption("time-at-processing", num_options, options)) != NULL && value[0]) - info_line_time(s, "Printed at", value); + info_line_time(s, &font_context, "Printed at", value); if ((banner->infos & INFO_JOB_BILLING) && (value = cupsGetOption("job-billing", num_options, options)) != NULL && value[0]) - info_line(s, "Billing Information\n", value); + info_line(s, &font_context, "Billing Information\n", value); if ((banner->infos & INFO_JOB_UUID) && (value = cupsGetOption("job-uuid", num_options, options)) != NULL && value[0]) - info_line(s, "Job UUID", value); + info_line(s, &font_context, "Job UUID", value); if ((banner->infos & INFO_PRINTER_DRIVER_NAME) || (banner->infos & INFO_PRINTER_MAKE_AND_MODEL)) @@ -787,18 +1092,18 @@ generate_banner_pdf(banner_t *banner, snprintf(buf2, sizeof(buf2), "%s %s%s", make, model, (is_fax ? " (Fax)" : "")); char *nickname = buf2; - info_line(s, "Make and Model", nickname); + info_line(s, &font_context, "Make and Model", nickname); } } if (banner->infos & INFO_IMAGEABLE_AREA) { - info_linef(s, "Media Limits", "%.2f x %.2f to %.2f x %.2f inches", + info_linef(s, &font_context, "Media Limits", "%.2f x %.2f to %.2f x %.2f inches", media_limits[0] / 72.0, media_limits[1] / 72.0, media_limits[2] / 72.0, media_limits[3] / 72.0); - info_linef(s, "Media Limits", "%.2f x %.2f to %.2f x %.2f cm", + info_linef(s, &font_context, "Media Limits", "%.2f x %.2f to %.2f x %.2f cm", media_limits[0] / 72.0 * 2.54, media_limits[1] / 72.0 * 2.54, media_limits[2] / 72.0 * 2.54, @@ -806,6 +1111,7 @@ generate_banner_pdf(banner_t *banner, } fprintf(s, "ET\n"); + banner_fonts_free(&font_context); #ifndef HAVE_OPEN_MEMSTREAM fflush(s); if (fstat(fileno(s), &st) < 0) diff --git a/cupsfilters/test-bannertopdf-unicode.c b/cupsfilters/test-bannertopdf-unicode.c new file mode 100644 index 000000000..9e40dfc62 --- /dev/null +++ b/cupsfilters/test-bannertopdf-unicode.c @@ -0,0 +1,199 @@ +// +// Checker for test-bannertopdf-unicode.sh: inspect the PDF generated by +// cfFilterBannerToPDF() for mixed ASCII and Unicode job information. +// +// Copyright © 2026 by OpenPrinting +// +// Licensed under Apache License v2.0. See the file "LICENSE" for more +// information. +// + +#include +#include +#include +#include + + +static pdfio_dict_t * +get_dict(pdfio_dict_t *dict, const char *key) +{ + pdfio_obj_t *obj; + + if (pdfioDictGetType(dict, key) == PDFIO_VALTYPE_DICT) + return pdfioDictGetDict(dict, key); + + if (pdfioDictGetType(dict, key) == PDFIO_VALTYPE_INDIRECT && + (obj = pdfioDictGetObj(dict, key)) != NULL) + return pdfioObjGetDict(obj); + + return NULL; +} + + +static int +has_embedded_font(pdfio_dict_t *font) +{ + pdfio_array_t *descendants; + pdfio_obj_t *descendant; + pdfio_dict_t *descendant_dict, *descriptor; + + if ((descendants = pdfioDictGetArray(font, "DescendantFonts")) == NULL || + (descendant = pdfioArrayGetObj(descendants, 0)) == NULL || + (descendant_dict = pdfioObjGetDict(descendant)) == NULL || + (descriptor = get_dict(descendant_dict, "FontDescriptor")) == NULL) + return 0; + + return pdfioDictGetObj(descriptor, "FontFile") != NULL || + pdfioDictGetObj(descriptor, "FontFile2") != NULL || + pdfioDictGetObj(descriptor, "FontFile3") != NULL; +} + + +static char * +read_page_streams(pdfio_obj_t *page) +{ + char *contents = NULL; + size_t alloc = 0, length = 0, i, count; + + count = pdfioPageGetNumStreams(page); + for (i = 0; i < count; i ++) + { + pdfio_stream_t *stream; + char buffer[4096]; + ssize_t bytes; + + if ((stream = pdfioPageOpenStream(page, i, true)) == NULL) + { + free(contents); + return NULL; + } + + while ((bytes = pdfioStreamRead(stream, buffer, sizeof(buffer))) > 0) + { + size_t needed = length + (size_t)bytes + 1; + + if (needed > alloc) + { + char *temp; + + alloc = needed + 4095; + if ((temp = realloc(contents, alloc)) == NULL) + { + free(contents); + pdfioStreamClose(stream); + return NULL; + } + contents = temp; + } + + memcpy(contents + length, buffer, (size_t)bytes); + length += (size_t)bytes; + } + + pdfioStreamClose(stream); + } + + if (!contents) + return NULL; + + contents[length] = '\0'; + return contents; +} + + +int +main(int argc, char *argv[]) +{ + pdfio_file_t *pdf; + pdfio_obj_t *page; + pdfio_dict_t *resources, *fonts, *unicode_font; + char *contents; + int status = 1; + + if (argc != 2) + { + fputs("Usage: test-bannertopdf-unicode 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)); + goto done; + } + + page = pdfioFileGetPage(pdf, 0); + if ((resources = get_dict(pdfioObjGetDict(page), "Resources")) == NULL || + (fonts = get_dict(resources, "Font")) == NULL) + { + fputs("Output page has no font resources.\n", stderr); + goto done; + } + + if (get_dict(fonts, "bannertopdf-font") == NULL) + { + fputs("Courier banner font resource is missing.\n", stderr); + goto done; + } + + if ((unicode_font = get_dict(fonts, "bannertopdf-unicode-0")) == NULL) + { + fputs("Unicode banner font resource is missing.\n", stderr); + goto done; + } + + if (!pdfioDictGetName(unicode_font, "Subtype") || + strcmp(pdfioDictGetName(unicode_font, "Subtype"), "Type0")) + { + fputs("Unicode banner font is not a Type0 font.\n", stderr); + goto done; + } + + if (pdfioDictGetObj(unicode_font, "ToUnicode") == NULL) + { + fputs("Unicode banner font has no ToUnicode map.\n", stderr); + goto done; + } + + if (!has_embedded_font(unicode_font)) + { + fputs("Unicode banner font is not embedded.\n", stderr); + goto done; + } + + if ((contents = read_page_streams(page)) == NULL) + { + fputs("Unable to read page content streams.\n", stderr); + goto done; + } + + if (!strstr(contents, "/bannertopdf-unicode-0 14 Tf") || + !strstr(contents, "<00E9> Tj")) + { + fputs("Unicode text was not emitted with the embedded font.\n", stderr); + free(contents); + goto done; + } + + if (!strstr(contents, "Office \\(Floor 2\\) \\\\ Queue")) + { + fputs("ASCII PDF literal string escaping is incorrect.\n", stderr); + free(contents); + goto done; + } + + free(contents); + puts("Unicode font embedding and banner content checks passed."); + status = 0; + + done: + pdfioFileClose(pdf); + return status; +} diff --git a/cupsfilters/test-bannertopdf-unicode.sh b/cupsfilters/test-bannertopdf-unicode.sh new file mode 100755 index 000000000..4226e48e1 --- /dev/null +++ b/cupsfilters/test-bannertopdf-unicode.sh @@ -0,0 +1,68 @@ +#!/usr/bin/env bash +# +# Regression test: bannertopdf must embed a fallback font for Unicode job +# information while keeping and safely escaping the ASCII Courier path. +# +# This follows the same pattern as test-pdftopdf-inherited-mediabox.sh: run a +# focused case through the testfilters harness, then inspect the resulting PDF +# with a small PDFio checker. +# +set -euo pipefail + +CC="${CC:-cc}" + +if [[ ! -x ./testfilters ]]; then + echo "testfilters harness not found at ./testfilters" >&2 + exit 99 +fi + +if [[ ! -f config.h ]] || ! grep -q '^#define HAVE_FONTCONFIG 1' config.h; then + echo "Fontconfig support is disabled; skipping." >&2 + exit 77 +fi + +PKG_CFLAGS="$(pkg-config --cflags pdfio fontconfig 2>/dev/null || true)" +PKG_LIBS="$(pkg-config --libs pdfio fontconfig 2>/dev/null || true)" +if [[ -z "${PKG_LIBS}" ]]; then + echo "pkg-config cannot find PDFio and Fontconfig; skipping." >&2 + exit 77 +fi + +if ! fc-match --format='%{file}\n' ':charset=00e9' 2>/dev/null | grep -q .; then + echo "No font covering U+00E9 is available; skipping." >&2 + exit 77 +fi + +CHECKER_SRC="cupsfilters/test-bannertopdf-unicode.c" +if [[ ! -f "${CHECKER_SRC}" ]]; then + echo "test file not found: ${CHECKER_SRC}" >&2 + exit 99 +fi + +WORKDIR="$(mktemp -d ./bannertopdf-unicode.XXXXXX)" +cleanup() { rm -rf "${WORKDIR}"; } +trap cleanup EXIT + +BANNER="${WORKDIR}/unicode-banner" +CASES="${WORKDIR}/cases.txt" +OUTPUT="${WORKDIR}/output.pdf" +CHECKER="${WORKDIR}/check" + +cat > "${BANNER}" <<'EOF' +#PDF-BANNER +Template default-testpage.pdf +Show printer-name job-name job-originating-user-name +EOF + +printf '%s\tapplication/vnd.cups-pdf-banner\t%s\tapplication/pdf\tGeneric\tPDF Color 2\t1\t1\tapplication/pdf\t42\tunicode-user\tcafé\t1\t \tbannertopdf\n' \ + "${BANNER}" "${OUTPUT}" > "${CASES}" + +PRINTER='Office (Floor 2) \ Queue' ./testfilters "${CASES}" + +if [[ ! -s "${OUTPUT}" ]]; then + echo "bannertopdf produced no output" >&2 + exit 1 +fi + +${CC} -std=gnu11 -O0 ${PKG_CFLAGS} "${CHECKER_SRC}" ${PKG_LIBS} -o "${CHECKER}" +"${CHECKER}" "${OUTPUT}" diff --git a/cupsfilters/testfilters.c b/cupsfilters/testfilters.c index bfa39f0c4..50c27466f 100644 --- a/cupsfilters/testfilters.c +++ b/cupsfilters/testfilters.c @@ -72,6 +72,13 @@ void // O - Pointer to parameters st return out; } +void +*bannertopdf_param_gen(const char *output_mime) +{ + (void)output_mime; + return strdup("data"); +} + // Define the filter mappings FilterMapping filter_mappings[] = { { "imagetoraster", cfFilterImageToRaster, NULL }, @@ -79,6 +86,7 @@ FilterMapping filter_mappings[] = { { "rastertopwg", cfFilterRasterToPWG, NULL }, { "pwgtopdf", cfFilterPWGToPDF, NULL }, { "pdftopdf", cfFilterPDFToPDF, NULL }, + { "bannertopdf", cfFilterBannerToPDF, bannertopdf_param_gen }, { "texttopdf", cfFilterTextToPDF, NULL }, { "texttotext", cfFilterTextToText, NULL }, }; From 1686aea2ad3c82d40fe4c6507a2c138860a1095f Mon Sep 17 00:00:00 2001 From: oubayashi <214973957@qq.com> Date: Thu, 24 Sep 2026 13:39:10 +0800 Subject: [PATCH 2/2] bannertopdf: preserve indirect page resources when adding fonts Resolve and copy an indirect page Resources dictionary before adding the banner font instead of replacing it with an empty dictionary. This preserves existing XObject, ExtGState, Font, and other resources from the banner template. Extend the Unicode banner regression test to verify that the template image resources and drawing commands remain present. --- cupsfilters/pdf.c | 22 ++++++++++++++++++---- cupsfilters/test-bannertopdf-unicode.c | 20 +++++++++++++++++++- 2 files changed, 37 insertions(+), 5 deletions(-) diff --git a/cupsfilters/pdf.c b/cupsfilters/pdf.c index 252f85b80..2645a5933 100644 --- a/cupsfilters/pdf.c +++ b/cupsfilters/pdf.c @@ -370,15 +370,30 @@ cfPDFAddType1Font1(cf_pdf_t *pdf, // I - Pointer to PDF object { pdfio_dict_t *fonts; pdfio_dict_t *pageDict = iterate_helper->page_dict; + pdfio_valtype_t resources_type; if (!pageDict) return 1; // Locate or create the Resources dictionary - pdfio_dict_t *resources = pdfioDictGetDict(pageDict, "Resources"); - if (!resources) + pdfio_dict_t *resources; + resources_type = pdfioDictGetType(pageDict, "Resources"); + if (resources_type == PDFIO_VALTYPE_INDIRECT) + { + pdfio_obj_t *resources_obj = pdfioDictGetObj(pageDict, "Resources"); + + if (!resources_obj || + (resources = pdfioDictCopy((pdfio_file_t *)iterate_helper->pdf, + pdfioObjGetDict(resources_obj))) == NULL || + !pdfioDictSetDict(pageDict, "Resources", resources)) + return 1; + } + else if (resources_type == PDFIO_VALTYPE_DICT) + resources = pdfioDictGetDict(pageDict, "Resources"); + else { resources = pdfioDictCreate((pdfio_file_t *)iterate_helper->pdf); - pdfioDictSetDict(pageDict, "Resources", resources); + if (!resources || !pdfioDictSetDict(pageDict, "Resources", resources)) + return 1; } // Locate or create the Font dictionary within Resources @@ -615,4 +630,3 @@ cfPDFFillForm(cf_pdf_t *doc, // I - PDF document // TODO: PDFio does not directly support form filling. return 1; } - diff --git a/cupsfilters/test-bannertopdf-unicode.c b/cupsfilters/test-bannertopdf-unicode.c index 9e40dfc62..a9877f9d5 100644 --- a/cupsfilters/test-bannertopdf-unicode.c +++ b/cupsfilters/test-bannertopdf-unicode.c @@ -106,7 +106,7 @@ main(int argc, char *argv[]) { pdfio_file_t *pdf; pdfio_obj_t *page; - pdfio_dict_t *resources, *fonts, *unicode_font; + pdfio_dict_t *resources, *fonts, *unicode_font, *xobjects; char *contents; int status = 1; @@ -137,6 +137,15 @@ main(int argc, char *argv[]) goto done; } + if ((xobjects = get_dict(resources, "XObject")) == NULL || + pdfioDictGetObj(xobjects, "x6") == NULL || + pdfioDictGetObj(xobjects, "x7") == NULL || + pdfioDictGetObj(xobjects, "x8") == NULL) + { + fputs("Template image resources were not preserved.\n", stderr); + goto done; + } + if (get_dict(fonts, "bannertopdf-font") == NULL) { fputs("Courier banner font resource is missing.\n", stderr); @@ -182,6 +191,15 @@ main(int argc, char *argv[]) goto done; } + if (!strstr(contents, "/x6 Do") || + !strstr(contents, "/x7 Do") || + !strstr(contents, "/x8 Do")) + { + fputs("Template image drawing commands were not preserved.\n", stderr); + free(contents); + goto done; + } + if (!strstr(contents, "Office \\(Floor 2\\) \\\\ Queue")) { fputs("ASCII PDF literal string escaping is incorrect.\n", stderr);