Skip to content

Add feature for Embedded Workflows for Images (PNG, SVG, JPEG)#376

Draft
GREENRAT-K405 wants to merge 4 commits intoControlCore-Project:devfrom
GREENRAT-K405:feature/embedded-images-graphml
Draft

Add feature for Embedded Workflows for Images (PNG, SVG, JPEG)#376
GREENRAT-K405 wants to merge 4 commits intoControlCore-Project:devfrom
GREENRAT-K405:feature/embedded-images-graphml

Conversation

@GREENRAT-K405
Copy link
Copy Markdown

@GREENRAT-K405 GREENRAT-K405 commented Mar 28, 2026

feature to let the users export their graphs as standard images (PNG, SVG, JPEG) while embedding the underlying graphml logic directly within the file metadata. These images can be dragged back into concore-editor to instantly reconstruct the interactive graph, making workflow sharing and documentation frictionless.

  • embed graphml data into PNG, SVG, and JPEG files.
  • Images act as both visual documentation and editable source files.
  • The Drag-and-Drop zone and File Open menu now automatically scan for embedded metadata in image files.
  • Large GraphML files are automatically split across multiple segments in JPEG to bypass binary length limits.
  • All byte-level manipulations are implemented using ESLint-friendly arithmetic to ensure build stability.

This closes #375

Copilot AI review requested due to automatic review settings March 28, 2026 12:17
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds “embedded workflow” image export/import so PNG/SVG/JPEG files can carry GraphML metadata and be re-opened in the editor as editable workflows.

Changes:

  • Adds new Export menu options for embedded/non-embedded PNG, SVG, and JPEG.
  • Implements embedding/extraction of GraphML in PNG (tEXt), SVG (<metadata>), and JPEG (COM segments with splitting).
  • Extends drag-and-drop file opening to accept common image formats; adds dependencies for PNG chunk handling and SVG export.

Reviewed changes

Copilot reviewed 6 out of 7 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
src/toolbarActions/toolbarList.js Adds export menu items for embedded/non-embedded PNG/SVG/JPEG.
src/toolbarActions/toolbarFunctions.js Extends readFile to detect and import embedded GraphML from PNG/SVG/JPEG.
src/graph-builder/graph-core/5-load-save.js Implements embedded export logic for PNG/SVG/JPEG, including JPEG segment splitting.
src/graph-builder/graph-core/1-core.js Registers cytoscape-svg extension to enable cy.svg().
src/component/File-drag-drop.jsx Allows drag/drop and file input selection for image formats.
package.json Adds dependencies for SVG export and PNG chunk encoding/decoding.
package-lock.json Locks new dependency tree for the added libraries.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 18 to +30
"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",
"konva": "^7.0.3",
"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",
Copy link

Copilot AI Mar 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New dependency cytoscape-svg is GPLv3-licensed (see package-lock.json entry), while this repo is BSD-3-Clause (LICENSE). This can create a license incompatibility for distribution. Please confirm the project can legally take a GPLv3 dependency or switch to an SVG export approach/library with a permissive license compatible with BSD.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pradeeban Actually, excellent catch by copilot there.

I think the main problem is not cytoscape or even svg as entirety but just this specific plugin cytoscape-svg that I used to add the functionality for svg import export (svg support didn't exist prior to this PR).

cystoscape-svg is GPLv3 licensed and will not be ideal for open source, (we should only stick to MIT licensed and open source).

It's an excellent note to take for me as a contributor to verify the licensing for any new packages we add for some new functionality or some fix. Will keep this in mind for future!

I have fixed this with my first follow-up commit.

Comment on lines +256 to +267
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;
}

Copy link

Copilot AI Mar 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JPEG embedded GraphML parsing concatenates TextDecoder().decode(...) results per COM segment. Since the exporter splits the UTF-8 byte stream into 65,533-byte chunks, it can split a multi-byte UTF-8 sequence across segments; decoding each segment independently can corrupt characters (U+FFFD) and make the reconstructed GraphML invalid. Consider collecting COM segment bytes first and decoding once, or use TextDecoder.decode(..., { stream: true }) across segments before a final flush.

Suggested change
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;
}
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
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 (const seg of comSegments) {
allBytes.set(seg, offset);
offset += seg.length;
}
graphMLData = new TextDecoder().decode(allBytes);
}

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed with second follow-up commit!

Comment on lines +45 to +50
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));
Copy link

Copilot AI Mar 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PNG-EMBEDDED export converts a base64 data URL to bytes via window.atob(...).split('').map(...), which creates large intermediate arrays/strings and can blow up memory for larger graphs. Prefer a single-pass conversion (e.g., preallocate a Uint8Array and fill it in a loop, or convert via fetch(dataUrl).then(r => r.arrayBuffer())) to reduce peak memory and improve performance.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed with second follow-up commit!

