Skip to content

Commit 7528c49

Browse files
authored
Merge pull request #376 from GREENRAT-K405/feature/embedded-images-graphml
Add feature for Embedded Workflows for Images (PNG, SVG, JPEG)
2 parents e6c728a + d277b62 commit 7528c49

7 files changed

Lines changed: 204 additions & 9 deletions

File tree

package-lock.json

Lines changed: 43 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@
2424
"lucide-react": "^0.487.0",
2525
"md5": "^2.3.0",
2626
"moment": "^2.29.4",
27+
"png-chunk-text": "^1.0.0",
28+
"png-chunks-encode": "^1.0.0",
29+
"png-chunks-extract": "^1.0.0",
2730
"prismjs": "^1.30.0",
2831
"process": "^0.11.10",
2932
"rc-slider": "^9.7.2",

src/component/File-drag-drop.jsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,9 @@ const app = ({ superState, dispatcher }) => {
4040
e.preventDefault();
4141
fileRef.current.value = null;
4242
const droppedFile = e.dataTransfer.files[0];
43-
const ext = droppedFile && droppedFile.name.split('.').slice(-1)[0];
44-
if (e.dataTransfer.files.length === 1 && (ext === 'graphml' || ext === 'json')) {
43+
const ext = droppedFile && droppedFile.name.split('.').slice(-1)[0]?.toLowerCase();
44+
const allowed = ['graphml', 'json', 'png', 'jpg', 'jpeg'];
45+
if (e.dataTransfer.files.length === 1 && allowed.includes(ext)) {
4546
readFile(superStateRef.current, dispatcherRef.current, droppedFile);
4647
}
4748
};
@@ -70,7 +71,7 @@ const app = ({ superState, dispatcher }) => {
7071
ref={fileRef}
7172
onClick={(e) => { e.target.value = null; }}
7273
style={{ display: 'none' }}
73-
accept=".graphml,.json"
74+
accept=".graphml,.json,.png,.jpg,.jpeg"
7475
onChange={(e) => readFile(superState, dispatcher, e.target.files[0])}
7576
/>
7677
<span className="arrow">&#10230;</span>

src/component/fileBrowser.jsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,15 +53,16 @@ const LocalFileBrowser = ({ superState, dispatcher }) => {
5353
}, [superState.fileState]);
5454

5555
const handleSelectFile = (data) => {
56-
const fileExtensions = ['jpeg', 'jpg', 'png', 'exe'];
56+
const fileExtensions = ['exe'];
5757
const fileExt = data.fileObj.name.split('.').pop().toLowerCase();
5858
if (fileExtensions.includes(fileExt)) {
5959
// eslint-disable-next-line no-alert
6060
alert('Wrong file extension');
6161
return;
6262
}
6363

64-
if (fileExt === 'graphml' || fileExt === 'json') {
64+
const allowedExts = ['graphml', 'json', 'png', 'jpg', 'jpeg'];
65+
if (allowedExts.includes(fileExt)) {
6566
let foundi = -1;
6667
superState.graphs.forEach((g, i) => {
6768
if ((g.fileName === data.fileObj.name)) {
@@ -140,6 +141,8 @@ const LocalFileBrowser = ({ superState, dispatcher }) => {
140141
accept: {
141142
'text/graphml': ['.graphml'],
142143
'application/json': ['.json'],
144+
'image/png': ['.png'],
145+
'image/jpeg': ['.jpg', '.jpeg'],
143146
},
144147
},
145148
],
@@ -207,7 +210,7 @@ const LocalFileBrowser = ({ superState, dispatcher }) => {
207210
ref={fileRef}
208211
onClick={(e) => { e.target.value = null; }}
209212
style={{ display: 'none' }}
210-
accept=".graphml,.json"
213+
accept=".graphml,.json,.png,.jpg,.jpeg"
211214
onChange={(e) => readFile(superState, dispatcher, e.target.files[0])}
212215
/>
213216
)}

src/graph-builder/graph-core/5-load-save.js

Lines changed: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import { saveAs } from 'file-saver';
22
import { toast } from 'react-toastify';
3+
import extractChunks from 'png-chunks-extract';
4+
import encodeChunks from 'png-chunks-encode';
5+
import textChunk from 'png-chunk-text';
36
import localStorageManager from '../local-storage-manager';
47
import graphmlBuilder from '../graphml/builder';
58
import BendingDistanceWeight from '../calculations/bending-dist-weight';
@@ -30,8 +33,64 @@ class GraphLoadSave extends GraphUndoRedo {
3033
downloadImg(format) {
3134
this.cy.emit('hide-bend');
3235
this.cy.$('.eh-handle').remove();
33-
if (format === 'PNG') saveAs(this.cy.png({ full: true }), `${this.getName()}-DHGWorkflow.png`);
34-
if (format === 'JPG') saveAs(this.cy.jpg({ full: true }), `${this.getName()}-DHGWorkflow.jpg`);
36+
if (format === 'JPG') {
37+
saveAs(this.cy.jpg({ full: true }), `${this.getName()}-DHGWorkflow.jpg`);
38+
return;
39+
}
40+
if (format === 'PNG') {
41+
saveAs(this.cy.png({ full: true }), `${this.getName()}-DHGWorkflow.png`);
42+
return;
43+
}
44+
if (format === 'PNG-EMBEDDED') {
45+
const b64Uri = this.cy.png({ full: true });
46+
const binaryString = window.atob(b64Uri.split(',')[1]);
47+
const buffer = new Uint8Array(binaryString.length);
48+
for (let i = 0; i < binaryString.length; i += 1) {
49+
buffer[i] = binaryString.charCodeAt(i);
50+
}
51+
const chunks = extractChunks(buffer);
52+
chunks.splice(-1, 0, textChunk.encode('graphml', this.getGraphML()));
53+
const newBuffer = new Uint8Array(encodeChunks(chunks));
54+
const blob = new Blob([newBuffer], { type: 'image/png' });
55+
saveAs(blob, `${this.getName()}.graphml.png`);
56+
return;
57+
}
58+
if (format === 'JPG-EMBEDDED') {
59+
const b64Uri = this.cy.jpg({ full: true });
60+
const binaryString = window.atob(b64Uri.split(',')[1]);
61+
const buffer = new Uint8Array(binaryString.length);
62+
for (let i = 0; i < binaryString.length; i += 1) {
63+
buffer[i] = binaryString.charCodeAt(i);
64+
}
65+
const graphMLStr = this.getGraphML();
66+
const graphMLBytes = new TextEncoder().encode(graphMLStr);
67+
68+
const comSegments = [];
69+
for (let i = 0; i < graphMLBytes.length; i += 65533) {
70+
const chunk = graphMLBytes.slice(i, i + 65533);
71+
const segmentLen = chunk.length + 2;
72+
const seg = new Uint8Array(4 + chunk.length);
73+
seg[0] = 0xFF;
74+
seg[1] = 0xFE;
75+
seg[2] = Math.floor(segmentLen / 256);
76+
seg[3] = segmentLen % 256;
77+
seg.set(chunk, 4);
78+
comSegments.push(seg);
79+
}
80+
81+
const totalComSize = comSegments.reduce((sum, seg) => sum + seg.length, 0);
82+
const newBuffer = new Uint8Array(buffer.length + totalComSize);
83+
newBuffer.set(buffer.slice(0, 2), 0); // FF D8
84+
let offset = 2;
85+
comSegments.forEach((seg) => {
86+
newBuffer.set(seg, offset);
87+
offset += seg.length;
88+
});
89+
newBuffer.set(buffer.slice(2), offset);
90+
91+
const blob = new Blob([newBuffer], { type: 'image/jpeg' });
92+
saveAs(blob, `${this.getName()}.graphml.jpg`);
93+
}
3594
}
3695

3796
shouldNodeBeSaved(nodeID) {

src/toolbarActions/toolbarFunctions.js

Lines changed: 85 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { saveAs } from 'file-saver';
22
import { toast } from 'react-toastify';
3+
import extractChunks from 'png-chunks-extract';
4+
import textChunk from 'png-chunk-text';
35
import parser from '../graph-builder/graphml/parser';
46
import { actionType as T } from '../reducer';
57

@@ -150,7 +152,7 @@ const readFile = async (state, setState, file, fileHandle) => {
150152
}
151153
const fr = new FileReader();
152154
const projectName = file.name;
153-
const ext = file.name.split('.').pop();
155+
const ext = file.name.split('.').pop()?.toLowerCase();
154156
if (ext === 'graphml') {
155157
fr.onload = (x) => {
156158
parser(x.target.result).then(({ authorName }) => {
@@ -186,6 +188,88 @@ const readFile = async (state, setState, file, fileHandle) => {
186188
}
187189
};
188190
fr.readAsText(file);
191+
} else if (ext === 'png') {
192+
fr.onload = (x) => {
193+
try {
194+
const buffer = new Uint8Array(x.target.result);
195+
const chunks = extractChunks(buffer);
196+
const textChunks = chunks.filter((c) => c.name === 'tEXt').map((c) => textChunk.decode(c));
197+
const graphMLMeta = textChunks.find((c) => c.keyword === 'graphml');
198+
if (graphMLMeta && graphMLMeta.text) {
199+
parser(graphMLMeta.text).then(({ authorName }) => {
200+
setState({
201+
type: T.ADD_GRAPH,
202+
payload: {
203+
projectName,
204+
graphML: graphMLMeta.text,
205+
fileHandle: null,
206+
fileName: file.name,
207+
authorName,
208+
},
209+
});
210+
toast.success('Imported embedded GraphML from Image!');
211+
}).catch(() => toast.error('Embedded GraphML inside PNG is invalid.'));
212+
} else {
213+
toast.error('This PNG does not contain an embedded GraphML Workflow.');
214+
}
215+
} catch (err) {
216+
toast.error('Could not parse the PNG file.');
217+
}
218+
};
219+
if (fileHandle) fr.readAsArrayBuffer(await fileHandle.getFile());
220+
else fr.readAsArrayBuffer(file);
221+
} else if (ext === 'jpg' || ext === 'jpeg') {
222+
fr.onload = (x) => {
223+
try {
224+
const buffer = new Uint8Array(x.target.result);
225+
let pos = 2; // skip SOI
226+
const comSegments = [];
227+
while (pos < buffer.length) {
228+
if (buffer[pos] !== 0xFF) break;
229+
const marker = buffer[pos + 1];
230+
if (marker === 0xDA) break; // SOS - Start of Scan
231+
const len = (buffer[pos + 2] * 256) + buffer[pos + 3];
232+
if (marker === 0xFE) { // COM Comment segment
233+
comSegments.push(buffer.slice(pos + 4, pos + 2 + len));
234+
}
235+
pos += 2 + len;
236+
}
237+
238+
let graphMLData = '';
239+
if (comSegments.length > 0) {
240+
const totalLength = comSegments.reduce((sum, seg) => sum + seg.length, 0);
241+
const allBytes = new Uint8Array(totalLength);
242+
let offset = 0;
243+
for (let i = 0; i < comSegments.length; i += 1) {
244+
allBytes.set(comSegments[i], offset);
245+
offset += comSegments[i].length;
246+
}
247+
graphMLData = new TextDecoder().decode(allBytes);
248+
}
249+
250+
if (graphMLData) {
251+
parser(graphMLData).then(({ authorName }) => {
252+
setState({
253+
type: T.ADD_GRAPH,
254+
payload: {
255+
projectName,
256+
graphML: graphMLData,
257+
fileHandle: null,
258+
fileName: file.name,
259+
authorName,
260+
},
261+
});
262+
toast.success('Imported embedded GraphML from JPEG!');
263+
}).catch(() => toast.error('Embedded GraphML inside JPEG is invalid.'));
264+
} else {
265+
toast.error('This JPEG does not contain an embedded GraphML Workflow.');
266+
}
267+
} catch (err) {
268+
toast.error('Could not parse the JPEG file.');
269+
}
270+
};
271+
if (fileHandle) fr.readAsArrayBuffer(await fileHandle.getFile());
272+
else fr.readAsArrayBuffer(file);
189273
}
190274
}
191275
};

src/toolbarActions/toolbarList.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,7 +303,9 @@ const toolbarList = (state, dispatcher) => [
303303
icon: FaDownload,
304304
action: (s, d) => [
305305
{ fn: () => downloadImg(s, d, 'JPG'), name: 'JPG' },
306+
{ fn: () => downloadImg(s, d, 'JPG-EMBEDDED'), name: 'JPG (with GraphML)' },
306307
{ fn: () => downloadImg(s, d, 'PNG'), name: 'PNG' },
308+
{ fn: () => downloadImg(s, d, 'PNG-EMBEDDED'), name: 'PNG (with GraphML)' },
307309
{ fn: () => saveAsJson(s, d), name: 'JSON' },
308310
],
309311
visibility: true,

0 commit comments

Comments
 (0)