Skip to content

Commit 137bfb9

Browse files
authored
Merge pull request #143 from adroitwhiz/gradient-stroke
Transform gradients in strokes
2 parents b1ab4e2 + 414c7d7 commit 137bfb9

File tree

2 files changed

+71
-24
lines changed

2 files changed

+71
-24
lines changed

src/element-categories.js

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
// Elements that can have paint attributes (e.g. fill and stroke)
2+
// Per the SVG spec, things like fill and stroke apply to shapes and text content elements
3+
// https://www.w3.org/TR/SVG11/painting.html#FillProperty
4+
// https://www.w3.org/TR/SVG11/painting.html#StrokeProperty
5+
const PAINTABLE_ELEMENTS = new Set([
6+
// Shape elements (https://www.w3.org/TR/SVG11/intro.html#TermShape)
7+
'path', 'rect', 'circle', 'ellipse', 'line', 'polyline', 'polygon',
8+
// Text content elements (https://www.w3.org/TR/SVG11/intro.html#TermTextContentElement)
9+
// The actual tag names are `altGlyph` and `textPath`, but we're lowercasing the tag name in isContainerElement,
10+
// so they should be lowercased here too.
11+
'altglyph', 'textpath', 'text', 'tref', 'tspan'
12+
]);
13+
14+
// "An element which can have graphics elements and other container elements as child elements."
15+
// https://www.w3.org/TR/SVG11/intro.html#TermContainerElement
16+
const CONTAINER_ELEMENTS = new Set([
17+
'a', 'defs', 'g', 'marker', 'glyph', 'missing-glyph', 'pattern', 'svg', 'switch', 'symbol'
18+
]);
19+
20+
const isPaintableElement = function (element) {
21+
return element.tagName && PAINTABLE_ELEMENTS.has(element.tagName.toLowerCase());
22+
};
23+
const isContainerElement = function (element) {
24+
return element.tagName && CONTAINER_ELEMENTS.has(element.tagName.toLowerCase());
25+
};
26+
27+
module.exports = {
28+
isPaintableElement,
29+
isContainerElement
30+
};

src/transform-applier.js

Lines changed: 41 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
const Matrix = require('transformation-matrix');
22
const SvgElement = require('./svg-element');
3+
const {isPaintableElement, isContainerElement} = require('./element-categories');
34
const log = require('./util/log');
45

