@svgedit/svgcanvas 7.2.3 → 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/CHANGES.md +4 -0
- package/core/coords.js +203 -99
- package/core/draw.js +129 -56
- package/core/event.js +1 -0
- package/core/math.js +138 -154
- package/core/recalculate.js +232 -613
- package/core/sanitize.js +3 -3
- package/core/selected-elem.js +1 -1
- package/core/selection.js +1 -1
- package/core/svg-exec.js +163 -140
- package/core/text-actions.js +160 -129
- package/dist/svgcanvas.js +20310 -19109
- package/dist/svgcanvas.js.map +1 -1
- package/package.json +1 -1
- package/svgcanvas.js +1 -1
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
|
package/core/selected-elem.js
CHANGED
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
|
|
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
|
-
} =
|
|
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(
|
|
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
|
-
*
|
|
850
|
-
*
|
|
851
|
-
*
|
|
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
|
|
863
|
-
const
|
|
864
|
-
const
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
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
|
-
*
|
|
926
|
-
* @
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
* @
|
|
930
|
-
* @
|
|
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
|
-
*
|
|
949
|
-
*
|
|
950
|
-
* @
|
|
951
|
-
* @
|
|
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 =
|
|
957
|
-
|
|
958
|
-
outputType = isChrome() ? 'save' :
|
|
966
|
+
const exportPDF = (
|
|
967
|
+
windowName = 'svg.pdf',
|
|
968
|
+
outputType = isChrome() ? 'save' : 'dataurlstring'
|
|
959
969
|
) => {
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1000
|
+
|
|
985
1001
|
const { issues, issueCodes } = getIssues()
|
|
986
|
-
|
|
987
|
-
|
|
1002
|
+
const obj = { issues, issueCodes, windowName, outputType }
|
|
1003
|
+
|
|
988
1004
|
obj.output = doc.output(
|
|
989
1005
|
outputType,
|
|
990
|
-
outputType === 'save' ?
|
|
1006
|
+
outputType === 'save' ? windowName : undefined
|
|
991
1007
|
)
|
|
992
|
-
|
|
993
|
-
|
|
1008
|
+
|
|
1009
|
+
svgCanvas.call('exportedPDF', obj)
|
|
1010
|
+
resolve(obj)
|
|
994
1011
|
}
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
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.
|