From 5c67796e6db472e91a9dff430c723948cbc12db8 Mon Sep 17 00:00:00 2001 From: blayzen-w Date: Wed, 31 Aug 2022 10:15:41 -0700 Subject: [PATCH 1/6] Added an export svg option to move images to the defs section and link them with a use tag. --- src/svg/SvgExport.js | 22 +++++++++++++++--- test/tests/SvgExport.js | 49 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+), 3 deletions(-) diff --git a/src/svg/SvgExport.js b/src/svg/SvgExport.js index 585cc5f652..2f1a1ed908 100644 --- a/src/svg/SvgExport.js +++ b/src/svg/SvgExport.js @@ -95,14 +95,30 @@ new function() { var attrs = getTransform(item._matrix, true), size = item.getSize(), image = item.getImage(); + // Take into account that rasters are centered: attrs.x -= size.width / 2; attrs.y -= size.height / 2; attrs.width = size.width; attrs.height = size.height; - attrs.href = options.embedImages == false && image && image.src - || item.toDataURL(); - return SvgElement.create('image', attrs, formatter); + + var image_href = options.embedImages == false && image && image.src + || item.toDataURL(); + + if (options.linkRaster) { + var raster = SvgElement.create('image', { + href: image_href + }, formatter); + + setDefinition(item, raster, 'image'); + + attrs.href = '#' + raster.id; + + return SvgElement.create('use', attrs, formatter); + } else { + attrs.href = image_href; + return SvgElement.create('image', attrs, formatter); + } } function exportPath(item, options) { diff --git a/test/tests/SvgExport.js b/test/tests/SvgExport.js index fb588a00b5..6cfe337b53 100644 --- a/test/tests/SvgExport.js +++ b/test/tests/SvgExport.js @@ -244,4 +244,53 @@ if (!isNodeContext) { var svg = project.exportSVG({ bounds: 'content', asString: true }); compareSVG(assert.async(), svg, project.activeLayer); }); + + test('Export raster inline from a data url', function (assert) { + var raster = new Raster('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAYAAAAeP4ixAAAARUlEQVR42u3PQQ0AAAjEMM6/aMACT5IuM9B01f6/gICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIBcGuAsY8/q7uoYAAAAAElFTkSuQmCC'); + + var done = assert.async(); + raster.onLoad = function() { + raster.setBounds(0, 0, 100, 100); + compareSVG(done, project.exportSVG({asString: true}), project.activeLayer); + }; + }); + test('Export raster inline from a url', function (assert) { + var raster = new Raster('assets/paper-js.gif'); + + var done = assert.async(); + raster.onLoad = function() { + raster.setBounds(0, 0, 100, 100); + compareSVG(done, project.exportSVG({asString: true}), project.activeLayer); + }; + }); + test('Export raster linked from a data url', function (assert) { + var raster = new Raster('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAYAAAAeP4ixAAAARUlEQVR42u3PQQ0AAAjEMM6/aMACT5IuM9B01f6/gICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIBcGuAsY8/q7uoYAAAAAElFTkSuQmCC'); + + var done = assert.async(); + raster.onLoad = function() { + raster.setBounds(0, 0, 100, 100); + + var defs = project.exportSVG({linkRaster: true}).getElementsByTagName('defs'); + if (defs.length !== 1 || defs[0].children.length !== 1) { + console.error('Image was not added to the defs'); + } + + compareSVG(done, project.exportSVG({linkRaster: true, asString: true}), project.activeLayer); + }; + }); + test('Export raster linked from a url', function (assert) { + var raster = new Raster('assets/paper-js.gif'); + + var done = assert.async(); + raster.onLoad = function() { + raster.setBounds(0, 0, 100, 100); + + var defs = project.exportSVG({linkRaster: true}).getElementsByTagName('defs'); + if (defs.length !== 1 || defs[0].children.length !== 1) { + console.error('Image was not added to the defs'); + } + + compareSVG(done, project.exportSVG({linkRaster: true, asString: true}), project.activeLayer); + }; + }); } From bf94baeb666326db2f9f75e9d0f85f3bfa4d79d3 Mon Sep 17 00:00:00 2001 From: blayzen-w Date: Wed, 31 Aug 2022 10:23:39 -0700 Subject: [PATCH 2/6] Changed to use assert equals instead of console error --- test/tests/SvgExport.js | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/test/tests/SvgExport.js b/test/tests/SvgExport.js index 6cfe337b53..480dda1d46 100644 --- a/test/tests/SvgExport.js +++ b/test/tests/SvgExport.js @@ -271,9 +271,8 @@ if (!isNodeContext) { raster.setBounds(0, 0, 100, 100); var defs = project.exportSVG({linkRaster: true}).getElementsByTagName('defs'); - if (defs.length !== 1 || defs[0].children.length !== 1) { - console.error('Image was not added to the defs'); - } + assert.equal(defs.length, 1, 'The svg is missing the defs element'); + assert.equal(defs[0].children.length, 1, 'The defs element is missing the image'); compareSVG(done, project.exportSVG({linkRaster: true, asString: true}), project.activeLayer); }; @@ -286,9 +285,8 @@ if (!isNodeContext) { raster.setBounds(0, 0, 100, 100); var defs = project.exportSVG({linkRaster: true}).getElementsByTagName('defs'); - if (defs.length !== 1 || defs[0].children.length !== 1) { - console.error('Image was not added to the defs'); - } + assert.equal(defs.length, 1, 'The svg is missing the defs element'); + assert.equal(defs[0].children.length, 1, 'The defs element is missing the image'); compareSVG(done, project.exportSVG({linkRaster: true, asString: true}), project.activeLayer); }; From e890ae45ac4aa224e90168982db8315f81cf6fab Mon Sep 17 00:00:00 2001 From: blayzen-w Date: Wed, 31 Aug 2022 11:35:36 -0700 Subject: [PATCH 3/6] Added support for linking multiple use tags to a single shared image --- src/svg/SvgExport.js | 62 +++++++++++++++++++++++++++++++++-------- test/tests/SvgExport.js | 62 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 113 insertions(+), 11 deletions(-) diff --git a/src/svg/SvgExport.js b/src/svg/SvgExport.js index 2f1a1ed908..0d30b1d4f3 100644 --- a/src/svg/SvgExport.js +++ b/src/svg/SvgExport.js @@ -106,11 +106,14 @@ new function() { || item.toDataURL(); if (options.linkRaster) { - var raster = SvgElement.create('image', { - href: image_href - }, formatter); + var raster = getDefinition(item, 'image'); - setDefinition(item, raster, 'image'); + if (!raster) { + raster = SvgElement.create('image', { + href: image_href + }, formatter); + setDefinition(item, raster, 'image'); + } attrs.href = '#' + raster.id; @@ -347,10 +350,25 @@ new function() { function getDefinition(item, type) { if (!definitions) definitions = { ids: {}, svgs: {} }; - // Use #__id for items that don't have internal #_id properties (Color), - // and give them ids from their own private id pool named 'svg'. - return item && definitions.svgs[type + '-' - + (item._id || item.__id || (item.__id = UID.get('svg')))]; + + var svgDefinitionId; + if (type === 'image') { + // Image ids in the definitions are based on the source + // instead of the element id in order to link multiple + // raster elements to the same image using the use tag + var imageSource = item.getSource(); + svgDefinitionId = definitions.ids[type] && + definitions.ids[type][imageSource] && + (type + '-' + definitions.ids[type][imageSource]); + } + else { + // Use #__id for items that don't have internal #_id properties (Color), + // and give them ids from their own private id pool named 'svg'. + svgDefinitionId = item && type + '-' + + (item._id || item.__id || (item.__id = UID.get('svg'))); + } + + return item && definitions.svgs[svgDefinitionId]; } function setDefinition(item, node, type) { @@ -358,12 +376,34 @@ new function() { // This is required by 'clip', where getDefinition() is not called. if (!definitions) getDefinition(); + // Have different id ranges per type - var typeId = definitions.ids[type] = (definitions.ids[type] || 0) + 1; + var typeId; + var svgDefinitionId; + if (type === 'image') { + // Images in the definitions needs to be unique to the source + // instead of the element + if (!definitions.ids[type]) { + definitions.ids[type] = Object.create(null); + } + var imageSource = item.getSource(); + typeId = definitions.ids[type][imageSource] = + definitions.ids[type][imageSource] || + Object.keys(definitions.ids[type]).length + 1; + + svgDefinitionId = type + '-' + typeId; + } + else { + typeId = definitions.ids[type] = (definitions.ids[type] || 0) + 1; + + // See getDefinition() for an explanation of #__id: + svgDefinitionId = type + '-' + (item._id || item.__id); + } + // Give the svg node an id, and link to it from the item id. node.id = type + '-' + typeId; - // See getDefinition() for an explanation of #__id: - definitions.svgs[type + '-' + (item._id || item.__id)] = node; + + definitions.svgs[svgDefinitionId] = node; } function exportDefinitions(node, options) { diff --git a/test/tests/SvgExport.js b/test/tests/SvgExport.js index 480dda1d46..4528835f1b 100644 --- a/test/tests/SvgExport.js +++ b/test/tests/SvgExport.js @@ -291,4 +291,66 @@ if (!isNodeContext) { compareSVG(done, project.exportSVG({linkRaster: true, asString: true}), project.activeLayer); }; }); + test('Export multiple rasters linked from a data url without duplicating data', function (assert) { + var dataURL = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAYAAAAeP4ixAAAARUlEQVR42u3PQQ0AAAjEMM6/aMACT5IuM9B01f6/gICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIBcGuAsY8/q7uoYAAAAAElFTkSuQmCC'; + var raster1 = new Raster(dataURL); + var raster2 = new Raster(dataURL); + + var done = assert.async(); + + function validate() { + raster1.setBounds(0, 0, 50, 50); + raster2.setBounds(50, 50, 50, 50); + + var defs = project.exportSVG({linkRaster: true}).getElementsByTagName('defs'); + assert.equal(defs.length, 1, 'The svg is missing the defs element'); + assert.equal(defs[0].children.length, 1, 'The defs element should only have a single image'); + + compareSVG(done, project.exportSVG({linkRaster: true, asString: true}), project.activeLayer); + } + + var raster1Loaded = new Promise(function (resolve, reject) { + raster1.onLoad = function() { + resolve(); + }; + }); + var raster2Loaded = new Promise(function (resolve, reject) { + raster2.onLoad = function() { + resolve(); + }; + }); + + Promise.all([raster1Loaded, raster2Loaded]).then(validate); + }); + test('Export multiple rasters linked from a url without duplicating data', function (assert) { + var standardURL = 'assets/paper-js.gif'; + var raster1 = new Raster(standardURL); + var raster2 = new Raster(standardURL); + + var done = assert.async(); + + function validate() { + raster1.setBounds(0, 0, 50, 50); + raster2.setBounds(50, 50, 50, 50); + + var defs = project.exportSVG({linkRaster: true}).getElementsByTagName('defs'); + assert.equal(defs.length, 1, 'The svg is missing the defs element'); + assert.equal(defs[0].children.length, 1, 'The defs element should only have a single image'); + + compareSVG(done, project.exportSVG({linkRaster: true, asString: true}), project.activeLayer); + } + + var raster1Loaded = new Promise(function (resolve, reject) { + raster1.onLoad = function() { + resolve(); + }; + }); + var raster2Loaded = new Promise(function (resolve, reject) { + raster2.onLoad = function() { + resolve(); + }; + }); + + Promise.all([raster1Loaded, raster2Loaded]).then(validate); + }); } From a91fcebd4d6a0a06e6b75707d1d709ac9f8fdd4a Mon Sep 17 00:00:00 2001 From: blayzen-w Date: Wed, 31 Aug 2022 11:47:10 -0700 Subject: [PATCH 4/6] Added name to the authors list --- AUTHORS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS.md b/AUTHORS.md index d8cb5a3a4c..6eec2e68f3 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -15,3 +15,4 @@ - Scott Kieronski - Samuel Asensi - Takahiro Nishino +- Blayze Wilhelm From 98755453ecfc99f285e2cc46911e928791ebed16 Mon Sep 17 00:00:00 2001 From: blayzen-w Date: Wed, 31 Aug 2022 12:54:40 -0700 Subject: [PATCH 5/6] Changed from linkRaster to linkImages to match the embedImages option format --- src/item/Item.js | 3 +++ src/svg/SvgExport.js | 2 +- test/tests/SvgExport.js | 16 ++++++++-------- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/item/Item.js b/src/item/Item.js index 83b34ed3f4..c4c1a8964f 100644 --- a/src/item/Item.js +++ b/src/item/Item.js @@ -2446,6 +2446,9 @@ new function() { // Injection scope for hit-test functions shared with project * @option [options.embedImages=true] {Boolean} whether raster images should * be embedded as base64 data inlined in the xlink:href attribute, or * kept as a link to their external URL. + * @option [options.linkImages=false] {Boolean} whether raster images should + * be linked using a definition and use tag, or place the data/url in + * the image href attribute. * * @param {Object} [options] the export options * @return {SVGElement|String} the item converted to an SVG node or a diff --git a/src/svg/SvgExport.js b/src/svg/SvgExport.js index 0d30b1d4f3..bc42aca9f8 100644 --- a/src/svg/SvgExport.js +++ b/src/svg/SvgExport.js @@ -105,7 +105,7 @@ new function() { var image_href = options.embedImages == false && image && image.src || item.toDataURL(); - if (options.linkRaster) { + if (options.linkImages) { var raster = getDefinition(item, 'image'); if (!raster) { diff --git a/test/tests/SvgExport.js b/test/tests/SvgExport.js index 4528835f1b..27057c49e9 100644 --- a/test/tests/SvgExport.js +++ b/test/tests/SvgExport.js @@ -270,11 +270,11 @@ if (!isNodeContext) { raster.onLoad = function() { raster.setBounds(0, 0, 100, 100); - var defs = project.exportSVG({linkRaster: true}).getElementsByTagName('defs'); + var defs = project.exportSVG({linkImages: true}).getElementsByTagName('defs'); assert.equal(defs.length, 1, 'The svg is missing the defs element'); assert.equal(defs[0].children.length, 1, 'The defs element is missing the image'); - compareSVG(done, project.exportSVG({linkRaster: true, asString: true}), project.activeLayer); + compareSVG(done, project.exportSVG({linkImages: true, asString: true}), project.activeLayer); }; }); test('Export raster linked from a url', function (assert) { @@ -284,11 +284,11 @@ if (!isNodeContext) { raster.onLoad = function() { raster.setBounds(0, 0, 100, 100); - var defs = project.exportSVG({linkRaster: true}).getElementsByTagName('defs'); + var defs = project.exportSVG({linkImages: true}).getElementsByTagName('defs'); assert.equal(defs.length, 1, 'The svg is missing the defs element'); assert.equal(defs[0].children.length, 1, 'The defs element is missing the image'); - compareSVG(done, project.exportSVG({linkRaster: true, asString: true}), project.activeLayer); + compareSVG(done, project.exportSVG({linkImages: true, asString: true}), project.activeLayer); }; }); test('Export multiple rasters linked from a data url without duplicating data', function (assert) { @@ -302,11 +302,11 @@ if (!isNodeContext) { raster1.setBounds(0, 0, 50, 50); raster2.setBounds(50, 50, 50, 50); - var defs = project.exportSVG({linkRaster: true}).getElementsByTagName('defs'); + var defs = project.exportSVG({linkImages: true}).getElementsByTagName('defs'); assert.equal(defs.length, 1, 'The svg is missing the defs element'); assert.equal(defs[0].children.length, 1, 'The defs element should only have a single image'); - compareSVG(done, project.exportSVG({linkRaster: true, asString: true}), project.activeLayer); + compareSVG(done, project.exportSVG({linkImages: true, asString: true}), project.activeLayer); } var raster1Loaded = new Promise(function (resolve, reject) { @@ -333,11 +333,11 @@ if (!isNodeContext) { raster1.setBounds(0, 0, 50, 50); raster2.setBounds(50, 50, 50, 50); - var defs = project.exportSVG({linkRaster: true}).getElementsByTagName('defs'); + var defs = project.exportSVG({linkImages: true}).getElementsByTagName('defs'); assert.equal(defs.length, 1, 'The svg is missing the defs element'); assert.equal(defs[0].children.length, 1, 'The defs element should only have a single image'); - compareSVG(done, project.exportSVG({linkRaster: true, asString: true}), project.activeLayer); + compareSVG(done, project.exportSVG({linkImages: true, asString: true}), project.activeLayer); } var raster1Loaded = new Promise(function (resolve, reject) { From 69ff64cce4c4fc77d7b381ae7ee35bb071879b2b Mon Sep 17 00:00:00 2001 From: blayzen-w Date: Wed, 31 Aug 2022 13:44:50 -0700 Subject: [PATCH 6/6] Code style fixes --- src/svg/SvgExport.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/svg/SvgExport.js b/src/svg/SvgExport.js index bc42aca9f8..d963779ce8 100644 --- a/src/svg/SvgExport.js +++ b/src/svg/SvgExport.js @@ -360,8 +360,7 @@ new function() { svgDefinitionId = definitions.ids[type] && definitions.ids[type][imageSource] && (type + '-' + definitions.ids[type][imageSource]); - } - else { + } else { // Use #__id for items that don't have internal #_id properties (Color), // and give them ids from their own private id pool named 'svg'. svgDefinitionId = item && type + '-' + @@ -392,8 +391,7 @@ new function() { Object.keys(definitions.ids[type]).length + 1; svgDefinitionId = type + '-' + typeId; - } - else { + } else { typeId = definitions.ids[type] = (definitions.ids[type] || 0) + 1; // See getDefinition() for an explanation of #__id: