{"record":{"id":"3c9c185af6d19e2a","repo":"GraphiteEditor/Graphite","slug":"failed-to-encode-image-as-png","errorCode":null,"errorMessage":"failed to encode image as png","messagePattern":"failed to encode image as png","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"node-graph/libraries/raster-types/src/image.rs","lineNumber":168,"sourceCode":"\t\t\t\t// `Image<Color>` pixels are stored linear-light with premultiplied alpha\n\t\t\t\tlet srgba = SRGBA8::new(v[0], v[1], v[2], v[3]);\n\t\t\t\tColor::from(srgba).apply_opacity(v[3] as f32 / 255.)\n\t\t\t})\n\t\t\t.collect();\n\t\tImage {\n\t\t\twidth,\n\t\t\theight,\n\t\t\tdata,\n\t\t\tbase64_string: None,\n\t\t}\n\t}\n\n\tpub fn to_png(&self) -> Vec<u8> {\n\t\tuse ::image::ImageEncoder;\n\t\tlet (data, width, height) = self.to_flat_u8();\n\t\tlet mut png = Vec::new();\n\t\tlet encoder = ::image::codecs::png::PngEncoder::new(&mut png);\n\t\tencoder.write_image(&data, width, height, ::image::ExtendedColorType::Rgba8).expect(\"failed to encode image as png\");\n\t\tpng\n\t}\n}\n\nuse super::*;\nimpl<P: Alpha + RGB + AssociatedAlpha> Image<P>\nwhere\n\tP::ColorChannel: Linear,\n\t<P as Alpha>::AlphaChannel: Linear,\n{\n\t/// Flattens each channel cast to a u8\n\tpub fn to_flat_u8(&self) -> (Vec<u8>, u32, u32) {\n\t\tlet Image { width, height, data, .. } = self;\n\t\tassert_eq!(data.len(), *width as usize * *height as usize);\n\n\t\t// Cache the last sRGB value we computed, speeds up fills.\n\t\tlet mut last_r = 0.;\n\t\tlet mut last_r_srgb = 0u8;","sourceCodeStart":150,"sourceCodeEnd":186,"githubUrl":"https://github.com/GraphiteEditor/Graphite/blob/c507b356453361e31638b8bff8f6d46b6da2961e/node-graph/libraries/raster-types/src/image.rs#L150-L186","documentation":"Image<Color>::to_png() in graphene-raster-types flattens the RGBA pixels and hands them to the image crate's PngEncoder, then calls .expect(\"failed to encode image as png\"), so any encoder error aborts the process instead of returning a Result. Because the encoder writes into an in-memory Vec, I/O errors are effectively impossible; the realistic failures are parameter errors. to_flat_u8() already asserts data.len() == width*height and produces exactly width*height*4 bytes, so the live triggers are degenerate images (width or height of 0) and size overflow on 32-bit/wasm32 targets.","triggerScenarios":"Calling to_png() on an Image whose width or height is 0 (empty artboard, zero-resolution footprint); rasters so large that width*height*4 overflows usize on 32-bit/wasm32 builds during flattening or encoding; any future refactor that breaks the data.len() == width*height invariant upheld by to_flat_u8() at image.rs:182, which makes write_image fail with a DimensionMismatch parameter error.","commonSituations":"Exporting a PNG from an empty document or an artboard sized to zero; multi-gigapixel exports under wasm32; builds where the Image.data vector was rebuilt or truncated by custom node code; upgrades of the image crate that validate dimensions more strictly.","solutions":["Guard degenerate sizes before encoding: if width or height is 0, return early or substitute a 1x1 transparent PNG instead of calling the encoder","Keep the invariant data.len() == width*height intact wherever Image fields are mutated, so to_flat_u8() and the encoder agree on dimensions","On 32-bit/wasm32 targets, do the size math in u64 and cap or tile the export so width*height*4 fits usize","Replace .expect with Result propagation (return image::ImageError from to_png) so callers can handle encoding failures"],"exampleFix":"// before\nlet (data, width, height) = self.to_flat_u8();\nlet encoder = ::image::codecs::png::PngEncoder::new(&mut png);\nencoder.write_image(&data, width, height, ::image::ExtendedColorType::Rgba8).expect(\"failed to encode image as png\");\n\n// after\nlet (data, width, height) = self.to_flat_u8();\nassert!(width > 0 && height > 0, \"cannot png-encode a zero-sized image\");\nassert_eq!(data.len(), width as usize * height as usize * 4, \"pixel buffer does not match dimensions\");\nlet encoder = ::image::codecs::png::PngEncoder::new(&mut png);\nencoder.write_image(&data, width, height, ::image::ExtendedColorType::Rgba8).expect(\"failed to encode image as png\");","handlingStrategy":"validation","validationCode":"// Validate before calling to_png()\nlet w = image.width as u64;\nlet h = image.height as u64;\nlet encodable = w > 0 && h > 0 && w * h * 4 <= usize::MAX as u64;\nif !encodable {\n    // substitute a placeholder or skip the export instead of panicking inside to_png()\n}","typeGuard":"fn is_png_encodable<P>(image: &Image<P>) -> bool {\n    image.width > 0 && image.height > 0 && image.width as u64 * image.height as u64 * 4 <= usize::MAX as u64\n}","tryCatchPattern":"let png = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| image.to_png()));\nmatch png {\n    Ok(bytes) => { /* proceed */ }\n    Err(payload) => { /* log payload, export a 1x1 transparent placeholder instead */ }\n}","preventionTips":["Never construct Image values with zero width or height; validate at construction and load sites","Keep data.len() == width*height on every mutation of Image fields (to_flat_u8 asserts it, but only at encode time)","On wasm32/32-bit targets, cap or tile exports so width*height*4 stays within usize"],"tags":["rust","png","image-encoding","panic","raster"],"backgroundTag":"image-encoding-failed","analyzedSha":"c507b356453361e31638b8bff8f6d46b6da2961e","analyzedAt":"2026-08-16T21:57:18.596Z","schemaVersion":2},"datasetVersion":"2026-08-16T23:17:17.608Z"}