Skip to content

Commit

Permalink
Implement currentColor support (yWorks#158)
Browse files Browse the repository at this point in the history
Co-authored-by: Lukas Holländer <[email protected]>
  • Loading branch information
Mrfence97 and HackbrettXXX authored Nov 6, 2020
1 parent 427a884 commit 6c2efd2
Show file tree
Hide file tree
Showing 16 changed files with 136 additions and 15 deletions.
15 changes: 14 additions & 1 deletion src/applyparseattributes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,22 @@ import { findFirstAvailableFontFamily, fontAliases } from './utils/fonts'
import { parseFill } from './fill/parseFill'
import { ColorFill } from './fill/ColorFill'
import { GState } from 'jspdf'
import { RGBColor } from './utils/rgbcolor'

export function parseAttributes(context: Context, svgNode: SvgNode, node?: Element): void {
const domNode = node || svgNode.element
// update color first so currentColor becomes available for this node
const color = getAttribute(domNode, context.styleSheets, 'color')
if (color) {
const fillColor = parseColor(color, context.attributeState.color)
if (fillColor.ok) {
context.attributeState.color = fillColor
} else {
// invalid color passed, reset to black
context.attributeState.color = new RGBColor('rgb(0,0,0)')
}
}

const visibility = getAttribute(domNode, context.styleSheets, 'visibility')
if (visibility) {
context.attributeState.visibility = visibility
Expand Down Expand Up @@ -47,7 +60,7 @@ export function parseAttributes(context: Context, svgNode: SvgNode, node?: Eleme
context.attributeState.stroke = null
} else {
// gradients, patterns not supported for strokes ...
const strokeRGB = parseColor(stroke)
const strokeRGB = parseColor(stroke, context.attributeState.color)
if (strokeRGB.ok) {
context.attributeState.stroke = new ColorFill(strokeRGB)
}
Expand Down
3 changes: 3 additions & 0 deletions src/context/attributestate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export class AttributeState {
public alignmentBaseline = ''
public textAnchor = ''
public visibility = ''
public color: RGBColor | null = null

clone(): AttributeState {
const clone = new AttributeState()
Expand All @@ -51,6 +52,7 @@ export class AttributeState {
clone.textAnchor = this.textAnchor
clone.alignmentBaseline = this.alignmentBaseline
clone.visibility = this.visibility
clone.color = this.color

return clone
}
Expand Down Expand Up @@ -80,6 +82,7 @@ export class AttributeState {
attributeState.alignmentBaseline = 'baseline'
attributeState.textAnchor = 'start'
attributeState.visibility = 'visible'
attributeState.color = new RGBColor('rgb(0, 0, 0)')

return attributeState
}
Expand Down
11 changes: 9 additions & 2 deletions src/context/referenceshandler.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import cssEsc from 'cssesc'
import { SvgNode } from '../nodes/svgnode'
import { RGBColor } from '../utils/rgbcolor'

export class ReferencesHandler {
private readonly renderedElements: { [key: string]: SvgNode }
Expand All @@ -12,14 +13,16 @@ export class ReferencesHandler {

public async getRendered(
id: string,
color: RGBColor | null,
renderCallback: (node: SvgNode) => Promise<void>
): Promise<SvgNode> {
if (this.renderedElements.hasOwnProperty(id)) {
const key = ReferencesHandler.generateKey(id, color)
if (this.renderedElements.hasOwnProperty(key)) {
return this.renderedElements[id]
}

const svgNode: SvgNode = this.get(id)
this.renderedElements[id] = svgNode
this.renderedElements[key] = svgNode

await renderCallback(svgNode)

Expand All @@ -29,4 +32,8 @@ export class ReferencesHandler {
get(id: string): SvgNode {
return this.idMap[cssEsc(id, { isIdentifier: true })]
}

public static generateKey(id: string, color: RGBColor | null): string {
return id + '|' + (color || new RGBColor('rgb(0,0,0)')).toRGBA()
}
}
2 changes: 1 addition & 1 deletion src/fill/GradientFill.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export class GradientFill implements Fill {
}

async getFillData(forNode: GraphicsNode, context: Context): Promise<FillData | undefined> {
await context.refsHandler.getRendered(this.key, node =>
await context.refsHandler.getRendered(this.key, null, node =>
(node as Gradient).apply(
new Context(context.pdf, {
refsHandler: context.refsHandler,
Expand Down
2 changes: 1 addition & 1 deletion src/fill/PatternFill.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export class PatternFill implements Fill {
}

async getFillData(forNode: GraphicsNode, context: Context): Promise<FillData | undefined> {
await context.refsHandler.getRendered(this.key, node =>
await context.refsHandler.getRendered(this.key, null, node =>
(node as Pattern).apply(
new Context(context.pdf, {
refsHandler: context.refsHandler,
Expand Down
2 changes: 1 addition & 1 deletion src/fill/parseFill.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export function parseFill(fill: string, context: Context): Fill | null {
}
} else {
// plain color
const fillColor = parseColor(fill)
const fillColor = parseColor(fill, context.attributeState.color)
if (fillColor.ok) {
return new ColorFill(fillColor)
} else if (fill === 'none') {
Expand Down
4 changes: 3 additions & 1 deletion src/markerlist.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,9 @@ export class MarkerList {
// as the marker is already scaled by the current line width we must not apply the line width twice!
context.pdf.saveGraphicsState()
context.pdf.setLineWidth(1.0)
await context.refsHandler.getRendered(marker.id, node => (node as MarkerNode).apply(context))
await context.refsHandler.getRendered(marker.id, null, node =>
(node as MarkerNode).apply(context)
)
context.pdf.doFormObject(marker.id, tf)
context.pdf.restoreGraphicsState()
}
Expand Down
23 changes: 22 additions & 1 deletion src/nodes/gradient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@ import { Rect } from '../utils/geometry'
import { RGBColor } from '../utils/rgbcolor'
import { SvgNode } from './svgnode'
import { GState, Matrix, ShadingPattern, ShadingPatternType } from 'jspdf'
import { parseColor } from '../utils/parsing'

export abstract class Gradient extends NonRenderedNode {
private readonly pdfGradientType: ShadingPatternType
private contextColor: RGBColor | null | undefined

protected constructor(
pdfGradientType: ShadingPatternType,
Expand All @@ -17,6 +19,7 @@ export abstract class Gradient extends NonRenderedNode {
) {
super(element, children)
this.pdfGradientType = pdfGradientType
this.contextColor = undefined
}

async apply(context: Context): Promise<void> {
Expand All @@ -25,14 +28,32 @@ export abstract class Gradient extends NonRenderedNode {
return
}

// 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')
if (colorAttr) {
this.contextColor = parseColor(colorAttr, null)
break
}
ancestor = ancestor.getParent()
}
}

const colors: StopData[] = []
let opacitySum = 0
let hasOpacity = false
let gState

this.children.forEach(stop => {
if (stop.element.tagName.toLowerCase() === 'stop') {
const color = new RGBColor(getAttribute(stop.element, context.styleSheets, 'stop-color'))
const colorAttr = getAttribute(stop.element, context.styleSheets, 'color')
const color = parseColor(
getAttribute(stop.element, context.styleSheets, 'stop-color') || '',
colorAttr ? parseColor(colorAttr, null) : (this.contextColor as RGBColor | null)
)
colors.push({
offset: Gradient.parseGradientOffset(stop.element.getAttribute('offset') || '0'),
color: [color.r, color.g, color.b]
Expand Down
10 changes: 10 additions & 0 deletions src/nodes/svgnode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,20 @@ import { Matrix } from 'jspdf'
export abstract class SvgNode {
readonly element: Element
readonly children: SvgNode[]
protected parent: SvgNode | null

constructor(element: Element, children: SvgNode[]) {
this.element = element
this.children = children
this.parent = null
}

setParent(parent: SvgNode): void {
this.parent = parent
}

getParent(): SvgNode | null {
return this.parent
}

abstract render(parentContext: Context): Promise<void>
Expand Down
20 changes: 16 additions & 4 deletions src/nodes/use.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import { parseFloats } from '../utils/parsing'
import { SvgNode } from './svgnode'
import { Symbol } from './symbol'
import { Viewport } from '../context/viewport'
import { RGBColor } from '../utils/rgbcolor'
import { ReferencesHandler } from '../context/referenceshandler'

/**
* Draws the element referenced by a use node, makes use of pdf's XObjects/FormObjects so nodes are only written once
Expand Down Expand Up @@ -67,7 +69,10 @@ export class Use extends GraphicsNode {
viewport: refNodeOpensViewport ? new Viewport(width!, height!) : context.viewport,
svg2pdfParameters: context.svg2pdfParameters
})
await context.refsHandler.getRendered(id, node => Use.renderReferencedNode(node, refContext))
const color = context.attributeState.color
await context.refsHandler.getRendered(id, color, node =>
Use.renderReferencedNode(node, id, color, refContext)
)

context.pdf.saveGraphicsState()
context.pdf.setCurrentTransformationMatrix(context.transform)
Expand All @@ -81,11 +86,16 @@ export class Use extends GraphicsNode {
context.pdf.clip().discardPath()
}

context.pdf.doFormObject(id, t)
context.pdf.doFormObject(ReferencesHandler.generateKey(id, color), t)
context.pdf.restoreGraphicsState()
}

private static async renderReferencedNode(node: SvgNode, refContext: Context): Promise<void> {
private static async renderReferencedNode(
node: SvgNode,
id: string,
color: RGBColor | null,
refContext: Context
): Promise<void> {
let bBox = node.getBoundingBox(refContext)

// The content of a PDF form object is implicitly clipped at its /BBox property.
Expand All @@ -94,13 +104,15 @@ export class Use extends GraphicsNode {
// still within.
bBox = [bBox[0] - 0.5 * bBox[2], bBox[1] - 0.5 * bBox[3], bBox[2] * 2, bBox[3] * 2]

// set the color to use for the referenced node
refContext.attributeState.color = color
refContext.pdf.beginFormObject(bBox[0], bBox[1], bBox[2], bBox[3], refContext.pdf.unitMatrix)
if (node instanceof Symbol) {
await node.apply(refContext)
} else {
await node.render(refContext)
}
refContext.pdf.endFormObject(node.element.getAttribute('id'))
refContext.pdf.endFormObject(ReferencesHandler.generateKey(id, color))
}

protected getBoundingBoxCore(context: Context): number[] {
Expand Down
2 changes: 2 additions & 0 deletions src/parse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,5 +93,7 @@ export function parse(node: Element, idMap?: { [id: string]: SvgNode }): SvgNode
idMap[id] = idMap[id] || svgnode
}

svgnode.children.forEach(c => c.setParent(svgnode))

return svgnode
}
11 changes: 9 additions & 2 deletions src/utils/parsing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,21 @@ export function parseFloats(str: string): number[] {
return floats
}

// extends RGBColor by rgba colors as RGBColor is not capable of it
export function parseColor(colorString: string): RGBColor {
/**
* extends RGBColor by rgba colors as RGBColor is not capable of it
* currentcolor: the color to return if colorString === 'currentcolor'
*/
export function parseColor(colorString: string, currentcolor: RGBColor | null): RGBColor {
if (colorString === 'transparent') {
const transparent = new RGBColor('rgb(0,0,0)')
transparent.a = 0
return transparent
}

if (colorString.toLowerCase() === 'currentcolor') {
return currentcolor || new RGBColor('rgb(0,0,0)')
}

const match = /\s*rgba\(((?:[^,\)]*,){3}[^,\)]*)\)\s*/.exec(colorString)
if (match) {
const floats = parseFloats(match[1])
Expand Down
3 changes: 3 additions & 0 deletions src/utils/rgbcolor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,9 @@ export class RGBColor {
toRGB() {
return 'rgb(' + this.r + ', ' + this.g + ', ' + this.b + ')'
}
toRGBA() {
return 'rgba(' + this.r + ', ' + this.g + ', ' + this.b + ', ' + (this.a || '1') + ')'
}
toHex() {
let r = this.r.toString(16)
let g = this.g.toString(16)
Expand Down
3 changes: 2 additions & 1 deletion test/common/tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,5 +59,6 @@ window.tests = [
'url-references-with-quotes',
'vertical-align',
'xml-space',
'zero-width-strokes'
'zero-width-strokes',
'current-color'
]
Binary file added test/specs/current-color/reference.pdf
Binary file not shown.
40 changes: 40 additions & 0 deletions test/specs/current-color/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 6c2efd2

Please sign in to comment.