Comment on lines +73 to +76
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();
Copy link

Copilot AI Mar 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JPG-EMBEDDED export also converts the base64 data URL to bytes via window.atob(...).split('').map(...), which is memory-heavy for large images. Please switch to a more memory-efficient conversion (single-pass loop into a preallocated Uint8Array, or fetch(dataUrl) to arrayBuffer()).

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed with second follow-up commit!

Comment on lines +43 to 46
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);
Copy link

Copilot AI Mar 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR description says the File Open menu scans image files for embedded metadata, but the file picker in src/component/fileBrowser.jsx still restricts selection to .graphml/.json only (both pickerOpts.types.accept and the fallback <input accept>). Please update the File Open menu accept/types to include .png, .svg, .jpg, .jpeg so the UI matches the new readFile support.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed in third follow up commit!

@GREENRAT-K405
Copy link
Copy Markdown
Author

Recording.2026-03-28.175136.mp4

@pradeeban please have a look!

@GREENRAT-K405
Copy link
Copy Markdown
Author

@pradeeban will create a PR with updates that are necessary to build study with these embedded type workflows in concore repo

@pradeeban
Copy link
Copy Markdown
Member

@GREENRAT-K405 can you check the bot comments above?

@pradeeban pradeeban marked this pull request as draft March 28, 2026 22:29
@GREENRAT-K405
Copy link
Copy Markdown
Author

@GREENRAT-K405 can you check the bot comments above?

Sure @pradeeban, I will get it done shortly!

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 7 changed files in this pull request and generated 3 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +238 to +247
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);
Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JPEG import concatenates all COM (0xFFFE) segments found before SOS and treats the result as GraphML. This will break for JPEGs that contain unrelated comment segments (or other embedded metadata added by external tools). Consider prefixing your GraphML payload with a unique signature (and optional segment index/count) and filtering/assembling only those segments that match, ignoring unrelated COM segments.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is probably too much edge case thinking, the images should not break in normal sharing

No changes required here in my opinion, still will like to know Mentors suggestion!

Comment on lines +68 to +77
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);
Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JPEG embedded export writes raw GraphML bytes directly into COM segments without any identifier/signature. Since the import path currently reads all COM segments, any pre-existing COM segments will corrupt the payload (and external tools may add COM segments later). Add a recognizable header (and segment index/count) to each COM segment so import can reliably select and reassemble only your GraphML segments.

Suggested change
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);
// Prepare a recognizable header for each COM segment carrying GraphML.
const headerBytes = new TextEncoder().encode('GRAPHMLCHUNK');
const perSegmentMetaLength = 4; // 2 bytes: total segment count, 2 bytes: segment index
const headerLength = headerBytes.length + perSegmentMetaLength;
// JPEG COM segment payload limit is 65533 bytes (length field is 2 bytes, max 65535).
const maxChunkSize = 65533 - headerLength;
const segmentCount = Math.ceil(graphMLBytes.length / maxChunkSize);
const comSegments = [];
for (let segIndex = 0, offset = 0; offset < graphMLBytes.length; segIndex += 1, offset += maxChunkSize) {
const chunk = graphMLBytes.slice(offset, offset + maxChunkSize);
const payloadLength = headerLength + chunk.length;
const segmentLen = payloadLength + 2; // includes these 2 bytes for the length field
const seg = new Uint8Array(4 + payloadLength);
// JPEG COM marker
seg[0] = 0xFF;
seg[1] = 0xFE;
// Big-endian segment length (includes these 2 bytes, excludes marker)
seg[2] = Math.floor(segmentLen / 256);
seg[3] = segmentLen % 256;
// Header: magic string
let writePos = 4;
seg.set(headerBytes, writePos);
writePos += headerBytes.length;
// Header: total segment count (2 bytes, big-endian)
seg[writePos] = Math.floor(segmentCount / 256);
seg[writePos + 1] = segmentCount % 256;
writePos += 2;
// Header: current segment index (0-based, 2 bytes, big-endian)
seg[writePos] = Math.floor(segIndex / 256);
seg[writePos + 1] = segIndex % 256;
writePos += 2;
// Payload: chunk of GraphML bytes
seg.set(chunk, writePos);

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is probably too much edge case thinking, the images should not break in normal sharing

No changes required here in my opinion, still will like to know Mentors suggestion!

@GREENRAT-K405
Copy link
Copy Markdown
Author

@pradeeban I have taken some improvements from the bot as you had suggested, this PR should be ready for your review, Thanks :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants