Skip to content

Commit

Permalink
fix gradients with zero or one stop (yWorks#185)
Browse files Browse the repository at this point in the history
  • Loading branch information
HackbrettXXX authored Jul 20, 2021
1 parent d1bc8a4 commit 100fd9d
Show file tree
Hide file tree
Showing 5 changed files with 80 additions and 25 deletions.
25 changes: 24 additions & 1 deletion src/fill/parseFill.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,15 @@ 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)
if (url) {
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 {
Expand All @@ -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)
}
64 changes: 40 additions & 24 deletions src/nodes/gradient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
Expand All @@ -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)
}
Expand All @@ -98,7 +113,8 @@ export abstract class Gradient extends NonRenderedNode {
}
}

interface StopData {
export interface StopData {
color: number[]
opacity: number
offset: number
}
1 change: 1 addition & 0 deletions test/common/tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
Binary file added test/specs/gradient-stop-defaults/reference.pdf
Binary file not shown.
15 changes: 15 additions & 0 deletions test/specs/gradient-stop-defaults/spec.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit 100fd9d

Please sign in to comment.