mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-21 15:38:12 +08:00
Code review fixes
This commit is contained in:
@@ -140,28 +140,29 @@ export function createPortfolioStore(subscriptions: SubscriptionsRouter, editor:
|
||||
const backgroundColor = mime.endsWith("jpeg") ? "white" : undefined;
|
||||
const padWidth = Math.max(4, String(frames.length).length);
|
||||
|
||||
// Materialize each frame to bytes, rasterizing SVG via canvas when the destination format is raster
|
||||
// Materialize each frame to bytes, rasterizing SVG via canvas when the destination format is raster.
|
||||
// Any per-frame failure aborts the export rather than silently dropping frames, so the user never gets
|
||||
// a zip with mismatched indices vs. the requested playback range.
|
||||
const entries: [string, Uint8Array][] = [];
|
||||
for (let i = 0; i < frames.length; i++) {
|
||||
const frame = frames[i];
|
||||
const filename = `${name}_${String(i + 1).padStart(padWidth, "0")}.${extension}`;
|
||||
try {
|
||||
for (let i = 0; i < frames.length; i++) {
|
||||
const frame = frames[i];
|
||||
const filename = `${name}_${String(i + 1).padStart(padWidth, "0")}.${extension}`;
|
||||
|
||||
let bytes: Uint8Array;
|
||||
if ("Bytes" in frame) {
|
||||
bytes = frame.Bytes;
|
||||
} else if (isRaster) {
|
||||
let blob: Blob;
|
||||
try {
|
||||
blob = await rasterizeSVG(frame.Svg, size[0], size[1], mime, backgroundColor);
|
||||
} catch {
|
||||
// Skip frames that fail to rasterize (e.g. zero-sized) rather than aborting the whole export
|
||||
continue;
|
||||
let bytes: Uint8Array;
|
||||
if ("Bytes" in frame) {
|
||||
bytes = frame.Bytes;
|
||||
} else if (isRaster) {
|
||||
const blob = await rasterizeSVG(frame.Svg, size[0], size[1], mime, backgroundColor);
|
||||
bytes = new Uint8Array(await blob.arrayBuffer());
|
||||
} else {
|
||||
bytes = new TextEncoder().encode(frame.Svg);
|
||||
}
|
||||
bytes = new Uint8Array(await blob.arrayBuffer());
|
||||
} else {
|
||||
bytes = new TextEncoder().encode(frame.Svg);
|
||||
entries.push([filename, bytes]);
|
||||
}
|
||||
entries.push([filename, bytes]);
|
||||
} catch (error) {
|
||||
editor.errorDialog("Animation export failed", error instanceof Error ? error.message : String(error));
|
||||
return;
|
||||
}
|
||||
|
||||
if (entries.length === 0) return;
|
||||
|
||||
@@ -17,18 +17,23 @@ export async function rasterizeSVGCanvas(svg: string, width: number, height: num
|
||||
const svgBlob = new Blob([svg], { type: "image/svg+xml;charset=utf-8" });
|
||||
const url = URL.createObjectURL(svgBlob);
|
||||
|
||||
// Load the Image from the URL and wait until it's done
|
||||
// Load the Image from the URL and wait until it's done. Reject on error so callers don't hang forever
|
||||
// if the SVG fails to decode (malformed markup, zero-sized viewport with no width/height attrs, etc.).
|
||||
const image = new Image();
|
||||
image.src = url;
|
||||
await new Promise<void>((resolve) => {
|
||||
image.onload = () => resolve();
|
||||
});
|
||||
try {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
image.onload = () => resolve();
|
||||
image.onerror = () => reject(new Error("Failed to decode SVG for rasterization"));
|
||||
});
|
||||
|
||||
// Draw our SVG to the canvas
|
||||
context?.drawImage(image, 0, 0, width, height);
|
||||
|
||||
// Clean up the SVG blob URL (once the URL is revoked, the SVG blob data itself is garbage collected after `svgBlob` goes out of scope)
|
||||
URL.revokeObjectURL(url);
|
||||
// Draw our SVG to the canvas
|
||||
context?.drawImage(image, 0, 0, width, height);
|
||||
} finally {
|
||||
// Always clean up the SVG blob URL (once the URL is revoked, the SVG blob data itself is garbage
|
||||
// collected after `svgBlob` goes out of scope), even if loading or drawing threw.
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
return canvas;
|
||||
}
|
||||
|
||||
@@ -996,10 +996,13 @@ pub fn create_zip_from_files(entries: js_sys::Array) -> Result<Vec<u8>, JsValue>
|
||||
.ok_or_else(|| JsValue::from_str("createZipFromFiles: each entry must be a [filename, bytes] array"))?;
|
||||
|
||||
let filename = pair.get(0).as_string().ok_or_else(|| JsValue::from_str("createZipFromFiles: filename must be a string"))?;
|
||||
// Defense in depth: callers should already sanitize, but if a stray path separator leaks through,
|
||||
// replace it here so the entry can't become a nested path inside the archive.
|
||||
let safe_filename: String = filename.chars().map(|c| if c == '/' || c == '\\' { '_' } else { c }).collect();
|
||||
let bytes = js_sys::Uint8Array::new(&pair.get(1)).to_vec();
|
||||
|
||||
writer
|
||||
.start_file(filename, options)
|
||||
.start_file(safe_filename, options)
|
||||
.map_err(|e| JsValue::from_str(&format!("createZipFromFiles: start_file failed: {e}")))?;
|
||||
writer.write_all(&bytes).map_err(|e| JsValue::from_str(&format!("createZipFromFiles: write_all failed: {e}")))?;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user