diff --git a/package-lock.json b/package-lock.json index 09be349bb..a7be70c36 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,7 +17,6 @@ "browser-fs-access": "0.35.0", "core-js": "3.38.1", "elix": "15.0.1", - "html2canvas": "1.4.1", "i18next": "23.15.1", "jspdf": "2.5.1", "pathseg": "1.2.1", @@ -5358,6 +5357,7 @@ "node_modules/base64-arraybuffer": { "version": "1.0.2", "license": "MIT", + "optional": true, "engines": { "node": ">= 0.6.0" } @@ -6593,6 +6593,7 @@ "node_modules/css-line-break": { "version": "2.1.0", "license": "MIT", + "optional": true, "dependencies": { "utrie": "^1.0.2" } @@ -9089,6 +9090,7 @@ "node_modules/html2canvas": { "version": "1.4.1", "license": "MIT", + "optional": true, "dependencies": { "css-line-break": "^2.1.0", "text-segmentation": "^1.0.3" @@ -16524,6 +16526,7 @@ "node_modules/text-segmentation": { "version": "1.0.3", "license": "MIT", + "optional": true, "dependencies": { "utrie": "^1.0.2" } @@ -17398,6 +17401,7 @@ "node_modules/utrie": { "version": "1.0.2", "license": "MIT", + "optional": true, "dependencies": { "base64-arraybuffer": "^1.0.2" } diff --git a/package.json b/package.json index d4d4cc446..c7a897541 100644 --- a/package.json +++ b/package.json @@ -89,7 +89,6 @@ "browser-fs-access": "0.35.0", "core-js": "3.38.1", "elix": "15.0.1", - "html2canvas": "1.4.1", "i18next": "23.15.1", "jspdf": "2.5.1", "pathseg": "1.2.1", @@ -108,8 +107,8 @@ "@rollup/plugin-dynamic-import-vars": "2.1.2", "@rollup/plugin-node-resolve": "15.2.3", "@rollup/plugin-replace": "5.0.7", - "@rollup/plugin-url": "8.0.2", "@rollup/plugin-terser": "0.4.4", + "@rollup/plugin-url": "8.0.2", "@web/dev-server": "0.4.6", "@web/dev-server-rollup": "0.6.4", "babel-plugin-istanbul": "7.0.0", @@ -140,6 +139,6 @@ "start-server-and-test": "2.0.8" }, "optionalDependencies": { - "@rollup/rollup-linux-x64-gnu": "4.21.3" + "@rollup/rollup-linux-x64-gnu": "4.21.3" } } diff --git a/packages/svgcanvas/core/svg-exec.js b/packages/svgcanvas/core/svg-exec.js index e22f97746..0fe07b50c 100644 --- a/packages/svgcanvas/core/svg-exec.js +++ b/packages/svgcanvas/core/svg-exec.js @@ -7,8 +7,7 @@ import { jsPDF as JsPDF } from 'jspdf' import 'svg2pdf.js' -import html2canvas from 'html2canvas' -import * as hstry from './history.js' +import * as history from './history.js' import { text2xml, cleanupElement, @@ -17,8 +16,6 @@ import { preventClickDefault, toXml, getStrokedBBoxDefaultVisible, - createObjectURL, - dataURLToObjectURL, walkTree, getBBox as utilsGetBBox, hashCode @@ -41,7 +38,7 @@ const { RemoveElementCommand, ChangeElementCommand, BatchCommand -} = hstry +} = history let svgCanvas = null @@ -846,156 +843,182 @@ const getIssues = () => { */ /** - * Generates a PNG (or JPG, BMP, WEBP) Data URL based on the current image, - * then calls "ed" with an object including the string, image - * information, and any issues found. - * @function module:svgcanvas.SvgCanvas#raster - * @param {"PNG"|"JPEG"|"BMP"|"WEBP"|"ICO"} [imgType="PNG"] - * @param {Float} [quality] Between 0 and 1 - * @param {string} [WindowName] - * @param {PlainObject} [opts] - * @param {boolean} [opts.avoidEvent] - * @fires module:svgcanvas.SvgCanvas#event:ed - * @todo Confirm/fix ICO type - * @returns {Promise} Resolves to {@link module:svgcanvas.ImageedResults} + * Utility function to convert all external image links in an SVG element to Base64 data URLs. + * @param {SVGElement} svgElement - The SVG element to process. + * @returns {Promise} */ -const rasterExport = async (imgType, quality, WindowName, opts = {}) => { - const type = imgType === 'ICO' ? 'BMP' : imgType || 'PNG' - const mimeType = 'image/' + type.toLowerCase() - const { issues, issueCodes } = getIssues() - const svg = svgCanvas.svgCanvasToString() - - const iframe = document.createElement('iframe') - iframe.onload = () => { - const iframedoc = iframe.contentDocument || iframe.contentWindow.document - const ele = svgCanvas.getSvgContent() - const cln = ele.cloneNode(true) - iframedoc.body.appendChild(cln) - setTimeout(() => { - // eslint-disable-next-line promise/catch-or-return - html2canvas(iframedoc.body, { useCORS: true, allowTaint: true }).then( - canvas => { - return new Promise(resolve => { - const dataURLType = type.toLowerCase() - const datauri = quality - ? canvas.toDataURL('image/' + dataURLType, quality) - : canvas.toDataURL('image/' + dataURLType) - iframe.parentNode.removeChild(iframe) - let bloburl - - const done = () => { - const obj = { - datauri, - bloburl, - svg, - issues, - issueCodes, - type: imgType, - mimeType, - quality, - WindowName - } - if (!opts.avoidEvent) { - svgCanvas.call('exported', obj) - } - resolve(obj) - } - if (canvas.toBlob) { - canvas.toBlob( - blob => { - bloburl = createObjectURL(blob) - done() - }, - mimeType, - quality - ) - return - } - bloburl = dataURLToObjectURL(datauri) - done() - }) - } - ) - }, 1000) - } - document.body.appendChild(iframe) +const convertImagesToBase64 = async svgElement => { + const imageElements = svgElement.querySelectorAll('image') + const promises = Array.from(imageElements).map(async img => { + const href = img.getAttribute('xlink:href') || img.getAttribute('href') + if (href && !href.startsWith('data:')) { + try { + const response = await fetch(href) + const blob = await response.blob() + const reader = new FileReader() + return new Promise(resolve => { + reader.onload = () => { + img.setAttribute('xlink:href', reader.result) + resolve() + } + reader.readAsDataURL(blob) + }) + } catch (error) { + console.error('Failed to fetch image:', error) + } + } + }) + await Promise.all(promises) } /** - * @typedef {void|"save"|"arraybuffer"|"blob"|"datauristring"|"dataurlstring"|"dataurlnewwindow"|"datauri"|"dataurl"} external:jsPDF.OutputType - * @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 - */ -/** - * @typedef {PlainObject} module:svgcanvas.PDFedResults - * @property {string} svg The SVG PDF output - * @property {string|ArrayBuffer|Blob|window} output The output based on the `outputType`; - * if `undefined`, "datauristring", "dataurlstring", "datauri", - * or "dataurl", will be a string (`undefined` gives a document, while the others - * build as Data URLs; "datauri" and "dataurl" change the location of the current page); if - * "arraybuffer", will return `ArrayBuffer`; if "blob", returns a `Blob`; - * if "dataurlnewwindow", will change the current page's location and return a string - * if in Safari and no window object is found; otherwise opens in, and returns, a new `window` - * object; if "save", will have the same return as "dataurlnewwindow" if - * `navigator.getUserMedia` support is found without `URL.createObjectURL` support; otherwise - * returns `undefined` but attempts to save - * @property {external:jsPDF.OutputType} outputType - * @property {string[]} issues The human-readable localization messages of corresponding `issueCodes` - * @property {module:svgcanvas.IssueCode[]} issueCodes - * @property {string} WindowName + * Generates a raster image (PNG, JPEG, etc.) from the SVG content. + * @param {string} [imgType='PNG'] - The image type to generate. + * @param {number} [quality=1.0] - The image quality (for JPEG). + * @param {string} [windowName='Exported Image'] - The window name. + * @param {Object} [opts={}] - Additional options. + * @returns {Promise} Resolves to an object containing export data. */ +const rasterExport = ( + imgType = 'PNG', + quality = 1.0, + windowName = 'Exported Image', + opts = {} +) => { + return new Promise((resolve, reject) => { + const type = imgType === 'ICO' ? 'BMP' : imgType + const mimeType = `image/${type.toLowerCase()}` + const { issues, issueCodes } = getIssues() + const svgElement = svgCanvas.getSvgContent() + + const svgClone = svgElement.cloneNode(true) + + convertImagesToBase64(svgClone) + .then(() => { + const svgData = new XMLSerializer().serializeToString(svgClone) + const svgBlob = new Blob([svgData], { + type: 'image/svg+xml;charset=utf-8' + }) + const url = URL.createObjectURL(svgBlob) + + const canvas = document.createElement('canvas') + const ctx = canvas.getContext('2d') + + const width = svgElement.clientWidth || svgElement.getAttribute('width') + const height = + svgElement.clientHeight || svgElement.getAttribute('height') + canvas.width = width + canvas.height = height + + const img = new Image() + img.onload = () => { + ctx.drawImage(img, 0, 0, width, height) + URL.revokeObjectURL(url) + + const datauri = canvas.toDataURL(mimeType, quality) + let blobUrl + + const onExportComplete = blobUrl => { + const exportObj = { + datauri, + bloburl: blobUrl, + svg: svgData, + issues, + issueCodes, + type: imgType, + mimeType, + quality, + windowName + } + if (!opts.avoidEvent) { + svgCanvas.call('exported', exportObj) + } + resolve(exportObj) + } + + canvas.toBlob( + blob => { + blobUrl = URL.createObjectURL(blob) + onExportComplete(blobUrl) + }, + mimeType, + quality + ) + } + + img.onerror = err => { + console.error('Failed to load SVG into image element:', err) + reject(err) + } + + img.src = url + }) + .catch(reject) + }) +} /** - * Generates a PDF based on the current image, then calls "edPDF" with - * an object including the string, the data URL, and any issues found. - * @function module:svgcanvas.SvgCanvas#PDF - * @param {string} [WindowName] Will also be used for the download file name here - * @param {external:jsPDF.OutputType} [outputType="dataurlstring"] - * @fires module:svgcanvas.SvgCanvas#event:edPDF - * @returns {Promise} Resolves to {@link module:svgcanvas.PDFedResults} + * Exports the SVG content as a PDF. + * @param {string} [windowName='svg.pdf'] - The window name or file name. + * @param {string} [outputType='save'|'dataurlstring'] - The output type for jsPDF. + * @returns {Promise} Resolves to an object containing PDF export data. */ -const exportPDF = async ( - WindowName, - outputType = isChrome() ? 'save' : undefined +const exportPDF = ( + windowName = 'svg.pdf', + outputType = isChrome() ? 'save' : 'dataurlstring' ) => { - const res = svgCanvas.getResolution() - const orientation = res.w > res.h ? 'landscape' : 'portrait' - const unit = 'pt' // curConfig.baseUnit; // We could use baseUnit, but that is presumably not intended for purposes - const iframe = document.createElement('iframe') - iframe.onload = () => { - const iframedoc = iframe.contentDocument || iframe.contentWindow.document - const ele = svgCanvas.getSvgContent() - const cln = ele.cloneNode(true) - iframedoc.body.appendChild(cln) - setTimeout(() => { - // eslint-disable-next-line promise/catch-or-return - html2canvas(iframedoc.body, { useCORS: true, allowTaint: true }).then( - canvas => { + return new Promise((resolve, reject) => { + const res = svgCanvas.getResolution() + const orientation = res.w > res.h ? 'landscape' : 'portrait' + const unit = 'pt' + const svgElement = svgCanvas.getSvgContent().cloneNode(true) + + convertImagesToBase64(svgElement) + .then(() => { + const svgData = new XMLSerializer().serializeToString(svgElement) + const svgBlob = new Blob([svgData], { + type: 'image/svg+xml;charset=utf-8' + }) + const url = URL.createObjectURL(svgBlob) + + const canvas = document.createElement('canvas') + const ctx = canvas.getContext('2d') + canvas.width = res.w + canvas.height = res.h + + const img = new Image() + img.onload = () => { + ctx.drawImage(img, 0, 0, res.w, res.h) + URL.revokeObjectURL(url) + const imgData = canvas.toDataURL('image/png') - const doc = new JsPDF({ - orientation, - unit, - format: [res.w, res.h] - }) + const doc = new JsPDF({ orientation, unit, format: [res.w, res.h] }) + const docTitle = svgCanvas.getDocumentTitle() - doc.setProperties({ - title: docTitle - }) + doc.setProperties({ title: docTitle }) doc.addImage(imgData, 'PNG', 0, 0, res.w, res.h) - iframe.parentNode.removeChild(iframe) + const { issues, issueCodes } = getIssues() - outputType = outputType || 'dataurlstring' - const obj = { issues, issueCodes, WindowName, outputType } + const obj = { issues, issueCodes, windowName, outputType } + obj.output = doc.output( outputType, - outputType === 'save' ? WindowName || 'svg.pdf' : undefined + outputType === 'save' ? windowName : undefined ) - svgCanvas.call('edPDF', obj) - return obj + + svgCanvas.call('exportedPDF', obj) + resolve(obj) } - ) - }, 1000) - } - document.body.appendChild(iframe) + + img.onerror = err => { + console.error('Failed to load SVG into image element:', err) + reject(err) + } + + img.src = url + }) + .catch(reject) + }) } /** * Ensure each element has a unique ID. diff --git a/src/editor/MainMenu.js b/src/editor/MainMenu.js index 53ad44cea..5101273ab 100644 --- a/src/editor/MainMenu.js +++ b/src/editor/MainMenu.js @@ -17,7 +17,7 @@ class MainMenu { /** * @type {Integer} */ - this.exportWindowCt = 0 + this.editor.exportWindowCt = 0 } /**