From 5fad91a680fb1da4fb27d0201e1dd62fc5341563 Mon Sep 17 00:00:00 2001 From: Ben Wagner Date: Fri, 3 Mar 2023 11:41:04 -0500 Subject: [PATCH] Better approximate gradient color stops PDF gradients do not have color stops but an arbitrary PDF function of the type f(t) -> color. CSS gradients are only based on color stops. Most PDF gradient functions are produced from color stop oriented gradients. Take advantage of this by sampling the PDF function at a higher frequency but not converting any samples which could be interpolated to color stops. The sampling frequency is chosen to be the least common multiple of as many values as practical to exactly re-create the common case of the PDF function implementing equally spaced linearly interpolated stops in RGB color space. This also allows for better approximation of other smooth PDF functions (non-linear, or non-equally spaced, or in different color space). Fixes: #10572, #14165 --- src/core/pattern.js | 80 ++++++++- test/pdfs/.gitignore | 2 + test/pdfs/issue10572.pdf | 374 +++++++++++++++++++++++++++++++++++++++ test/pdfs/issue14165.pdf | 336 +++++++++++++++++++++++++++++++++++ test/test_manifest.json | 12 ++ 5 files changed, 797 insertions(+), 7 deletions(-) create mode 100644 test/pdfs/issue10572.pdf create mode 100644 test/pdfs/issue14165.pdf diff --git a/src/core/pattern.js b/src/core/pattern.js index ad89e4fb7..90c00ff50 100644 --- a/src/core/pattern.js +++ b/src/core/pattern.js @@ -160,10 +160,9 @@ class RadialAxialShading extends BaseShading { const fnObj = dict.getRaw("Function"); const fn = pdfFunctionFactory.createFromArray(fnObj); - // 10 samples seems good enough for now, but probably won't work - // if there are sharp color changes. Ideally, we would implement - // the spec faithfully and add lossless optimizations. - const NUMBER_OF_SAMPLES = 10; + // Use lcm(1,2,3,4,5,6,7,8,10) = 840 (including 9 increases this to 2520) + // to catch evenly spaced stops. oeis.org/A003418 + const NUMBER_OF_SAMPLES = 840; const step = (t1 - t0) / NUMBER_OF_SAMPLES; const colorStops = (this.colorStops = []); @@ -179,13 +178,80 @@ class RadialAxialShading extends BaseShading { const color = new Float32Array(cs.numComps), ratio = new Float32Array(1); let rgbColor; - for (let i = 0; i <= NUMBER_OF_SAMPLES; i++) { + + let iBase = 0; + ratio[0] = t0; + fn(ratio, 0, color, 0); + let rgbBase = cs.getRgb(color, 0); + const cssColorBase = Util.makeHexColor(rgbBase[0], rgbBase[1], rgbBase[2]); + colorStops.push([0, cssColorBase]); + + let iPrev = 1; + ratio[0] = t0 + step; + fn(ratio, 0, color, 0); + let rgbPrev = cs.getRgb(color, 0); + + // Slopes are rise / run. + // A max slope is from the least value the base component could have been + // to the greatest value the current component could have been. + // A min slope is from the greatest value the base component could have been + // to the least value the current component could have been. + // Each component could have been rounded up to .5 from its original value + // so the conservative deltas are +-1 (+-.5 for base and -+.5 for current). + + // The run is iPrev - iBase = 1, so omitted. + let maxSlopeR = rgbPrev[0] - rgbBase[0] + 1; + let maxSlopeG = rgbPrev[1] - rgbBase[1] + 1; + let maxSlopeB = rgbPrev[2] - rgbBase[2] + 1; + let minSlopeR = rgbPrev[0] - rgbBase[0] - 1; + let minSlopeG = rgbPrev[1] - rgbBase[1] - 1; + let minSlopeB = rgbPrev[2] - rgbBase[2] - 1; + + for (let i = 2; i < NUMBER_OF_SAMPLES; i++) { ratio[0] = t0 + i * step; fn(ratio, 0, color, 0); rgbColor = cs.getRgb(color, 0); - const cssColor = Util.makeHexColor(rgbColor[0], rgbColor[1], rgbColor[2]); - colorStops.push([i / NUMBER_OF_SAMPLES, cssColor]); + + // Keep going if the maximum minimum slope <= the minimum maximum slope. + // Otherwise add a rgbPrev color stop and make it the new base. + + const run = i - iBase; + maxSlopeR = Math.min(maxSlopeR, (rgbColor[0] - rgbBase[0] + 1) / run); + maxSlopeG = Math.min(maxSlopeG, (rgbColor[1] - rgbBase[1] + 1) / run); + maxSlopeB = Math.min(maxSlopeB, (rgbColor[2] - rgbBase[2] + 1) / run); + minSlopeR = Math.max(minSlopeR, (rgbColor[0] - rgbBase[0] - 1) / run); + minSlopeG = Math.max(minSlopeG, (rgbColor[1] - rgbBase[1] - 1) / run); + minSlopeB = Math.max(minSlopeB, (rgbColor[2] - rgbBase[2] - 1) / run); + + const slopesExist = + minSlopeR <= maxSlopeR && + minSlopeG <= maxSlopeG && + minSlopeB <= maxSlopeB; + + if (!slopesExist) { + const cssColor = Util.makeHexColor(rgbPrev[0], rgbPrev[1], rgbPrev[2]); + colorStops.push([iPrev / NUMBER_OF_SAMPLES, cssColor]); + + // TODO: When fn frequency is high (iPrev - iBase === 1 twice in a row), + // send the color space and function to do the sampling display side. + + // The run is i - iPrev = 1, so omitted. + maxSlopeR = rgbColor[0] - rgbPrev[0] + 1; + maxSlopeG = rgbColor[1] - rgbPrev[1] + 1; + maxSlopeB = rgbColor[2] - rgbPrev[2] + 1; + minSlopeR = rgbColor[0] - rgbPrev[0] - 1; + minSlopeG = rgbColor[1] - rgbPrev[1] - 1; + minSlopeB = rgbColor[2] - rgbPrev[2] - 1; + + iBase = iPrev; + rgbBase = rgbPrev; + } + + iPrev = i; + rgbPrev = rgbColor; } + const cssColor = Util.makeHexColor(rgbPrev[0], rgbPrev[1], rgbPrev[2]); + colorStops.push([1, cssColor]); let background = "transparent"; if (dict.has("Background")) { diff --git a/test/pdfs/.gitignore b/test/pdfs/.gitignore index 86847a119..037fe8c2b 100644 --- a/test/pdfs/.gitignore +++ b/test/pdfs/.gitignore @@ -341,6 +341,7 @@ !issue12810.pdf !bug866395.pdf !issue12010_reduced.pdf +!issue10572.pdf !issue11718_reduced.pdf !bug1027533.pdf !bug1028735.pdf @@ -529,6 +530,7 @@ !issue15012.pdf !issue15150.pdf !poppler-395-0-fuzzed.pdf +!issue14165.pdf !GHOSTSCRIPT-698804-1-fuzzed.pdf !issue14814.pdf !poppler-91414-0-53.pdf diff --git a/test/pdfs/issue10572.pdf b/test/pdfs/issue10572.pdf new file mode 100644 index 000000000..5ff34d707 --- /dev/null +++ b/test/pdfs/issue10572.pdf @@ -0,0 +1,374 @@ +%PDF-1.5 +% +%QDF-1.0 + +%% Original object ID: 20 0 +1 0 obj +<< + /Pages 3 0 R + /Type /Catalog +>> +endobj + +%% Original object ID: 19 0 +2 0 obj +<< + /Creator (cairo 1.9.5 \(http://cairographics.org\)) + /Producer (cairo 1.9.5 \(http://cairographics.org\)) +>> +endobj + +%% Original object ID: 1 0 +3 0 obj +<< + /Count 1 + /Kids [ + 4 0 R + ] + /Type /Pages +>> +endobj + +%% Page 1 +%% Original object ID: 7 0 +4 0 obj +<< + /Contents 5 0 R + /Group << + /CS /DeviceRGB + /S /Transparency + /Type /Group + >> + /MediaBox [ + 0 + 0 + 612 + 792 + ] + /Parent 3 0 R + /Resources 7 0 R + /Type /Page +>> +endobj + +%% Contents for page 1 +%% Original object ID: 3 0 +5 0 obj +<< + /Length 6 0 R +>> +stream +q +Q q +54 738 504 -661.699 re W n +1 1 1 rg /a0 gs +54 738 504 -661.699 re f +Q q +61 716 225 -450 re W n +q /Pattern cs /p5 scn /a0 gs +61 716 225 -450 re f +Q +Q q +54 738 504 -661.699 re W n +0 0 0 RG /a0 gs +1 w +0 J +0 j +[] 0.0 d +10 M 60.5 716.5 226 -451 re S +56 736 424 -23 re W n +Q q +Q +endstream +endobj + +6 0 obj +279 +endobj + +%% Original object ID: 2 0 +7 0 obj +<< + /ExtGState << + /a0 << + /CA 1 + /ca 1 + >> + >> + /Pattern << + /p5 9 0 R + >> +>> +endobj + +%% Original object ID: 6 0 +8 0 obj +<< +>> +endobj + +%% Original object ID: 5 0 +9 0 obj +<< + /Matrix [ + 1 + 0 + 0 + -1 + 61 + 716 + ] + /PatternType 2 + /Shading << + /ColorSpace /DeviceRGB + /Coords [ + 112.5 + -900 + 112.5 + 900 + ] + /Domain [ + -6 + 6 + ] + /Extend [ + false + false + ] + /Function 13 0 R + /ShadingType 2 + >> + /Type /Pattern +>> +endobj + +%% Original object ID: 18 0 +10 0 obj +<< +>> +endobj + +%% Original object ID: 15 0 +11 0 obj +<< + /Length 12 0 R +>> +stream +endstream +endobj + +12 0 obj +0 +endobj + +%% Original object ID: 12 0 +13 0 obj +<< + /Bounds [ + -5 + -4 + -3 + -2 + -1 + 0 + 1 + 2 + 3 + 4 + 5 + ] + /Domain [ + -6 + 6 + ] + /Encode [ + 0 + 1 + 0 + 1 + 0 + 1 + 0 + 1 + 0 + 1 + 0 + 1 + 0 + 1 + 0 + 1 + 0 + 1 + 0 + 1 + 0 + 1 + 0 + 1 + ] + /FunctionType 3 + /Functions [ + 15 0 R + 15 0 R + 15 0 R + 15 0 R + 15 0 R + 15 0 R + 15 0 R + 15 0 R + 15 0 R + 15 0 R + 15 0 R + 15 0 R + ] +>> +endobj + +%% Original object ID: 17 0 +14 0 obj +<< +>> +endobj + +%% Original object ID: 11 0 +15 0 obj +<< + /Bounds [ + 0.5 + 0.5 + ] + /Domain [ + 0 + 1 + ] + /Encode [ + 0 + 1 + 0 + 1 + 0 + 1 + ] + /FunctionType 3 + /Functions [ + 18 0 R + 19 0 R + 20 0 R + ] +>> +endobj + +%% Original object ID: 13 0 +16 0 obj +<< + /Length1 9948 + /Length 17 0 R +>> +stream +endstream +endobj + +%QDF: ignore_newline +17 0 obj +0 +endobj + +%% Original object ID: 8 0 +18 0 obj +<< + /C0 [ + 0 + 1 + 0 + ] + /C1 [ + 0 + 1 + 0 + ] + /Domain [ + 0 + 1 + ] + /FunctionType 2 + /N 1 +>> +endobj + +%% Original object ID: 9 0 +19 0 obj +<< + /C0 [ + 0 + 1 + 0 + ] + /C1 [ + 0 + 0 + 1 + ] + /Domain [ + 0 + 1 + ] + /FunctionType 2 + /N 1 +>> +endobj + +%% Original object ID: 10 0 +20 0 obj +<< + /C0 [ + 0 + 0 + 1 + ] + /C1 [ + 0 + 0 + 1 + ] + /Domain [ + 0 + 1 + ] + /FunctionType 2 + /N 1 +>> +endobj + +xref +0 21 +0000000000 65535 f +0000000053 00000 n +0000000135 00000 n +0000000293 00000 n +0000000402 00000 n +0000000661 00000 n +0000000995 00000 n +0000001042 00000 n +0000001187 00000 n +0000001236 00000 n +0000001608 00000 n +0000001659 00000 n +0000001716 00000 n +0000001763 00000 n +0000002259 00000 n +0000002310 00000 n +0000002543 00000 n +0000002637 00000 n +0000002683 00000 n +0000002846 00000 n +0000003010 00000 n +trailer << + /Info 2 0 R + /Root 1 0 R + /Size 21 + /ID [<68de518de1319b4dc1c1bfbdee95b02c><68de518de1319b4dc1c1bfbdee95b02c>] +>> +startxref +3146 +%%EOF diff --git a/test/pdfs/issue14165.pdf b/test/pdfs/issue14165.pdf new file mode 100644 index 000000000..9d963ee44 --- /dev/null +++ b/test/pdfs/issue14165.pdf @@ -0,0 +1,336 @@ +%PDF-1.5 +% +%QDF-1.0 + +%% Original object ID: 1 0 +1 0 obj +<< + /Lang (de-DE) + /MarkInfo << + /Marked true + >> + /Pages 3 0 R + /StructTreeRoot 5 0 R + /Type /Catalog +>> +endobj + +%% Original object ID: 11 0 +2 0 obj +<< + /Author (Kinski, Andreas) + /CreationDate (D:20210930092306+02'00') + /Creator + /ModDate (D:20210930092306+02'00') + /Producer +>> +endobj + +%% Original object ID: 2 0 +3 0 obj +<< + /Count 1 + /Kids [ + 13 0 R + ] + /Type /Pages +>> +endobj + +4 0 obj +<< + /Type /ObjStm + /Length 1320 + /N 8 + /First 110 +>> +stream +5 0 +6 171 +7 531 +8 628 +9 779 +10 908 +11 999 +12 1135 +%% Object stream: object 5, index 0; original object ID: 12 +<< + /K [ + 8 0 R + ] + /ParentTree 7 0 R + /ParentTreeNextKey 1 + /RoleMap 6 0 R + /Type /StructTreeRoot +>> +%% Object stream: object 6, index 1; original object ID: 13 +<< + /Annotation /Sect + /Artifact /Sect + /Chart /Sect + /Chartsheet /Part + /Diagram /Figure + /Dialogsheet /Part + /Endnote /Note + /Footer /Sect + /Footnote /Note + /Header /Sect + /InlineShape /Sect + /Macrosheet /Part + /Slide /Part + /Textbox /Sect + /Workbook /Document + /Worksheet /Part +>> +%% Object stream: object 7, index 2; original object ID: 14 +<< + /Nums [ + 0 + 10 0 R + ] +>> +%% Object stream: object 8, index 3; original object ID: 15 +<< + /K [ + 9 0 R + 11 0 R + 12 0 R + ] + /P 5 0 R + /S /Part + /Type /StructElem +>> +%% Object stream: object 9, index 4; original object ID: 16 +<< + /K 0 + /P 8 0 R + /Pg 13 0 R + /S /Span + /Type /StructElem +>> +%% Object stream: object 10, index 5; original object ID: 17 +[ + 9 0 R + 11 0 R + 12 0 R +] +%% Object stream: object 11, index 6; original object ID: 19 +<< + /K [ + 1 + ] + /P 8 0 R + /Pg 13 0 R + /S /P + /Type /StructElem +>> +%% Object stream: object 12, index 7; original object ID: 20 +<< + /K [ + 2 + ] + /P 8 0 R + /Pg 13 0 R + /S /P + /Type /StructElem +>> +endstream +endobj + +%% Page 1 +%% Original object ID: 3 0 +13 0 obj +<< + /Contents 14 0 R + /Group << + /CS /DeviceRGB + /S /Transparency + /Type /Group + >> + /MediaBox [ + 0 + 0 + 1190.52 + 841.92 + ] + /Parent 3 0 R + /Resources << + /ExtGState << + /GS10 16 0 R + /GS5 17 0 R + >> + /Pattern << + /P7 19 0 R + >> + /ProcSet [ + /PDF + /Text + /ImageB + /ImageC + /ImageI + ] + >> + /StructParents 0 + /Tabs /S + /Type /Page +>> +endobj + +%% Contents for page 1 +%% Original object ID: 4 0 +14 0 obj +<< + /Length 15 0 R +>> +stream + /P <> BDC q +0 0.000061035 1190.52 841.92 re +W* n +/GS5 gs +/Pattern cs /P7 scn +0 -0.22998 1191.1 842.15 re +f* +Q + EMC /P <> BDC q +0.00001774 0 1190.52 841.92 re +W* n +BT +Q + EMC +endstream +endobj + +%QDF: ignore_newline +15 0 obj +193 +endobj + +%% Original object ID: 10 0 +16 0 obj +<< + /BM /Normal + /CA 1 + /Type /ExtGState +>> +endobj + +%% Original object ID: 5 0 +17 0 obj +<< + /BM /Normal + /Type /ExtGState + /ca 1 +>> +endobj + +%QDF: ignore_newline +18 0 obj +552604 +endobj + +%% Original object ID: 7 0 +19 0 obj +<< + /PatternType 2 + /Shading << + /ColorSpace /DeviceRGB + /Coords [ + 595.56 + 888 + 595.56 + -33.6 + ] + /Extend [ + true + true + ] + /Function 22 0 R + /ShadingType 2 + >> +>> +endobj + +%QDF: ignore_newline +20 0 obj +552604 +endobj + +%QDF: ignore_newline +21 0 obj +552604 +endobj + +%% Original object ID: 6 0 +22 0 obj +<< + /BitsPerSample 8 + /Decode [ + 0 + 1 + 0 + 1 + 0 + 1 + ] + /Domain [ + 0 + 1 + ] + /Encode [ + 0 + 5119 + ] + /FunctionType 0 + /Order 1 + /Range [ + 0 + 1 + 0 + 1 + 0 + 1 + ] + /Size [ + 5120 + ] + /Length 23 0 R +>> +stream +~~~~~~}}}}}}}}}|||||||||{{{{{{{{{zzzzzzzzzyyyyyyyyyxxxxxxxxxxxxwwwwwwwwwwwwvvvvvvvvvvvvvvvvvvwwwwwwwwwwwwxxxxxxxxxxxxyyyyyyyyyzzzzzzzzz{{{{{{{{{|||||||||}}}}}}}}}~~~~~~~~~~~~}}}}}}}}}|||||||||{{{{{{{{{zzzzzzzzzyyyyyyyyyxxxxxxxxxxxxwwwwwwwwwwwwvvvvvvvvvvvvvvvvvvwwwwwwwwwwwwxxxxxxxxxxxxyyyyyyyyyzzzzzzzzz{{{{{{{{{|||||||||}}}}}}}}}~~~~~~~~~~~~}}}}}}}}}|||||||||{{{{{{{{{zzzzzzzzzyyyyyyyyyxxxxxxxxxxxxwwwwwwwwwwwwvvvvvvvvvvvvvvvvvvwwwwwwwwwwwwxxxxxxxxxxxxyyyyyyyyyzzzzzzzzz{{{{{{{{{|||||||||}}}}}}}}}~~~~~~~~~~~~}}}}}}}}}|||||||||{{{{{{{{{zzzzzzzzzyyyyyyyyyxxxxxxxxxxxxwwwwwwwwwwwwvvvvvvvvvvvvvvvvvvwwwwwwwwwwwwxxxxxxxxxxxxyyyyyyyyyzzzzzzzzz{{{{{{{{{|||||||||}}}}}}}}}~~~~~~~~~~~~}}}}}}}}}|||||||||{{{{{{{{{zzzzzzzzzyyyyyyyyyxxxxxxxxxxxxwwwwwwwwwwwwvvvvvvvvvvvvvvvvvvwwwwwwwwwwwwxxxxxxxxxxxxyyyyyyyyyzzzzzzzzz{{{{{{{{{|||||||||}}}}}}}}}~~~~~~~~~~~~}}}}}}}}}|||||||||{{{{{{{{{zzzzzzzzzyyyyyyyyyxxxxxxxxxxxxwwwwwwwwwwwwvvvvvvvvvvvvvvvvvvwwwwwwwwwwwwxxxxxxxxxxxxyyyyyyyyyzzzzzzzzz{{{{{{{{{|||||||||}}}}}}}}}~~~~~~~~~~~~}}}}}}}}}|||||||||{{{{{{{{{zzzzzzzzzyyyyyyyyyxxxxxxxxxxxxwwwwwwwwwwwwvvvvvvvvvvvvvvvvvvwwwwwwwwwwwwxxxxxxxxxxxxyyyyyyyyyzzzzzzzzz{{{{{{{{{|||||||||}}}}}}}}}~~~~~~~~~~~~}}}}}}}}}|||||||||{{{{{{{{{zzzzzzzzzyyyyyyyyyxxxxxxxxxxxxwwwwwwwwwwwwvvvvvvvvvvvvvvvvvvwwwwwwwwwwwwxxxxxxxxxxxxyyyyyyyyyzzzzzzzzz{{{{{{{{{|||||||||}}}}}}}}}~~~~~~~~~~~~}}}}}}}}}|||||||||{{{{{{{{{zzzzzzzzzyyyyyyyyyxxxxxxxxxxxxwwwwwwwwwwwwvvvvvvvvvvvvvvvvvvwwwwwwwwwwwwxxxxxxxxxxxxyyyyyyyyyzzzzzzzzz{{{{{{{{{|||||||||}}}}}}}}}~~~~~~~~~~~~}}}}}}}}}|||||||||{{{{{{{{{zzzzzzzzzyyyyyyyyyxxxxxxxxxxxxwwwwwwwwwwwwvvvvvvvvvvvvvvvvvvwwwwwwwwwwwwxxxxxxxxxxxxyyyyyyyyyzzzzzzzzz{{{{{{{{{|||||||||}}}}}}}}}~~~~~~ +endstream +endobj + +%QDF: ignore_newline +23 0 obj +15360 +endobj + +%QDF: ignore_newline +24 0 obj +552604 +endobj + +%QDF: ignore_newline +25 0 obj +552604 +endobj + +26 0 obj +<< + /Type /XRef + /Length 108 + /W [ 1 2 1 ] + /Info 2 0 R + /Root 1 0 R + /Size 27 + /ID [<1f73712092d451469489d26e4b30a56f>] +>> +stream +44}$ +   M   0 1 ^ JJK"K: +endstream +endobj + +startxref +19258 +%%EOF diff --git a/test/test_manifest.json b/test/test_manifest.json index 7daa6caeb..44e260304 100644 --- a/test/test_manifest.json +++ b/test/test_manifest.json @@ -5376,6 +5376,12 @@ "enableXfa": true, "type": "eq" }, + { "id": "issue14165", + "file": "pdfs/issue14165.pdf", + "md5": "917850af2a387475b34b00010200897d", + "rounds": 1, + "type": "eq" + }, { "id": "scorecard_reduced", "file": "pdfs/scorecard_reduced.pdf", "md5": "aa8ed0827092c963eea64adb718a3806", @@ -6251,6 +6257,12 @@ } } }, + { "id": "issue10572", + "file": "pdfs/issue10572.pdf", + "md5": "48ad69ed106338b3c6845fc7101488b2", + "rounds": 1, + "type": "eq" + }, { "id": "issue11931", "file": "pdfs/issue11931.pdf", "md5": "9ea233037992e1f10280420a49e72845",