mirror of
https://github.com/storytold/photocraft.git
synced 2026-11-12 05:02:51 +00:00
* Raw: measure the Nikon D90 image area (4288x2848 at (10, 10)) Aligning the decoded D90 sensor plane with the camera's own JPEG puts the image at (10, 10) in the 4352x2868 sensor plane, giving the camera's 4288x2848 frame (10-pixel margins; the right border reads the camera's flat 255 from column 4310). Add the measured ImageArea so a D90 NEF opens on the camera's frame instead of the uncropped plane, and make a measured profile leave a file that carries its own colour calibration (a DNG) untouched. * Codecs: open Cineon (.cin) film frames read-only Cineon is the 10-bit logarithmic film-scan format DPX grew from. Read it from the same Kodak header family: the generic header (big-endian, orientation at 192, channel count at 193, the channel-1 bits/dimensions), one or three channels, three 10-bit samples packed into each 32-bit word on a word boundary. The 10-bit code values open as U16 (round(c x 65535 / 1023)); the file's log black/white points are a display choice and are not applied. A depth other than 10 bits or a channel count other than 1/3 is Unsupported; truncation errors rather than panicking. Read-only (no Cineon writer), so .cin joins the open filters and Cineon read closes part of #1049's G10 gap. * PSD: write tiff blocks without copies, inflate merged ZIP once - ImageSourceData::to_bytes writes the global mask and global blocks straight from the model with 4-byte alignment instead of cloning each block (data included) to set its padding. Same bytes. - The tiff transcoder's nested regions and blocks write into the parent's buffer (taken and handed back, error or not) instead of building their own output and copying it up per nesting level. Bodies that fail or are not transcoded are truncated before being copied as stored or dropped. Same bytes. - PsdFile::from_bytes_and_merged decodes the merged image with the parse when asked: a ZIP stream is inflated once (the decode's stream check is the parse's); the parse-only check runs only when the decode fails, to keep from_bytes' errors. PSD import uses it with merged_needed. - PsdBuilder::composite documents that Gray/CMYK composites stay straight. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Keyboard shortcut sets: Set menu, New Set, Save, Delete, .kys Export and Summarize (K2) Edit › Keyboard Shortcuts gets Photoshop's set management. A set is a saved copy of the bindings (Preferences.shortcutSets, the one in use in shortcutSet; "" is PhotoCraft Defaults), and the Set menu shows "(modified)" when the bindings differ from it. New Set…, Save and Delete wait for OK like the rest of the dialog. Export… writes the dialog's bindings as a .kys file (commands then tools, Photoshop's macOS key spelling), each row carrying our id as pc-id so PhotoCraft imports its own sets back exactly, whatever the label; Summarize… writes Photoshop's HTML list of every bound command by menu. edit.keyboardShortcuts takes useSet (before the call's edits), saveSet, deleteSet, sets and activeSet (after them), exportKys and summarize; every reply reports the sets, the set in use and whether it is modified. Bad names, keys or counts are errors that change nothing. * .kys import: tool keys and sets saved on Windows (K3) Photoshop's <tool> rows now import: each matches our tool by name (our tool names are Photoshop's) or by pc-id in sets PhotoCraft exported, and a letter other than the tool's own becomes its override, while its own letter keeps the tool cycling with the others (#5036). A set saved on Windows spells the primary modifier Ctrl where a Mac set writes Cmd and keeps Control for the Control key. A set with no Cmd or Opt anywhere but some Ctrl is read as a Windows set, its Ctrl as our Cmd: Ctrl+L is the Cmd+L default, not a Control-key override that would also mean Control on a Mac. The import reports `windows`. * cargo fmt the extension lists Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Vikram Bhaskaran <vikram.bhaskaran@outlook.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> Co-authored-by: Tanish Manku <tmanku.dev@gmail.com>
471 lines
17 KiB
Rust
471 lines
17 KiB
Rust
//! [`PsdBuilder`]: construct PSD/PSB files from pixel buffers.
|
||
//!
|
||
//! The builder never composites. Supply the merged image with
|
||
//! [`PsdBuilder::composite`]; otherwise a white placeholder is written and
|
||
//! resource 1057 records `has_real_merged_data = false`.
|
||
|
||
use crate::blend::BlendMode;
|
||
use crate::compression::Compression;
|
||
use crate::error::{PsdError, Result};
|
||
use crate::file::{GlobalLayerMask, LayerInfoPlacement, PsdFile};
|
||
use crate::header::{ColorMode, Header, Version};
|
||
use crate::image_data::ImageData;
|
||
use crate::io::encode_legacy_name;
|
||
use crate::layer::{BlendingRanges, ChannelData, LayerFlags, LayerInfo, LayerMask, LayerRecord, MaskData, Rect};
|
||
use crate::resources::{ImageResource, ResolutionInfo, ids, version_info_resource};
|
||
use crate::tagged::{SectionType, TaggedBlock};
|
||
|
||
/// Interleaved pixel data with alpha. CMYK values are *ink amounts*
|
||
/// (0 = no ink); the builder stores them inverted as Photoshop does.
|
||
#[derive(Debug, Clone, PartialEq)]
|
||
pub enum PixelData {
|
||
/// RGBA, 8-bit.
|
||
Rgba8(Vec<u8>),
|
||
/// RGBA, 16-bit.
|
||
Rgba16(Vec<u16>),
|
||
/// Gray + alpha, 8-bit.
|
||
GrayA8(Vec<u8>),
|
||
/// Gray + alpha, 16-bit.
|
||
GrayA16(Vec<u16>),
|
||
/// CMYK ink + alpha, 8-bit.
|
||
Cmyka8(Vec<u8>),
|
||
/// CMYK ink + alpha, 16-bit.
|
||
Cmyka16(Vec<u16>),
|
||
}
|
||
|
||
impl PixelData {
|
||
fn mode_depth(&self) -> (ColorMode, u16) {
|
||
match self {
|
||
PixelData::Rgba8(_) => (ColorMode::Rgb, 8),
|
||
PixelData::Rgba16(_) => (ColorMode::Rgb, 16),
|
||
PixelData::GrayA8(_) => (ColorMode::Grayscale, 8),
|
||
PixelData::GrayA16(_) => (ColorMode::Grayscale, 16),
|
||
PixelData::Cmyka8(_) => (ColorMode::Cmyk, 8),
|
||
PixelData::Cmyka16(_) => (ColorMode::Cmyk, 16),
|
||
}
|
||
}
|
||
/// Samples per pixel including alpha.
|
||
fn spp(&self) -> usize {
|
||
match self.mode_depth().0 {
|
||
ColorMode::Rgb => 4,
|
||
ColorMode::Grayscale => 2,
|
||
_ => 5,
|
||
}
|
||
}
|
||
fn len(&self) -> usize {
|
||
match self {
|
||
PixelData::Rgba8(v) | PixelData::GrayA8(v) | PixelData::Cmyka8(v) => v.len(),
|
||
PixelData::Rgba16(v) | PixelData::GrayA16(v) | PixelData::Cmyka16(v) => v.len(),
|
||
}
|
||
}
|
||
/// Planar big-endian plane for interleaved component `c`, as stored
|
||
/// (CMYK inverted).
|
||
fn plane(&self, c: usize, n: usize) -> Vec<u8> {
|
||
let spp = self.spp();
|
||
let invert = self.mode_depth().0 == ColorMode::Cmyk && c < 4;
|
||
match self {
|
||
PixelData::Rgba8(v) | PixelData::GrayA8(v) | PixelData::Cmyka8(v) => {
|
||
(0..n).map(|i| if invert { 255 - v[i * spp + c] } else { v[i * spp + c] }).collect()
|
||
}
|
||
PixelData::Rgba16(v) | PixelData::GrayA16(v) | PixelData::Cmyka16(v) => (0..n)
|
||
.flat_map(|i| {
|
||
let s = v[i * spp + c];
|
||
(if invert { 65535 - s } else { s }).to_be_bytes()
|
||
})
|
||
.collect(),
|
||
}
|
||
}
|
||
fn alpha_is_opaque(&self, n: usize) -> bool {
|
||
let spp = self.spp();
|
||
match self {
|
||
PixelData::Rgba8(v) | PixelData::GrayA8(v) | PixelData::Cmyka8(v) => (0..n).all(|i| v[i * spp + spp - 1] == 255),
|
||
PixelData::Rgba16(v) | PixelData::GrayA16(v) | PixelData::Cmyka16(v) => (0..n).all(|i| v[i * spp + spp - 1] == 65535),
|
||
}
|
||
}
|
||
|
||
fn composite_plane(&self, c: usize, n: usize) -> Vec<u8> {
|
||
// Photoshop stores transparent RGB composites against white; layer channels stay straight,
|
||
// and so do Gray and CMYK composites here (see `PsdBuilder::composite`).
|
||
match self {
|
||
Self::Rgba8(v) if c < 3 => v
|
||
.as_chunks::<4>()
|
||
.0
|
||
.iter()
|
||
.take(n)
|
||
.map(|p| {
|
||
let (color, alpha) = (u32::from(p[c]), u32::from(p[3]));
|
||
if alpha == 0 { p[c] } else { ((color * alpha + 127) / 255 + 255 - alpha) as u8 }
|
||
})
|
||
.collect(),
|
||
Self::Rgba16(v) if c < 3 => v
|
||
.as_chunks::<4>()
|
||
.0
|
||
.iter()
|
||
.take(n)
|
||
.flat_map(|p| {
|
||
let (color, alpha) = (u32::from(p[c]), u32::from(p[3]));
|
||
let value = if alpha == 0 { p[c] } else { ((color * alpha + 32767) / 65535 + 65535 - alpha) as u16 };
|
||
value.to_be_bytes()
|
||
})
|
||
.collect(),
|
||
_ => self.plane(c, n),
|
||
}
|
||
}
|
||
}
|
||
|
||
/// A layer mask given as 8-bit samples.
|
||
#[derive(Debug, Clone, PartialEq)]
|
||
pub struct MaskSpec {
|
||
/// Mask rectangle in document coordinates.
|
||
pub rect: Rect,
|
||
/// `rect.width × rect.height` samples (0 = hidden, 255 = shown).
|
||
pub data: Vec<u8>,
|
||
/// Value outside the rectangle.
|
||
pub default_color: u8,
|
||
/// Mask disabled.
|
||
pub disabled: bool,
|
||
}
|
||
|
||
/// A raster layer.
|
||
#[derive(Debug, Clone, PartialEq)]
|
||
pub struct LayerSpec {
|
||
/// Name (written both as legacy Pascal name and `luni`).
|
||
pub name: String,
|
||
/// Left edge.
|
||
pub left: i32,
|
||
/// Top edge.
|
||
pub top: i32,
|
||
/// Width.
|
||
pub width: u32,
|
||
/// Height.
|
||
pub height: u32,
|
||
/// Pixels (must match the builder's color mode and depth).
|
||
pub pixels: PixelData,
|
||
/// Blend mode.
|
||
pub blend_mode: BlendMode,
|
||
/// Opacity 0..=255.
|
||
pub opacity: u8,
|
||
/// Fill opacity (`iOpa`), if not 255.
|
||
pub fill_opacity: Option<u8>,
|
||
/// Visible.
|
||
pub visible: bool,
|
||
/// Clipped to the layer below.
|
||
pub clipping: bool,
|
||
/// Optional user mask.
|
||
pub mask: Option<MaskSpec>,
|
||
/// Additional blocks appended verbatim.
|
||
pub extra_blocks: Vec<TaggedBlock>,
|
||
}
|
||
|
||
impl LayerSpec {
|
||
/// A visible normal layer at full opacity.
|
||
pub fn new(name: impl Into<String>, left: i32, top: i32, width: u32, height: u32, pixels: PixelData) -> Self {
|
||
LayerSpec {
|
||
name: name.into(),
|
||
left,
|
||
top,
|
||
width,
|
||
height,
|
||
pixels,
|
||
blend_mode: BlendMode::Normal,
|
||
opacity: 255,
|
||
fill_opacity: None,
|
||
visible: true,
|
||
clipping: false,
|
||
mask: None,
|
||
extra_blocks: Vec::new(),
|
||
}
|
||
}
|
||
}
|
||
|
||
/// A group (folder).
|
||
#[derive(Debug, Clone, PartialEq)]
|
||
pub struct GroupSpec {
|
||
/// Name.
|
||
pub name: String,
|
||
/// Blend mode (default pass-through).
|
||
pub blend_mode: BlendMode,
|
||
/// Opacity.
|
||
pub opacity: u8,
|
||
/// Visible.
|
||
pub visible: bool,
|
||
/// Open (expanded) in the layers panel.
|
||
pub open: bool,
|
||
}
|
||
|
||
impl GroupSpec {
|
||
/// A visible, open, pass-through group.
|
||
pub fn new(name: impl Into<String>) -> Self {
|
||
GroupSpec { name: name.into(), blend_mode: BlendMode::PassThrough, opacity: 255, visible: true, open: true }
|
||
}
|
||
}
|
||
|
||
#[derive(Debug, Clone)]
|
||
enum Entry {
|
||
Layer(LayerSpec),
|
||
GroupStart,
|
||
GroupEnd(GroupSpec),
|
||
}
|
||
|
||
/// Builds [`PsdFile`]s. Layers are pushed bottom-to-top.
|
||
#[derive(Debug, Clone)]
|
||
pub struct PsdBuilder {
|
||
width: u32,
|
||
height: u32,
|
||
color_mode: ColorMode,
|
||
depth: u16,
|
||
version: Version,
|
||
compression: Compression,
|
||
entries: Vec<Entry>,
|
||
open_groups: Vec<GroupSpec>,
|
||
composite: Option<PixelData>,
|
||
resources: Vec<ImageResource>,
|
||
}
|
||
|
||
impl PsdBuilder {
|
||
/// An 8-bit RGB PSD using RLE compression.
|
||
pub fn new(width: u32, height: u32) -> Self {
|
||
PsdBuilder {
|
||
width,
|
||
height,
|
||
color_mode: ColorMode::Rgb,
|
||
depth: 8,
|
||
version: Version::Psd,
|
||
compression: Compression::Rle,
|
||
entries: Vec::new(),
|
||
open_groups: Vec::new(),
|
||
composite: None,
|
||
resources: Vec::new(),
|
||
}
|
||
}
|
||
/// Sets the color mode (RGB, Grayscale or CMYK).
|
||
pub fn color_mode(mut self, mode: ColorMode) -> Self {
|
||
self.color_mode = mode;
|
||
self
|
||
}
|
||
/// Sets the depth (8 or 16).
|
||
pub fn depth(mut self, depth: u16) -> Self {
|
||
self.depth = depth;
|
||
self
|
||
}
|
||
/// Sets PSD or PSB.
|
||
pub fn version(mut self, v: Version) -> Self {
|
||
self.version = v;
|
||
self
|
||
}
|
||
/// Sets the compression used for all channels and the merged image.
|
||
pub fn compression(mut self, c: Compression) -> Self {
|
||
self.compression = c;
|
||
self
|
||
}
|
||
/// Adds a resolution resource.
|
||
pub fn resolution(mut self, dpi: f64) -> Self {
|
||
self.resources.push(ImageResource::new(ids::RESOLUTION_INFO, ResolutionInfo::from_dpi(dpi).to_bytes()));
|
||
self
|
||
}
|
||
/// Adds an ICC profile resource.
|
||
pub fn icc_profile(mut self, icc: Vec<u8>) -> Self {
|
||
self.resources.push(ImageResource::new(ids::ICC_PROFILE, icc));
|
||
self
|
||
}
|
||
/// Adds an arbitrary image resource.
|
||
pub fn resource(mut self, r: ImageResource) -> Self {
|
||
self.resources.push(r);
|
||
self
|
||
}
|
||
/// Pushes a layer above the previous one.
|
||
pub fn push_layer(&mut self, spec: LayerSpec) -> &mut Self {
|
||
self.entries.push(Entry::Layer(spec));
|
||
self
|
||
}
|
||
/// Opens a group; subsequent layers go inside until [`Self::end_group`].
|
||
pub fn begin_group(&mut self, spec: GroupSpec) -> &mut Self {
|
||
self.entries.push(Entry::GroupStart);
|
||
self.open_groups.push(spec);
|
||
self
|
||
}
|
||
/// Closes the innermost open group.
|
||
pub fn end_group(&mut self) -> Result<&mut Self> {
|
||
let g = self.open_groups.pop().ok_or_else(|| PsdError::invalid("end_group without begin_group"))?;
|
||
self.entries.push(Entry::GroupEnd(g));
|
||
Ok(self)
|
||
}
|
||
/// Supplies the merged composite (straight samples, same format as layers, full canvas).
|
||
/// Transparent RGB is stored against white, quantized at the document depth. Gray and CMYK
|
||
/// composites with alpha are stored straight (not matted against white as Photoshop would):
|
||
/// a test that reads them as a Photoshop merged image has to allow for that.
|
||
pub fn composite(&mut self, pixels: PixelData) -> &mut Self {
|
||
self.composite = Some(pixels);
|
||
self
|
||
}
|
||
|
||
fn color_channels(&self) -> Result<usize> {
|
||
match self.color_mode {
|
||
ColorMode::Rgb => Ok(3),
|
||
ColorMode::Grayscale => Ok(1),
|
||
ColorMode::Cmyk => Ok(4),
|
||
m => Err(PsdError::Unsupported(format!("builder color mode {m:?}"))),
|
||
}
|
||
}
|
||
|
||
fn check_pixels(&self, p: &PixelData, n: usize) -> Result<()> {
|
||
if p.mode_depth() != (self.color_mode, self.depth) {
|
||
return Err(PsdError::invalid(format!("pixel format {:?} does not match document {:?}/{}", p.mode_depth(), self.color_mode, self.depth)));
|
||
}
|
||
if p.len() != n * p.spp() {
|
||
return Err(PsdError::invalid(format!("pixel buffer has {} samples, expected {}", p.len(), n * p.spp())));
|
||
}
|
||
Ok(())
|
||
}
|
||
|
||
fn channel(&self, id: i16, plane: &[u8], w: usize, h: usize) -> Result<ChannelData> {
|
||
ChannelData::encode(id, self.compression, plane, w, h, self.depth, self.version)
|
||
}
|
||
|
||
fn empty_channels(&self, cc: usize) -> Result<Vec<ChannelData>> {
|
||
let mut v = vec![self.channel(-1, &[], 0, 0)?];
|
||
for c in 0..cc {
|
||
v.push(self.channel(c as i16, &[], 0, 0)?);
|
||
}
|
||
Ok(v)
|
||
}
|
||
|
||
fn layer_record(&self, spec: &LayerSpec, cc: usize, id: u32) -> Result<LayerRecord> {
|
||
let (w, h) = (spec.width as usize, spec.height as usize);
|
||
let n = w * h;
|
||
self.check_pixels(&spec.pixels, n)?;
|
||
let mut channels = vec![self.channel(-1, &spec.pixels.plane(cc, n), w, h)?];
|
||
for c in 0..cc {
|
||
channels.push(self.channel(c as i16, &spec.pixels.plane(c, n), w, h)?);
|
||
}
|
||
let mut mask = MaskData::None;
|
||
if let Some(m) = &spec.mask {
|
||
let (mw, mh) = m.rect.size()?;
|
||
if m.data.len() != mw * mh {
|
||
return Err(PsdError::invalid("mask buffer size mismatch"));
|
||
}
|
||
let plane: Vec<u8> = if self.depth == 16 { m.data.iter().flat_map(|&v| (u16::from(v) * 257).to_be_bytes()).collect() } else { m.data.clone() };
|
||
channels.push(self.channel(-2, &plane, mw, mh)?);
|
||
let flags = if m.disabled { LayerMask::FLAG_DISABLED } else { 0 };
|
||
mask = MaskData::Mask(LayerMask::new(m.rect, m.default_color, flags));
|
||
}
|
||
let mut flags = LayerFlags(0);
|
||
flags.set_hidden(!spec.visible);
|
||
let mut blocks = vec![TaggedBlock::unicode_name(&spec.name), TaggedBlock::layer_id(id)];
|
||
if let Some(f) = spec.fill_opacity {
|
||
blocks.push(TaggedBlock::fill_opacity(f));
|
||
}
|
||
blocks.extend(spec.extra_blocks.iter().cloned());
|
||
Ok(LayerRecord {
|
||
rect: Rect::from_xywh(spec.left, spec.top, spec.width, spec.height),
|
||
channels,
|
||
blend_mode: spec.blend_mode,
|
||
opacity: spec.opacity,
|
||
clipping: u8::from(spec.clipping),
|
||
flags,
|
||
filler: 0,
|
||
mask,
|
||
blending_ranges: BlendingRanges::full(cc),
|
||
name: encode_legacy_name(&spec.name),
|
||
blocks,
|
||
extra_trailing: Vec::new(),
|
||
})
|
||
}
|
||
|
||
/// Builds the file model.
|
||
pub fn build(&self) -> Result<PsdFile> {
|
||
if !self.open_groups.is_empty() {
|
||
return Err(PsdError::invalid("unclosed group"));
|
||
}
|
||
if !matches!(self.depth, 8 | 16) {
|
||
return Err(PsdError::Unsupported(format!("builder depth {}", self.depth)));
|
||
}
|
||
let cc = self.color_channels()?;
|
||
let mut layers = Vec::new();
|
||
for (id, e) in (1u32..).zip(self.entries.iter()) {
|
||
match e {
|
||
Entry::Layer(spec) => layers.push(self.layer_record(spec, cc, id)?),
|
||
Entry::GroupStart => layers.push(LayerRecord {
|
||
channels: self.empty_channels(cc)?,
|
||
blending_ranges: BlendingRanges::full(cc),
|
||
name: b"</Layer group>".to_vec(),
|
||
blocks: vec![
|
||
TaggedBlock::unicode_name("</Layer group>"),
|
||
TaggedBlock::layer_id(id),
|
||
TaggedBlock::section_divider(SectionType::BoundingDivider, None, None),
|
||
],
|
||
..Default::default()
|
||
}),
|
||
Entry::GroupEnd(g) => {
|
||
let kind = if g.open { SectionType::OpenFolder } else { SectionType::ClosedFolder };
|
||
let mut flags = LayerFlags(0);
|
||
flags.set_hidden(!g.visible);
|
||
layers.push(LayerRecord {
|
||
channels: self.empty_channels(cc)?,
|
||
blend_mode: g.blend_mode,
|
||
opacity: g.opacity,
|
||
flags,
|
||
blending_ranges: BlendingRanges::full(cc),
|
||
name: encode_legacy_name(&g.name),
|
||
blocks: vec![
|
||
TaggedBlock::unicode_name(&g.name),
|
||
TaggedBlock::layer_id(id),
|
||
TaggedBlock::section_divider(kind, Some(g.blend_mode), None),
|
||
],
|
||
..Default::default()
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
let (w, h) = (self.width as usize, self.height as usize);
|
||
let n = w * h;
|
||
let (real, planes, with_alpha) = match &self.composite {
|
||
Some(p) => {
|
||
self.check_pixels(p, n)?;
|
||
let with_alpha = !p.alpha_is_opaque(n);
|
||
let mut planes = Vec::new();
|
||
for c in 0..cc + usize::from(with_alpha) {
|
||
planes.extend(if with_alpha { p.composite_plane(c, n) } else { p.plane(c, n) });
|
||
}
|
||
(true, planes, with_alpha)
|
||
}
|
||
None => {
|
||
let bps = usize::from(self.depth / 8);
|
||
// White: 0xff in RGB/gray; no ink (stored inverted as 0xff) in CMYK.
|
||
(false, vec![0xff; n * bps * cc], false)
|
||
}
|
||
};
|
||
let channels = (cc + usize::from(with_alpha)) as u16;
|
||
let header = Header::new(self.version, self.width, self.height, channels, self.depth, self.color_mode);
|
||
header.validate()?;
|
||
let image_data = ImageData::encode(self.compression, &planes, &header)?;
|
||
let mut resources = self.resources.clone();
|
||
resources.push(version_info_resource(real));
|
||
|
||
let has_layers = !layers.is_empty();
|
||
let layer_info = has_layers.then_some(LayerInfo { merged_alpha: with_alpha, layers, padding: None });
|
||
let layer_info_placement = if has_layers && self.depth == 16 {
|
||
LayerInfoPlacement::GlobalBlock { index: 0, signature: *b"8BIM", key: *b"Lr16", padding: None }
|
||
} else {
|
||
LayerInfoPlacement::Section
|
||
};
|
||
Ok(PsdFile {
|
||
header,
|
||
color_mode_data: Vec::new(),
|
||
resources,
|
||
layer_info,
|
||
layer_info_placement,
|
||
global_layer_mask: has_layers.then(GlobalLayerMask::default),
|
||
global_blocks: Vec::new(),
|
||
layer_mask_trailing: Vec::new(),
|
||
image_data,
|
||
})
|
||
}
|
||
|
||
/// Builds and serializes.
|
||
pub fn to_bytes(&self) -> Result<Vec<u8>> {
|
||
self.build()?.to_bytes()
|
||
}
|
||
}
|