mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-13 13:07:00 +00:00
craft-psd: shared descriptor lookups
Descriptor::get_bool, get_number, get_text, get_enum and get_object, used by asl, grd and slices in place of their own copies. Ported from storytold/photocraft#9552 (its crates/psd part).
This commit is contained in:
committed by
Dylan M. Taylor
parent
c6db11f281
commit
c21343800e
@@ -65,28 +65,24 @@ impl<'a> Rd<'a> {
|
||||
let b = self.take(2)?;
|
||||
Ok(u16::from_be_bytes([b[0], b[1]]))
|
||||
}
|
||||
/// The next big-endian `u32`.
|
||||
fn u32(&mut self) -> Result<u32> {
|
||||
let b = self.take(4)?;
|
||||
Ok(u32::from_be_bytes([b[0], b[1], b[2], b[3]]))
|
||||
}
|
||||
/// Bytes left to read.
|
||||
fn remaining(&self) -> usize {
|
||||
self.b.len().saturating_sub(self.p)
|
||||
}
|
||||
}
|
||||
|
||||
fn text(d: &Descriptor, key: &str) -> String {
|
||||
match d.get(key) {
|
||||
Some(Value::Text(t)) => t.to_string_lossy(),
|
||||
_ => String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// One style from an ASL body: its identity descriptor, then the style itself.
|
||||
fn read_style(body: &[u8]) -> Result<AslStyle> {
|
||||
let (identity, used) = VersionedDescriptor::parse_prefix(body)?;
|
||||
let rest = body.get(used..).ok_or(PsdError::UnexpectedEof { offset: used, needed: 4 })?;
|
||||
let (style, _) = VersionedDescriptor::parse_prefix(rest)?;
|
||||
let id = &identity.descriptor;
|
||||
Ok(AslStyle { name: text(id, "Nm "), id: text(id, "Idnt"), style: style.descriptor })
|
||||
Ok(AslStyle { name: id.get_text("Nm ").unwrap_or_default(), id: id.get_text("Idnt").unwrap_or_default(), style: style.descriptor })
|
||||
}
|
||||
|
||||
/// Parses an `.asl` style library.
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
use crate::error::{PsdError, Result};
|
||||
use crate::io::{Reader, WriteExt, read_unicode_units, write_unicode_units};
|
||||
|
||||
mod lookup;
|
||||
|
||||
/// Maximum nesting depth accepted when parsing (fuzz safety).
|
||||
pub const MAX_DEPTH: usize = 64;
|
||||
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
use super::{Descriptor, Value};
|
||||
|
||||
impl Descriptor {
|
||||
/// The boolean at `key`.
|
||||
pub fn get_bool(&self, key: &str) -> Option<bool> {
|
||||
match self.get(key)? {
|
||||
Value::Boolean(b) => Some(*b),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// The number at `key`, from any numeric value type.
|
||||
pub fn get_number(&self, key: &str) -> Option<f64> {
|
||||
match self.get(key)? {
|
||||
Value::Double(v) => Some(*v),
|
||||
Value::UnitFloat { value, .. } => Some(*value),
|
||||
Value::Integer(v) => Some(f64::from(*v)),
|
||||
Value::LargeInteger(v) => Some(*v as f64),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// The text at `key`.
|
||||
pub fn get_text(&self, key: &str) -> Option<String> {
|
||||
match self.get(key)? {
|
||||
Value::Text(t) => Some(t.to_string_lossy()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// The enumerated value at `key`.
|
||||
pub fn get_enum(&self, key: &str) -> Option<String> {
|
||||
match self.get(key)? {
|
||||
Value::Enumerated { value, .. } => Some(String::from_utf8_lossy(value.as_bytes()).into_owned()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// The object at `key`, plain or global.
|
||||
pub fn get_object(&self, key: &str) -> Option<&Descriptor> {
|
||||
match self.get(key)? {
|
||||
Value::Descriptor(o) | Value::GlobalObject(o) => Some(o),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::super::{Id, UnicodeString};
|
||||
use super::*;
|
||||
|
||||
fn sample() -> Descriptor {
|
||||
Descriptor::new("null")
|
||||
.with("enab", Value::Boolean(true))
|
||||
.with("doub", Value::Double(0.25))
|
||||
.with("Opct", Value::UnitFloat { unit: *b"#Prc", value: 75.5 })
|
||||
.with("long", Value::Integer(-5))
|
||||
.with("comp", Value::LargeInteger(1 << 40))
|
||||
.with("Nm ", Value::Text(UnicodeString::new("Café")))
|
||||
.with("Md ", Value::Enumerated { type_id: Id::new("BlnM"), value: Id::new("Nrml") })
|
||||
.with("Objc", Value::Descriptor(Descriptor::new("Clr ").with("Rd ", Value::Double(1.0))))
|
||||
.with("GlbO", Value::GlobalObject(Descriptor::new("Grsc")))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn each_lookup_reads_its_own_value_type() {
|
||||
let d = sample();
|
||||
assert_eq!(d.get_bool("enab"), Some(true));
|
||||
assert_eq!(d.get_number("doub"), Some(0.25));
|
||||
assert_eq!(d.get_number("Opct"), Some(75.5));
|
||||
assert_eq!(d.get_number("long"), Some(-5.0));
|
||||
assert_eq!(d.get_number("comp"), Some((1u64 << 40) as f64));
|
||||
assert_eq!(d.get_text("Nm ").as_deref(), Some("Café"));
|
||||
assert_eq!(d.get_enum("Md ").as_deref(), Some("Nrml"));
|
||||
assert_eq!(d.get_object("Objc").and_then(|o| o.get_number("Rd ")), Some(1.0));
|
||||
assert!(d.get_object("GlbO").is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lookups_of_other_types_or_missing_keys_are_none() {
|
||||
let d = sample();
|
||||
assert_eq!(d.get_bool("doub"), None);
|
||||
assert_eq!(d.get_number("enab"), None);
|
||||
assert_eq!(d.get_text("long"), None);
|
||||
assert_eq!(d.get_enum("Nm "), None);
|
||||
assert!(d.get_object("Md ").is_none());
|
||||
for key in ["none", " "] {
|
||||
assert_eq!((d.get_bool(key), d.get_number(key), d.get_text(key), d.get_enum(key)), (None, None, None, None));
|
||||
assert!(d.get_object(key).is_none());
|
||||
}
|
||||
}
|
||||
}
|
||||
+12
-23
@@ -58,18 +58,6 @@ pub struct GrdGradient {
|
||||
pub opacity: Vec<(f32, f32, f32)>,
|
||||
}
|
||||
|
||||
/// The raw number behind a key. It is deliberately *not* filtered: every caller passes the result
|
||||
/// through `finite`, so filtering here as well would give two guards with two different answers to
|
||||
/// the same input, and mutation testing showed the second one was unreachable.
|
||||
fn num(d: &Descriptor, k: &str) -> Option<f64> {
|
||||
match d.get(k)? {
|
||||
Value::Double(v) => Some(*v),
|
||||
Value::UnitFloat { value, .. } => Some(*value),
|
||||
Value::Integer(v) => Some(f64::from(*v)),
|
||||
Value::LargeInteger(v) => Some(*v as f64),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
/// The single guard every number passes through on its way to an `f32`. Only a NaN is truly
|
||||
/// without a value, so only a NaN returns `None`; the caller then falls back to the default its
|
||||
/// own field documents (opacity opaque, a midpoint centred, a channel zero).
|
||||
@@ -85,9 +73,11 @@ fn finite(v: f64) -> Option<f32> {
|
||||
}
|
||||
Some(v.clamp(f64::from(f32::MIN), f64::from(f32::MAX)) as f32)
|
||||
}
|
||||
/// The finite `f32` at `k`, or 0.
|
||||
fn f(d: &Descriptor, k: &str) -> f32 {
|
||||
finite(num(d, k).unwrap_or(0.0)).unwrap_or(0.0)
|
||||
finite(d.get_number(k).unwrap_or(0.0)).unwrap_or(0.0)
|
||||
}
|
||||
/// `v` clamped to 0..=1.
|
||||
fn clamp01(v: f32) -> f32 {
|
||||
v.clamp(0.0, 1.0)
|
||||
}
|
||||
@@ -99,12 +89,7 @@ fn hue(h: f32) -> f32 {
|
||||
if (0.0..=360.0).contains(&h) { h } else { h.rem_euclid(360.0) }
|
||||
}
|
||||
|
||||
fn obj<'a>(d: &'a Descriptor, k: &str) -> Option<&'a Descriptor> {
|
||||
match d.get(k)? {
|
||||
Value::Descriptor(o) | Value::GlobalObject(o) => Some(o),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
/// The list at `k`, empty when absent.
|
||||
fn list<'a>(d: &'a Descriptor, k: &str) -> &'a [Value] {
|
||||
match d.get(k) {
|
||||
Some(Value::List(v)) => v,
|
||||
@@ -115,6 +100,7 @@ fn enum_is(d: &Descriptor, k: &str, v: &str) -> bool {
|
||||
matches!(d.get(k), Some(Value::Enumerated { value, .. }) if value.is(v))
|
||||
}
|
||||
|
||||
/// A gradient stop colour in its recorded colour model.
|
||||
fn color(c: &Descriptor) -> Option<GrdColor> {
|
||||
let id = c.class_id.as_bytes();
|
||||
Some(match id {
|
||||
@@ -135,13 +121,16 @@ fn color(c: &Descriptor) -> Option<GrdColor> {
|
||||
})
|
||||
}
|
||||
|
||||
/// A stop's location as a 0..1 fraction.
|
||||
fn location(d: &Descriptor) -> f32 {
|
||||
(f(d, "Lctn") / 4096.0).clamp(0.0, 1.0)
|
||||
}
|
||||
/// A stop's midpoint as a 0..1 fraction, 0.5 when absent.
|
||||
fn midpoint(d: &Descriptor) -> f32 {
|
||||
finite(num(d, "Mdpn").unwrap_or(50.0) / 100.0).unwrap_or(0.5).clamp(0.0, 1.0)
|
||||
finite(d.get_number("Mdpn").unwrap_or(50.0) / 100.0).unwrap_or(0.5).clamp(0.0, 1.0)
|
||||
}
|
||||
|
||||
/// One gradient from its descriptor.
|
||||
fn gradient(g: &Descriptor) -> GrdGradient {
|
||||
let name = match g.get("Nm ") {
|
||||
Some(Value::Text(t)) => t.to_string_lossy(),
|
||||
@@ -155,7 +144,7 @@ fn gradient(g: &Descriptor) -> GrdGradient {
|
||||
} else if enum_is(s, "Type", "BckC") {
|
||||
Some(GrdColor::Background)
|
||||
} else {
|
||||
obj(s, "Clr ").and_then(color)
|
||||
s.get_object("Clr ").and_then(color)
|
||||
};
|
||||
if let Some(color) = c {
|
||||
out.stops.push(GrdStop { location: location(s), midpoint: midpoint(s), color });
|
||||
@@ -163,7 +152,7 @@ fn gradient(g: &Descriptor) -> GrdGradient {
|
||||
}
|
||||
for v in list(g, "Trns").iter().take(MAX_STOPS) {
|
||||
let Value::Descriptor(s) = v else { continue };
|
||||
out.opacity.push((location(s), finite(num(s, "Opct").unwrap_or(100.0) / 100.0).unwrap_or(1.0).clamp(0.0, 1.0), midpoint(s)));
|
||||
out.opacity.push((location(s), finite(s.get_number("Opct").unwrap_or(100.0) / 100.0).unwrap_or(1.0).clamp(0.0, 1.0), midpoint(s)));
|
||||
}
|
||||
out
|
||||
}
|
||||
@@ -185,7 +174,7 @@ pub fn parse(data: &[u8]) -> Result<Vec<GrdGradient>> {
|
||||
.iter()
|
||||
.take(MAX_GRADIENTS)
|
||||
.filter_map(|v| match v {
|
||||
Value::Descriptor(d) => obj(d, "Grad").map(gradient),
|
||||
Value::Descriptor(d) => d.get_object("Grad").map(gradient),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
|
||||
+18
-24
@@ -108,20 +108,7 @@ fn d_u32(d: &Descriptor, key: &str) -> Option<u32> {
|
||||
}
|
||||
}
|
||||
|
||||
fn d_text(d: &Descriptor, key: &str) -> String {
|
||||
match d.get(key) {
|
||||
Some(Value::Text(t)) => t.to_string_lossy(),
|
||||
_ => String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn d_enum(d: &Descriptor, key: &str) -> Option<String> {
|
||||
match d.get(key)? {
|
||||
Value::Enumerated { value, .. } => Some(String::from_utf8_lossy(value.as_bytes()).into_owned()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the boolean at `key` is true.
|
||||
fn d_bool(d: &Descriptor, key: &str) -> bool {
|
||||
matches!(d.get(key), Some(Value::Boolean(true)))
|
||||
}
|
||||
@@ -141,6 +128,7 @@ fn origin_from(s: &str) -> u32 {
|
||||
}
|
||||
}
|
||||
|
||||
/// The slice type code a slice type name stands for.
|
||||
fn kind_from(s: &str) -> u32 {
|
||||
match s {
|
||||
"noImage" => 0,
|
||||
@@ -149,9 +137,10 @@ fn kind_from(s: &str) -> u32 {
|
||||
}
|
||||
}
|
||||
|
||||
/// A slice record from its descriptor.
|
||||
fn slice_from_descriptor(d: &Descriptor) -> SliceRecord {
|
||||
let b = d_bounds(d, "bounds").unwrap_or_default();
|
||||
let color = match (d_enum(d, "bgColorType").as_deref(), d.get("bgColor")) {
|
||||
let color = match (d.get_enum("bgColorType").as_deref(), d.get("bgColor")) {
|
||||
(Some("Clr "), Some(Value::Descriptor(c))) => {
|
||||
let g = |k: &str| d_int(c, k).unwrap_or(0).clamp(0, 255) as u8;
|
||||
let a = if c.get("alpha").is_some() { g("alpha") } else { 255 };
|
||||
@@ -162,18 +151,18 @@ fn slice_from_descriptor(d: &Descriptor) -> SliceRecord {
|
||||
SliceRecord {
|
||||
id: d_u32(d, "sliceID").unwrap_or(0),
|
||||
group_id: d_u32(d, "groupID").unwrap_or(0),
|
||||
origin: d_enum(d, "origin").map_or(2, |s| origin_from(&s)),
|
||||
origin: d.get_enum("origin").map_or(2, |s| origin_from(&s)),
|
||||
layer_id: d_u32(d, "layerID"),
|
||||
name: d_text(d, "Nm "),
|
||||
kind: d_enum(d, "Type").map_or(1, |s| kind_from(&s)),
|
||||
name: d.get_text("Nm ").unwrap_or_default(),
|
||||
kind: d.get_enum("Type").map_or(1, |s| kind_from(&s)),
|
||||
// Descriptor bounds are top/left/bottom/right; records use left/top/right/bottom.
|
||||
rect: [b[1], b[0], b[3], b[2]],
|
||||
url: d_text(d, "url"),
|
||||
target: d_text(d, "null"),
|
||||
message: d_text(d, "Msge"),
|
||||
alt: d_text(d, "altTag"),
|
||||
url: d.get_text("url").unwrap_or_default(),
|
||||
target: d.get_text("null").unwrap_or_default(),
|
||||
message: d.get_text("Msge").unwrap_or_default(),
|
||||
alt: d.get_text("altTag").unwrap_or_default(),
|
||||
cell_text_is_html: d_bool(d, "cellTextIsHTML"),
|
||||
cell_text: d_text(d, "cellText"),
|
||||
cell_text: d.get_text("cellText").unwrap_or_default(),
|
||||
horizontal_align: 0,
|
||||
vertical_align: 0,
|
||||
color,
|
||||
@@ -220,7 +209,12 @@ impl SlicesResource {
|
||||
.collect(),
|
||||
_ => Vec::new(),
|
||||
};
|
||||
Ok(SlicesResource { version, bounds: d_bounds(d, "bounds").unwrap_or_default(), group_name: d_text(d, "baseName"), slices })
|
||||
Ok(SlicesResource {
|
||||
version,
|
||||
bounds: d_bounds(d, "bounds").unwrap_or_default(),
|
||||
group_name: d.get_text("baseName").unwrap_or_default(),
|
||||
slices,
|
||||
})
|
||||
}
|
||||
v => Err(PsdError::invalid(format!("unsupported slices resource version {v}"))),
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user