56
/**
@@ -288,14 +289,6 @@ const _transformPath = function (pathString, transform) {
288289
return result;
289290
};
290291

291-
const GRAPHICS_ELEMENTS = ['circle', 'ellipse', 'image', 'line', 'path', 'polygon', 'polyline', 'rect', 'text', 'use'];
292-
const CONTAINER_ELEMENTS = ['a', 'defs', 'g', 'marker', 'glyph', 'missing-glyph', 'pattern', 'svg', 'switch', 'symbol'];
293-
const _isContainerElement = function (element) {
294-
return element.tagName && CONTAINER_ELEMENTS.includes(element.tagName.toLowerCase());
295-
};
296-
const _isGraphicsElement = function (element) {
297-
return element.tagName && GRAPHICS_ELEMENTS.includes(element.tagName.toLowerCase());
298-
};
299292
const _isPathWithTransformAndStroke = function (element, strokeWidth) {
300293
if (!element.attributes) return false;
301294
strokeWidth = element.attributes['stroke-width'] ?
@@ -372,6 +365,13 @@ const _createGradient = function (gradientId, svgTag, bbox, matrix) {
372365
const newGradientId = `${gradientId}-${matrixString}`;
373366
newGradient.setAttribute('id', newGradientId);
374367

368+
// This gradient already exists and was transformed before. Just reuse the already-transformed one.
369+
if (svgTag.getElementById(newGradientId)) {
370+
// This is the same code as in the end of the function, but I don't feel like wrapping the next 80 lines
371+
// in an `if (!svgTag.getElementById(newGradientId))` block
372+
return `url(#${newGradientId})`;
373+
}
374+
375375
const scaleToBounds = getValue(newGradient, 'gradientUnits', true) !==
376376
'userSpaceOnUse';
377377
let origin;
@@ -505,14 +505,16 @@ const _parseUrl = (value, windowRef) => {
505505
*/
506506
const transformStrokeWidths = function (svgTag, windowRef, bboxForTesting) {
507507
const inherited = Matrix.identity();
508-
const applyTransforms = (element, matrix, strokeWidth, fill) => {
509-
if (_isContainerElement(element)) {
508+
509+
const applyTransforms = (element, matrix, strokeWidth, fill, stroke) => {
510+
if (isContainerElement(element)) {
510511
// Push fills and stroke width down to leaves
511512
if (element.attributes['stroke-width']) {
512513
strokeWidth = element.attributes['stroke-width'].value;
513514
}
514-
if (element.attributes && element.attributes.fill) {
515-
fill = element.attributes.fill.value;
515+
if (element.attributes) {
516+
if (element.attributes.fill) fill = element.attributes.fill.value;
517+
if (element.attributes.stroke) stroke = element.attributes.stroke.value;
516518
}
517519

518520
// If any child nodes don't take attributes, leave the attributes
@@ -522,30 +524,34 @@ const transformStrokeWidths = function (svgTag, windowRef, bboxForTesting) {
522524
element.childNodes[i],
523525
Matrix.compose(matrix, _parseTransform(element)),
524526
strokeWidth,
525-
fill
527+
fill,
528+
stroke
526529
);
527530
}
528531
element.removeAttribute('transform');
529532
element.removeAttribute('stroke-width');
530533
element.removeAttribute('fill');
534+
element.removeAttribute('stroke');
531535
} else if (_isPathWithTransformAndStroke(element, strokeWidth)) {
532536
if (element.attributes['stroke-width']) {
533537
strokeWidth = element.attributes['stroke-width'].value;
534538
}
535-
if (element.attributes.fill) {
536-
fill = element.attributes.fill.value;
537-
}
539+
if (element.attributes.fill) fill = element.attributes.fill.value;
540+
if (element.attributes.stroke) stroke = element.attributes.stroke.value;
538541
matrix = Matrix.compose(matrix, _parseTransform(element));
539542
if (Matrix.toString(matrix) === Matrix.toString(Matrix.identity())) {
540543
element.removeAttribute('transform');
541544
element.setAttribute('stroke-width', strokeWidth);
542545
if (fill) element.setAttribute('fill', fill);
546+
if (stroke) element.setAttribute('stroke', stroke);
543547
return;
544548
}
545549

546550
// Transform gradient
547-
const gradientId = _parseUrl(fill, windowRef);
548-
if (gradientId) {
551+
const fillGradientId = _parseUrl(fill, windowRef);
552+
const strokeGradientId = _parseUrl(stroke, windowRef);
553+
554+
if (fillGradientId || strokeGradientId) {
549555
const doc = windowRef.document;
550556
// Need path bounds to transform gradient
551557
const svgSpot = doc.createElement('span');
@@ -555,8 +561,8 @@ const transformStrokeWidths = function (svgTag, windowRef, bboxForTesting) {
555561
} else {
556562
try {
557563
doc.body.appendChild(svgSpot);
558-
const svg = SvgElement.set(windowRef.document.createElementNS(SvgElement.svg, 'svg'));
559-
const path = SvgElement.set(windowRef.document.createElementNS(SvgElement.svg, 'path'));
564+
const svg = SvgElement.set(doc.createElementNS(SvgElement.svg, 'svg'));
565+
const path = SvgElement.set(doc.createElementNS(SvgElement.svg, 'path'));
560566
path.setAttribute('d', element.attributes.d.value);
561567
svg.appendChild(path);
562568
svgSpot.appendChild(svg);
@@ -568,8 +574,15 @@ const transformStrokeWidths = function (svgTag, windowRef, bboxForTesting) {
568574
}
569575
}
570576

571-
const newRef = _createGradient(gradientId, svgTag, bbox, matrix);
572-
if (newRef) fill = newRef;
577+
if (fillGradientId) {
578+
const newFillRef = _createGradient(fillGradientId, svgTag, bbox, matrix);
579+
if (newFillRef) fill = newFillRef;
580+
}
581+
582+
if (strokeGradientId) {
583+
const newStrokeRef = _createGradient(strokeGradientId, svgTag, bbox, matrix);
584+
if (newStrokeRef) stroke = newStrokeRef;
585+
}
573586
}
574587

575588
// Transform path data
@@ -580,14 +593,18 @@ const transformStrokeWidths = function (svgTag, windowRef, bboxForTesting) {
580593
const matrixScale = _getScaleFactor(matrix);
581594
element.setAttribute('stroke-width', _quadraticMean(matrixScale.x, matrixScale.y) * strokeWidth);
582595
if (fill) element.setAttribute('fill', fill);
583-
} else if (_isGraphicsElement(element)) {
584-
// Push stroke width and fill down to leaves
596+
if (stroke) element.setAttribute('stroke', stroke);
597+
} else if (isPaintableElement(element)) {
598+
// Push stroke width, fill, and stroke down to leaves
585599
if (strokeWidth && !element.attributes['stroke-width']) {
586600
element.setAttribute('stroke-width', strokeWidth);
587601
}
588602
if (fill && !element.attributes.fill) {
589603
element.setAttribute('fill', fill);
590604
}
605+
if (stroke && !element.attributes.stroke) {
606+
element.setAttribute('stroke', stroke);
607+
}
591608

592609
// Push transform down to leaves
593610
matrix = Matrix.compose(matrix, _parseTransform(element));

0 commit comments

Comments
 (0)