//! PSD export: a layered bitmap at 8 bits per channel, written from the format's public //! specification. The file holds the header, the resolution and colour profile resources, the //! layer records and their channels (PackBits row by row; groups as section dividers), then the //! merged image, which readers that skip layers show. RGB, CMYK (stored as 255 − ink, as the //! format keeps inks) or grayscale. use std::sync::Arc; use vectorcraft_color::{BlendMode, cms}; use vectorcraft_doc::{Document, Knockout, LayerColor, Node, NodeKind}; use vectorcraft_geom::Rect; use super::jpeg::{self, ColorModel}; use super::tiff::packbits; use super::{RasterExportOptions, RasterFormat, on_white}; use crate::{RenderOptions, Rendered, Renderer, fx}; /// Most pixels a side of a PSD can have (larger images need the format's large-document variant). pub const MAX_SIDE: u32 = 30_000; /// The name of the record that closes a group (the format's convention). const GROUP_END: &str = ""; /// The PSD options of a raster export. #[derive(Clone, Debug, PartialEq)] pub struct PsdOptions { pub color_model: ColorModel, /// Write Layers: each top-level layer becomes a pixel layer. Off: one flat image, on white where /// nothing is drawn. pub layers: bool, /// With `layers`: layers and sublayers become groups, and every object in them a pixel layer of /// its own, named as the Layers panel names it (text by its text). A layer whose look comes from /// its objects together (a clipping mask, an opacity mask, its own appearance, knockout) stays /// one pixel layer. pub max_editability: bool, /// With `layers`: hidden layers (and hidden objects) are written as hidden layers instead of /// being left out. pub hidden_layers: bool, /// Embed the colour profile of the colour model. pub embed_icc: bool, } impl Default for PsdOptions { fn default() -> Self { Self { color_model: ColorModel::Rgb, layers: true, max_editability: false, hidden_layers: false, embed_icc: true } } } /// The format's colour mode number. fn mode(model: ColorModel) -> u16 { match model { ColorModel::Gray => 1, ColorModel::Rgb => 3, ColorModel::Cmyk => 4, } } /// The format's key for a blend mode. fn blend_key(b: BlendMode) -> [u8; 4] { *match b { BlendMode::Normal => b"norm", BlendMode::Darken => b"dark", BlendMode::Multiply => b"mul ", BlendMode::ColorBurn => b"idiv", BlendMode::Lighten => b"lite", BlendMode::Screen => b"scrn", BlendMode::ColorDodge => b"div ", BlendMode::Overlay => b"over", BlendMode::SoftLight => b"sLit", BlendMode::HardLight => b"hLit", BlendMode::Difference => b"diff", BlendMode::Exclusion => b"smud", BlendMode::Hue => b"hue ", BlendMode::Saturation => b"sat ", BlendMode::Color => b"colr", BlendMode::Luminosity => b"lum ", } } /// An opacity 0–1 as a byte. fn opacity_byte(o: f32) -> u8 { (o.clamp(0.0, 1.0) * 255.0).round() as u8 } /// Pixels in `model`'s planes, then alpha when `alpha`. `px` is straight RGBA; `inks` the CMYK ink /// amounts of the same pixels, as drawn on white paper (CMYK only). The merged image (`matte`) is /// stored on white, as readers that skip alpha show it; layers keep their colours straight under /// their alpha. fn planes(model: ColorModel, px: &[u8], inks: Option<&[u8]>, matte: bool, alpha: bool) -> Vec> { let rgba = px.as_chunks::<4>().0; let rgb = |p: &[u8; 4]| if matte { on_white(p) } else { [p[0], p[1], p[2]] }; let mut out: Vec> = match model { ColorModel::Rgb => (0..3).map(|c| rgba.iter().map(|p| rgb(p)[c]).collect()).collect(), ColorModel::Gray => { let gray = jpeg::to_gray(); vec![rgba.iter().map(|p| gray(rgb(p))).collect()] } ColorModel::Cmyk => { let inks = inks.map_or(&[][..], |i| i.as_chunks::<4>().0); (0..4) .map(|c| { inks.iter() .zip(rgba) .map(|(k, p)| { // Inks are drawn premultiplied by coverage: a layer's are straightened. let a = u32::from(p[3]); let ink = if matte || a == 0 { k[c] } else { ((u32::from(k[c]) * 255 + a / 2) / a).min(255) as u8 }; 255 - ink }) .collect() }) .collect() } }; if alpha { out.push(rgba.iter().map(|p| p[3]).collect()); } out } /// Rows of `plane` (`width` bytes each) PackBits-compressed one by one: their byte counts go to /// `counts`, the rows to `data`. fn rle(plane: &[u8], width: usize, counts: &mut Vec, data: &mut Vec) { for row in plane.chunks(width.max(1)) { let at = data.len(); packbits(row, data); // A row of at most MAX_SIDE bytes packs into fewer than 65536. counts.extend(u16::try_from(data.len() - at).unwrap_or(u16::MAX).to_be_bytes()); } } /// One layer record (records go bottom first). struct Record { name: String, /// Top, left, bottom, right in canvas pixels. rect: [i32; 4], /// Channel id (−1: transparency) and its data, compression code first. channels: Vec<(i16, Vec)>, blend: [u8; 4], opacity: u8, hidden: bool, /// Section divider: [`OPEN_GROUP`] or [`END_GROUP`]. divider: Option, } /// Section divider kinds. const OPEN_GROUP: u32 = 1; const END_GROUP: u32 = 3; impl Record { /// A record without pixels: an empty layer, or a group's divider. fn empty(name: String, colors: usize) -> Self { Self { name, rect: [0; 4], // Raw (code 0) and no data. channels: std::iter::once(-1).chain(0..colors as i16).map(|id| (id, vec![0, 0])).collect(), blend: *b"norm", opacity: 255, hidden: false, divider: None, } } /// A pixel layer of `planes` (colours, then alpha), `width` pixels wide, at `left`, `top`. fn pixels(name: String, planes: &[Vec], width: u32, height: u32, left: u32, top: u32) -> Self { let Some((alpha, colors)) = planes.split_last() else { return Self::empty(name, 0) }; let channels = std::iter::once((-1, alpha)) .chain(colors.iter().enumerate().map(|(i, p)| (i as i16, p))) .map(|(id, plane)| { let (mut counts, mut data) = (vec![0, 1], vec![]); rle(plane, width as usize, &mut counts, &mut data); counts.extend(data); (id, counts) }) .collect(); let [l, t, w, h] = [left, top, width, height].map(|v| i32::try_from(v).unwrap_or(i32::MAX)); Self { channels, rect: [t, l, t.saturating_add(h), l.saturating_add(w)], ..Self::empty(name, 0) } } /// A layer of one colour (`values`: a value per plane, colours then alpha) over the whole /// `width` × `height` canvas, without drawing its pixels first: one packed row, repeated. fn solid(name: String, values: &[u8], width: u32, height: u32) -> Self { let Some((alpha, colors)) = values.split_last() else { return Self::empty(name, 0) }; let channels = std::iter::once((-1, *alpha)) .chain(colors.iter().enumerate().map(|(i, v)| (i as i16, *v))) .map(|(id, v)| { let (mut count, mut row) = (vec![], vec![]); rle(&vec![v; width as usize], width as usize, &mut count, &mut row); let mut data = vec![0, 1]; data.extend(count.repeat(height as usize)); data.extend(row.repeat(height as usize)); (id, data) }) .collect(); let [w, h] = [width, height].map(|v| i32::try_from(v).unwrap_or(i32::MAX)); Self { channels, rect: [0, 0, h, w], ..Self::empty(name, 0) } } fn write(&self, out: &mut Vec) -> Result<(), String> { for v in self.rect { out.extend(v.to_be_bytes()); } out.extend((self.channels.len() as u16).to_be_bytes()); for (id, data) in &self.channels { out.extend(id.to_be_bytes()); out.extend(len32(data.len())?.to_be_bytes()); } out.extend(b"8BIM"); out.extend(self.blend); out.push(self.opacity); // Clipping: base. out.push(0); // Flags: bit 1 hidden, bit 3 "bit 4 is set", bit 4 no pixels that matter (dividers). out.push(0x08 | if self.hidden { 0x02 } else { 0 } | if self.divider.is_some() { 0x10 } else { 0 }); out.push(0); // Extra data: no layer mask, no blending ranges, the name, then tagged blocks. let mut extra = vec![0; 8]; // The name as a Pascal string of its ASCII, padded to a multiple of 4 bytes; readers take // the Unicode name that follows. let ascii: Vec = self.name.chars().map(|c| if c.is_ascii() && !c.is_ascii_control() { c as u8 } else { b'_' }).take(255).collect(); extra.push(ascii.len() as u8); extra.extend(ascii); extra.resize(extra.len().next_multiple_of(4), 0); let units: Vec = self.name.encode_utf16().take(255).collect(); let mut luni = (units.len() as u32).to_be_bytes().to_vec(); luni.extend(units.iter().flat_map(|u| u.to_be_bytes())); block(&mut extra, b"luni", &luni)?; if let Some(kind) = self.divider { block(&mut extra, b"lsct", &[&kind.to_be_bytes()[..], b"8BIM", &self.blend].concat())?; } out.extend(len32(extra.len())?.to_be_bytes()); out.extend(extra); Ok(()) } } fn too_large() -> String { "the image is too large for PSD (at most 4 GB): lower the resolution".into() } fn len32(n: usize) -> Result { u32::try_from(n).map_err(|_| too_large()) } /// A tagged block of additional layer information, padded to an even length. fn block(out: &mut Vec, key: &[u8; 4], data: &[u8]) -> Result<(), String> { let len = data.len().next_multiple_of(2); out.extend(b"8BIM"); out.extend(key); out.extend(len32(len)?.to_be_bytes()); out.extend(data); out.resize(out.len() + len - data.len(), 0); Ok(()) } /// An image resource block (no name), padded to an even length. fn resource(out: &mut Vec, id: u16, data: &[u8]) -> Result<(), String> { out.extend(b"8BIM"); out.extend(id.to_be_bytes()); // An empty Pascal name, padded to an even length. out.extend([0, 0]); out.extend(len32(data.len())?.to_be_bytes()); out.extend(data); if data.len() % 2 == 1 { out.push(0); } Ok(()) } /// Everything a PSD file holds. struct File { width: u32, height: u32, model: ColorModel, /// The merged image's planes: colours, then alpha when `alpha`. merged: Vec>, alpha: bool, /// Bottom first. records: Vec, ppi: f64, icc: Option>, } /// Refuse a size the format can't store. fn check_size(w: u32, h: u32) -> Result<(), String> { if w > MAX_SIDE || h > MAX_SIDE { return Err(format!("{w} × {h} pixels is too large for PSD (at most {MAX_SIDE} pixels a side): lower the resolution")); } Ok(()) } impl File { fn write(&self) -> Result, String> { let (w, h) = (self.width, self.height); check_size(w, h)?; let n = w as usize * h as usize; if n == 0 || self.merged.is_empty() || self.merged.iter().any(|p| p.len() != n) { return Err("PSD encoding failed: the pixel buffer doesn't match the image size".into()); } let mut out = Vec::with_capacity(n * self.merged.len() / 2 + 1024); // Header: signature, version 1, reserved, channels, size, depth, colour mode. out.extend(b"8BPS"); out.extend(1u16.to_be_bytes()); out.extend([0; 6]); out.extend((self.merged.len() as u16).to_be_bytes()); out.extend(h.to_be_bytes()); out.extend(w.to_be_bytes()); out.extend(8u16.to_be_bytes()); out.extend(mode(self.model).to_be_bytes()); // No colour mode data. out.extend(0u32.to_be_bytes()); // Image resources: the resolution (16.16 fixed point, pixels per inch shown in inches), the profile. let ppi = if self.ppi.is_finite() && self.ppi > 0.0 { self.ppi } else { 72.0 }; let fixed = (ppi.min(30_000.0) * 65536.0).round() as u32; let res = [fixed.to_be_bytes(), [0, 1, 0, 1], fixed.to_be_bytes(), [0, 1, 0, 1]].concat(); let mut resources = vec![]; resource(&mut resources, 1005, &res)?; if let Some(icc) = &self.icc { resource(&mut resources, 1039, icc)?; } out.extend(len32(resources.len())?.to_be_bytes()); out.extend(resources); self.write_layers(&mut out)?; // The merged image: PackBits, every row's byte count first. out.extend(1u16.to_be_bytes()); let (mut counts, mut data) = (Vec::with_capacity(self.merged.len() * h as usize * 2), vec![]); for p in &self.merged { rle(p, w as usize, &mut counts, &mut data); } out.extend(counts); out.extend(data); Ok(out) } /// The layer and mask information section. fn write_layers(&self, out: &mut Vec) -> Result<(), String> { if self.records.is_empty() { out.extend(0u32.to_be_bytes()); return Ok(()); } let count = i16::try_from(self.records.len()).map_err(|_| "too many layers for PSD (at most 32767)".to_string())?; // A negative count: the merged image's alpha is its transparency. let mut info = (if self.alpha { -count } else { count }).to_be_bytes().to_vec(); for r in &self.records { r.write(&mut info)?; } for (_, data) in self.records.iter().flat_map(|r| &r.channels) { info.extend(data); } info.resize(info.len().next_multiple_of(2), 0); let len = len32(info.len())?; // The section: the layer info's length and the info, then no global layer mask. out.extend(len.checked_add(8).ok_or_else(too_large)?.to_be_bytes()); out.extend(len.to_be_bytes()); out.extend(info); out.extend(0u32.to_be_bytes()); Ok(()) } } fn icc(o: &PsdOptions) -> Result>, String> { o.embed_icc.then(|| cms::icc_bytes(&o.color_model.profile()).map_err(|e| format!("PSD colour profile: {e}"))).transpose() } /// `img` as a flat PSD: the merged image alone, on white where it is transparent (CMYK separated /// from the screen colours; [`export`] draws inks). pub fn encode(img: &Rendered, ppi: f64, o: &PsdOptions) -> Result, String> { let inks = (o.color_model == ColorModel::Cmyk).then(|| jpeg::separated(img)); let merged = planes(o.color_model, &img.to_straight(), inks.as_deref(), true, false); File { width: img.width, height: img.height, model: o.color_model, merged, alpha: false, records: vec![], ppi, icc: icc(o)? }.write() } /// `region` of `doc` as a PSD with `opts` (see [`PsdOptions`]). The merged image is the export's /// render; with Write Layers each part is also drawn alone as its own layer, cropped to what it /// paints, its opacity and blend mode in the layer's record. pub(crate) fn export(r: &mut Renderer, doc: &Document, region: Rect, opts: &RasterExportOptions) -> Result, String> { let o = &opts.psd; let scale = opts.scale(); let (width, height) = crate::region_pixels(region, scale); check_size(width, height)?; let ro = opts.render_options(RasterFormat::Psd); let (mut merged, mut alpha) = { let px = r.render_region_with(doc, region, scale, &ro).to_straight(); let inks = (o.color_model == ColorModel::Cmyk).then(|| r.render_region_inks(doc, region, scale, &ro)); let alpha = px.as_chunks::<4>().0.iter().any(|p| p[3] < 255); (planes(o.color_model, &px, inks.as_deref(), true, alpha), alpha) }; let mut records = vec![]; if o.layers { let mut base = doc.clone(); base.layers.clear(); let mut parts = Parts { r, base, region, scale, width, height, ro: RenderOptions { background: None, ..ro }, o, records: vec![] }; if let Some(bg) = opts.background { parts.background(bg); } for l in &doc.layers { parts.add(l); } records = parts.records; } // Without layers an alpha plane would be read as an extra channel: the colours, on white, stay. if records.is_empty() && alpha { merged.pop(); alpha = false; } File { width, height, model: o.color_model, merged, alpha, records, ppi: opts.ppi, icc: icc(o)? }.write() } /// Draws the parts of a document as layer records, bottom first. struct Parts<'a> { r: &'a mut Renderer, /// The document without its layers: each part is drawn on it alone. base: Document, region: Rect, scale: f64, width: u32, height: u32, /// Transparent: a part's pixels alone. ro: RenderOptions, o: &'a PsdOptions, records: Vec, } impl Parts<'_> { fn colors(&self) -> usize { self.o.color_model.channels() } /// The export's background colour as the bottom layer. fn background(&mut self, rgb: [u8; 3]) { let px = [rgb[0], rgb[1], rgb[2], 255]; let inks = (self.o.color_model == ColorModel::Cmyk).then(|| jpeg::separated(&Rendered { width: 1, height: 1, pixels: px.to_vec() })); let values: Vec = planes(self.o.color_model, &px, inks.as_deref(), false, true).iter().map(|p| p.first().copied().unwrap_or(0)).collect(); self.records.push(Record::solid("Background".into(), &values, self.width, self.height)); } /// Node `n` (a top-level layer, or with Maximum Editability anything in one) and its parts. fn add(&mut self, n: &Node) { // Templates and guides aren't artwork. if matches!(n.kind, NodeKind::Layer { template: true, .. } | NodeKind::Path { guide: true, .. }) || !(n.visible || self.o.hidden_layers) { return; } let (hidden, opacity) = (!n.visible, opacity_byte(n.opacity)); if self.o.max_editability && splits(n) && let Some(children) = n.children() { self.records.push(Record { divider: Some(END_GROUP), ..Record::empty(GROUP_END.into(), self.colors()) }); for c in children { self.add(c); } // A group in normal blending lets its layers blend with what is below it. let blend = if n.blend == BlendMode::Normal && !n.isolate { *b"pass" } else { blend_key(n.blend) }; self.records.push(Record { divider: Some(OPEN_GROUP), blend, opacity, hidden, ..Record::empty(n.display_name(), self.colors()) }); return; } let record = self.draw(n); self.records.push(Record { blend: blend_key(n.blend), opacity, hidden, ..record }); } /// `n` drawn alone, visible, at full opacity in normal blending (its record carries those). fn draw(&mut self, n: &Node) -> Record { let name = n.display_name(); let mut part = n.clone(); (part.visible, part.opacity, part.blend) = (true, 1.0, BlendMode::Normal); let layer = if part.is_layer() { part } else { let mut l = Node::layer(self.base.alloc_id(), "", LayerColor::Preset(0)); if let Some(children) = l.children_mut() { children.push(Arc::new(part)); } l }; // What the part paints, in canvas pixels (all of it when effects may reach further than the // art's bounds say). let (w, h) = (f64::from(self.width), f64::from(self.height)); let bounds = if reaches_beyond(&layer) { Some(Rect::new(0.0, 0.0, w, h)) } else { None }; self.base.layers = vec![Arc::new(layer)]; let (region, s) = (self.region, self.scale); let bounds = bounds.or_else(|| { let b = super::art_bounds(&self.base)?; // A pixel's margin for anti-aliasing. Some(Rect::new( ((b.x0 - region.x0) * s).floor() - 1.0, ((b.y0 - region.y0) * s).floor() - 1.0, ((b.x1 - region.x0) * s).ceil() + 1.0, ((b.y1 - region.y0) * s).ceil() + 1.0, )) }); let colors = self.colors(); let Some(b) = bounds.filter(|b| [b.x0, b.y0, b.x1, b.y1].iter().all(|v| v.is_finite())) else { return Record::empty(name, colors) }; let (x0, y0, x1, y1) = (b.x0.clamp(0.0, w), b.y0.clamp(0.0, h), b.x1.clamp(0.0, w), b.y1.clamp(0.0, h)); if x1 <= x0 || y1 <= y0 { return Record::empty(name, colors); } let sub = Rect::new(region.x0 + x0 / s, region.y0 + y0 / s, region.x0 + x1 / s, region.y0 + y1 / s); let img = self.r.render_region_with(&self.base, sub, s, &self.ro); let inks = (self.o.color_model == ColorModel::Cmyk).then(|| self.r.render_region_inks(&self.base, sub, s, &self.ro)); let (left, top) = (x0 as u32, y0 as u32); // Within the canvas, cropped to the pixels it paints. let (iw, ih) = (img.width.min(self.width - left) as usize, img.height.min(self.height - top) as usize); let px = img.to_straight(); let painted = |x: usize, y: usize| px.get((y * img.width as usize + x) * 4 + 3).is_some_and(|a| *a > 0); let rows: Vec = (0..ih).filter(|y| (0..iw).any(|x| painted(x, *y))).collect(); let (Some(&cy0), Some(&cy1)) = (rows.first(), rows.last()) else { return Record::empty(name, colors) }; let cols: Vec = (0..iw).filter(|x| (cy0..=cy1).any(|y| painted(*x, y))).collect(); let (Some(&cx0), Some(&cx1)) = (cols.first(), cols.last()) else { return Record::empty(name, colors) }; let crop = |buf: &[u8]| -> Vec { (cy0..=cy1) .flat_map(|y| buf.get((y * img.width as usize + cx0) * 4..(y * img.width as usize + cx1 + 1) * 4).unwrap_or_default()) .copied() .collect() }; let planes = planes(self.o.color_model, &crop(&px), inks.as_deref().map(crop).as_deref(), false, true); Record::pixels(name, &planes, (cx1 - cx0 + 1) as u32, (cy1 - cy0 + 1) as u32, left + cx0 as u32, top + cy0 as u32) } } /// Can layer `n` be written as a group of its parts without changing its look? fn splits(n: &Node) -> bool { matches!(n.kind, NodeKind::Layer { clip: false, .. }) && n.mask.is_none() && n.knockout != Knockout::On && !fx::has_object_fx(n) } /// Does anything in `n` carry effects of its own that may paint outside the art's bounds? fn reaches_beyond(n: &Node) -> bool { fx::has_object_fx(n) || n.children().is_some_and(|c| c.iter().any(|c| reaches_beyond(c))) }