Skip to content

Commit 508edf6

Browse files
committed
improvement(sandbox): upgrade pptx/docx/pdf bootstrap with image helpers, MIME guards, and 256 MB isolate limit
1 parent f708462 commit 508edf6

4 files changed

Lines changed: 149 additions & 9 deletions

File tree

apps/sim/lib/execution/isolated-vm-worker.cjs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ async function executeCode(request, executionId) {
183183
const externalCopies = []
184184

185185
try {
186-
isolate = new ivm.Isolate({ memoryLimit: 128 })
186+
isolate = new ivm.Isolate({ memoryLimit: 256 })
187187
if (executionId !== undefined) activeIsolates.set(executionId, isolate)
188188
context = await isolate.createContext()
189189
const jail = context.global
@@ -511,7 +511,7 @@ async function executeTask(request, executionId) {
511511
let tPhase = tStart
512512

513513
try {
514-
isolate = new ivm.Isolate({ memoryLimit: 128 })
514+
isolate = new ivm.Isolate({ memoryLimit: 256 })
515515
if (executionId !== undefined) activeIsolates.set(executionId, isolate)
516516
context = await isolate.createContext()
517517
const jail = context.global

apps/sim/sandbox-tasks/docx-generate.ts

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,57 @@ export const docxGenerateTask = defineSandboxTask<SandboxTaskInput>({
1515
globalThis.addSection = (section) => {
1616
globalThis.__docxSections.push(section);
1717
};
18-
globalThis.getFileBase64 = async (fileId) => {
18+
19+
// Page geometry constants (twips, 1 twip = 1/1440 inch) for US Letter
20+
globalThis.PAGE_W = 12240; // 8.5"
21+
globalThis.PAGE_H = 15840; // 11"
22+
globalThis.MARGIN = 1440; // 1" margins
23+
globalThis.CONTENT_W = 9360; // PAGE_W - 2 * MARGIN
24+
25+
// 6 MB raw ≈ 8 MB base64; reject above this to avoid sandbox OOM.
26+
const _MAX_IMG_B64 = 8 * 1024 * 1024;
27+
28+
/**
29+
* getFileBase64(fileId) — load a workspace file as a full data URI string.
30+
* Returns the complete "data:image/png;base64,..." string.
31+
* Use addImage() rather than passing this directly to ImageRun.
32+
*/
33+
globalThis.getFileBase64 = async function getFileBase64(fileId) {
34+
if (!fileId || typeof fileId !== 'string') {
35+
throw new Error('getFileBase64: fileId must be a non-empty string');
36+
}
1937
const res = await globalThis.__brokers.workspaceFile({ fileId });
38+
if (!res || !res.dataUri) {
39+
throw new Error('getFileBase64: broker returned no data for file ' + fileId);
40+
}
41+
if (res.dataUri.length > _MAX_IMG_B64) {
42+
throw new Error(
43+
'getFileBase64: image exceeds the 6 MB embed limit (~8 MB base64). Use a smaller/compressed image.'
44+
);
45+
}
2046
return res.dataUri;
2147
};
48+
49+
/**
50+
* addImage(fileId, opts) — fetch a workspace file and return a docx.ImageRun.
51+
* Required opts: width, height (pixels or EMUs via transformation option).
52+
* Example:
53+
* new docx.Paragraph({ children: [await addImage('abc123', { width: 200, height: 100 })] })
54+
*/
55+
globalThis.addImage = async function addImage(fileId, opts) {
56+
const dataUri = await globalThis.getFileBase64(fileId);
57+
const comma = dataUri.indexOf(',');
58+
const header = comma !== -1 ? dataUri.slice(0, comma) : '';
59+
const base64 = comma !== -1 ? dataUri.slice(comma + 1) : dataUri;
60+
const mime = header.split(';')[0].replace('data:', '') || 'image/png';
61+
const ext = mime.includes('png') ? 'png' : mime.includes('gif') ? 'gif' : mime.includes('bmp') ? 'bmp' : 'jpg';
62+
if (!globalThis.Buffer) throw new Error('addImage: Buffer polyfill missing — ensure docx bundle is loaded');
63+
return new globalThis.docx.ImageRun(Object.assign({
64+
data: globalThis.Buffer.from(base64, 'base64'),
65+
transformation: { width: (opts && opts.width) || 200, height: (opts && opts.height) || 200 },
66+
type: ext,
67+
}, opts || {}));
68+
};
2269
`,
2370
// JSZip's browser build doesn't support nodebuffer output, so we go through
2471
// base64 and decode back to bytes inside the isolate (avoids DataURL / Blob).

apps/sim/sandbox-tasks/pdf-generate.ts

Lines changed: 57 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,20 +12,73 @@ export const pdfGenerateTask = defineSandboxTask<SandboxTaskInput>({
1212
if (!PDFLib) throw new Error('pdf-lib bundle not loaded');
1313
globalThis.PDFLib = PDFLib;
1414
globalThis.pdf = await PDFLib.PDFDocument.create();
15-
globalThis.embedImage = async (dataUri) => {
15+
16+
// Convenience shortcuts — avoids verbose PDFLib.rgb() / PDFLib.StandardFonts.Helvetica
17+
globalThis.rgb = PDFLib.rgb;
18+
globalThis.StandardFonts = PDFLib.StandardFonts;
19+
20+
// Page-size constants in points (1pt = 1/72 inch)
21+
globalThis.LETTER = [612, 792]; // 8.5" × 11"
22+
globalThis.A4 = [595.28, 841.89]; // 210mm × 297mm
23+
24+
// 6 MB raw ≈ 8 MB base64; reject above this to avoid sandbox OOM.
25+
const _MAX_IMG_B64 = 8 * 1024 * 1024;
26+
27+
/**
28+
* embedImage(dataUri) — embed a data-URI image into the active PDF document.
29+
* Dispatches to embedPng or embedJpg based on MIME type.
30+
*/
31+
globalThis.embedImage = async function embedImage(dataUri) {
32+
if (!dataUri || typeof dataUri !== 'string') {
33+
throw new Error('embedImage: dataUri must be a non-empty string');
34+
}
35+
if (dataUri.length > _MAX_IMG_B64) {
36+
throw new Error(
37+
'embedImage: image exceeds the 6 MB embed limit (~8 MB base64). Use a smaller/compressed image.'
38+
);
39+
}
1640
const comma = dataUri.indexOf(',');
41+
if (comma === -1) throw new Error('embedImage: invalid data URI (no comma separator)');
1742
const header = dataUri.slice(0, comma);
1843
const base64 = dataUri.slice(comma + 1);
1944
const binary = globalThis.Buffer ? globalThis.Buffer.from(base64, 'base64') : null;
2045
if (!binary) throw new Error('Buffer polyfill missing');
2146
const mime = header.split(';')[0].split(':')[1] || '';
22-
if (mime.includes('png')) return globalThis.pdf.embedPng(binary);
23-
return globalThis.pdf.embedJpg(binary);
47+
// image/jpg is non-standard but tolerated; the canonical MIME is image/jpeg
48+
if (mime === 'image/png') return globalThis.pdf.embedPng(binary);
49+
if (mime === 'image/jpeg' || mime === 'image/jpg') return globalThis.pdf.embedJpg(binary);
50+
throw new Error('embedImage: only PNG and JPEG are supported (got ' + (mime || 'unknown — check data URI header') + ')');
2451
};
25-
globalThis.getFileBase64 = async (fileId) => {
52+
53+
/**
54+
* getFileBase64(fileId) — load a workspace file as a data URI string.
55+
*/
56+
globalThis.getFileBase64 = async function getFileBase64(fileId) {
57+
if (!fileId || typeof fileId !== 'string') {
58+
throw new Error('getFileBase64: fileId must be a non-empty string');
59+
}
2660
const res = await globalThis.__brokers.workspaceFile({ fileId });
61+
if (!res || !res.dataUri) {
62+
throw new Error('getFileBase64: broker returned no data for file ' + fileId);
63+
}
64+
if (res.dataUri.length > _MAX_IMG_B64) {
65+
throw new Error(
66+
'getFileBase64: image exceeds the 6 MB embed limit (~8 MB base64). Use a smaller/compressed image.'
67+
);
68+
}
2769
return res.dataUri;
2870
};
71+
72+
/**
73+
* drawImage(page, fileId, opts) — fetch a workspace file and draw it on the given page.
74+
* Required opts: x, y, width, height (points).
75+
* Example: await drawImage(page, 'abc123', { x: 50, y: 700, width: 200, height: 100 });
76+
*/
77+
globalThis.drawImage = async function drawImage(page, fileId, opts) {
78+
const dataUri = await globalThis.getFileBase64(fileId);
79+
const img = await globalThis.embedImage(dataUri);
80+
page.drawImage(img, opts || {});
81+
};
2982
`,
3083
finalize: `
3184
const pdf = globalThis.pdf;

apps/sim/sandbox-tasks/pptx-generate.ts

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,49 @@ export const pptxGenerateTask = defineSandboxTask<SandboxTaskInput>({
1111
const PptxGenJS = globalThis.__bundles['pptxgenjs'];
1212
if (!PptxGenJS) throw new Error('pptxgenjs bundle not loaded');
1313
globalThis.pptx = new PptxGenJS();
14-
globalThis.getFileBase64 = async (fileId) => {
14+
globalThis.pptx.layout = 'LAYOUT_16x9';
15+
16+
// Slide geometry for LAYOUT_16x9 (inches)
17+
globalThis.SLIDE_W = 10;
18+
globalThis.SLIDE_H = 5.625;
19+
globalThis.MARGIN = 0.5;
20+
globalThis.CONTENT_W = 9; // SLIDE_W - 2 * MARGIN
21+
globalThis.CONTENT_H = 3.8; // usable body height below a standard title row
22+
23+
// ── Image helpers ──────────────────────────────────────────────────────────
24+
// 6 MB raw ≈ 8 MB base64; reject above this to avoid sandbox OOM.
25+
const _MAX_IMG_B64 = 8 * 1024 * 1024;
26+
27+
/**
28+
* getFileBase64(fileId) — load a workspace file as a data URI string.
29+
* PptxGenJS data format: "image/png;base64,<data>" (no "data:" prefix).
30+
* Use as: slide.addImage({ data: await getFileBase64(fileId), x, y, w, h })
31+
*/
32+
globalThis.getFileBase64 = async function getFileBase64(fileId) {
33+
if (!fileId || typeof fileId !== 'string') {
34+
throw new Error('getFileBase64: fileId must be a non-empty string');
35+
}
1536
const res = await globalThis.__brokers.workspaceFile({ fileId });
16-
return res.dataUri;
37+
if (!res || !res.dataUri) {
38+
throw new Error('getFileBase64: broker returned no data for file ' + fileId);
39+
}
40+
if (res.dataUri.length > _MAX_IMG_B64) {
41+
throw new Error(
42+
'getFileBase64: image exceeds the 6 MB embed limit (~8 MB base64). Use a smaller/compressed image.'
43+
);
44+
}
45+
// PptxGenJS expects "image/png;base64,..." — strip the leading "data:" if present
46+
return res.dataUri.replace(/^data:/, '');
47+
};
48+
49+
/**
50+
* addImage(slide, fileId, opts) — fetch a workspace file and embed it.
51+
* Required opts: x, y, w, h (inches).
52+
* Example: await addImage(slide, 'abc123', { x: 0.5, y: 1, w: 2, h: 1 });
53+
*/
54+
globalThis.addImage = async function addImage(slide, fileId, opts) {
55+
const data = await globalThis.getFileBase64(fileId);
56+
slide.addImage(Object.assign({ data }, opts || {}));
1757
};
1858
`,
1959
finalize: `

0 commit comments

Comments
 (0)