1use crate::descriptor::{Descriptor, DescriptorValue, UnitDoubleValue};
25use crate::psd::{BlendMode, ColorMode, PatternBounds, PatternInfo};
26use crate::reader::{
27 check_signature, read_bytes, read_int16, read_int32, read_pascal_string, read_signature,
28 read_uint16, read_uint32, read_uint8, read_unicode_string, skip_bytes, PsdReader, ReadError,
29 ReadResult,
30};
31
32#[derive(Debug, Clone, Default)]
38pub struct Abr {
39 pub brushes: Vec<Brush>,
40 pub samples: Vec<SampleInfo>,
41 pub patterns: Vec<PatternInfo>,
42}
43
44#[derive(Debug, Clone)]
46pub struct SampleInfo {
47 pub id: String,
48 pub bounds: SampleBounds,
49 pub alpha: Vec<u8>,
50}
51
52#[derive(Debug, Clone, Copy, Default)]
53pub struct SampleBounds {
54 pub x: i32,
55 pub y: i32,
56 pub w: i32,
57 pub h: i32,
58}
59
60#[derive(Debug, Clone)]
62pub struct BrushDynamics {
63 pub control: String,
65 pub steps: f64,
66 pub jitter: f64,
67 pub minimum: f64,
68}
69
70const DYNAMICS_CONTROL: &[&str] = &[
71 "off",
72 "fade",
73 "pen pressure",
74 "pen tilt",
75 "stylus wheel",
76 "initial direction",
77 "direction",
78 "initial rotation",
79 "rotation",
80];
81
82const DYNAMIC_BRUSH_SHAPE_SHAPES: &[&str] = &[
83 "round point",
84 "round blunt",
85 "round curve",
86 "round angle",
87 "round fan",
88 "flat point",
89 "flat blunt",
90 "flat curve",
91 "flat angle",
92 "flat fan",
93];
94
95const TIPS_BRUSH_SHAPE_SHAPES: &[&str] = &[
96 "erodible point",
97 "erodible flat",
98 "erodible round",
99 "erodible square",
100 "erodible triangle",
101 "custom",
102];
103
104#[derive(Debug, Clone)]
106pub enum BrushShape {
107 Computed {
108 size: f64,
109 angle: f64,
110 roundness: f64,
111 hardness: f64,
112 spacing_on: bool,
113 spacing: f64,
114 flip_x: bool,
115 flip_y: bool,
116 },
117 Sampled {
118 name: String,
119 size: f64,
120 angle: f64,
121 roundness: f64,
122 spacing_on: bool,
123 spacing: f64,
124 flip_x: bool,
125 flip_y: bool,
126 sampled_data: String,
127 },
128 Tips {
129 angle: f64,
130 size: f64,
131 shape: String,
132 physics: bool,
133 spacing: f64,
134 spacing_on: bool,
135 flip_x: bool,
136 flip_y: bool,
137 tips_type: String,
138 tips_length_ratio: f64,
139 tips_hardness: f64,
140 tips_grid_size: Option<f64>,
141 tips_erodible_tip_height_map: Option<Vec<u8>>,
142 tips_airbrush_cutoff_angle: f64,
143 tips_airbrush_granularity: f64,
144 tips_airbrush_streakiness: f64,
145 tips_airbrush_splat_size: f64,
146 tips_airbrush_splat_count: f64,
147 },
148 Dynamic {
149 size: f64,
150 angle: f64,
151 shape: String,
152 density: f64,
153 length: f64,
154 clumping: f64,
155 thickness: f64,
156 stiffness: f64,
157 physics: bool,
158 spacing: f64,
159 spacing_on: bool,
160 flip_x: bool,
161 flip_y: bool,
162 },
163}
164
165#[derive(Debug, Clone)]
166pub struct ShapeDynamics {
167 pub size_dynamics: BrushDynamics,
168 pub minimum_diameter: f64,
169 pub tilt_scale: f64,
170 pub angle_dynamics: BrushDynamics,
171 pub roundness_dynamics: BrushDynamics,
172 pub minimum_roundness: f64,
173 pub flip_x: bool,
174 pub flip_y: bool,
175 pub brush_projection: bool,
176}
177
178#[derive(Debug, Clone)]
179pub struct Scatter {
180 pub both_axes: bool,
181 pub scatter_dynamics: BrushDynamics,
182 pub count_dynamics: BrushDynamics,
183 pub count: f64,
184}
185
186#[derive(Debug, Clone)]
187pub struct Texture {
188 pub id: String,
189 pub name: String,
190 pub invert: bool,
191 pub scale: f64,
192 pub brightness: f64,
193 pub contrast: f64,
194 pub blend_mode: BlendMode,
195 pub depth: f64,
196 pub depth_minimum: f64,
197 pub depth_dynamics: BrushDynamics,
198 pub texture_each_tip: bool,
199}
200
201#[derive(Debug, Clone)]
202pub struct DualBrush {
203 pub flip: bool,
204 pub shape: BrushShape,
205 pub blend_mode: BlendMode,
206 pub use_scatter: bool,
207 pub spacing: f64,
208 pub count: f64,
209 pub both_axes: bool,
210 pub count_dynamics: BrushDynamics,
211 pub scatter_dynamics: BrushDynamics,
212}
213
214#[derive(Debug, Clone)]
215pub struct ColorDynamics {
216 pub foreground_background: BrushDynamics,
217 pub hue: f64,
218 pub saturation: f64,
219 pub brightness: f64,
220 pub purity: f64,
221 pub per_tip: bool,
222}
223
224#[derive(Debug, Clone)]
225pub struct Transfer {
226 pub flow_dynamics: BrushDynamics,
227 pub opacity_dynamics: BrushDynamics,
228 pub wetness_dynamics: BrushDynamics,
229 pub mix_dynamics: BrushDynamics,
230}
231
232#[derive(Debug, Clone)]
233pub struct BrushPose {
234 pub override_angle: bool,
235 pub override_tilt_x: bool,
236 pub override_tilt_y: bool,
237 pub override_pressure: bool,
238 pub pressure: f64,
239 pub tilt_x: f64,
240 pub tilt_y: f64,
241 pub angle: f64,
242}
243
244#[derive(Debug, Clone, Copy, PartialEq, Eq)]
246pub enum ToolType {
247 Brush,
248 MixerBrush,
249 SmudgeBrush,
250}
251
252#[derive(Debug, Clone)]
253pub struct ToolOptions {
254 pub type_: ToolType,
255 pub brush_preset: bool,
256 pub flow: f64,
257 pub wetness: Option<f64>,
258 pub dryness: Option<f64>,
259 pub mix: Option<f64>,
260 pub smooth: f64,
261 pub mode: BlendMode,
262 pub opacity: f64,
263 pub smoothing: bool,
264 pub smoothing_value: f64,
265 pub smoothing_radius_mode: bool,
266 pub smoothing_catchup: bool,
267 pub smoothing_catchup_at_end: bool,
268 pub smoothing_zoom_compensation: bool,
269 pub pressure_smoothing: bool,
270 pub use_pressure_overrides_size: bool,
271 pub use_pressure_overrides_opacity: bool,
272 pub use_legacy: bool,
273 pub auto_fill: Option<bool>,
274 pub auto_clean: Option<bool>,
275 pub load_solid_color_only: Option<bool>,
276 pub sample_all_layers: Option<bool>,
277 pub flow_dynamics: Option<BrushDynamics>,
278 pub opacity_dynamics: Option<BrushDynamics>,
279 pub size_dynamics: Option<BrushDynamics>,
280 pub smudge_finger_painting: Option<bool>,
281 pub smudge_sample_all_layers: Option<bool>,
282 pub strength: Option<f64>,
283}
284
285#[derive(Debug, Clone)]
287pub struct Brush {
288 pub name: String,
289 pub shape: BrushShape,
290 pub shape_dynamics: Option<ShapeDynamics>,
291 pub scatter: Option<Scatter>,
292 pub texture: Option<Texture>,
293 pub dual_brush: Option<DualBrush>,
294 pub color_dynamics: Option<ColorDynamics>,
295 pub transfer: Option<Transfer>,
296 pub brush_pose: Option<BrushPose>,
297 pub noise: bool,
298 pub wet_edges: bool,
299 pub protect_texture: Option<bool>,
300 pub spacing: f64,
301 pub interpretation: Option<bool>,
302 pub use_brush_size: bool,
303 pub tool_options: Option<ToolOptions>,
304}
305
306fn dget<'a>(d: &'a Descriptor, key: &str) -> Option<&'a DescriptorValue> {
311 d.get(key)
312}
313
314fn as_bool(v: Option<&DescriptorValue>) -> bool {
315 matches!(v, Some(DescriptorValue::Boolean(true)))
316}
317
318fn as_opt_bool(v: Option<&DescriptorValue>) -> Option<bool> {
319 match v {
320 Some(DescriptorValue::Boolean(b)) => Some(*b),
321 _ => None,
322 }
323}
324
325fn as_number(v: Option<&DescriptorValue>) -> f64 {
326 match v {
327 Some(DescriptorValue::Integer(i)) => *i as f64,
328 Some(DescriptorValue::Double(d)) => *d,
329 Some(DescriptorValue::UnitDouble(u)) => u.value,
330 _ => 0.0,
331 }
332}
333
334fn as_opt_number(v: Option<&DescriptorValue>) -> Option<f64> {
335 match v {
336 Some(DescriptorValue::Integer(i)) => Some(*i as f64),
337 Some(DescriptorValue::Double(d)) => Some(*d),
338 Some(DescriptorValue::UnitDouble(u)) => Some(u.value),
339 _ => None,
340 }
341}
342
343fn as_text(v: Option<&DescriptorValue>) -> String {
344 match v {
345 Some(DescriptorValue::Text(s)) => s.clone(),
346 Some(DescriptorValue::Enum(s)) => s.clone(),
347 _ => String::new(),
348 }
349}
350
351fn as_descriptor(v: Option<&DescriptorValue>) -> Option<&Descriptor> {
352 match v {
353 Some(DescriptorValue::Descriptor(d)) => Some(d),
354 _ => None,
355 }
356}
357
358fn as_units<'a>(v: Option<&'a DescriptorValue>) -> Option<&'a UnitDoubleValue> {
359 match v {
360 Some(DescriptorValue::UnitDouble(u)) => Some(u),
361 _ => None,
362 }
363}
364
365fn as_raw(v: Option<&DescriptorValue>) -> Option<&[u8]> {
366 match v {
367 Some(DescriptorValue::RawData(b)) => Some(b),
368 _ => None,
369 }
370}
371
372fn parse_angle(v: Option<&DescriptorValue>) -> ReadResult<f64> {
378 match as_units(v) {
379 None => Ok(0.0),
380 Some(u) => {
381 if u.units != "Angle" {
382 return Err(ReadError::StrictViolation(format!(
383 "Invalid units: {}",
384 u.units
385 )));
386 }
387 Ok(u.value)
388 }
389 }
390}
391
392fn parse_percent(v: Option<&DescriptorValue>) -> ReadResult<f64> {
394 match as_units(v) {
395 None => Ok(1.0),
396 Some(u) => {
397 if u.units != "Percent" {
398 return Err(ReadError::StrictViolation(format!(
399 "Invalid units: {}",
400 u.units
401 )));
402 }
403 Ok(u.value / 100.0)
404 }
405 }
406}
407
408fn parse_units_to_number(v: Option<&DescriptorValue>, expected: &str) -> ReadResult<f64> {
410 match as_units(v) {
411 Some(u) if u.units == expected => Ok(u.value),
412 Some(u) => Err(ReadError::StrictViolation(format!(
413 "Invalid units: {}",
414 u.units
415 ))),
416 None => Err(ReadError::StrictViolation(format!(
417 "Invalid units: missing (expected {})",
418 expected
419 ))),
420 }
421}
422
423fn blnm_decode(code: &str) -> BlendMode {
426 let key = code.split('.').nth(1).unwrap_or(code);
428 match key {
429 "Nrml" => BlendMode::Normal,
430 "Dslv" => BlendMode::Dissolve,
431 "Drkn" => BlendMode::Darken,
432 "Mltp" => BlendMode::Multiply,
433 "CBrn" => BlendMode::ColorBurn,
434 "linearBurn" => BlendMode::LinearBurn,
435 "darkerColor" => BlendMode::DarkerColor,
436 "Lghn" => BlendMode::Lighten,
437 "Scrn" => BlendMode::Screen,
438 "CDdg" => BlendMode::ColorDodge,
439 "linearDodge" => BlendMode::LinearDodge,
440 "lighterColor" => BlendMode::LighterColor,
441 "Ovrl" => BlendMode::Overlay,
442 "SftL" => BlendMode::SoftLight,
443 "HrdL" => BlendMode::HardLight,
444 "vividLight" => BlendMode::VividLight,
445 "linearLight" => BlendMode::LinearLight,
446 "pinLight" => BlendMode::PinLight,
447 "hardMix" => BlendMode::HardMix,
448 "Dfrn" => BlendMode::Difference,
449 "Xclu" => BlendMode::Exclusion,
450 "blendSubtraction" => BlendMode::Subtract,
451 "blendDivide" => BlendMode::Divide,
452 "H " => BlendMode::Hue,
453 "Strt" => BlendMode::Saturation,
454 "Clr " => BlendMode::Color,
455 "Lmns" => BlendMode::Luminosity,
456 _ => BlendMode::Normal,
459 }
460}
461
462fn parse_dynamics(desc: &Descriptor) -> ReadResult<BrushDynamics> {
463 let control_index = as_number(dget(desc, "bVTy")) as usize;
464 Ok(BrushDynamics {
465 control: DYNAMICS_CONTROL
466 .get(control_index)
467 .copied()
468 .unwrap_or("off")
469 .to_string(),
470 steps: as_number(dget(desc, "fStp")),
471 jitter: parse_percent(dget(desc, "jitter"))?,
472 minimum: parse_percent(dget(desc, "Mnm "))?,
473 })
474}
475
476fn parse_dynamics_opt(v: Option<&DescriptorValue>) -> ReadResult<Option<BrushDynamics>> {
477 match as_descriptor(v) {
478 Some(d) => Ok(Some(parse_dynamics(d)?)),
479 None => Ok(None),
480 }
481}
482
483fn parse_heightmap(array: &[u8]) -> Vec<u8> {
484 array.to_vec()
485}
486
487fn parse_brush_shape(desc: &Descriptor) -> ReadResult<BrushShape> {
488 match desc.class_id.as_str() {
489 "computedBrush" => Ok(BrushShape::Computed {
490 size: parse_units_to_number(dget(desc, "Dmtr"), "Pixels")?,
491 angle: parse_angle(dget(desc, "Angl"))?,
492 roundness: parse_percent(dget(desc, "Rndn"))?,
493 spacing_on: as_bool(dget(desc, "Intr")),
494 spacing: parse_percent(dget(desc, "Spcn"))?,
495 flip_x: as_bool(dget(desc, "flipX")),
496 flip_y: as_bool(dget(desc, "flipY")),
497 hardness: parse_percent(dget(desc, "Hrdn"))?,
498 }),
499 "sampledBrush" => Ok(BrushShape::Sampled {
500 size: parse_units_to_number(dget(desc, "Dmtr"), "Pixels")?,
501 angle: parse_angle(dget(desc, "Angl"))?,
502 roundness: parse_percent(dget(desc, "Rndn"))?,
503 spacing_on: as_bool(dget(desc, "Intr")),
504 spacing: parse_percent(dget(desc, "Spcn"))?,
505 flip_x: as_bool(dget(desc, "flipX")),
506 flip_y: as_bool(dget(desc, "flipY")),
507 name: as_text(dget(desc, "Nm ")),
508 sampled_data: as_text(dget(desc, "sampledData")),
509 }),
510 "dBrush" => Ok(BrushShape::Dynamic {
511 shape: shape_name(DYNAMIC_BRUSH_SHAPE_SHAPES, as_number(dget(desc, "Shp "))),
512 angle: parse_angle(dget(desc, "Angl"))?,
513 size: parse_units_to_number(dget(desc, "Dmtr"), "Pixels")?,
514 density: parse_percent(dget(desc, "Dnst"))?,
515 length: parse_percent(dget(desc, "Lngt"))?,
516 clumping: parse_percent(dget(desc, "clumping"))?,
517 thickness: parse_percent(dget(desc, "thickness"))?,
518 stiffness: parse_percent(dget(desc, "stiffness"))?,
519 physics: as_bool(dget(desc, "physics")),
520 spacing: parse_percent(dget(desc, "Spcn"))?,
521 spacing_on: as_bool(dget(desc, "Intr")),
522 flip_x: as_bool(dget(desc, "flipX")),
523 flip_y: as_bool(dget(desc, "flipY")),
524 }),
525 "dTips" => {
526 let grid_size = as_number(dget(desc, "dtipsGridSize"));
527 let height_map = as_raw(dget(desc, "dtipsErodibleTipHeightMap"));
528 let (tips_grid_size, tips_erodible_tip_height_map) =
529 if grid_size != 0.0 && height_map.is_some() {
530 (
531 Some(grid_size),
532 Some(parse_heightmap(height_map.unwrap())),
533 )
534 } else {
535 (None, None)
536 };
537 Ok(BrushShape::Tips {
538 angle: parse_angle(dget(desc, "Angl"))?,
539 size: parse_units_to_number(dget(desc, "Dmtr"), "Pixels")?,
540 shape: shape_name(DYNAMIC_BRUSH_SHAPE_SHAPES, as_number(dget(desc, "Shp "))),
541 physics: as_bool(dget(desc, "physics")),
542 spacing: parse_percent(dget(desc, "Spcn"))?,
543 spacing_on: as_bool(dget(desc, "Intr")),
544 flip_x: as_bool(dget(desc, "flipX")),
545 flip_y: as_bool(dget(desc, "flipY")),
546 tips_type: shape_name(TIPS_BRUSH_SHAPE_SHAPES, as_number(dget(desc, "dtipsType"))),
547 tips_length_ratio: parse_percent(dget(desc, "dtipsLengthRatio"))?,
548 tips_hardness: parse_percent(dget(desc, "dtipsHardness"))?,
549 tips_grid_size,
550 tips_erodible_tip_height_map,
551 tips_airbrush_cutoff_angle: as_number(dget(desc, "dtipsAirbrushCutoffAngle")),
552 tips_airbrush_granularity: parse_percent(dget(desc, "dtipsAirbrushGranularity"))?,
553 tips_airbrush_streakiness: parse_percent(dget(desc, "dtipsAirbrushStreakiness"))?,
554 tips_airbrush_splat_size: parse_percent(dget(desc, "dtipsAirbrushSplatSize"))?,
555 tips_airbrush_splat_count: as_number(dget(desc, "dtipsAirbrushSplatCount")),
556 })
557 }
558 other => Err(ReadError::StrictViolation(format!(
559 "Unknown brush classId: {}",
560 other
561 ))),
562 }
563}
564
565fn shape_name(table: &[&str], index: f64) -> String {
566 table
567 .get(index as usize)
568 .copied()
569 .unwrap_or("")
570 .to_string()
571}
572
573const TO_BRUSH_TYPE: &[(&str, ToolType)] = &[
574 ("_", ToolType::Brush),
575 ("MixB", ToolType::MixerBrush),
576 ("SmTl", ToolType::SmudgeBrush),
577];
578
579fn to_brush_type(class_id: &str) -> ToolType {
580 TO_BRUSH_TYPE
581 .iter()
582 .find(|(k, _)| *k == class_id)
583 .map(|(_, t)| *t)
584 .unwrap_or(ToolType::Brush)
585}
586
587struct PixelData<'a> {
592 data: &'a mut [u8],
593 width: usize,
594 #[allow(dead_code)]
595 height: usize,
596}
597
598fn read_data_rle(
601 reader: &mut PsdReader,
602 pixel_data: Option<&mut PixelData>,
603 width: usize,
604 height: usize,
605 _bit_depth: i32,
606 step: usize,
607 offsets: &[usize],
608) -> ReadResult<()> {
609 let mut lengths: Vec<u16> = vec![0; offsets.len() * height];
610 let mut li = 0usize;
611 for _ in 0..offsets.len() {
612 for _ in 0..height {
613 lengths[li] = read_uint16(reader)?;
614 li += 1;
615 }
616 }
617
618 let extra_limit = step.wrapping_sub(1);
619
620 let has_data = pixel_data.is_some();
621 let mut data: Option<&mut [u8]> = pixel_data.map(|p| &mut *p.data);
623
624 li = 0;
625 for c in 0..offsets.len() {
626 let offset = offsets[c];
627 let extra = c > extra_limit || offset > extra_limit;
628
629 if !has_data || extra {
630 for _ in 0..height {
631 skip_bytes(reader, lengths[li] as usize);
632 li += 1;
633 }
634 } else {
635 let mut p = offset;
636 for _ in 0..height {
637 let length = lengths[li] as usize;
638 let buffer = read_bytes(reader, length)?;
639 li += 1;
640
641 let buf = data.as_deref_mut().unwrap();
642 let mut i = 0usize;
643 let mut x = 0usize;
644 while i < length {
645 let mut header = buffer[i] as i32;
646 if header > 128 {
647 i += 1;
648 let value = buffer[i];
649 header = 256 - header;
650 let mut j = 0;
651 while j <= header && x < width {
652 buf[p] = value;
653 p += step;
654 j += 1;
655 x += 1;
656 }
657 } else if header < 128 {
658 let mut j = 0;
659 while j <= header && x < width {
660 i += 1;
661 buf[p] = buffer[i];
662 p += step;
663 j += 1;
664 x += 1;
665 }
666 }
667 i += 1;
669 }
670 }
671 }
672 }
673 Ok(())
674}
675
676fn setup_grayscale(data: &mut [u8], width: usize, height: usize) {
677 let size = width * height * 4;
678 let mut i = 0;
679 while i < size {
680 let c = data[i];
681 data[i + 1] = c;
682 data[i + 2] = c;
683 i += 4;
684 }
685}
686
687fn copy_channel_to_rgba(
688 src: &[u8],
689 src_w: usize,
690 src_h: usize,
691 dst: &mut [u8],
692 dst_w: usize,
693 ox: usize,
694 oy: usize,
695 offset: usize,
696) {
697 for y in 0..src_h {
698 for x in 0..src_w {
699 let s = x + y * src_w;
700 let d = (ox + x + (y + oy) * dst_w) * 4;
701 dst[d + offset] = src[s];
702 }
703 }
704}
705
706fn read_pattern(reader: &mut PsdReader) -> ReadResult<PatternInfo> {
709 let mut length = read_uint32(reader)? as usize;
710 while length % 4 != 0 {
711 length += 1;
712 }
713 let end = reader.offset + length;
714 let version = read_uint32(reader)?;
715 if version != 1 {
716 return Err(ReadError::StrictViolation(format!(
717 "Invalid pattern version: {}",
718 version
719 )));
720 }
721
722 let color_mode = read_uint32(reader)? as i32;
723 let x = read_int16(reader)?;
724 let y = read_int16(reader)?;
725
726 let rgb = ColorMode::Rgb as i32;
727 let grayscale = ColorMode::Grayscale as i32;
728 let indexed = ColorMode::Indexed as i32;
729 if color_mode != rgb && color_mode != grayscale && color_mode != indexed {
730 return Err(ReadError::StrictViolation(format!(
731 "Unsupported pattern color mode: {}",
732 color_mode
733 )));
734 }
735
736 let name = read_unicode_string(reader)?;
737 let id = read_pascal_string(reader, 1)?;
738
739 if color_mode == indexed {
740 for _ in 0..256 {
741 read_uint8(reader)?;
742 read_uint8(reader)?;
743 read_uint8(reader)?;
744 }
745 skip_bytes(reader, 4);
746 }
747
748 let version2 = read_uint32(reader)?;
750 if version2 != 3 {
751 return Err(ReadError::StrictViolation(format!(
752 "Invalid pattern VMAL version: {}",
753 version2
754 )));
755 }
756
757 read_uint32(reader)?; let top = read_uint32(reader)?;
759 let left = read_uint32(reader)?;
760 let bottom = read_uint32(reader)?;
761 let right = read_uint32(reader)?;
762 let channels_count = read_uint32(reader)?;
763 let width = (right - left) as usize;
764 let height = (bottom - top) as usize;
765 let mut data = vec![0u8; width * height * 4];
766
767 let mut i = 3;
768 while i < data.len() {
769 data[i] = 255;
770 i += 4;
771 }
772
773 let mut ch = 0usize;
774 for _ in 0..(channels_count + 2) {
775 let has = read_uint32(reader)?;
776 if has == 0 {
777 continue;
778 }
779
780 let clen = read_uint32(reader)? as usize;
781 let pixel_depth = read_uint32(reader)?;
782 let ctop = read_uint32(reader)?;
783 let cleft = read_uint32(reader)?;
784 let cbottom = read_uint32(reader)?;
785 let cright = read_uint32(reader)?;
786 let pixel_depth2 = read_uint16(reader)?;
787 let compression_mode = read_uint8(reader)?; let data_length = clen - (4 + 16 + 2 + 1);
789 let cdata = read_bytes(reader, data_length)?;
790
791 if pixel_depth != 8 || pixel_depth2 != 8 {
792 return Err(ReadError::StrictViolation(
793 "16bit pixel depth not supported for patterns".to_string(),
794 ));
795 }
796
797 let w = (cright - cleft) as usize;
798 let h = (cbottom - ctop) as usize;
799 let ox = (cleft - left) as usize;
800 let oy = (ctop - top) as usize;
801
802 if compression_mode == 0 {
803 if color_mode == rgb && ch < 3 {
804 for yy in 0..h {
805 for xx in 0..w {
806 let src = xx + yy * w;
807 let dst = (ox + xx + (yy + oy) * width) * 4;
808 data[dst + ch] = cdata[src];
809 }
810 }
811 }
812 if color_mode == grayscale && ch < 1 {
813 for yy in 0..h {
814 for xx in 0..w {
815 let src = xx + yy * w;
816 let dst = (ox + xx + (yy + oy) * width) * 4;
817 let value = cdata[src];
818 data[dst] = value;
819 data[dst + 1] = value;
820 data[dst + 2] = value;
821 }
822 }
823 }
824 if color_mode == indexed {
825 return Err(ReadError::StrictViolation(
826 "Indexed pattern color mode not implemented".to_string(),
827 ));
828 }
829 } else if compression_mode == 1 {
830 let mut temp = vec![0u8; w * h];
831 let mut cdata_reader = PsdReader::new(&cdata, None, None);
832
833 if color_mode == rgb && ch < 3 {
834 {
835 let mut pd = PixelData {
836 data: &mut temp,
837 width: w,
838 height: h,
839 };
840 read_data_rle(&mut cdata_reader, Some(&mut pd), w, h, 8, 1, &[0])?;
841 }
842 copy_channel_to_rgba(&temp, w, h, &mut data, width, ox, oy, ch);
843 }
844 if color_mode == grayscale && ch < 1 {
845 {
846 let mut pd = PixelData {
847 data: &mut temp,
848 width: w,
849 height: h,
850 };
851 read_data_rle(&mut cdata_reader, Some(&mut pd), w, h, 8, 1, &[0])?;
852 }
853 copy_channel_to_rgba(&temp, w, h, &mut data, width, ox, oy, 0);
854 setup_grayscale(&mut data, width, height);
855 }
856 if color_mode == indexed {
857 return Err(ReadError::StrictViolation(
858 "Indexed pattern color mode not implemented".to_string(),
859 ));
860 }
861 } else {
862 return Err(ReadError::StrictViolation(
863 "Invalid pattern compression mode".to_string(),
864 ));
865 }
866
867 ch += 1;
868 }
869
870 reader.offset = end;
871
872 Ok(PatternInfo {
873 id,
874 name,
875 x: x as f64,
876 y: y as f64,
877 bounds: PatternBounds {
878 x: left as f64,
879 y: top as f64,
880 w: width as f64,
881 h: height as f64,
882 },
883 data,
884 })
885}
886
887fn parse_brush(brush: &Descriptor) -> ReadResult<Brush> {
892 let shape_desc = as_descriptor(dget(brush, "Brsh"))
893 .ok_or_else(|| ReadError::StrictViolation("Missing brush shape descriptor".to_string()))?;
894
895 let mut b = Brush {
896 name: as_text(dget(brush, "Nm ")),
897 shape: parse_brush_shape(shape_desc)?,
898 spacing: parse_percent(dget(brush, "Spcn"))?,
899 wet_edges: as_bool(dget(brush, "Wtdg")),
900 noise: as_bool(dget(brush, "Nose")),
901 use_brush_size: as_bool(dget(brush, "useBrushSize")),
902 shape_dynamics: None,
903 scatter: None,
904 texture: None,
905 dual_brush: None,
906 color_dynamics: None,
907 transfer: None,
908 brush_pose: None,
909 protect_texture: None,
910 interpretation: None,
911 tool_options: None,
912 };
913
914 if let Some(v) = as_opt_bool(dget(brush, "interpretation")) {
915 b.interpretation = Some(v);
916 }
917 if let Some(v) = as_opt_bool(dget(brush, "protectTexture")) {
918 b.protect_texture = Some(v);
919 }
920
921 if as_bool(dget(brush, "useTipDynamics")) {
922 b.shape_dynamics = Some(ShapeDynamics {
923 tilt_scale: parse_percent(dget(brush, "tiltScale"))?,
924 size_dynamics: parse_dynamics_desc(brush, "szVr")?,
925 angle_dynamics: parse_dynamics_desc(brush, "angleDynamics")?,
926 roundness_dynamics: parse_dynamics_desc(brush, "roundnessDynamics")?,
927 flip_x: as_bool(dget(brush, "flipX")),
928 flip_y: as_bool(dget(brush, "flipY")),
929 brush_projection: as_bool(dget(brush, "brushProjection")),
930 minimum_diameter: parse_percent(dget(brush, "minimumDiameter"))?,
931 minimum_roundness: parse_percent(dget(brush, "minimumRoundness"))?,
932 });
933 }
934
935 if as_bool(dget(brush, "useScatter")) {
936 b.scatter = Some(Scatter {
937 count: as_number(dget(brush, "Cnt ")),
938 both_axes: as_bool(dget(brush, "bothAxes")),
939 count_dynamics: parse_dynamics_desc(brush, "countDynamics")?,
940 scatter_dynamics: parse_dynamics_desc(brush, "scatterDynamics")?,
941 });
942 }
943
944 if as_bool(dget(brush, "useTexture")) {
945 if let Some(txtr) = as_descriptor(dget(brush, "Txtr")) {
946 b.texture = Some(Texture {
947 id: as_text(dget(txtr, "Idnt")),
948 name: as_text(dget(txtr, "Nm ")),
949 blend_mode: blnm_decode(&as_text(dget(brush, "textureBlendMode"))),
950 depth: parse_percent(dget(brush, "textureDepth"))?,
951 depth_minimum: parse_percent(dget(brush, "minimumDepth"))?,
952 depth_dynamics: parse_dynamics_desc(brush, "textureDepthDynamics")?,
953 scale: parse_percent(dget(brush, "textureScale"))?,
954 invert: as_bool(dget(brush, "InvT")),
955 brightness: as_number(dget(brush, "textureBrightness")),
956 contrast: as_number(dget(brush, "textureContrast")),
957 texture_each_tip: as_bool(dget(brush, "TxtC")),
958 });
959 }
960 }
961
962 if let Some(db) = as_descriptor(dget(brush, "dualBrush")) {
963 if as_bool(dget(db, "useDualBrush")) {
964 let db_shape = as_descriptor(dget(db, "Brsh")).ok_or_else(|| {
965 ReadError::StrictViolation("Missing dual brush shape".to_string())
966 })?;
967 b.dual_brush = Some(DualBrush {
968 flip: as_bool(dget(db, "Flip")),
969 shape: parse_brush_shape(db_shape)?,
970 blend_mode: blnm_decode(&as_text(dget(db, "BlnM"))),
971 use_scatter: as_bool(dget(db, "useScatter")),
972 spacing: parse_percent(dget(db, "Spcn"))?,
973 count: as_number(dget(db, "Cnt ")),
974 both_axes: as_bool(dget(db, "bothAxes")),
975 count_dynamics: parse_dynamics_desc(db, "countDynamics")?,
976 scatter_dynamics: parse_dynamics_desc(db, "scatterDynamics")?,
977 });
978 }
979 }
980
981 if as_bool(dget(brush, "useColorDynamics")) {
982 b.color_dynamics = Some(ColorDynamics {
983 foreground_background: parse_dynamics_desc(brush, "clVr")?,
984 hue: parse_percent(dget(brush, "H "))?,
985 saturation: parse_percent(dget(brush, "Strt"))?,
986 brightness: parse_percent(dget(brush, "Brgh"))?,
987 purity: parse_percent(dget(brush, "purity"))?,
988 per_tip: as_bool(dget(brush, "colorDynamicsPerTip")),
989 });
990 }
991
992 if as_bool(dget(brush, "usePaintDynamics")) {
993 b.transfer = Some(Transfer {
994 flow_dynamics: parse_dynamics_desc(brush, "prVr")?,
995 opacity_dynamics: parse_dynamics_desc(brush, "opVr")?,
996 wetness_dynamics: parse_dynamics_desc(brush, "wtVr")?,
997 mix_dynamics: parse_dynamics_desc(brush, "mxVr")?,
998 });
999 }
1000
1001 if as_bool(dget(brush, "useBrushPose")) {
1002 b.brush_pose = Some(BrushPose {
1003 override_angle: as_bool(dget(brush, "overridePoseAngle")),
1004 override_tilt_x: as_bool(dget(brush, "overridePoseTiltX")),
1005 override_tilt_y: as_bool(dget(brush, "overridePoseTiltY")),
1006 override_pressure: as_bool(dget(brush, "overridePosePressure")),
1007 pressure: parse_percent(dget(brush, "brushPosePressure"))?,
1008 tilt_x: as_number(dget(brush, "brushPoseTiltX")),
1009 tilt_y: as_number(dget(brush, "brushPoseTiltY")),
1010 angle: as_number(dget(brush, "brushPoseAngle")),
1011 });
1012 }
1013
1014 if let Some(to) = as_descriptor(dget(brush, "toolOptions")) {
1015 let mut opts = ToolOptions {
1016 type_: to_brush_type(&to.class_id),
1017 brush_preset: as_bool(dget(to, "brushPreset")),
1018 flow: as_opt_number(dget(to, "flow")).unwrap_or(100.0),
1019 smooth: as_opt_number(dget(to, "Smoo")).unwrap_or(0.0),
1020 mode: blnm_decode(&{
1021 let m = as_text(dget(to, "Md "));
1022 if m.is_empty() {
1023 "BlnM.Nrml".to_string()
1024 } else {
1025 m
1026 }
1027 }),
1028 opacity: as_opt_number(dget(to, "Opct")).unwrap_or(100.0),
1029 smoothing: as_bool(dget(to, "smoothing")),
1030 smoothing_value: as_opt_number(dget(to, "smoothingValue")).unwrap_or(0.0),
1031 smoothing_radius_mode: as_bool(dget(to, "smoothingRadiusMode")),
1032 smoothing_catchup: as_bool(dget(to, "smoothingCatchup")),
1033 smoothing_catchup_at_end: as_bool(dget(to, "smoothingCatchupAtEnd")),
1034 smoothing_zoom_compensation: as_bool(dget(to, "smoothingZoomCompensation")),
1035 pressure_smoothing: as_bool(dget(to, "pressureSmoothing")),
1036 use_pressure_overrides_size: as_bool(dget(to, "usePressureOverridesSize")),
1037 use_pressure_overrides_opacity: as_bool(dget(to, "usePressureOverridesOpacity")),
1038 use_legacy: as_bool(dget(to, "useLegacy")),
1039 wetness: None,
1040 dryness: None,
1041 mix: None,
1042 auto_fill: None,
1043 auto_clean: None,
1044 load_solid_color_only: None,
1045 sample_all_layers: None,
1046 flow_dynamics: None,
1047 opacity_dynamics: None,
1048 size_dynamics: None,
1049 smudge_finger_painting: None,
1050 smudge_sample_all_layers: None,
1051 strength: None,
1052 };
1053
1054 opts.flow_dynamics = parse_dynamics_opt(dget(to, "prVr"))?;
1055 opts.opacity_dynamics = parse_dynamics_opt(dget(to, "opVr"))?;
1056 opts.size_dynamics = parse_dynamics_opt(dget(to, "szVr"))?;
1057 if let Some(v) = as_opt_number(dget(to, "wetness")) {
1058 opts.wetness = Some(v);
1059 }
1060 if let Some(v) = as_opt_number(dget(to, "dryness")) {
1061 opts.dryness = Some(v);
1062 }
1063 if let Some(v) = as_opt_number(dget(to, "mix")) {
1064 opts.mix = Some(v);
1065 }
1066 if let Some(v) = as_opt_bool(dget(to, "autoFill")) {
1067 opts.auto_fill = Some(v);
1068 }
1069 if let Some(v) = as_opt_bool(dget(to, "autoClean")) {
1070 opts.auto_clean = Some(v);
1071 }
1072 if let Some(v) = as_opt_bool(dget(to, "loadSolidColorOnly")) {
1073 opts.load_solid_color_only = Some(v);
1074 }
1075 if let Some(v) = as_opt_bool(dget(to, "sampleAllLayers")) {
1076 opts.sample_all_layers = Some(v);
1077 }
1078 if let Some(v) = as_opt_bool(dget(to, "SmdF")) {
1079 opts.smudge_finger_painting = Some(v);
1080 }
1081 if let Some(v) = as_opt_bool(dget(to, "SmdS")) {
1082 opts.smudge_sample_all_layers = Some(v);
1083 }
1084 if let Some(v) = as_opt_number(dget(to, "Prs ")) {
1085 opts.strength = Some(v);
1086 }
1087
1088 b.tool_options = Some(opts);
1089 }
1090
1091 Ok(b)
1092}
1093
1094fn parse_dynamics_desc(parent: &Descriptor, key: &str) -> ReadResult<BrushDynamics> {
1095 match as_descriptor(dget(parent, key)) {
1096 Some(d) => parse_dynamics(d),
1097 None => Err(ReadError::StrictViolation(format!(
1098 "Missing dynamics descriptor: {}",
1099 key
1100 ))),
1101 }
1102}
1103
1104#[derive(Debug, Clone, Default)]
1110pub struct ReadAbrOptions {
1111 pub log_missing_features: bool,
1112}
1113
1114pub fn read_abr(buffer: &[u8], _options: &ReadAbrOptions) -> ReadResult<Abr> {
1116 let reader = &mut PsdReader::new(buffer, None, None);
1117 let version = read_int16(reader)?;
1118 let mut samples: Vec<SampleInfo> = Vec::new();
1119 let mut brushes: Vec<Brush> = Vec::new();
1120 let mut patterns: Vec<PatternInfo> = Vec::new();
1121
1122 if version == 1 || version == 2 {
1123 return Err(ReadError::StrictViolation(format!(
1124 "Unsupported ABR version ({})",
1125 version
1126 )));
1127 } else if version == 6 || version == 7 || version == 9 || version == 10 {
1128 let minor_version = read_int16(reader)?;
1129 if minor_version != 1 && minor_version != 2 {
1130 return Err(ReadError::StrictViolation(
1131 "Unsupported ABR minor version".to_string(),
1132 ));
1133 }
1134
1135 while reader.offset < reader.buffer.len() {
1136 check_signature(reader, "8BIM", None)?;
1137 let type_ = read_signature(reader)?;
1138 let mut size = read_uint32(reader)? as usize;
1139 let end = reader.offset + size;
1140
1141 match type_.as_str() {
1142 "samp" => {
1143 while reader.offset < end {
1144 let mut brush_length = read_uint32(reader)? as usize;
1145 while brush_length & 0b11 != 0 {
1146 brush_length += 1; }
1148 let brush_end = reader.offset + brush_length;
1149
1150 let id = read_pascal_string(reader, 1)?;
1151
1152 skip_bytes(reader, if minor_version == 1 { 10 } else { 264 });
1155
1156 let y = read_int32(reader)?;
1157 let x = read_int32(reader)?;
1158 let h = read_int32(reader)? - y;
1159 let w = read_int32(reader)? - x;
1160 if w <= 0 || h <= 0 {
1161 return Err(ReadError::StrictViolation("Invalid bounds".to_string()));
1162 }
1163
1164 let bit_depth = read_int16(reader)?;
1165 let compression = read_uint8(reader)?; let mut alpha = vec![0u8; (w * h) as usize];
1167
1168 if bit_depth == 8 {
1169 if compression == 0 {
1170 let bytes = read_bytes(reader, alpha.len())?;
1171 alpha.copy_from_slice(&bytes);
1172 } else if compression == 1 {
1173 let mut pd = PixelData {
1174 data: &mut alpha,
1175 width: w as usize,
1176 height: h as usize,
1177 };
1178 read_data_rle(
1179 reader,
1180 Some(&mut pd),
1181 w as usize,
1182 h as usize,
1183 bit_depth as i32,
1184 1,
1185 &[0],
1186 )?;
1187 } else {
1188 return Err(ReadError::StrictViolation(
1189 "Invalid compression".to_string(),
1190 ));
1191 }
1192 } else if bit_depth == 16 {
1193 if compression == 0 {
1194 for i in 0..alpha.len() {
1195 alpha[i] = (read_uint16(reader)? >> 8) as u8; }
1197 } else if compression == 1 {
1198 return Err(ReadError::StrictViolation(
1199 "not implemented (16bit RLE)".to_string(),
1200 ));
1201 } else {
1202 return Err(ReadError::StrictViolation(
1203 "Invalid compression".to_string(),
1204 ));
1205 }
1206 } else {
1207 return Err(ReadError::StrictViolation("Invalid depth".to_string()));
1208 }
1209
1210 samples.push(SampleInfo {
1211 id,
1212 bounds: SampleBounds { x, y, w, h },
1213 alpha,
1214 });
1215 reader.offset = brush_end;
1216 }
1217 }
1218 "desc" => {
1219 let desc = crate::descriptor::read_version_and_descriptor(reader)?;
1220 if let Some(DescriptorValue::List(list)) = dget(&desc, "Brsh") {
1221 for item in list {
1222 if let DescriptorValue::Descriptor(brush) = item {
1223 brushes.push(parse_brush(brush)?);
1224 }
1225 }
1226 }
1227 }
1228 "patt" => {
1229 while reader.offset < end {
1230 patterns.push(read_pattern(reader)?);
1231 }
1232 reader.offset = end;
1233 }
1234 "phry" => {
1235 let _desc = crate::descriptor::read_version_and_descriptor(reader)?;
1237 }
1238 other => {
1239 return Err(ReadError::StrictViolation(format!(
1240 "Invalid brush type: {}",
1241 other
1242 )));
1243 }
1244 }
1245
1246 while size % 4 != 0 {
1248 reader.offset += 1;
1249 size += 1;
1250 }
1251 }
1252 } else {
1253 return Err(ReadError::StrictViolation(format!(
1254 "Unsupported ABR version ({})",
1255 version
1256 )));
1257 }
1258
1259 Ok(Abr {
1260 samples,
1261 patterns,
1262 brushes,
1263 })
1264}
1265
1266#[cfg(test)]
1267mod tests {
1268 use super::*;
1269 use std::path::PathBuf;
1270
1271 fn fixture_dir() -> PathBuf {
1272 let mut p = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
1274 p.pop(); p.pop(); p.push("test/ag-psd/test/abr-read");
1277 p
1278 }
1279
1280 fn find_abr(dir: &std::path::Path) -> Option<PathBuf> {
1281 let entries = std::fs::read_dir(dir).ok()?;
1282 for e in entries.flatten() {
1283 let path = e.path();
1284 if path.extension().map(|x| x == "abr").unwrap_or(false) {
1285 return Some(path);
1286 }
1287 }
1288 None
1289 }
1290
1291 #[test]
1292 fn abr_rejects_v1() {
1293 let bytes = [0u8, 1u8];
1295 assert!(read_abr(&bytes, &ReadAbrOptions::default()).is_err());
1296 }
1297
1298 #[test]
1299 fn abr_rejects_unknown_version() {
1300 let bytes = [0u8, 99u8];
1301 assert!(read_abr(&bytes, &ReadAbrOptions::default()).is_err());
1302 }
1303
1304 #[test]
1305 fn abr_decodes_fixtures_if_present() {
1306 let base = fixture_dir();
1307 if !base.exists() {
1308 eprintln!("abr fixtures not present, skipping");
1309 return;
1310 }
1311
1312 let mut decoded_any = false;
1313 for sub in ["simple", "sample-and-pattern", "tilt", "special"] {
1314 let dir = base.join(sub);
1315 if let Some(abr_path) = find_abr(&dir) {
1316 let data = std::fs::read(&abr_path).expect("read fixture");
1317 match read_abr(&data, &ReadAbrOptions::default()) {
1318 Ok(abr) => {
1319 decoded_any = true;
1320 for s in &abr.samples {
1322 assert!(s.bounds.w > 0 && s.bounds.h > 0);
1323 assert_eq!(s.alpha.len(), (s.bounds.w * s.bounds.h) as usize);
1324 }
1325 eprintln!(
1326 "decoded {:?}: {} brushes, {} samples, {} patterns",
1327 abr_path.file_name().unwrap(),
1328 abr.brushes.len(),
1329 abr.samples.len(),
1330 abr.patterns.len()
1331 );
1332 }
1333 Err(e) => {
1334 panic!("failed to decode {:?}: {:?}", abr_path, e);
1336 }
1337 }
1338 }
1339 }
1340
1341 if !decoded_any {
1342 eprintln!("no .abr fixture files found, smoke-only");
1343 }
1344 }
1345
1346 #[test]
1347 fn abr_simple_fixture_content() {
1348 let path = fixture_dir().join("simple/src.abr");
1349 if !path.exists() {
1350 eprintln!("simple fixture missing, skipping");
1351 return;
1352 }
1353 let data = std::fs::read(&path).unwrap();
1354 let abr = read_abr(&data, &ReadAbrOptions::default()).expect("decode simple");
1355 assert_eq!(abr.brushes.len(), 1);
1356 let b = &abr.brushes[0];
1357 assert_eq!(b.name, "Soft Round");
1358 assert_eq!(b.spacing, 1.0);
1359 assert!(!b.wet_edges);
1360 assert!(!b.noise);
1361 assert!(b.use_brush_size);
1362 match &b.shape {
1363 BrushShape::Computed {
1364 size,
1365 angle,
1366 roundness,
1367 spacing_on,
1368 spacing,
1369 hardness,
1370 ..
1371 } => {
1372 assert_eq!(*size, 30.0);
1373 assert_eq!(*angle, 0.0);
1374 assert_eq!(*roundness, 1.0);
1375 assert!(*spacing_on);
1376 assert_eq!(*spacing, 0.25);
1377 assert_eq!(*hardness, 0.0);
1378 }
1379 other => panic!("expected computed brush, got {:?}", other),
1380 }
1381 }
1382}