From d4ca220c4825ab41207ea060935ca730e32a6ee3 Mon Sep 17 00:00:00 2001 From: Valentin Ionita Date: Fri, 28 Jun 2019 08:01:03 +0000 Subject: hatch.js Polyfill to render hatches by converting paths to patterns. Used standardjs for linting, but kept the semicolons. Part of 2019 GSoC project. --- src/extension/internal/polyfill/README.md | 5 +- src/extension/internal/polyfill/hatch.js | 400 + .../internal/polyfill/hatch_compressed.include | 4 + .../internal/polyfill/hatch_tests/hatch.svg | 63 + .../polyfill/hatch_tests/hatch01_with_js.svg | 134 + .../internal/polyfill/hatch_tests/hatch_test.svg | 11731 +++++++++++++++++++ src/extension/internal/svg.cpp | 61 +- src/ui/dialog/inkscape-preferences.cpp | 8 +- 8 files changed, 12395 insertions(+), 11 deletions(-) create mode 100644 src/extension/internal/polyfill/hatch.js create mode 100644 src/extension/internal/polyfill/hatch_compressed.include create mode 100644 src/extension/internal/polyfill/hatch_tests/hatch.svg create mode 100644 src/extension/internal/polyfill/hatch_tests/hatch01_with_js.svg create mode 100644 src/extension/internal/polyfill/hatch_tests/hatch_test.svg (limited to 'src') diff --git a/src/extension/internal/polyfill/README.md b/src/extension/internal/polyfill/README.md index adfc31555..2677a504b 100644 --- a/src/extension/internal/polyfill/README.md +++ b/src/extension/internal/polyfill/README.md @@ -1,4 +1,4 @@ -# Gradient Mesh JavaScript polyfill +# JavaScript polyfills This directory contains JavaScript "Polyfills" to support rendering of SVG 2 features that are not well supported by browsers, but appeared in the 2016 @@ -7,6 +7,9 @@ features that are not well supported by browsers, but appeared in the 2016 The included files are: - `mesh.js` mesh gradients supporting bicubic meshes and mesh on strokes. - `mesh_compressed.include` mesh.js minified and wrapped as a C++11 raw string literal. + - `hatch.js` hatch paint server supporting linear and absolute paths hatches + (relative paths are not fully supported) + - `hatch_tests` folder with tests used for `hatch.js` rendering ## Details The coding standard used is [semistandard](https://github.com/Flet/semistandard), diff --git a/src/extension/internal/polyfill/hatch.js b/src/extension/internal/polyfill/hatch.js new file mode 100644 index 000000000..db3d88c4f --- /dev/null +++ b/src/extension/internal/polyfill/hatch.js @@ -0,0 +1,400 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Use patterns to render a hatch paint server via this polyfill + *//* + * Copyright (C) 2019 Valentin Ionita + * Distributed under GNU General Public License version 2 or later. See . + */ + +(function () { + // Name spaces ----------------------------------- + const svgNS = 'http://www.w3.org/2000/svg'; + const xlinkNS = 'http://www.w3.org/1999/xlink'; + const unitObjectBoundingBox = 'objectBoundingBox'; + const unitUserSpace = 'userSpaceOnUse'; + + // Set multiple attributes to an element + const setAttributes = (el, attrs) => { + for (let key in attrs) { + el.setAttribute(key, attrs[key]); + } + }; + + // Copy attributes from the hatch with 'id' to the current element + const setReference = (el, id) => { + const attr = [ + 'x', 'y', 'pitch', 'rotate', + 'hatchUnits', 'hatchContentUnits', 'transform' + ]; + const template = document.getElementById(id.slice(1)); + + if (template && template.nodeName === 'hatch') { + attr.forEach(a => { + let t = template.getAttribute(a); + if (el.getAttribute(a) === null && t !== null) { + el.setAttribute(a, t); + } + }); + + if (el.children.length === 0) { + Array.from(template.children).forEach(c => { + el.appendChild(c.cloneNode(true)); + }); + } + } + }; + + // Order pain-order of hatchpaths relative to their pitch + const orderHatchPaths = (paths) => { + const nodeArray = []; + paths.forEach(p => nodeArray.push(p)); + + return nodeArray.sort((a, b) => + // (pitch - a.offset) - (pitch - b.offset) + Number(b.getAttribute('offset')) - Number(a.getAttribute('offset')) + ); + }; + + // Generate x-axis coordinates for the pattern paths + const generatePositions = (width, diagonal, initial, distance) => { + const offset = (diagonal - width) / 2; + const leftDistance = initial + offset; + const rightDistance = width + offset + distance; + const units = Math.round(leftDistance / distance) + 1; + let array = []; + + for (let i = initial - units * distance; i < rightDistance; i += distance) { + array.push(i); + } + + return array; + }; + + // Turn a path array into a tokenized version of it + const parsePath = (data) => { + let array = []; + let i = 0; + let len = data.length; + let last = 0; + + /* + * Last state (last) index map + * 0 => () + * 1 => (x y) + * 2 => (x) + * 3 => (y) + * 4 => (x1 y1 x2 y2 x y) + * 5 => (x2 y2 x y) + * 6 => (_ _ _ _ _ x y) + * 7 => (_) + */ + + while (i < len) { + switch (data[i].toUpperCase()) { + case 'Z': + array.push(data[i]); + i += 1; + last = 0; + break; + case 'M': + case 'L': + case 'T': + array.push(data[i], new Point(Number(data[i + 1]), Number(data[i + 2]))); + i += 3; + last = 1; + break; + case 'H': + array.push(data[i], new Point(Number(data[i + 1]), null)); + i += 2; + last = 2; + break; + case 'V': + array.push(data[i], new Point(null, Number(data[i + 1]))); + i += 2; + last = 3; + break; + case 'C': + array.push( + data[i], new Point(Number(data[i + 1]), Number(data[i + 2])), + new Point(Number(data[i + 3]), Number(data[i + 4])), + new Point(Number(data[i + 5]), Number(data[i + 6])) + ); + i += 7; + last = 4; + break; + case 'S': + case 'Q': + array.push( + data[i], new Point(Number(data[i + 1]), Number(data[i + 2])), + new Point(Number(data[i + 3]), Number(data[i + 4])) + ); + i += 5; + last = 5; + break; + case 'A': + array.push( + data[i], data[i + 1], data[i + 2], data[i + 3], data[i + 4], + data[i + 5], new Point(Number(data[i + 6]), Number(data[i + 7])) + ); + i += 8; + last = 6; + break; + case 'B': + array.push(data[i], data[i + 1]); + i += 2; + last = 7; + break; + default: + switch (last) { + case 1: + array.push(new Point(Number(data[i]), Number(data[i + 1]))); + i += 2; + break; + case 2: + array.push(new Point(Number(data[i]), null)); + i += 1; + break; + case 3: + array.push(new Point(null, Number(data[i]))); + i += 1; + break; + case 4: + array.push( + new Point(Number(data[i]), Number(data[i + 1])), + new Point(Number(data[i + 2]), Number(data[i + 3])), + new Point(Number(data[i + 4]), Number(data[i + 5])) + ); + i += 6; + break; + case 5: + array.push( + new Point(Number(data[i]), Number(data[i + 1])), + new Point(Number(data[i + 2]), Number(data[i + 3])) + ); + i += 4; + break; + case 6: + array.push( + data[i], data[i + 1], data[i + 2], data[i + 3], data[i + 4], + new Point(Number(data[i + 5]), Number(data[i + 6])) + ); + i += 7; + break; + default: + array.push(data[i]); + i += 1; + } + } + } + + return array; + }; + + const getYDistance = (hatchpath) => { + const path = document.createElementNS(svgNS, 'path'); + let d = hatchpath.getAttribute('d'); + + if (d[0].toUpperCase() !== 'M') { + d = `M 0,0 ${d}`; + } + + path.setAttribute('d', d); + + return path.getPointAtLength(path.getTotalLength()).y - + path.getPointAtLength(0).y; + }; + + // Point class -------------------------------------- + class Point { + constructor (x, y) { + this.x = x; + this.y = y; + } + + toString () { + return `${this.x} ${this.y}`; + } + + isPoint () { + return true; + } + + clone () { + return new Point(this.x, this.y); + } + + add (v) { + return new Point(this.x + v.x, this.y + v.y); + } + + distSquared (v) { + let x = this.x - v.x; + let y = this.y - v.y; + return (x * x + y * y); + } + } + + // Start of document processing --------------------- + const shapes = document.querySelectorAll('rect,circle,ellipse,path,text'); + + shapes.forEach((shape, i) => { + // Get id. If no id, create one. + let shapeId = shape.getAttribute('id'); + if (!shapeId) { + shapeId = 'hatch_shape_' + i; + shape.setAttribute('id', shapeId); + } + + const fill = shape.getAttribute('fill') || shape.style.fill; + const fillURL = fill.match(/^url\(\s*"?\s*#([^\s"]+)"?\s*\)/); + + if (fillURL && fillURL[1]) { + const hatch = document.getElementById(fillURL[1]); + + if (hatch && hatch.nodeName === 'hatch') { + const href = hatch.getAttributeNS(xlinkNS, 'href'); + + if (href !== null && href !== '') { + setReference(hatch, href); + } + + // Degenerate hatch, with no hatchpath children + if (hatch.children.length === 0) { + return; + } + + const bbox = shape.getBBox(); + const hatchDiag = Math.ceil(Math.sqrt( + bbox.width * bbox.width + bbox.height * bbox.height + )); + + // Hatch variables + const units = hatch.getAttribute('hatchUnits') || unitObjectBoundingBox; + const contentUnits = hatch.getAttribute('hatchContentUnits') || unitUserSpace; + const rotate = Number(hatch.getAttribute('rotate')) || 0; + const transform = hatch.getAttribute('transform') || + hatch.getAttribute('hatchTransform') || ''; + const hatchpaths = orderHatchPaths(hatch.querySelectorAll('hatchpath,hatchPath')); + const x = units === unitObjectBoundingBox + ? (Number(hatch.getAttribute('x')) * bbox.width) || 0 + : Number(hatch.getAttribute('x')) || 0; + const y = units === unitObjectBoundingBox + ? (Number(hatch.getAttribute('y')) * bbox.width) || 0 + : Number(hatch.getAttribute('y')) || 0; + let pitch = units === unitObjectBoundingBox + ? (Number(hatch.getAttribute('pitch')) * bbox.width) || 0 + : Number(hatch.getAttribute('pitch')) || 0; + + if (contentUnits === unitObjectBoundingBox && bbox.height) { + pitch /= bbox.height; + } + + // A negative value is an error. + // A value of zero disables rendering of the element + if (pitch <= 0) { + console.error('Non-positive pitch'); + return; + } + + // Pattern variables + const pattern = document.createElementNS(svgNS, 'pattern'); + const patternId = `${fillURL[1]}_pattern`; + let patternWidth = bbox.width - bbox.width % pitch; + let patternHeight = 0; + + const xPositions = generatePositions(patternWidth, hatchDiag, x, pitch); + + hatchpaths.forEach(hatchpath => { + let offset = Number(hatchpath.getAttribute('offset')) || 0; + offset = offset > pitch ? (offset % pitch) : offset; + const currentXPositions = xPositions.map(p => p + offset); + + const path = document.createElementNS(svgNS, 'path'); + let d = ''; + + for (let j = 0; j < hatchpath.attributes.length; ++j) { + const attr = hatchpath.attributes.item(j); + if (attr.name !== 'd') { + path.setAttribute(attr.name, attr.value); + } + } + + if (hatchpath.getAttribute('d') === null) { + d += currentXPositions.reduce( + (acc, xPos) => `${acc}M ${xPos} ${y} V ${hatchDiag} `, '' + ); + patternHeight = hatchDiag; + } else { + const hatchData = hatchpath.getAttribute('d'); + const data = parsePath( + hatchData.match(/([+-]?(\d+(\.\d+)?))|[MmZzLlHhVvCcSsQqTtAaBb]/g) + ); + const len = data.length; + const startsWithM = data[0] === 'M'; + const relative = data[0].toLowerCase() === data[0]; + const point = new Point(0, 0); + let yOffset = getYDistance(hatchpath); + + if (data[len - 1].y !== undefined && yOffset < data[len - 1].y) { + yOffset = data[len - 1].y; + } + + // The offset must be positive + if (yOffset <= 0) { + console.error('y offset is non-positive'); + return; + } + patternHeight = bbox.height - bbox.height % yOffset; + + const currentYPositions = generatePositions( + patternHeight, hatchDiag, y, yOffset + ); + + currentXPositions.forEach(xPos => { + point.x = xPos; + + if (!startsWithM && !relative) { + d += `M ${xPos} 0`; + } + + currentYPositions.forEach(yPos => { + point.y = yPos; + + if (relative) { + // Path is relative, set the first point in each path render + d += `M ${xPos} ${yPos} ${hatchData}`; + } else { + // Path is absolute, translate every point + d += data.map(e => e.isPoint && e.isPoint() ? e.add(point) : e) + .map(e => e.isPoint && e.isPoint() ? e.toString() : e) + .reduce((acc, e) => `${acc} ${e}`, ''); + } + }); + }); + + // The hatchpaths are infinite, so they have no fill + path.style.fill = 'none'; + } + + path.setAttribute('d', d); + pattern.appendChild(path); + }); + + setAttributes(pattern, { + 'id': patternId, + 'patternUnits': unitUserSpace, + 'patternContentUnits': contentUnits, + 'width': patternWidth, + 'height': patternHeight, + 'x': bbox.x, + 'y': bbox.y, + 'patternTransform': `rotate(${rotate} ${0} ${0}) ${transform}` + }); + hatch.parentElement.insertBefore(pattern, hatch); + + shape.style.fill = `url(#${patternId})`; + shape.setAttribute('fill', `url(#${patternId})`); + } + } + }); +})(); diff --git a/src/extension/internal/polyfill/hatch_compressed.include b/src/extension/internal/polyfill/hatch_compressed.include new file mode 100644 index 000000000..2db612454 --- /dev/null +++ b/src/extension/internal/polyfill/hatch_compressed.include @@ -0,0 +1,4 @@ +//SPDX-License-Identifier: GPL-2.0-or-later +R"=====( +!function(){const t="http://www.w3.org/2000/svg",e=(t,e,r,n)=>{const u=(e-t)/2,i=r+u,s=t+u+n;let h=[];for(let t=r-(Math.round(i/n)+1)*n;t{let i=n.getAttribute("id");i||(i="hatch_shape_"+u,n.setAttribute("id",i));const s=(n.getAttribute("fill")||n.style.fill).match(/^url\(\s*"?\s*#([^\s"]+)"?\s*\)/);if(s&&s[1]){const u=document.getElementById(s[1]);if(u&&"hatch"===u.nodeName){const i=u.getAttributeNS("http://www.w3.org/1999/xlink","href");if(null!==i&&""!==i&&((t,e)=>{const r=["x","y","pitch","rotate","hatchUnits","hatchContentUnits","transform"],n=document.getElementById(e.slice(1));n&&"hatch"===n.nodeName&&(r.forEach(e=>{let r=n.getAttribute(e);null===t.getAttribute(e)&&null!==r&&t.setAttribute(e,r)}),0===t.children.length&&Array.from(n.children).forEach(e=>{t.appendChild(e.cloneNode(!0))}))})(u,i),0===u.children.length)return;const h=n.getBBox(),o=Math.ceil(Math.sqrt(h.width*h.width+h.height*h.height)),a=u.getAttribute("hatchUnits")||"objectBoundingBox",c=u.getAttribute("hatchContentUnits")||"userSpaceOnUse",b=Number(u.getAttribute("rotate"))||0,l=u.getAttribute("transform")||u.getAttribute("hatchTransform")||"",m=(t=>{const e=[];return t.forEach(t=>e.push(t)),e.sort((t,e)=>Number(e.getAttribute("offset"))-Number(t.getAttribute("offset")))})(u.querySelectorAll("hatchpath,hatchPath")),d="objectBoundingBox"===a?Number(u.getAttribute("x"))*h.width||0:Number(u.getAttribute("x"))||0,g="objectBoundingBox"===a?Number(u.getAttribute("y"))*h.width||0:Number(u.getAttribute("y"))||0;let p="objectBoundingBox"===a?Number(u.getAttribute("pitch"))*h.width||0:Number(u.getAttribute("pitch"))||0;if("objectBoundingBox"===c&&h.height&&(p/=h.height),p<=0)return void console.error("Non-positive pitch");const N=document.createElementNS(t,"pattern"),f=`${s[1]}_pattern`;let w=h.width-h.width%p,A=0;const y=e(w,o,d,p);m.forEach(n=>{let u=Number(n.getAttribute("offset"))||0;u=u>p?u%p:u;const i=y.map(t=>t+u),s=document.createElementNS(t,"path");let a="";for(let t=0;t`${t}M ${e} ${g} V ${o} `,""),A=o;else{const u=n.getAttribute("d"),c=(t=>{let e=[],n=0,u=t.length,i=0;for(;n{const r=document.createElementNS(t,"path");let n=e.getAttribute("d");return"M"!==n[0].toUpperCase()&&(n=`M 0,0 ${n}`),r.setAttribute("d",n),r.getPointAtLength(r.getTotalLength()).y-r.getPointAtLength(0).y})(n);if(void 0!==c[b-1].y&&p{d.x=t,l||m||(a+=`M ${t} 0`),N.forEach(e=>{d.y=e,a+=m?`M ${t} ${e} ${u}`:c.map(t=>t.isPoint&&t.isPoint()?t.add(d):t).map(t=>t.isPoint&&t.isPoint()?t.toString():t).reduce((t,e)=>`${t} ${e}`,"")})}),s.style.fill="none"}s.setAttribute("d",a),N.appendChild(s)}),((t,e)=>{for(let r in e)t.setAttribute(r,e[r])})(N,{id:f,patternUnits:"userSpaceOnUse",patternContentUnits:c,width:w,height:A,x:h.x,y:h.y,patternTransform:`rotate(${b} 0 0) ${l}`}),u.parentElement.insertBefore(N,u),n.style.fill=`url(#${f})`,n.setAttribute("fill",`url(#${f})`)}}})}(); +)=====" diff --git a/src/extension/internal/polyfill/hatch_tests/hatch.svg b/src/extension/internal/polyfill/hatch_tests/hatch.svg new file mode 100644 index 000000000..7e2f8de96 --- /dev/null +++ b/src/extension/internal/polyfill/hatch_tests/hatch.svg @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/extension/internal/polyfill/hatch_tests/hatch01_with_js.svg b/src/extension/internal/polyfill/hatch_tests/hatch01_with_js.svg new file mode 100644 index 000000000..ca9ea2f85 --- /dev/null +++ b/src/extension/internal/polyfill/hatch_tests/hatch01_with_js.svg @@ -0,0 +1,134 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/extension/internal/polyfill/hatch_tests/hatch_test.svg b/src/extension/internal/polyfill/hatch_tests/hatch_test.svg new file mode 100644 index 000000000..49fecfbef --- /dev/null +++ b/src/extension/internal/polyfill/hatch_tests/hatch_test.svg @@ -0,0 +1,11731 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + Simple hatches + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Since hatchUnits="userSpaceOnUse" is usedthe rendering will match when hatched shapeis moved to the point 0,0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + <hatch id="simple1" hatchUnits="userSpaceOnUse" pitch="15"> <hatchPath stroke="#a080ff" offset="5" stroke-width="2"/></hatch> + <hatch id="simple2" hatchUnits="userSpaceOnUse" pitch="15"> <hatchPath stroke="#a080ff" offset="15"/></hatch> + <hatch id="simple3" hatchUnits="userSpaceOnUse" pitch="15"> <hatchPath stroke="#a080ff" offset="5" d="M 0,0 5,10"/></hatch> + <hatch id="simple4" hatchUnits="userSpaceOnUse" pitch="15"> <hatchPath stroke="#a080ff" offset="5" d="L 0,0 5,10"/></hatch> + <hatch id="simple5" hatchUnits="userSpaceOnUse" pitch="15"> <hatchPath stroke="#a080ff" offset="5" d="M 0,0 5,10 10,5"/></hatch> + <hatch id="simple6" hatchUnits="userSpaceOnUse" pitch="15"> <hatchPath stroke="#a080ff" offset="5" d="m 0,0 5,10 5,-5"/></hatch> + <hatch id="simple7" hatchUnits="userSpaceOnUse" pitch="15"> <hatchPath stroke="#a080ff" offset="5" d="M 0,0 5,10 M 5,20"/></hatch>  + + + + + + + + + + + + + + + + + + + + + + + + + + <hatch id="transform1" hatchUnits="userSpaceOnUse" pitch="15"> <hatchPath stroke="#a080ff" offset="5" d="L 0,0 5,10 0,20"/></hatch> + <hatch id="transform2" hatchUnits="userSpaceOnUse" pitch="15" rotate="30"> <hatchPath stroke="#a080ff" offset="10" d="L 0,0 5,10 0,20"/></hatch>  + <hatch id="transform4" hatchUnits="userSpaceOnUse" pitch="15" rotate="45"> <hatchPath stroke="#a080ff" offset="10" d="L 0,0 5,10 0,20"/></hatch> + <hatch id="transform7" hatchUnits="userSpaceOnUse" pitch="15" x="-5" y="-10"> <hatchPath stroke="#a080ff" offset="10" d="L 0,0 5,10 0,20"/></hatch> + <hatch id="transform8" hatchUnits="userSpaceOnUse" pitch="15" x="-5" y="-10" rotate="30"> <hatchPath stroke="#a080ff" offset="10" d="L 0,0 5,10 0,20"/></hatch> + <hatch id="transform9" hatchUnits="userSpaceOnUse" pitch="15" rotate="30" x="-5" y="-10" hatchTransform="matrix(0.96592583,-0.25881905,0.25881905,0.96592583,-8.4757068,43.273395)"> <hatchPath stroke="#a080ff" offset="10" d="L 0,0 5,10 0,20"/></hatch> + + + <hatch id="multiple1" hatchUnits="userSpaceOnUse" pitch="15"> <hatchPath stroke="#a080ff" offset="5"/> <hatchPath stroke="#32ff3f" offset="10"/></hatch> + <hatch id="multiple2" hatchUnits="userSpaceOnUse" pitch="15"> <hatchPath stroke="#a080ff" offset="5"/> <hatchPath stroke="#a080ff" offset="10" d="L 0,0 5,10"/></hatch> + <hatch id="multiple3" hatchUnits="userSpaceOnUse" pitch="15"> <hatchPath stroke="#a080ff" offset="5" d="L 0,0 5,17" /> <hatchPath stroke="#a080ff" offset="10" d="L 0,0 5,10"/></hatch> + + + + <hatch id="ref1" hatchUnits="userSpaceOnUse" pitch="15"> <hatchPath stroke="#a080ff" offset="5"/></hatch> + <hatch id="ref2" xlink:href="#ref1"></hatch> + <hatch id="ref3" xlink:href="#ref1" pitch="45"></hatch> + + <hatch id="stroke1" hatchUnits="userSpaceOnUse" pitch="15"> <hatchPath stroke="#a080ff" offset="5" stroke-width="5" stroke-dasharray="10 4 2 4"/></hatch>  + + + + + + + + + <hatch id="overflow1" hatchUnits="userSpaceOnUse" pitch="15"> <hatchPath stroke="#a080ff" offset="5" d="L 0,0 5,5 -5,15, 0,20"/></hatch> + <hatch id="overflow2" style="overflow:hidden" hatchUnits="userSpaceOnUse" pitch="15"> <hatchPath stroke="#a080ff" offset="0" d="L 0,0 5,5 -5,15, 0,20"/></hatch> + <hatch id="overflow3" style="overflow:visible" hatchUnits="userSpaceOnUse" pitch="15"> <hatchPath stroke="#a080ff" offset="0" d="L 0,0 5,5 -5,15, 0,20"/></hatch>  + <hatch id="overflow4" style="overflow:visible" hatchUnits="userSpaceOnUse" pitch="15"> <hatchPath stroke="#32ff3f" offset="5" > <hatchPath stroke="#ff0000" offset="20" ></hatch> + <hatch id="degenerate1" pitch="45"></hatch> + <hatch id="degenerate2" xlink:href="#nonexisting" pitch="45"></hatch> + <hatch id="degenerate3" pitch="30"> <hatchPath stroke="#a080ff" offset="10" d="L 0,0 5,10 0,15"/></hatch> + <hatch id="degenerate4" pitch="30"> <hatchPath stroke="#a080ff" offset="10" d="L 0,0 5,10 -5,15"/></hatch> + Hatch transforms + Multiple hatch paths + Hatch linking + Stroke style + Overflow property + Degenerate cases + + + + diff --git a/src/extension/internal/svg.cpp b/src/extension/internal/svg.cpp index e473f0df4..172a85a7f 100644 --- a/src/extension/internal/svg.cpp +++ b/src/extension/internal/svg.cpp @@ -57,7 +57,7 @@ using Inkscape::XML::AttributeRecord; using Inkscape::XML::Node; /* - * Removes all sodipodi and inkscape elements and attributes from an xml tree. + * Removes all sodipodi and inkscape elements and attributes from an xml tree. * used to make plain svg output. */ static void pruneExtendedNamespaces( Inkscape::XML::Node *repr ) @@ -100,7 +100,7 @@ static void pruneProprietaryGarbage( Inkscape::XML::Node *repr ) { if (repr) { std::vector nodesRemoved; - for ( Node *child = repr->firstChild(); child; child = child->next() ) { + for ( Node *child = repr->firstChild(); child; child = child->next() ) { if((strncmp("i:pgf", child->name(), 5) == 0)) { nodesRemoved.push_back(child); g_warning( "An Adobe proprietary tag was found which is known to cause issues. It was removed before saving."); @@ -108,7 +108,7 @@ static void pruneProprietaryGarbage( Inkscape::XML::Node *repr ) pruneProprietaryGarbage(child); } } - for (auto & it : nodesRemoved) { + for (auto & it : nodesRemoved) { repr->removeChild(it); } } @@ -423,7 +423,7 @@ static void insert_text_fallback( Inkscape::XML::Node *repr, SPDocument *doc, In } sp_repr_set_svg_double(line_tspan, "y", line_y); // FIXME: this will pick up the wrong end of counter-directional runs } else { - // std::cout << " vertical: " << line_anchor_point[Geom::X] << " " << text_y << std::endl; + // std::cout << " vertical: " << line_anchor_point[Geom::X] << " " << text_y << std::endl; sp_repr_set_svg_double(line_tspan, "x", line_x); // FIXME: this will pick up the wrong end of counter-directional runs if (text->has_inline_size()) { sp_repr_set_svg_double(line_tspan, "y", text_y); @@ -558,6 +558,46 @@ static void insert_mesh_polyfill( Inkscape::XML::Node *repr ) } } + +static void insert_hatch_polyfill( Inkscape::XML::Node *repr ) +{ + if (repr) { + + Inkscape::XML::Node *defs = sp_repr_lookup_name (repr, "svg:defs"); + + if (defs == nullptr) { + // We always put meshes in , no defs -> no mesh. + return; + } + + bool has_hatch = false; + for ( Node *child = defs->firstChild(); child; child = child->next() ) { + if (strncmp("svg:hatch", child->name(), 16) == 0) { + has_hatch = true; + break; + } + } + + Inkscape::XML::Node *script = sp_repr_lookup_child (repr, "id", "hatch_polyfill"); + + if (has_hatch && script == nullptr) { + + script = repr->document()->createElement("svg:script"); + script->setAttribute ("id", "hatch_polyfill"); + script->setAttribute ("type", "text/javascript"); + repr->root()->appendChild(script); // Must be last + + // Insert JavaScript via raw string literal. + Glib::ustring js = +#include "polyfill/hatch_compressed.include" +; + + Inkscape::XML::Node *script_text = repr->document()->createTextNode(js.c_str()); + script->appendChild(script_text); + } + } +} + /* * Recursively transform SVG 2 to SVG 1.1, if possible. */ @@ -655,7 +695,7 @@ Svg::init() "" SP_MODULE_KEY_OUTPUT_SVG_INKSCAPE "\n" "\n" "", new Svg()); - + /* SVG out Inkscape */ Inkscape::Extension::build_from_mem( "\n" @@ -881,12 +921,15 @@ Svg::save(Inkscape::Extension::Output *mod, SPDocument *doc, gchar const *filena prefs->getBool("/options/svgexport/text_insertfallback", true); bool const insert_mesh_polyfill_flag = prefs->getBool("/options/svgexport/mesh_insertpolyfill", true); + bool const insert_hatch_polyfill_flag = + prefs->getBool("/options/svgexport/hatch_insertpolyfill", true); bool createNewDoc = !exportExtensions || transform_2_to_1_flag || insert_text_fallback_flag || - insert_mesh_polyfill_flag; + insert_mesh_polyfill_flag || + insert_hatch_polyfill_flag; // We prune the in-use document and deliberately loose data, because there // is no known use for this data at the present time. @@ -899,7 +942,7 @@ Svg::save(Inkscape::Extension::Output *mod, SPDocument *doc, gchar const *filena Inkscape::XML::Document *new_rdoc = new Inkscape::XML::SimpleDocument(); // Comments and PI nodes are not included in this duplication - // TODO: Move this code into xml/document.h and duplicate rdoc instead of root. + // TODO: Move this code into xml/document.h and duplicate rdoc instead of root. new_rdoc->setAttribute("standalone", "no"); new_rdoc->setAttribute("version", "2.0"); @@ -927,6 +970,10 @@ Svg::save(Inkscape::Extension::Output *mod, SPDocument *doc, gchar const *filena insert_mesh_polyfill (root); } + if (insert_hatch_polyfill_flag) { + insert_hatch_polyfill (root); + } + rdoc = new_rdoc; } diff --git a/src/ui/dialog/inkscape-preferences.cpp b/src/ui/dialog/inkscape-preferences.cpp index f7af33845..98f60f89b 100644 --- a/src/ui/dialog/inkscape-preferences.cpp +++ b/src/ui/dialog/inkscape-preferences.cpp @@ -505,7 +505,7 @@ void InkscapePreferences::initPageTools() _page_text.add_line( true, "", _font_fontsdir_user, "", _("Load additional fonts from \"fonts\" directory located in Inkscape's user configuration directory")); _font_fontdirs_custom.init("/options/font/custom_fontdirs", 50); _page_text.add_line(true, _("Additional font directories"), _font_fontdirs_custom, "", _("Load additional fonts from custom locations (one path per line)"), true); - + this->AddNewObjectsStyle(_page_text, "/tools/text"); @@ -1307,9 +1307,11 @@ void InkscapePreferences::initPageIO() _page_svgexport.add_group_header( _("SVG 2")); _svgexport_insert_text_fallback.init( _("Insert SVG 1.1 fallback in text."), "/options/svgexport/text_insertfallback", true ); _svgexport_insert_mesh_polyfill.init( _("Insert Mesh Gradient JavaScript polyfill."), "/options/svgexport/mesh_insertpolyfill", true ); + _svgexport_insert_mesh_polyfill.init( _("Insert Hatch Paint Server JavaScript polyfill."), "/options/svgexport/hatch_insertpolyfill", true ); - _page_svgexport.add_line( false, "", _svgexport_insert_text_fallback, "", _("Adds fallback options for non-SVG 2 renderers."), false); - _page_svgexport.add_line( false, "", _svgexport_insert_mesh_polyfill, "", _("Adds JavaScript polyfill to render meshes (only fill."), false); + _page_svgexport.add_line( false, "", _svgexport_insert_text_fallback, "", _("Adds fallback options for non-SVG 2 renderers."), false); + _page_svgexport.add_line( false, "", _svgexport_insert_mesh_polyfill, "", _("Adds JavaScript polyfill to render meshes."), false); + _page_svgexport.add_line( false, "", _svgexport_insert_mesh_polyfill, "", _("Adds JavaScript polyfill to render hatches (linear and absolute paths)."), false); // SVG Export Options (SVG 2 -> SVG 1) _page_svgexport.add_group_header( _("SVG 2 to SVG 1.1")); -- cgit v1.2.3