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 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 585cc5f652..d963779ce8 100644 --- a/src/svg/SvgExport.js +++ b/src/svg/SvgExport.js @@ -95,14 +95,33 @@ 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.linkImages) { + var raster = getDefinition(item, 'image'); + + if (!raster) { + 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) { @@ -331,10 +350,24 @@ 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) { @@ -342,12 +375,33 @@ 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 fb588a00b5..27057c49e9 100644 --- a/test/tests/SvgExport.js +++ b/test/tests/SvgExport.js @@ -244,4 +244,113 @@ 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({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({linkImages: 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({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({linkImages: 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({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({linkImages: 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({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({linkImages: 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); + }); }