From 100fd9d369daed065dbd30d9cec9edcc32de5719 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20Holl=C3=A4nder?= Date: Tue, 20 Jul 2021 12:20:41 +0200 Subject: [PATCH] fix gradients with zero or one stop (#185) fix #165 --- src/fill/parseFill.ts | 25 ++++++- src/nodes/gradient.ts | 64 +++++++++++------- test/common/tests.js | 1 + .../gradient-stop-defaults/reference.pdf | Bin 0 -> 3947 bytes test/specs/gradient-stop-defaults/spec.svg | 15 ++++ 5 files changed, 80 insertions(+), 25 deletions(-) create mode 100644 test/specs/gradient-stop-defaults/reference.pdf create mode 100644 test/specs/gradient-stop-defaults/spec.svg diff --git a/src/fill/parseFill.ts b/src/fill/parseFill.ts index 1fb4341b..d58535e7 100644 --- a/src/fill/parseFill.ts +++ b/src/fill/parseFill.ts @@ -9,6 +9,7 @@ import { PatternFill } from './PatternFill' import { ColorFill } from './ColorFill' import { RGBColor } from '../utils/rgbcolor' import { parseColor } from '../utils/parsing' +import { Gradient } from '../nodes/gradient' export function parseFill(fill: string, context: Context): Fill | null { const url = iriReference.exec(fill) @@ -16,7 +17,7 @@ export function parseFill(fill: string, context: Context): Fill | null { const fillUrl = url[1] const fillNode = context.refsHandler.get(fillUrl) if (fillNode && (fillNode instanceof LinearGradient || fillNode instanceof RadialGradient)) { - return new GradientFill(fillUrl, fillNode) + return getGradientFill(fillUrl, fillNode, context) } else if (fillNode && fillNode instanceof Pattern) { return new PatternFill(fillUrl, fillNode) } else { @@ -35,3 +36,25 @@ export function parseFill(fill: string, context: Context): Fill | null { } } } + +function getGradientFill(fillUrl: string, gradient: Gradient, context: Context): Fill | null { + // "It is necessary that at least two stops are defined to have a gradient effect. If no stops are + // defined, then painting shall occur as if 'none' were specified as the paint style. If one stop + // is defined, then paint with the solid color fill using the color defined for that gradient + // stop." + const stops = gradient.getStops(context.styleSheets) + if (stops.length === 0) { + return null + } + if (stops.length === 1) { + const stopColor = stops[0].color + const rgbColor = new RGBColor() + rgbColor.ok = true + rgbColor.r = stopColor[0] + rgbColor.g = stopColor[1] + rgbColor.b = stopColor[2] + rgbColor.a = stops[0].opacity + return new ColorFill(rgbColor) + } + return new GradientFill(fillUrl, gradient) +} diff --git a/src/nodes/gradient.ts b/src/nodes/gradient.ts index dc0bde93..659508d1 100644 --- a/src/nodes/gradient.ts +++ b/src/nodes/gradient.ts @@ -7,10 +7,12 @@ import { RGBColor } from '../utils/rgbcolor' import { SvgNode } from './svgnode' import { GState, Matrix, ShadingPattern, ShadingPatternType } from 'jspdf' import { parseColor } from '../utils/parsing' +import { StyleSheets } from '../context/stylesheets' export abstract class Gradient extends NonRenderedNode { private readonly pdfGradientType: ShadingPatternType private contextColor: RGBColor | null | undefined + private stops: StopData[] | undefined protected constructor( pdfGradientType: ShadingPatternType, @@ -28,12 +30,39 @@ export abstract class Gradient extends NonRenderedNode { return } + const colors: StopData[] = this.getStops(context.styleSheets) + let opacitySum = 0 + let hasOpacity = false + let gState + + colors.forEach(({ opacity }) => { + if (opacity && opacity !== 1) { + opacitySum += opacity + hasOpacity = true + } + }) + + if (hasOpacity) { + gState = new GState({ opacity: opacitySum / colors.length }) + } + + const pattern = new ShadingPattern(this.pdfGradientType, this.getCoordinates(), colors, gState) + context.pdf.addShadingPattern(id, pattern) + } + + abstract getCoordinates(): number[] + + public getStops(styleSheets: StyleSheets): StopData[] { + if (this.stops) { + return this.stops + } + // Only need to calculate contextColor once if (this.contextColor === undefined) { this.contextColor = null let ancestor: SvgNode | null = this as SvgNode while (ancestor) { - const colorAttr = getAttribute(ancestor.element, context.styleSheets, 'color') + const colorAttr = getAttribute(ancestor.element, styleSheets, 'color') if (colorAttr) { this.contextColor = parseColor(colorAttr, null) break @@ -42,40 +71,26 @@ export abstract class Gradient extends NonRenderedNode { } } - const colors: StopData[] = [] - let opacitySum = 0 - let hasOpacity = false - let gState - + const stops: StopData[] = [] this.children.forEach(stop => { if (stop.element.tagName.toLowerCase() === 'stop') { - const colorAttr = getAttribute(stop.element, context.styleSheets, 'color') + const colorAttr = getAttribute(stop.element, styleSheets, 'color') const color = parseColor( - getAttribute(stop.element, context.styleSheets, 'stop-color') || '', + getAttribute(stop.element, styleSheets, 'stop-color') || '', colorAttr ? parseColor(colorAttr, null) : (this.contextColor as RGBColor | null) ) - colors.push({ + const opacity = parseFloat(getAttribute(stop.element, styleSheets, 'stop-opacity') || '1') + stops.push({ offset: Gradient.parseGradientOffset(stop.element.getAttribute('offset') || '0'), - color: [color.r, color.g, color.b] + color: [color.r, color.g, color.b], + opacity }) - const opacity = getAttribute(stop.element, context.styleSheets, 'stop-opacity') - if (opacity && opacity !== '1') { - opacitySum += parseFloat(opacity) - hasOpacity = true - } } }) - if (hasOpacity) { - gState = new GState({ opacity: opacitySum / colors.length }) - } - - const pattern = new ShadingPattern(this.pdfGradientType, this.getCoordinates(), colors, gState) - context.pdf.addShadingPattern(id, pattern) + return (this.stops = stops) } - abstract getCoordinates(): number[] - protected getBoundingBoxCore(context: Context): Rect { return defaultBoundingBox(this.element, context) } @@ -98,7 +113,8 @@ export abstract class Gradient extends NonRenderedNode { } } -interface StopData { +export interface StopData { color: number[] + opacity: number offset: number } diff --git a/test/common/tests.js b/test/common/tests.js index c647c379..79783870 100644 --- a/test/common/tests.js +++ b/test/common/tests.js @@ -25,6 +25,7 @@ window.tests = [ 'font-style', 'gradient-default-coordinates', 'gradient-percent-offset', + 'gradient-stop-defaults', 'gradient-units', 'gradients-and-patterns-mixed', 'hidden-clippath', diff --git a/test/specs/gradient-stop-defaults/reference.pdf b/test/specs/gradient-stop-defaults/reference.pdf new file mode 100644 index 0000000000000000000000000000000000000000..1d62047bcb54895f144516d8ac6da68488a50e8f GIT binary patch literal 3947 zcmcIn-EQJW6uzIQI5$yuRRzzCv4JXz6d>$IyH!$%l&aBQ7&Cw)V`pu%P5MlIg1$`q zDE-dZ4h}RwTLrM}GVb%9XXfxw}zaHU||IA znMn##kdB2jHhH?vLX(p&89$p@6!gMQou5&w$(g77ABc-0Ddn6 z3Fos(7R8dFgm{%8a-Y3ADem*@sQMrq|L4-g%6;;*zqjG22ztQcT1z&B)o}d9O;txbkc1%Cly^ z^;}U)(YKz#emY0p-gvWg<;`Wj^;S_!x z*i^zfO~P~*B@1${qU0pWqnaU{QIzGyU>RiOY9K7AUbN>mg8Q!ze%Mdr+1qE(`Hl?E zr*ZUkjotXx)6o5nBzPM3mPqf#L^&htZMM);JE2Zs zZ}YFXp1*z)_Ls-z7M1L5%)(T`lD)I}HO@h^JY^aO2D@Ya)R?6r;tWo`h#n9k5d{(3C zw3PPwgp3Q5k#jN`oSl6%ccpcs1Dns$fXTU*zLdt7Gbv|1qrR(~5Xy1oa^*WHsSqDCZ6q@meKC{z|8xXWL<)vDfr+OJk@#3JNJ-tJd zI-~0%TN~ormqwG&tce#{8cs|>*ACidaxTps8kDmYdgOlf98}LUj>G-S(GdtmyrmIo zq>1+yQ!X*UnQ9Y*kSULs8W-bC;PpJ~6Z_s$B3O-}79L z=2Y{%E+(6Ql+--k^{_&Zc>ppNdW(m4euw9`cX=Iu*S6c+;VG#Bm&Y-b+O<`>yQ|k` z5Nz3cZGdiz=XZDGb{TM6dcECopkD0w!?z1o^RcaVB|y8yL)F;fbzCg;V?9^x#&xv@ z*lWEa3!)f}B5yxiRI$F!I86)Ej4N!9vt*u6(tL^)t8Q*QnUqkrzQ$SY){< LisInl^nCO$b`2rn literal 0 HcmV?d00001 diff --git a/test/specs/gradient-stop-defaults/spec.svg b/test/specs/gradient-stop-defaults/spec.svg new file mode 100644 index 00000000..77c3c0d8 --- /dev/null +++ b/test/specs/gradient-stop-defaults/spec.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + +