From 85f560324b04da3d69b0cc4639dfbbedf66c0bb7 Mon Sep 17 00:00:00 2001 From: Paulo Gustavo Veiga Date: Fri, 31 Dec 2021 01:18:21 -0800 Subject: [PATCH] Add support for image export. --- packages/mindplot/src/components/Designer.js | 25 ++++++++--- .../src/components/export/Exporter.ts | 2 +- .../src/components/export/FreemindExporter.ts | 14 ------ .../src/components/export/PNGExporter.ts | 43 +++++++++++++++++++ .../components/export/PlainTextExporter.ts | 15 ------- .../src/components/export/SVGExporter.ts | 13 +++--- .../mindplot/src/components/widget/Menu.js | 32 +++++++------- packages/mindplot/src/index.js | 2 - .../export/PlainTextExporterTestSuite.test.ts | 20 --------- .../unit/export/SVGExporterTestSuite.test.ts | 36 +++++++++------- 10 files changed, 106 insertions(+), 96 deletions(-) delete mode 100644 packages/mindplot/src/components/export/FreemindExporter.ts create mode 100644 packages/mindplot/src/components/export/PNGExporter.ts delete mode 100644 packages/mindplot/src/components/export/PlainTextExporter.ts delete mode 100644 packages/mindplot/test/unit/export/PlainTextExporterTestSuite.test.ts diff --git a/packages/mindplot/src/components/Designer.js b/packages/mindplot/src/components/Designer.js index 6f3c8507..553ef0b8 100644 --- a/packages/mindplot/src/components/Designer.js +++ b/packages/mindplot/src/components/Designer.js @@ -36,6 +36,7 @@ import DragManager from './DragManager'; import RelationshipPivot from './RelationshipPivot'; import Relationship from './Relationship'; import SVGExporter from './export/SVGExporter'; +import PNGExporter from './export/PNGExporter'; import TopicEventDispatcher, { TopicEvent } from './TopicEventDispatcher'; import TopicFeatureFactory from './TopicFeature'; @@ -355,15 +356,27 @@ class Designer extends Events { } export(formatType) { - const svgElement = this._workspace.getSVGElement(); + const workspace = this._workspace; + const svgElement = workspace.getSVGElement(); + const size = workspace.getSize(); + const mindmap = this._mindmap; - let result = ''; - if (formatType === 'svg') { - const exporter = new SVGExporter(mindmap, svgElement); - result = exporter.export(); + let exporter; + switch (formatType) { + case 'svg': { + exporter = new SVGExporter(mindmap, svgElement); + break; + } + case 'png': { + exporter = new PNGExporter(mindmap, svgElement, size.width, size.height); + break; + } + default: + throw new Error('Unsupported encoding'); } - return result; + + return exporter.export(); } /** diff --git a/packages/mindplot/src/components/export/Exporter.ts b/packages/mindplot/src/components/export/Exporter.ts index 65b196a5..55bb29c4 100644 --- a/packages/mindplot/src/components/export/Exporter.ts +++ b/packages/mindplot/src/components/export/Exporter.ts @@ -1,6 +1,6 @@ interface Exporter { - export(): string; + export(): Promise; } export default Exporter; \ No newline at end of file diff --git a/packages/mindplot/src/components/export/FreemindExporter.ts b/packages/mindplot/src/components/export/FreemindExporter.ts deleted file mode 100644 index 57ba68f4..00000000 --- a/packages/mindplot/src/components/export/FreemindExporter.ts +++ /dev/null @@ -1,14 +0,0 @@ -import Exporter from "./Exporter"; -import Mindmap from "../model/Mindmap"; - -class FreemindExporter implements Exporter { - mindplot: Mindmap; - constructor(mindplot: Mindmap) { - this.mindplot = mindplot; - } - - export(): string { - return "TBI"; - } -} -export default FreemindExporter; \ No newline at end of file diff --git a/packages/mindplot/src/components/export/PNGExporter.ts b/packages/mindplot/src/components/export/PNGExporter.ts new file mode 100644 index 00000000..b3e01c8e --- /dev/null +++ b/packages/mindplot/src/components/export/PNGExporter.ts @@ -0,0 +1,43 @@ + +import { Mindmap } from "../.."; +import Exporter from "./Exporter"; +import SVGExporter from "./SVGExporter"; + +class PNGExporter implements Exporter { + svgElement: Element; + mindmap: Mindmap; + width: number; + height: number; + + constructor(mindmap: Mindmap, svgElement: Element, width: number, height: number) { + this.svgElement = svgElement; + this.mindmap = mindmap; + this.width = width; + this.height = height; + } + + async export(): Promise { + const svgExporter = new SVGExporter(this.mindmap, this.svgElement); + const svgUrl = await svgExporter.export(); + + // Create canvas ... + const canvas = document.createElement('canvas'); + canvas.setAttribute('width', this.width.toString()); + canvas.setAttribute('height', this.height.toString()); + + // Render the image and wait for the response ... + const img = new Image(); + const result = new Promise((resolve, reject) => { + + img.onload = () => { + canvas.getContext('2d').drawImage(img, 0, 0); + const imgDataUri = canvas.toDataURL('image/png').replace('image/png', 'octet/stream'); + URL.revokeObjectURL(imgDataUri); + resolve(imgDataUri); + } + }); + img.src = svgUrl; + return result; + } +} +export default PNGExporter; diff --git a/packages/mindplot/src/components/export/PlainTextExporter.ts b/packages/mindplot/src/components/export/PlainTextExporter.ts deleted file mode 100644 index 31fad13d..00000000 --- a/packages/mindplot/src/components/export/PlainTextExporter.ts +++ /dev/null @@ -1,15 +0,0 @@ -import Exporter from "./Exporter"; -import Mindmap from "../model/Mindmap"; - - -class PlainTextExporter implements Exporter { - mindplot: Mindmap; - constructor(mindplot: Mindmap) { - this.mindplot = mindplot; - } - - export(): string { - return "TBI"; - } -} -export default PlainTextExporter; \ No newline at end of file diff --git a/packages/mindplot/src/components/export/SVGExporter.ts b/packages/mindplot/src/components/export/SVGExporter.ts index c54e01aa..63b8c7f7 100644 --- a/packages/mindplot/src/components/export/SVGExporter.ts +++ b/packages/mindplot/src/components/export/SVGExporter.ts @@ -8,16 +8,19 @@ class SVGExporter implements Exporter { this.svgElement = svgElement; } - export(): string { + export(): Promise { // Replace all images for in-line images ... const imagesElements: HTMLCollection = this.svgElement.getElementsByTagName('image'); - let result:string = new XMLSerializer().serializeToString(this.svgElement); + let svgTxt:string = new XMLSerializer().serializeToString(this.svgElement); // Are namespace declared ?. Otherwise, force the declaration ... - if(result.indexOf('xmlns:xlink=')!=-1){ - result.replace(' { - const svgContent = designer.export('svg'); + const formatType = 'png'; + designer.export(formatType).then((url) => { + // Create hidden anchor to force download ... + const anchor = document.createElement('a'); + anchor.style = 'display: none'; + anchor.download = `${mapId}.${formatType}`; + anchor.href = url; + document.body.appendChild(anchor); - // Encode content ... - const blob = new Blob([svgContent], { type: 'image/svg+xml' }); - const win = window.URL || window.webkitURL || window; - const svgUri = win.createObjectURL(blob); - console.log(svgContent); + // Trigger click ... + anchor.click(); + + // Clean up ... + document.body.removeChild(anchor); + }); // Create anchor element ... - const anchor = document.createElement('a'); - anchor.style = 'display: none'; - anchor.download = `${mapId}.svg`; - anchor.href = svgUri; - document.body.appendChild(anchor); - - // Trigger click ... - anchor.click(); - - // Clean up ... - document.body.removeChild(anchor); }); + Menu._registerTooltip('export', $msg('EXPORT')); this._addButton('print', false, false, () => { diff --git a/packages/mindplot/src/index.js b/packages/mindplot/src/index.js index 368b677f..859eea00 100644 --- a/packages/mindplot/src/index.js +++ b/packages/mindplot/src/index.js @@ -22,7 +22,6 @@ import Mindmap from './components/model/Mindmap'; import PersistenceManager from './components/PersistenceManager'; import Designer from './components/Designer'; import LocalStorageManager from './components/LocalStorageManager'; -import TxtExporter from './components/export/PlainTextExporter'; import Menu from './components/widget/Menu'; @@ -37,5 +36,4 @@ export { LocalStorageManager, Menu, DesignerBuilder, - TxtExporter, }; diff --git a/packages/mindplot/test/unit/export/PlainTextExporterTestSuite.test.ts b/packages/mindplot/test/unit/export/PlainTextExporterTestSuite.test.ts deleted file mode 100644 index 7c8dfa01..00000000 --- a/packages/mindplot/test/unit/export/PlainTextExporterTestSuite.test.ts +++ /dev/null @@ -1,20 +0,0 @@ -import PlainTextExporter from '../../../src/components/export/PlainTextExporter'; -import Mindmap from '../../../src/components/model/Mindmap'; -import fs from 'fs'; -import path from 'path'; -import XMLSerializerFactory from '../../../src/components/persistence/XMLSerializerFactory'; - -test('mindplot generation of simple maps', () => { - const parser = new DOMParser(); - - // Load DOM ... - const mapStream =fs.readFileSync(path.resolve(__dirname, './samples/welcome.xml'),{encoding: 'utf-8'}); - const document = parser.parseFromString(mapStream.toString(), 'text/xml') - - // Convert to mindmap ... - const serializer = XMLSerializerFactory.getSerializerFromDocument(document); - const mindmap:Mindmap = serializer.loadFromDom(document,'welcome'); - - const exporter = new PlainTextExporter(mindmap); - console.log(exporter.export()); -}); diff --git a/packages/mindplot/test/unit/export/SVGExporterTestSuite.test.ts b/packages/mindplot/test/unit/export/SVGExporterTestSuite.test.ts index 44dedb81..925265e7 100644 --- a/packages/mindplot/test/unit/export/SVGExporterTestSuite.test.ts +++ b/packages/mindplot/test/unit/export/SVGExporterTestSuite.test.ts @@ -3,8 +3,9 @@ import fs from 'fs'; import path from 'path'; import XMLSerializerFactory from '../../../src/components/persistence/XMLSerializerFactory'; import SVGExporter from '../../../src/components/export/SVGExporter'; +import PNGExporter from '../../../src/components/export/PNGExporter'; -test('mindplot generation of simple maps', () => { +test('mindplot generation of simple maps', async () => { // Load mindmap DOM ... const mindmapPath = path.resolve(__dirname, './samples/welcome.xml'); const mapDocument = parseXMLFile(mindmapPath, 'text/xml'); @@ -18,21 +19,24 @@ test('mindplot generation of simple maps', () => { const svgDocument = parseXMLFile(svgPath, 'image/svg+xml'); // Inspect ... - const exporter = new SVGExporter(mindmap, svgDocument.documentElement); - console.log('Exported map:' + exporter.export()); + // const svgExporter = new SVGExporter(mindmap, svgDocument.documentElement); + // console.log('Exported map:' + await svgExporter.export()); + // const pngExporter = new PNGExporter(mindmap, svgDocument.documentElement, 400, 400); + // console.log('Exported map:' + await pngExporter.export()); - function parseXMLFile(filePath: fs.PathOrFileDescriptor, mimeType: DOMParserSupportedType) { - const parser = new DOMParser(); - const stream = fs.readFileSync(filePath, { encoding: 'utf-8' }); - const xmlDoc = parser.parseFromString(stream.toString(), mimeType); - - // Is there any parsing error ?. - if (xmlDoc.getElementsByTagName("parsererror").length > 0) { - console.log(new XMLSerializer().serializeToString(xmlDoc)); - throw new Error(`Unexpected error parsing: ${filePath}. Error: ${new XMLSerializer().serializeToString(xmlDoc)}`); - } - - return xmlDoc; - } }); + +function parseXMLFile(filePath: fs.PathOrFileDescriptor, mimeType: DOMParserSupportedType) { + const parser = new DOMParser(); + const stream = fs.readFileSync(filePath, { encoding: 'utf-8' }); + const xmlDoc = parser.parseFromString(stream.toString(), mimeType); + + // Is there any parsing error ?. + if (xmlDoc.getElementsByTagName("parsererror").length > 0) { + console.log(new XMLSerializer().serializeToString(xmlDoc)); + throw new Error(`Unexpected error parsing: ${filePath}. Error: ${new XMLSerializer().serializeToString(xmlDoc)}`); + } + + return xmlDoc; +}