@svgedit/svgcanvas 7.2.2 → 7.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/core/sanitize.js CHANGED
@@ -56,10 +56,10 @@ const svgWhiteList_ = {
56
56
  svg: ['clip-path', 'clip-rule', 'enable-background', 'filter', 'height', 'mask', 'preserveAspectRatio', 'requiredFeatures', 'systemLanguage', 'version', 'viewBox', 'width', 'x', 'xmlns', 'xmlns:se', 'xmlns:xlink', 'xmlns:oi', 'oi:animations', 'y', 'stroke-linejoin', 'fill-rule', 'aria-label', 'stroke-width', 'fill-rule', 'xml:space'],
57
57
  switch: ['requiredFeatures', 'systemLanguage'],
58
58
  symbol: ['fill', 'fill-opacity', 'fill-rule', 'filter', 'font-family', 'font-size', 'font-style', 'font-weight', 'opacity', 'overflow', 'preserveAspectRatio', 'requiredFeatures', 'stroke', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke-width', 'systemLanguage', 'viewBox', 'width', 'height'],
59
- text: ['clip-path', 'clip-rule', 'fill', 'fill-opacity', 'fill-rule', 'filter', 'font-family', 'font-size', 'font-style', 'font-weight', 'mask', 'opacity', 'requiredFeatures', 'stroke', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke-width', 'systemLanguage', 'text-anchor', 'letter-spacing', 'word-spacing', 'text-decoration', 'textLength', 'lengthAdjust', 'x', 'xml:space', 'y'],
60
- textPath: ['method', 'requiredFeatures', 'spacing', 'startOffset', 'systemLanguage', 'xlink:href'],
59
+ text: ['clip-path', 'clip-rule', 'dominant-baseline', 'fill', 'fill-opacity', 'fill-rule', 'filter', 'font-family', 'font-size', 'font-style', 'font-weight', 'mask', 'opacity', 'requiredFeatures', 'stroke', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke-width', 'systemLanguage', 'text-anchor', 'letter-spacing', 'word-spacing', 'text-decoration', 'textLength', 'lengthAdjust', 'x', 'xml:space', 'y'],
60
+ textPath: ['dominant-baseline', 'method', 'requiredFeatures', 'spacing', 'startOffset', 'systemLanguage', 'xlink:href'],
61
61
  title: [],
62
- tspan: ['clip-path', 'clip-rule', 'dx', 'dy', 'fill', 'fill-opacity', 'fill-rule', 'filter', 'font-family', 'font-size', 'font-style', 'font-weight', 'mask', 'opacity', 'requiredFeatures', 'rotate', 'stroke', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke-width', 'systemLanguage', 'text-anchor', 'textLength', 'x', 'xml:space', 'y'],
62
+ tspan: ['clip-path', 'clip-rule', 'dx', 'dy', 'dominant-baseline', 'fill', 'fill-opacity', 'fill-rule', 'filter', 'font-family', 'font-size', 'font-style', 'font-weight', 'mask', 'opacity', 'requiredFeatures', 'rotate', 'stroke', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke-width', 'systemLanguage', 'text-anchor', 'textLength', 'x', 'xml:space', 'y'],
63
63
  use: ['clip-path', 'clip-rule', 'fill', 'fill-opacity', 'fill-rule', 'filter', 'height', 'mask', 'stroke', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke-width', 'width', 'x', 'xlink:href', 'y', 'overflow'],
64
64
 
65
65
  // MathML Elements
@@ -982,7 +982,7 @@ const convertToGroup = elem => {
982
982
  const vb = elem.getAttribute('viewBox')
983
983
 
984
984
  if (vb) {
985
- const nums = vb.split(' ')
985
+ const nums = vb.split(/[ ,]+/)
986
986
  pos.x -= Number(nums[0])
987
987
  pos.y -= Number(nums[1])
988
988
  }
package/core/selection.js CHANGED
@@ -427,7 +427,7 @@ const setRotationAngle = (val, preventUndo) => {
427
427
  // new transform is something like: 'rotate(5 1.39625e-8 -11)'
428
428
  // we round the x so it becomes 'rotate(5 0 -11)'
429
429
  if (newTransform) {
430
- const newTransformArray = newTransform.split(' ')
430
+ const newTransformArray = newTransform.split(/[ ,]+/)
431
431
  const round = (num) => Math.round(Number(num) + Number.EPSILON)
432
432
  const x = round(newTransformArray[1])
433
433
  newTransform = `${newTransformArray[0]} ${x} ${newTransformArray[2]}`
package/core/svg-exec.js CHANGED
@@ -7,8 +7,7 @@
7
7
 
8
8
  import { jsPDF as JsPDF } from 'jspdf'
9
9
  import 'svg2pdf.js'
10
- import html2canvas from 'html2canvas'
11
- import * as hstry from './history.js'
10
+ import * as history from './history.js'
12
11
  import {
13
12
  text2xml,
14
13
  cleanupElement,
@@ -17,8 +16,6 @@ import {
17
16
  preventClickDefault,
18
17
  toXml,
19
18
  getStrokedBBoxDefaultVisible,
20
- createObjectURL,
21
- dataURLToObjectURL,
22
19
  walkTree,
23
20
  getBBox as utilsGetBBox,
24
21
  hashCode
@@ -41,7 +38,7 @@ const {
41
38
  RemoveElementCommand,
42
39
  ChangeElementCommand,
43
40
  BatchCommand
44
- } = hstry
41
+ } = history
45
42
 
46
43
  let svgCanvas = null
47
44
 
@@ -525,7 +522,7 @@ const setSvgString = (xmlString, preventUndo) => {
525
522
  // determine proper size
526
523
  if (content.getAttribute('viewBox')) {
527
524
  const viBox = content.getAttribute('viewBox')
528
- const vb = viBox.split(' ')
525
+ const vb = viBox.split(/[ ,]+/)
529
526
  attrs.width = vb[2]
530
527
  attrs.height = vb[3]
531
528
  // handle content that doesn't have a viewBox
@@ -660,7 +657,7 @@ const importSvgString = (xmlString, preserveDimension) => {
660
657
  const innerh = convertToNum('height', svg.getAttribute('height'))
661
658
  const innervb = svg.getAttribute('viewBox')
662
659
  // if no explicit viewbox, create one out of the width and height
663
- const vb = innervb ? innervb.split(' ') : [0, 0, innerw, innerh]
660
+ const vb = innervb ? innervb.split(/[ ,]+/) : [0, 0, innerw, innerh]
664
661
  for (j = 0; j < 4; ++j) {
665
662
  vb[j] = Number(vb[j])
666
663
  }
@@ -846,156 +843,182 @@ const getIssues = () => {
846
843
  */
847
844
 
848
845
  /**
849
- * Generates a PNG (or JPG, BMP, WEBP) Data URL based on the current image,
850
- * then calls "ed" with an object including the string, image
851
- * information, and any issues found.
852
- * @function module:svgcanvas.SvgCanvas#raster
853
- * @param {"PNG"|"JPEG"|"BMP"|"WEBP"|"ICO"} [imgType="PNG"]
854
- * @param {Float} [quality] Between 0 and 1
855
- * @param {string} [WindowName]
856
- * @param {PlainObject} [opts]
857
- * @param {boolean} [opts.avoidEvent]
858
- * @fires module:svgcanvas.SvgCanvas#event:ed
859
- * @todo Confirm/fix ICO type
860
- * @returns {Promise<module:svgcanvas.ImageedResults>} Resolves to {@link module:svgcanvas.ImageedResults}
846
+ * Utility function to convert all external image links in an SVG element to Base64 data URLs.
847
+ * @param {SVGElement} svgElement - The SVG element to process.
848
+ * @returns {Promise<void>}
861
849
  */
862
- const rasterExport = async (imgType, quality, WindowName, opts = {}) => {
863
- const type = imgType === 'ICO' ? 'BMP' : imgType || 'PNG'
864
- const mimeType = 'image/' + type.toLowerCase()
865
- const { issues, issueCodes } = getIssues()
866
- const svg = svgCanvas.svgCanvasToString()
867
-
868
- const iframe = document.createElement('iframe')
869
- iframe.onload = () => {
870
- const iframedoc = iframe.contentDocument || iframe.contentWindow.document
871
- const ele = svgCanvas.getSvgContent()
872
- const cln = ele.cloneNode(true)
873
- iframedoc.body.appendChild(cln)
874
- setTimeout(() => {
875
- // eslint-disable-next-line promise/catch-or-return
876
- html2canvas(iframedoc.body, { useCORS: true, allowTaint: true }).then(
877
- canvas => {
878
- return new Promise(resolve => {
879
- const dataURLType = type.toLowerCase()
880
- const datauri = quality
881
- ? canvas.toDataURL('image/' + dataURLType, quality)
882
- : canvas.toDataURL('image/' + dataURLType)
883
- iframe.parentNode.removeChild(iframe)
884
- let bloburl
885
-
886
- const done = () => {
887
- const obj = {
888
- datauri,
889
- bloburl,
890
- svg,
891
- issues,
892
- issueCodes,
893
- type: imgType,
894
- mimeType,
895
- quality,
896
- WindowName
897
- }
898
- if (!opts.avoidEvent) {
899
- svgCanvas.call('exported', obj)
900
- }
901
- resolve(obj)
902
- }
903
- if (canvas.toBlob) {
904
- canvas.toBlob(
905
- blob => {
906
- bloburl = createObjectURL(blob)
907
- done()
908
- },
909
- mimeType,
910
- quality
911
- )
912
- return
913
- }
914
- bloburl = dataURLToObjectURL(datauri)
915
- done()
916
- })
917
- }
918
- )
919
- }, 1000)
920
- }
921
- document.body.appendChild(iframe)
850
+ const convertImagesToBase64 = async svgElement => {
851
+ const imageElements = svgElement.querySelectorAll('image')
852
+ const promises = Array.from(imageElements).map(async img => {
853
+ const href = img.getAttribute('xlink:href') || img.getAttribute('href')
854
+ if (href && !href.startsWith('data:')) {
855
+ try {
856
+ const response = await fetch(href)
857
+ const blob = await response.blob()
858
+ const reader = new FileReader()
859
+ return new Promise(resolve => {
860
+ reader.onload = () => {
861
+ img.setAttribute('xlink:href', reader.result)
862
+ resolve()
863
+ }
864
+ reader.readAsDataURL(blob)
865
+ })
866
+ } catch (error) {
867
+ console.error('Failed to fetch image:', error)
868
+ }
869
+ }
870
+ })
871
+ await Promise.all(promises)
922
872
  }
923
873
 
924
874
  /**
925
- * @typedef {void|"save"|"arraybuffer"|"blob"|"datauristring"|"dataurlstring"|"dataurlnewwindow"|"datauri"|"dataurl"} external:jsPDF.OutputType
926
- * @todo Newer version to add also allows these `outputType` values "bloburi"|"bloburl" which return strings, so document here and for `outputType` of `module:svgcanvas.PDFedResults` below if added
927
- */
928
- /**
929
- * @typedef {PlainObject} module:svgcanvas.PDFedResults
930
- * @property {string} svg The SVG PDF output
931
- * @property {string|ArrayBuffer|Blob|window} output The output based on the `outputType`;
932
- * if `undefined`, "datauristring", "dataurlstring", "datauri",
933
- * or "dataurl", will be a string (`undefined` gives a document, while the others
934
- * build as Data URLs; "datauri" and "dataurl" change the location of the current page); if
935
- * "arraybuffer", will return `ArrayBuffer`; if "blob", returns a `Blob`;
936
- * if "dataurlnewwindow", will change the current page's location and return a string
937
- * if in Safari and no window object is found; otherwise opens in, and returns, a new `window`
938
- * object; if "save", will have the same return as "dataurlnewwindow" if
939
- * `navigator.getUserMedia` support is found without `URL.createObjectURL` support; otherwise
940
- * returns `undefined` but attempts to save
941
- * @property {external:jsPDF.OutputType} outputType
942
- * @property {string[]} issues The human-readable localization messages of corresponding `issueCodes`
943
- * @property {module:svgcanvas.IssueCode[]} issueCodes
944
- * @property {string} WindowName
875
+ * Generates a raster image (PNG, JPEG, etc.) from the SVG content.
876
+ * @param {string} [imgType='PNG'] - The image type to generate.
877
+ * @param {number} [quality=1.0] - The image quality (for JPEG).
878
+ * @param {string} [windowName='Exported Image'] - The window name.
879
+ * @param {Object} [opts={}] - Additional options.
880
+ * @returns {Promise<Object>} Resolves to an object containing export data.
945
881
  */
882
+ const rasterExport = (
883
+ imgType = 'PNG',
884
+ quality = 1.0,
885
+ windowName = 'Exported Image',
886
+ opts = {}
887
+ ) => {
888
+ return new Promise((resolve, reject) => {
889
+ const type = imgType === 'ICO' ? 'BMP' : imgType
890
+ const mimeType = `image/${type.toLowerCase()}`
891
+ const { issues, issueCodes } = getIssues()
892
+ const svgElement = svgCanvas.getSvgContent()
893
+
894
+ const svgClone = svgElement.cloneNode(true)
895
+
896
+ convertImagesToBase64(svgClone)
897
+ .then(() => {
898
+ const svgData = new XMLSerializer().serializeToString(svgClone)
899
+ const svgBlob = new Blob([svgData], {
900
+ type: 'image/svg+xml;charset=utf-8'
901
+ })
902
+ const url = URL.createObjectURL(svgBlob)
903
+
904
+ const canvas = document.createElement('canvas')
905
+ const ctx = canvas.getContext('2d')
906
+
907
+ const width = svgElement.clientWidth || svgElement.getAttribute('width')
908
+ const height =
909
+ svgElement.clientHeight || svgElement.getAttribute('height')
910
+ canvas.width = width
911
+ canvas.height = height
912
+
913
+ const img = new Image()
914
+ img.onload = () => {
915
+ ctx.drawImage(img, 0, 0, width, height)
916
+ URL.revokeObjectURL(url)
917
+
918
+ const datauri = canvas.toDataURL(mimeType, quality)
919
+ let blobUrl
920
+
921
+ const onExportComplete = blobUrl => {
922
+ const exportObj = {
923
+ datauri,
924
+ bloburl: blobUrl,
925
+ svg: svgData,
926
+ issues,
927
+ issueCodes,
928
+ type: imgType,
929
+ mimeType,
930
+ quality,
931
+ windowName
932
+ }
933
+ if (!opts.avoidEvent) {
934
+ svgCanvas.call('exported', exportObj)
935
+ }
936
+ resolve(exportObj)
937
+ }
938
+
939
+ canvas.toBlob(
940
+ blob => {
941
+ blobUrl = URL.createObjectURL(blob)
942
+ onExportComplete(blobUrl)
943
+ },
944
+ mimeType,
945
+ quality
946
+ )
947
+ }
948
+
949
+ img.onerror = err => {
950
+ console.error('Failed to load SVG into image element:', err)
951
+ reject(err)
952
+ }
953
+
954
+ img.src = url
955
+ })
956
+ .catch(reject)
957
+ })
958
+ }
946
959
 
947
960
  /**
948
- * Generates a PDF based on the current image, then calls "edPDF" with
949
- * an object including the string, the data URL, and any issues found.
950
- * @function module:svgcanvas.SvgCanvas#PDF
951
- * @param {string} [WindowName] Will also be used for the download file name here
952
- * @param {external:jsPDF.OutputType} [outputType="dataurlstring"]
953
- * @fires module:svgcanvas.SvgCanvas#event:edPDF
954
- * @returns {Promise<module:svgcanvas.PDFedResults>} Resolves to {@link module:svgcanvas.PDFedResults}
961
+ * Exports the SVG content as a PDF.
962
+ * @param {string} [windowName='svg.pdf'] - The window name or file name.
963
+ * @param {string} [outputType='save'|'dataurlstring'] - The output type for jsPDF.
964
+ * @returns {Promise<Object>} Resolves to an object containing PDF export data.
955
965
  */
956
- const exportPDF = async (
957
- WindowName,
958
- outputType = isChrome() ? 'save' : undefined
966
+ const exportPDF = (
967
+ windowName = 'svg.pdf',
968
+ outputType = isChrome() ? 'save' : 'dataurlstring'
959
969
  ) => {
960
- const res = svgCanvas.getResolution()
961
- const orientation = res.w > res.h ? 'landscape' : 'portrait'
962
- const unit = 'pt' // curConfig.baseUnit; // We could use baseUnit, but that is presumably not intended for purposes
963
- const iframe = document.createElement('iframe')
964
- iframe.onload = () => {
965
- const iframedoc = iframe.contentDocument || iframe.contentWindow.document
966
- const ele = svgCanvas.getSvgContent()
967
- const cln = ele.cloneNode(true)
968
- iframedoc.body.appendChild(cln)
969
- setTimeout(() => {
970
- // eslint-disable-next-line promise/catch-or-return
971
- html2canvas(iframedoc.body, { useCORS: true, allowTaint: true }).then(
972
- canvas => {
970
+ return new Promise((resolve, reject) => {
971
+ const res = svgCanvas.getResolution()
972
+ const orientation = res.w > res.h ? 'landscape' : 'portrait'
973
+ const unit = 'pt'
974
+ const svgElement = svgCanvas.getSvgContent().cloneNode(true)
975
+
976
+ convertImagesToBase64(svgElement)
977
+ .then(() => {
978
+ const svgData = new XMLSerializer().serializeToString(svgElement)
979
+ const svgBlob = new Blob([svgData], {
980
+ type: 'image/svg+xml;charset=utf-8'
981
+ })
982
+ const url = URL.createObjectURL(svgBlob)
983
+
984
+ const canvas = document.createElement('canvas')
985
+ const ctx = canvas.getContext('2d')
986
+ canvas.width = res.w
987
+ canvas.height = res.h
988
+
989
+ const img = new Image()
990
+ img.onload = () => {
991
+ ctx.drawImage(img, 0, 0, res.w, res.h)
992
+ URL.revokeObjectURL(url)
993
+
973
994
  const imgData = canvas.toDataURL('image/png')
974
- const doc = new JsPDF({
975
- orientation,
976
- unit,
977
- format: [res.w, res.h]
978
- })
995
+ const doc = new JsPDF({ orientation, unit, format: [res.w, res.h] })
996
+
979
997
  const docTitle = svgCanvas.getDocumentTitle()
980
- doc.setProperties({
981
- title: docTitle
982
- })
998
+ doc.setProperties({ title: docTitle })
983
999
  doc.addImage(imgData, 'PNG', 0, 0, res.w, res.h)
984
- iframe.parentNode.removeChild(iframe)
1000
+
985
1001
  const { issues, issueCodes } = getIssues()
986
- outputType = outputType || 'dataurlstring'
987
- const obj = { issues, issueCodes, WindowName, outputType }
1002
+ const obj = { issues, issueCodes, windowName, outputType }
1003
+
988
1004
  obj.output = doc.output(
989
1005
  outputType,
990
- outputType === 'save' ? WindowName || 'svg.pdf' : undefined
1006
+ outputType === 'save' ? windowName : undefined
991
1007
  )
992
- svgCanvas.call('edPDF', obj)
993
- return obj
1008
+
1009
+ svgCanvas.call('exportedPDF', obj)
1010
+ resolve(obj)
994
1011
  }
995
- )
996
- }, 1000)
997
- }
998
- document.body.appendChild(iframe)
1012
+
1013
+ img.onerror = err => {
1014
+ console.error('Failed to load SVG into image element:', err)
1015
+ reject(err)
1016
+ }
1017
+
1018
+ img.src = url
1019
+ })
1020
+ .catch(reject)
1021
+ })
999
1022
  }
1000
1023
  /**
1001
1024
  * Ensure each element has a unique ID.