Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion packages/scratch-gui/src/lib/blocks-screenshot.js
Original file line number Diff line number Diff line change
Expand Up @@ -171,10 +171,14 @@ const buildExportSVG = async function (workspace, bbox, scale, width, height, pa
canvasClone.setAttribute('transform', canvasTransform);
svg.appendChild(canvasClone);

// Clone bubble canvas (comment bubbles) with the same transform
// Clone bubble canvas (comment bubbles) with the same transform.
// Remove <foreignObject> elements which contain HTML <textarea> for editing;
// they cause tainted canvas errors when the SVG is loaded via blob URL.
// The visible comment text is already in <text> elements, so nothing is lost.
const bubbleCanvas = workspace.svgBubbleCanvas_;
if (bubbleCanvas && bubbleCanvas.children.length > 0) {
const bubbleClone = bubbleCanvas.cloneNode(true);
bubbleClone.querySelectorAll('foreignObject').forEach(fo => fo.remove());
bubbleClone.setAttribute('transform', canvasTransform);
svg.appendChild(bubbleClone);
}
Expand Down
20 changes: 19 additions & 1 deletion packages/scratch-gui/test/unit/lib/blocks-screenshot.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ jest.mock('../../../src/lib/download-blob', () => jest.fn());
import downloadBlob from '../../../src/lib/download-blob';

// Helper: create a mock Blockly workspace
const makeMockWorkspace = ({boundingBox = null, scale = 1, bubbleChildren = 0} = {}) => {
const makeMockWorkspace = ({boundingBox = null, scale = 1, bubbleChildren = 0, withForeignObject = false} = {}) => {
const svgNS = 'http://www.w3.org/2000/svg';
const svg = document.createElementNS(svgNS, 'svg');
const group = document.createElementNS(svgNS, 'g');
Expand All @@ -27,6 +27,13 @@ const makeMockWorkspace = ({boundingBox = null, scale = 1, bubbleChildren = 0} =
rect.setAttribute('height', '20');
bubbleGroup.appendChild(rect);
}
if (withForeignObject) {
const fo = document.createElementNS(svgNS, 'foreignObject');
fo.setAttribute('class', 'scratchCommentForeignObject');
const body = document.createElementNS('http://www.w3.org/1999/xhtml', 'body');
fo.appendChild(body);
bubbleGroup.appendChild(fo);
}
return {
getBlocksBoundingBox: jest.fn(() => boundingBox),
scale,
Expand Down Expand Up @@ -166,6 +173,17 @@ describe('buildExportSVG', () => {
expect(rectMatches.length).toBeGreaterThanOrEqual(4);
});

test('strips foreignObject elements from bubble canvas clone to avoid tainted canvas', async () => {
const workspace = makeMockWorkspace({
boundingBox: {x: 0, y: 0, width: 200, height: 100},
bubbleChildren: 1,
withForeignObject: true
});
const bbox = {x: 0, y: 0, width: 200, height: 100};
const svgStr = await buildExportSVG(workspace, bbox, 1, 232, 132);
expect(svgStr).not.toMatch(/foreignObject/);
});

test('does not include bubble canvas when it has no children', async () => {
const workspace = makeMockWorkspace({
boundingBox: {x: 0, y: 0, width: 200, height: 100},
Expand Down
Loading