vectorcraft/crates/render/src/encode/psd.rs

530 lines
23 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! 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 = "</Layer group>";
/// 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<Vec<u8>> {
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<Vec<u8>> = 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<u8>, data: &mut Vec<u8>) {
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<u8>)>,
blend: [u8; 4],
opacity: u8,
hidden: bool,
/// Section divider: [`OPEN_GROUP`] or [`END_GROUP`].
divider: Option<u32>,
}
/// 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<u8>], 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<u8>) -> 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<u8> = 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<u16> = 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, String> {
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<u8>, 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<u8>, 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<Vec<u8>>,
alpha: bool,
/// Bottom first.
records: Vec<Record>,
ppi: f64,
icc: Option<Arc<[u8]>>,
}
/// 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<Vec<u8>, 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<u8>) -> 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<Option<Arc<[u8]>>, 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<Vec<u8>, 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<Vec<u8>, 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<Record>,
}
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<u8> = 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<usize> = (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<usize> = (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<u8> {
(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)))
}