Add support for image export.

This commit is contained in:
Paulo Gustavo Veiga 2021-12-31 01:18:21 -08:00
parent 478ba7a417
commit 85f560324b
10 changed files with 106 additions and 96 deletions

View File

@ -36,6 +36,7 @@ import DragManager from './DragManager';
import RelationshipPivot from './RelationshipPivot'; import RelationshipPivot from './RelationshipPivot';
import Relationship from './Relationship'; import Relationship from './Relationship';
import SVGExporter from './export/SVGExporter'; import SVGExporter from './export/SVGExporter';
import PNGExporter from './export/PNGExporter';
import TopicEventDispatcher, { TopicEvent } from './TopicEventDispatcher'; import TopicEventDispatcher, { TopicEvent } from './TopicEventDispatcher';
import TopicFeatureFactory from './TopicFeature'; import TopicFeatureFactory from './TopicFeature';
@ -355,15 +356,27 @@ class Designer extends Events {
} }
export(formatType) { export(formatType) {
const svgElement = this._workspace.getSVGElement(); const workspace = this._workspace;
const svgElement = workspace.getSVGElement();
const size = workspace.getSize();
const mindmap = this._mindmap; const mindmap = this._mindmap;
let result = ''; let exporter;
if (formatType === 'svg') { switch (formatType) {
const exporter = new SVGExporter(mindmap, svgElement); case 'svg': {
result = exporter.export(); 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();
} }
/** /**

View File

@ -1,6 +1,6 @@
interface Exporter { interface Exporter {
export(): string; export(): Promise<string>;
} }
export default Exporter; export default Exporter;

View File

@ -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;

View File

@ -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<string> {
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<string>((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;

View File

@ -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;

View File

@ -8,16 +8,19 @@ class SVGExporter implements Exporter {
this.svgElement = svgElement; this.svgElement = svgElement;
} }
export(): string { export(): Promise<string> {
// Replace all images for in-line images ... // Replace all images for in-line images ...
const imagesElements: HTMLCollection = this.svgElement.getElementsByTagName('image'); 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 ... // Are namespace declared ?. Otherwise, force the declaration ...
if(result.indexOf('xmlns:xlink=')!=-1){ if(svgTxt.indexOf('xmlns:xlink=')!==-1){
result.replace('<svg ', '<svg xmlns:xlink="http://www.w3.org/1999/xlink" ') svgTxt.replace('<svg ', '<svg xmlns:xlink="http://www.w3.org/1999/xlink" ')
} }
return result;
const blob = new Blob([svgTxt], { type: 'image/svg+xml' });
const result = URL.createObjectURL(blob);
return Promise.resolve(result);
} }
} }

View File

@ -212,27 +212,25 @@ class Menu extends IMenu {
} }
this._addButton('export', false, false, () => { this._addButton('export', false, false, () => {
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 ... // Trigger click ...
const blob = new Blob([svgContent], { type: 'image/svg+xml' }); anchor.click();
const win = window.URL || window.webkitURL || window;
const svgUri = win.createObjectURL(blob); // Clean up ...
console.log(svgContent); document.body.removeChild(anchor);
});
// Create anchor element ... // 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')); Menu._registerTooltip('export', $msg('EXPORT'));
this._addButton('print', false, false, () => { this._addButton('print', false, false, () => {

View File

@ -22,7 +22,6 @@ import Mindmap from './components/model/Mindmap';
import PersistenceManager from './components/PersistenceManager'; import PersistenceManager from './components/PersistenceManager';
import Designer from './components/Designer'; import Designer from './components/Designer';
import LocalStorageManager from './components/LocalStorageManager'; import LocalStorageManager from './components/LocalStorageManager';
import TxtExporter from './components/export/PlainTextExporter';
import Menu from './components/widget/Menu'; import Menu from './components/widget/Menu';
@ -37,5 +36,4 @@ export {
LocalStorageManager, LocalStorageManager,
Menu, Menu,
DesignerBuilder, DesignerBuilder,
TxtExporter,
}; };

View File

@ -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());
});

View File

@ -3,8 +3,9 @@ import fs from 'fs';
import path from 'path'; import path from 'path';
import XMLSerializerFactory from '../../../src/components/persistence/XMLSerializerFactory'; import XMLSerializerFactory from '../../../src/components/persistence/XMLSerializerFactory';
import SVGExporter from '../../../src/components/export/SVGExporter'; 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 ... // Load mindmap DOM ...
const mindmapPath = path.resolve(__dirname, './samples/welcome.xml'); const mindmapPath = path.resolve(__dirname, './samples/welcome.xml');
const mapDocument = parseXMLFile(mindmapPath, 'text/xml'); const mapDocument = parseXMLFile(mindmapPath, 'text/xml');
@ -18,21 +19,24 @@ test('mindplot generation of simple maps', () => {
const svgDocument = parseXMLFile(svgPath, 'image/svg+xml'); const svgDocument = parseXMLFile(svgPath, 'image/svg+xml');
// Inspect ... // Inspect ...
const exporter = new SVGExporter(mindmap, svgDocument.documentElement); // const svgExporter = new SVGExporter(mindmap, svgDocument.documentElement);
console.log('Exported map:' + exporter.export()); // 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;
}