From fb4646ba3a65c1a9449caabc36409a1b62e3a457 Mon Sep 17 00:00:00 2001 From: GREENRAT-K405 Date: Sat, 28 Mar 2026 17:38:05 +0530 Subject: [PATCH 1/4] Add feature for embedded images --- package-lock.json | 53 ++++++++++ package.json | 4 + src/component/File-drag-drop.jsx | 7 +- src/graph-builder/graph-core/1-core.js | 4 + src/graph-builder/graph-core/5-load-save.js | 74 +++++++++++++- src/toolbarActions/toolbarFunctions.js | 104 +++++++++++++++++++- src/toolbarActions/toolbarList.js | 4 + 7 files changed, 244 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index 543af29..f2d5eed 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,6 +21,7 @@ "cytoscape-edgehandles": "^3.6.0", "cytoscape-grid-guide": "^2.3.2", "cytoscape-node-editing": "^4.0.0", + "cytoscape-svg": "^0.4.0", "file-saver": "^2.0.5", "hotkeys-js": "^3.8.7", "jquery": "^3.0.0", @@ -28,6 +29,9 @@ "lucide-react": "^0.487.0", "md5": "^2.3.0", "moment": "^2.29.4", + "png-chunk-text": "^1.0.0", + "png-chunks-encode": "^1.0.0", + "png-chunks-extract": "^1.0.0", "prismjs": "^1.30.0", "process": "^0.11.10", "rc-slider": "^9.7.2", @@ -6935,6 +6939,15 @@ "typescript": ">=3" } }, + "node_modules/crc-32": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-0.3.0.tgz", + "integrity": "sha512-kucVIjOmMc1f0tv53BJ/5WIX+MGLcKuoBhnGqQrgKJNqLByb/sVMWfW/Aw6hw0jgcqjJ2pi9E5y32zOIpaUlsA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/create-require": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", @@ -7372,6 +7385,15 @@ "konva": "^7.0.3" } }, + "node_modules/cytoscape-svg": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/cytoscape-svg/-/cytoscape-svg-0.4.0.tgz", + "integrity": "sha512-omqIzfPd1Vy9mk6lHTiR2wTbjxELxb9GXSQ2pE6W+GwAe/6/yvOUQ2h5ApFf2QhCBnpMwLkCTq5DZXxBCgUpDw==", + "license": "GNU GPLv3", + "peerDependencies": { + "cytoscape": "^3.2.0" + } + }, "node_modules/damerau-levenshtein": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", @@ -15055,6 +15077,31 @@ "node": ">=4" } }, + "node_modules/png-chunk-text": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/png-chunk-text/-/png-chunk-text-1.0.0.tgz", + "integrity": "sha512-DEROKU3SkkLGWNMzru3xPVgxyd48UGuMSZvioErCure6yhOc/pRH2ZV+SEn7nmaf7WNf3NdIpH+UTrRdKyq9Lw==", + "license": "MIT" + }, + "node_modules/png-chunks-encode": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/png-chunks-encode/-/png-chunks-encode-1.0.0.tgz", + "integrity": "sha512-J1jcHgbQRsIIgx5wxW9UmCymV3wwn4qCCJl6KYgEU/yHCh/L2Mwq/nMOkRPtmV79TLxRZj5w3tH69pvygFkDqA==", + "license": "MIT", + "dependencies": { + "crc-32": "^0.3.0", + "sliced": "^1.0.1" + } + }, + "node_modules/png-chunks-extract": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/png-chunks-extract/-/png-chunks-extract-1.0.0.tgz", + "integrity": "sha512-ZiVwF5EJ0DNZyzAqld8BP1qyJBaGOFaq9zl579qfbkcmOwWLLO4I9L8i2O4j3HkI6/35i0nKG2n+dZplxiT89Q==", + "license": "MIT", + "dependencies": { + "crc-32": "^0.3.0" + } + }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", @@ -18674,6 +18721,12 @@ "url": "https://github.com/chalk/slice-ansi?sponsor=1" } }, + "node_modules/sliced": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/sliced/-/sliced-1.0.1.tgz", + "integrity": "sha512-VZBmZP8WU3sMOZm1bdgTadsQbcscK0UM8oKxKVBs4XAhUo2Xxzm/OFMGBkPusxw9xL3Uy8LrzEqGqJhclsr0yA==", + "license": "MIT" + }, "node_modules/sockjs": { "version": "0.3.24", "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz", diff --git a/package.json b/package.json index 55462af..446deeb 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "cytoscape-edgehandles": "^3.6.0", "cytoscape-grid-guide": "^2.3.2", "cytoscape-node-editing": "^4.0.0", + "cytoscape-svg": "^0.4.0", "file-saver": "^2.0.5", "hotkeys-js": "^3.8.7", "jquery": "^3.0.0", @@ -24,6 +25,9 @@ "lucide-react": "^0.487.0", "md5": "^2.3.0", "moment": "^2.29.4", + "png-chunk-text": "^1.0.0", + "png-chunks-encode": "^1.0.0", + "png-chunks-extract": "^1.0.0", "prismjs": "^1.30.0", "process": "^0.11.10", "rc-slider": "^9.7.2", diff --git a/src/component/File-drag-drop.jsx b/src/component/File-drag-drop.jsx index bde6938..8c2bd92 100644 --- a/src/component/File-drag-drop.jsx +++ b/src/component/File-drag-drop.jsx @@ -40,8 +40,9 @@ const app = ({ superState, dispatcher }) => { e.preventDefault(); fileRef.current.value = null; const droppedFile = e.dataTransfer.files[0]; - const ext = droppedFile && droppedFile.name.split('.').slice(-1)[0]; - if (e.dataTransfer.files.length === 1 && (ext === 'graphml' || ext === 'json')) { + const ext = droppedFile && droppedFile.name.split('.').slice(-1)[0]?.toLowerCase(); + const allowed = ['graphml', 'json', 'png', 'svg', 'jpg', 'jpeg']; + if (e.dataTransfer.files.length === 1 && allowed.includes(ext)) { readFile(superStateRef.current, dispatcherRef.current, droppedFile); } }; @@ -70,7 +71,7 @@ const app = ({ superState, dispatcher }) => { ref={fileRef} onClick={(e) => { e.target.value = null; }} style={{ display: 'none' }} - accept=".graphml,.json" + accept=".graphml,.json,.png,.svg,.jpg,.jpeg" onChange={(e) => readFile(superState, dispatcher, e.target.files[0])} /> diff --git a/src/graph-builder/graph-core/1-core.js b/src/graph-builder/graph-core/1-core.js index 14d2b79..dc75e13 100644 --- a/src/graph-builder/graph-core/1-core.js +++ b/src/graph-builder/graph-core/1-core.js @@ -1,6 +1,7 @@ import cytoscape from 'cytoscape'; import edgehandles from 'cytoscape-edgehandles'; import gridGuide from 'cytoscape-grid-guide'; +import svg from 'cytoscape-svg'; import Konva from 'konva'; import nodeEditing from 'cytoscape-node-editing'; import $ from 'jquery'; @@ -44,6 +45,9 @@ class CoreGraph { if (typeof cytoscape('core', 'gridGuide') !== 'function') { gridGuide(cytoscape); } + if (typeof cytoscape('core', 'svg') !== 'function') { + cytoscape.use(svg); + } // if (cy) this.cy = cy; this.cy = cytoscape({ ...cyOptions(darkMode), container: element }); this.cy.on('position', 'node', () => { diff --git a/src/graph-builder/graph-core/5-load-save.js b/src/graph-builder/graph-core/5-load-save.js index da74be7..3080a3c 100644 --- a/src/graph-builder/graph-core/5-load-save.js +++ b/src/graph-builder/graph-core/5-load-save.js @@ -1,5 +1,8 @@ import { saveAs } from 'file-saver'; import { toast } from 'react-toastify'; +import extractChunks from 'png-chunks-extract'; +import encodeChunks from 'png-chunks-encode'; +import textChunk from 'png-chunk-text'; import localStorageManager from '../local-storage-manager'; import graphmlBuilder from '../graphml/builder'; import BendingDistanceWeight from '../calculations/bending-dist-weight'; @@ -30,8 +33,75 @@ class GraphLoadSave extends GraphUndoRedo { downloadImg(format) { this.cy.emit('hide-bend'); this.cy.$('.eh-handle').remove(); - if (format === 'PNG') saveAs(this.cy.png({ full: true }), `${this.getName()}-DHGWorkflow.png`); - if (format === 'JPG') saveAs(this.cy.jpg({ full: true }), `${this.getName()}-DHGWorkflow.jpg`); + if (format === 'JPG') { + saveAs(this.cy.jpg({ full: true }), `${this.getName()}-DHGWorkflow.jpg`); + return; + } + if (format === 'PNG') { + saveAs(this.cy.png({ full: true }), `${this.getName()}-DHGWorkflow.png`); + return; + } + if (format === 'PNG-EMBEDDED') { + const b64Uri = this.cy.png({ full: true }); + const b64Data = b64Uri.split(',')[1]; + const buffer = new Uint8Array(window.atob(b64Data).split('').map((c) => c.charCodeAt(0))); + const chunks = extractChunks(buffer); + chunks.splice(-1, 0, textChunk.encode('graphml', this.getGraphML())); + const newBuffer = new Uint8Array(encodeChunks(chunks)); + const blob = new Blob([newBuffer], { type: 'image/png' }); + saveAs(blob, `${this.getName()}.graphml.png`); + return; + } + if (format === 'SVG') { + const blob = new Blob([this.cy.svg({ full: true })], { type: 'image/svg+xml;charset=utf-8' }); + saveAs(blob, `${this.getName()}-DHGWorkflow.svg`); + return; + } + if (format === 'SVG-EMBEDDED') { + const svgStr = this.cy.svg({ full: true }); + const parser = new DOMParser(); + const doc = parser.parseFromString(svgStr, 'image/svg+xml'); + const metadata = doc.createElementNS('http://www.w3.org/2000/svg', 'metadata'); + metadata.setAttribute('data-graphml', this.getGraphML()); + doc.documentElement.insertBefore(metadata, doc.documentElement.firstChild); + const newSvg = new XMLSerializer().serializeToString(doc); + const blob = new Blob([newSvg], { type: 'image/svg+xml;charset=utf-8' }); + saveAs(blob, `${this.getName()}.graphml.svg`); + return; + } + if (format === 'JPG-EMBEDDED') { + const b64Uri = this.cy.jpg({ full: true }); + const b64Data = b64Uri.split(',')[1]; + const buffer = new Uint8Array(window.atob(b64Data).split('').map((c) => c.charCodeAt(0))); + const graphMLStr = this.getGraphML(); + const graphMLBytes = new TextEncoder().encode(graphMLStr); + + const comSegments = []; + for (let i = 0; i < graphMLBytes.length; i += 65533) { + const chunk = graphMLBytes.slice(i, i + 65533); + const segmentLen = chunk.length + 2; + const seg = new Uint8Array(4 + chunk.length); + seg[0] = 0xFF; + seg[1] = 0xFE; + seg[2] = Math.floor(segmentLen / 256); + seg[3] = segmentLen % 256; + seg.set(chunk, 4); + comSegments.push(seg); + } + + const totalComSize = comSegments.reduce((sum, seg) => sum + seg.length, 0); + const newBuffer = new Uint8Array(buffer.length + totalComSize); + newBuffer.set(buffer.slice(0, 2), 0); // FF D8 + let offset = 2; + comSegments.forEach((seg) => { + newBuffer.set(seg, offset); + offset += seg.length; + }); + newBuffer.set(buffer.slice(2), offset); + + const blob = new Blob([newBuffer], { type: 'image/jpeg' }); + saveAs(blob, `${this.getName()}.graphml.jpg`); + } } shouldNodeBeSaved(nodeID) { diff --git a/src/toolbarActions/toolbarFunctions.js b/src/toolbarActions/toolbarFunctions.js index 9dda574..fbe6c32 100644 --- a/src/toolbarActions/toolbarFunctions.js +++ b/src/toolbarActions/toolbarFunctions.js @@ -1,5 +1,7 @@ import { saveAs } from 'file-saver'; import { toast } from 'react-toastify'; +import extractChunks from 'png-chunks-extract'; +import textChunk from 'png-chunk-text'; import parser from '../graph-builder/graphml/parser'; import { actionType as T } from '../reducer'; @@ -150,7 +152,7 @@ const readFile = async (state, setState, file, fileHandle) => { } const fr = new FileReader(); const projectName = file.name; - const ext = file.name.split('.').pop(); + const ext = file.name.split('.').pop()?.toLowerCase(); if (ext === 'graphml') { fr.onload = (x) => { parser(x.target.result).then(({ authorName }) => { @@ -186,6 +188,106 @@ const readFile = async (state, setState, file, fileHandle) => { } }; fr.readAsText(file); + } else if (ext === 'png') { + fr.onload = (x) => { + try { + const buffer = new Uint8Array(x.target.result); + const chunks = extractChunks(buffer); + const textChunks = chunks.filter((c) => c.name === 'tEXt').map((c) => textChunk.decode(c)); + const graphMLMeta = textChunks.find((c) => c.keyword === 'graphml'); + if (graphMLMeta && graphMLMeta.text) { + parser(graphMLMeta.text).then(({ authorName }) => { + setState({ + type: T.ADD_GRAPH, + payload: { + projectName, + graphML: graphMLMeta.text, + fileHandle: null, + fileName: file.name, + authorName, + }, + }); + toast.success('Imported embedded GraphML from Image!'); + }).catch(() => toast.error('Embedded GraphML inside PNG is invalid.')); + } else { + toast.error('This PNG does not contain an embedded GraphML Workflow.'); + } + } catch (err) { + toast.error('Could not parse the PNG file.'); + } + }; + if (fileHandle) fr.readAsArrayBuffer(await fileHandle.getFile()); + else fr.readAsArrayBuffer(file); + } else if (ext === 'svg') { + fr.onload = (x) => { + try { + const parserDOM = new DOMParser(); + const svgDoc = parserDOM.parseFromString(x.target.result, 'image/svg+xml'); + const metadata = svgDoc.getElementsByTagName('metadata')[0]; + const graphML = metadata ? metadata.getAttribute('data-graphml') : null; + if (graphML) { + parser(graphML).then(({ authorName }) => { + setState({ + type: T.ADD_GRAPH, + payload: { + projectName, + graphML, + fileHandle: null, + fileName: file.name, + authorName, + }, + }); + toast.success('Imported embedded GraphML from SVG!'); + }).catch(() => toast.error('Embedded GraphML inside SVG is invalid.')); + } else { + toast.error('This SVG does not contain an embedded GraphML Workflow.'); + } + } catch (err) { + toast.error('Could not parse the SVG file.'); + } + }; + if (fileHandle) fr.readAsText(await fileHandle.getFile()); + else fr.readAsText(file); + } else if (ext === 'jpg' || ext === 'jpeg') { + fr.onload = (x) => { + try { + const buffer = new Uint8Array(x.target.result); + let pos = 2; // skip SOI + let graphMLData = ''; + while (pos < buffer.length) { + if (buffer[pos] !== 0xFF) break; + const marker = buffer[pos + 1]; + if (marker === 0xDA) break; // SOS - Start of Scan + const len = (buffer[pos + 2] * 256) + buffer[pos + 3]; + if (marker === 0xFE) { // COM Comment segment + graphMLData += new TextDecoder().decode(buffer.slice(pos + 4, pos + 2 + len)); + } + pos += 2 + len; + } + + if (graphMLData) { + parser(graphMLData).then(({ authorName }) => { + setState({ + type: T.ADD_GRAPH, + payload: { + projectName, + graphML: graphMLData, + fileHandle: null, + fileName: file.name, + authorName, + }, + }); + toast.success('Imported embedded GraphML from JPEG!'); + }).catch(() => toast.error('Embedded GraphML inside JPEG is invalid.')); + } else { + toast.error('This JPEG does not contain an embedded GraphML Workflow.'); + } + } catch (err) { + toast.error('Could not parse the JPEG file.'); + } + }; + if (fileHandle) fr.readAsArrayBuffer(await fileHandle.getFile()); + else fr.readAsArrayBuffer(file); } } }; diff --git a/src/toolbarActions/toolbarList.js b/src/toolbarActions/toolbarList.js index 77ae50c..54d1260 100644 --- a/src/toolbarActions/toolbarList.js +++ b/src/toolbarActions/toolbarList.js @@ -303,7 +303,11 @@ const toolbarList = (state, dispatcher) => [ icon: FaDownload, action: (s, d) => [ { fn: () => downloadImg(s, d, 'JPG'), name: 'JPG' }, + { fn: () => downloadImg(s, d, 'JPG-EMBEDDED'), name: 'JPG (with GraphML)' }, { fn: () => downloadImg(s, d, 'PNG'), name: 'PNG' }, + { fn: () => downloadImg(s, d, 'PNG-EMBEDDED'), name: 'PNG (with GraphML)' }, + { fn: () => downloadImg(s, d, 'SVG'), name: 'SVG' }, + { fn: () => downloadImg(s, d, 'SVG-EMBEDDED'), name: 'SVG (with GraphML)' }, { fn: () => saveAsJson(s, d), name: 'JSON' }, ], visibility: true, From feea304c01aa984d7b244f27737521f6177d28a6 Mon Sep 17 00:00:00 2001 From: GREENRAT-K405 Date: Sun, 29 Mar 2026 11:37:33 +0530 Subject: [PATCH 2/4] remove cytoscape-svg causing licensing conflicts --- package-lock.json | 10 ------- package.json | 1 - src/component/File-drag-drop.jsx | 4 +-- src/graph-builder/graph-core/1-core.js | 4 --- src/graph-builder/graph-core/5-load-save.js | 17 ------------ src/toolbarActions/toolbarFunctions.js | 30 --------------------- src/toolbarActions/toolbarList.js | 2 -- 7 files changed, 2 insertions(+), 66 deletions(-) diff --git a/package-lock.json b/package-lock.json index f2d5eed..10ff670 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,7 +21,6 @@ "cytoscape-edgehandles": "^3.6.0", "cytoscape-grid-guide": "^2.3.2", "cytoscape-node-editing": "^4.0.0", - "cytoscape-svg": "^0.4.0", "file-saver": "^2.0.5", "hotkeys-js": "^3.8.7", "jquery": "^3.0.0", @@ -7385,15 +7384,6 @@ "konva": "^7.0.3" } }, - "node_modules/cytoscape-svg": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/cytoscape-svg/-/cytoscape-svg-0.4.0.tgz", - "integrity": "sha512-omqIzfPd1Vy9mk6lHTiR2wTbjxELxb9GXSQ2pE6W+GwAe/6/yvOUQ2h5ApFf2QhCBnpMwLkCTq5DZXxBCgUpDw==", - "license": "GNU GPLv3", - "peerDependencies": { - "cytoscape": "^3.2.0" - } - }, "node_modules/damerau-levenshtein": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", diff --git a/package.json b/package.json index 446deeb..3627c88 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,6 @@ "cytoscape-edgehandles": "^3.6.0", "cytoscape-grid-guide": "^2.3.2", "cytoscape-node-editing": "^4.0.0", - "cytoscape-svg": "^0.4.0", "file-saver": "^2.0.5", "hotkeys-js": "^3.8.7", "jquery": "^3.0.0", diff --git a/src/component/File-drag-drop.jsx b/src/component/File-drag-drop.jsx index 8c2bd92..a629e84 100644 --- a/src/component/File-drag-drop.jsx +++ b/src/component/File-drag-drop.jsx @@ -41,7 +41,7 @@ const app = ({ superState, dispatcher }) => { fileRef.current.value = null; const droppedFile = e.dataTransfer.files[0]; const ext = droppedFile && droppedFile.name.split('.').slice(-1)[0]?.toLowerCase(); - const allowed = ['graphml', 'json', 'png', 'svg', 'jpg', 'jpeg']; + const allowed = ['graphml', 'json', 'png', 'jpg', 'jpeg']; if (e.dataTransfer.files.length === 1 && allowed.includes(ext)) { readFile(superStateRef.current, dispatcherRef.current, droppedFile); } @@ -71,7 +71,7 @@ const app = ({ superState, dispatcher }) => { ref={fileRef} onClick={(e) => { e.target.value = null; }} style={{ display: 'none' }} - accept=".graphml,.json,.png,.svg,.jpg,.jpeg" + accept=".graphml,.json,.png,.jpg,.jpeg" onChange={(e) => readFile(superState, dispatcher, e.target.files[0])} /> diff --git a/src/graph-builder/graph-core/1-core.js b/src/graph-builder/graph-core/1-core.js index dc75e13..14d2b79 100644 --- a/src/graph-builder/graph-core/1-core.js +++ b/src/graph-builder/graph-core/1-core.js @@ -1,7 +1,6 @@ import cytoscape from 'cytoscape'; import edgehandles from 'cytoscape-edgehandles'; import gridGuide from 'cytoscape-grid-guide'; -import svg from 'cytoscape-svg'; import Konva from 'konva'; import nodeEditing from 'cytoscape-node-editing'; import $ from 'jquery'; @@ -45,9 +44,6 @@ class CoreGraph { if (typeof cytoscape('core', 'gridGuide') !== 'function') { gridGuide(cytoscape); } - if (typeof cytoscape('core', 'svg') !== 'function') { - cytoscape.use(svg); - } // if (cy) this.cy = cy; this.cy = cytoscape({ ...cyOptions(darkMode), container: element }); this.cy.on('position', 'node', () => { diff --git a/src/graph-builder/graph-core/5-load-save.js b/src/graph-builder/graph-core/5-load-save.js index 3080a3c..6c3b8b4 100644 --- a/src/graph-builder/graph-core/5-load-save.js +++ b/src/graph-builder/graph-core/5-load-save.js @@ -52,23 +52,6 @@ class GraphLoadSave extends GraphUndoRedo { saveAs(blob, `${this.getName()}.graphml.png`); return; } - if (format === 'SVG') { - const blob = new Blob([this.cy.svg({ full: true })], { type: 'image/svg+xml;charset=utf-8' }); - saveAs(blob, `${this.getName()}-DHGWorkflow.svg`); - return; - } - if (format === 'SVG-EMBEDDED') { - const svgStr = this.cy.svg({ full: true }); - const parser = new DOMParser(); - const doc = parser.parseFromString(svgStr, 'image/svg+xml'); - const metadata = doc.createElementNS('http://www.w3.org/2000/svg', 'metadata'); - metadata.setAttribute('data-graphml', this.getGraphML()); - doc.documentElement.insertBefore(metadata, doc.documentElement.firstChild); - const newSvg = new XMLSerializer().serializeToString(doc); - const blob = new Blob([newSvg], { type: 'image/svg+xml;charset=utf-8' }); - saveAs(blob, `${this.getName()}.graphml.svg`); - return; - } if (format === 'JPG-EMBEDDED') { const b64Uri = this.cy.jpg({ full: true }); const b64Data = b64Uri.split(',')[1]; diff --git a/src/toolbarActions/toolbarFunctions.js b/src/toolbarActions/toolbarFunctions.js index fbe6c32..e9e47b5 100644 --- a/src/toolbarActions/toolbarFunctions.js +++ b/src/toolbarActions/toolbarFunctions.js @@ -218,36 +218,6 @@ const readFile = async (state, setState, file, fileHandle) => { }; if (fileHandle) fr.readAsArrayBuffer(await fileHandle.getFile()); else fr.readAsArrayBuffer(file); - } else if (ext === 'svg') { - fr.onload = (x) => { - try { - const parserDOM = new DOMParser(); - const svgDoc = parserDOM.parseFromString(x.target.result, 'image/svg+xml'); - const metadata = svgDoc.getElementsByTagName('metadata')[0]; - const graphML = metadata ? metadata.getAttribute('data-graphml') : null; - if (graphML) { - parser(graphML).then(({ authorName }) => { - setState({ - type: T.ADD_GRAPH, - payload: { - projectName, - graphML, - fileHandle: null, - fileName: file.name, - authorName, - }, - }); - toast.success('Imported embedded GraphML from SVG!'); - }).catch(() => toast.error('Embedded GraphML inside SVG is invalid.')); - } else { - toast.error('This SVG does not contain an embedded GraphML Workflow.'); - } - } catch (err) { - toast.error('Could not parse the SVG file.'); - } - }; - if (fileHandle) fr.readAsText(await fileHandle.getFile()); - else fr.readAsText(file); } else if (ext === 'jpg' || ext === 'jpeg') { fr.onload = (x) => { try { diff --git a/src/toolbarActions/toolbarList.js b/src/toolbarActions/toolbarList.js index 54d1260..f9ff6c7 100644 --- a/src/toolbarActions/toolbarList.js +++ b/src/toolbarActions/toolbarList.js @@ -306,8 +306,6 @@ const toolbarList = (state, dispatcher) => [ { fn: () => downloadImg(s, d, 'JPG-EMBEDDED'), name: 'JPG (with GraphML)' }, { fn: () => downloadImg(s, d, 'PNG'), name: 'PNG' }, { fn: () => downloadImg(s, d, 'PNG-EMBEDDED'), name: 'PNG (with GraphML)' }, - { fn: () => downloadImg(s, d, 'SVG'), name: 'SVG' }, - { fn: () => downloadImg(s, d, 'SVG-EMBEDDED'), name: 'SVG (with GraphML)' }, { fn: () => saveAsJson(s, d), name: 'JSON' }, ], visibility: true, From 3d9d8a858480961eecccdd3b384c90c36b6e7e2c Mon Sep 17 00:00:00 2001 From: GREENRAT-K405 Date: Sun, 29 Mar 2026 11:44:24 +0530 Subject: [PATCH 3/4] add memory optimization and UTF-8 safety --- src/graph-builder/graph-core/5-load-save.js | 14 ++++++++++---- src/toolbarActions/toolbarFunctions.js | 16 ++++++++++++++-- 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/src/graph-builder/graph-core/5-load-save.js b/src/graph-builder/graph-core/5-load-save.js index 6c3b8b4..078d9fb 100644 --- a/src/graph-builder/graph-core/5-load-save.js +++ b/src/graph-builder/graph-core/5-load-save.js @@ -43,8 +43,11 @@ class GraphLoadSave extends GraphUndoRedo { } if (format === 'PNG-EMBEDDED') { const b64Uri = this.cy.png({ full: true }); - const b64Data = b64Uri.split(',')[1]; - const buffer = new Uint8Array(window.atob(b64Data).split('').map((c) => c.charCodeAt(0))); + const binaryString = window.atob(b64Uri.split(',')[1]); + const buffer = new Uint8Array(binaryString.length); + for (let i = 0; i < binaryString.length; i += 1) { + buffer[i] = binaryString.charCodeAt(i); + } const chunks = extractChunks(buffer); chunks.splice(-1, 0, textChunk.encode('graphml', this.getGraphML())); const newBuffer = new Uint8Array(encodeChunks(chunks)); @@ -54,8 +57,11 @@ class GraphLoadSave extends GraphUndoRedo { } if (format === 'JPG-EMBEDDED') { const b64Uri = this.cy.jpg({ full: true }); - const b64Data = b64Uri.split(',')[1]; - const buffer = new Uint8Array(window.atob(b64Data).split('').map((c) => c.charCodeAt(0))); + const binaryString = window.atob(b64Uri.split(',')[1]); + const buffer = new Uint8Array(binaryString.length); + for (let i = 0; i < binaryString.length; i += 1) { + buffer[i] = binaryString.charCodeAt(i); + } const graphMLStr = this.getGraphML(); const graphMLBytes = new TextEncoder().encode(graphMLStr); diff --git a/src/toolbarActions/toolbarFunctions.js b/src/toolbarActions/toolbarFunctions.js index e9e47b5..134f70a 100644 --- a/src/toolbarActions/toolbarFunctions.js +++ b/src/toolbarActions/toolbarFunctions.js @@ -223,18 +223,30 @@ const readFile = async (state, setState, file, fileHandle) => { try { const buffer = new Uint8Array(x.target.result); let pos = 2; // skip SOI - let graphMLData = ''; + const comSegments = []; while (pos < buffer.length) { if (buffer[pos] !== 0xFF) break; const marker = buffer[pos + 1]; if (marker === 0xDA) break; // SOS - Start of Scan const len = (buffer[pos + 2] * 256) + buffer[pos + 3]; if (marker === 0xFE) { // COM Comment segment - graphMLData += new TextDecoder().decode(buffer.slice(pos + 4, pos + 2 + len)); + comSegments.push(buffer.slice(pos + 4, pos + 2 + len)); } pos += 2 + len; } + let graphMLData = ''; + if (comSegments.length > 0) { + const totalLength = comSegments.reduce((sum, seg) => sum + seg.length, 0); + const allBytes = new Uint8Array(totalLength); + let offset = 0; + for (let i = 0; i < comSegments.length; i += 1) { + allBytes.set(comSegments[i], offset); + offset += comSegments[i].length; + } + graphMLData = new TextDecoder().decode(allBytes); + } + if (graphMLData) { parser(graphMLData).then(({ authorName }) => { setState({ From d277b6281a2a4766ad5205014b969c987880de5d Mon Sep 17 00:00:00 2001 From: GREENRAT-K405 Date: Sun, 29 Mar 2026 11:50:45 +0530 Subject: [PATCH 4/4] add UI drag-drop and manual file supported for embedded images --- src/component/fileBrowser.jsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/component/fileBrowser.jsx b/src/component/fileBrowser.jsx index af8da77..1ab72c1 100644 --- a/src/component/fileBrowser.jsx +++ b/src/component/fileBrowser.jsx @@ -53,7 +53,7 @@ const LocalFileBrowser = ({ superState, dispatcher }) => { }, [superState.fileState]); const handleSelectFile = (data) => { - const fileExtensions = ['jpeg', 'jpg', 'png', 'exe']; + const fileExtensions = ['exe']; const fileExt = data.fileObj.name.split('.').pop().toLowerCase(); if (fileExtensions.includes(fileExt)) { // eslint-disable-next-line no-alert @@ -61,7 +61,8 @@ const LocalFileBrowser = ({ superState, dispatcher }) => { return; } - if (fileExt === 'graphml' || fileExt === 'json') { + const allowedExts = ['graphml', 'json', 'png', 'jpg', 'jpeg']; + if (allowedExts.includes(fileExt)) { let foundi = -1; superState.graphs.forEach((g, i) => { if ((g.fileName === data.fileObj.name)) { @@ -140,6 +141,8 @@ const LocalFileBrowser = ({ superState, dispatcher }) => { accept: { 'text/graphml': ['.graphml'], 'application/json': ['.json'], + 'image/png': ['.png'], + 'image/jpeg': ['.jpg', '.jpeg'], }, }, ], @@ -207,7 +210,7 @@ const LocalFileBrowser = ({ superState, dispatcher }) => { ref={fileRef} onClick={(e) => { e.target.value = null; }} style={{ display: 'none' }} - accept=".graphml,.json" + accept=".graphml,.json,.png,.jpg,.jpeg" onChange={(e) => readFile(superState, dispatcher, e.target.files[0])} /> )}