GraphiteEditor/Graphite · error
failed to encode image as png
Error message
failed to encode image as png
What it means
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.
Source
Thrown at node-graph/libraries/raster-types/src/image.rs:168
// `Image<Color>` pixels are stored linear-light with premultiplied alpha
let srgba = SRGBA8::new(v[0], v[1], v[2], v[3]);
Color::from(srgba).apply_opacity(v[3] as f32 / 255.)
})
.collect();
Image {
width,
height,
data,
base64_string: None,
}
}
pub fn to_png(&self) -> Vec<u8> {
use ::image::ImageEncoder;
let (data, width, height) = self.to_flat_u8();
let mut png = Vec::new();
let encoder = ::image::codecs::png::PngEncoder::new(&mut png);
encoder.write_image(&data, width, height, ::image::ExtendedColorType::Rgba8).expect("failed to encode image as png");
png
}
}
use super::*;
impl<P: Alpha + RGB + AssociatedAlpha> Image<P>
where
P::ColorChannel: Linear,
<P as Alpha>::AlphaChannel: Linear,
{
/// Flattens each channel cast to a u8
pub fn to_flat_u8(&self) -> (Vec<u8>, u32, u32) {
let Image { width, height, data, .. } = self;
assert_eq!(data.len(), *width as usize * *height as usize);
// Cache the last sRGB value we computed, speeds up fills.
let mut last_r = 0.;
let mut last_r_srgb = 0u8;View on GitHub (pinned to c507b35645)
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
Example fix
// before
let (data, width, height) = self.to_flat_u8();
let encoder = ::image::codecs::png::PngEncoder::new(&mut png);
encoder.write_image(&data, width, height, ::image::ExtendedColorType::Rgba8).expect("failed to encode image as png");
// after
let (data, width, height) = self.to_flat_u8();
assert!(width > 0 && height > 0, "cannot png-encode a zero-sized image");
assert_eq!(data.len(), width as usize * height as usize * 4, "pixel buffer does not match dimensions");
let encoder = ::image::codecs::png::PngEncoder::new(&mut png);
encoder.write_image(&data, width, height, ::image::ExtendedColorType::Rgba8).expect("failed to encode image as png"); Defensive patterns
Strategy: validation
Validate before calling
// Validate before calling to_png()
let w = image.width as u64;
let h = image.height as u64;
let encodable = w > 0 && h > 0 && w * h * 4 <= usize::MAX as u64;
if !encodable {
// substitute a placeholder or skip the export instead of panicking inside to_png()
} Type guard
fn is_png_encodable<P>(image: &Image<P>) -> bool {
image.width > 0 && image.height > 0 && image.width as u64 * image.height as u64 * 4 <= usize::MAX as u64
} Try / catch
let png = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| image.to_png()));
match png {
Ok(bytes) => { /* proceed */ }
Err(payload) => { /* log payload, export a 1x1 transparent placeholder instead */ }
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- Image node does not exist
- Ungrouped folder must have a parent
- Solidify Stroke node should exist
- In `check_layer()`: there should be a `target`
- Artboard should have a primary input
AI-assisted analysis of GraphiteEditor/Graphite@c507b35645 (2026-08-16).
Data as JSON: /api/errors/3c9c185af6d19e2a.
Report an issue: GitHub.