Skip to main content

libfreeform/
types.rs

1//! Lossless, format-agnostic data produced by the Apple Freeform pasteboard
2//! decoders.
3//!
4//! Values with unknown provenance remain optional, unsupported records retain
5//! their raw bytes, and pasteboard tiers report failures independently.
6
7use std::collections::BTreeMap;
8
9/// A two-dimensional point in Freeform coordinates.
10#[derive(Debug, Clone, Copy, PartialEq, Default)]
11#[cfg_attr(
12   feature = "serde",
13   derive(serde::Serialize, serde::Deserialize),
14   serde(rename_all = "camelCase")
15)]
16pub struct FreeformPoint {
17   /// Horizontal coordinate in points.
18   pub x: f64,
19   /// Vertical coordinate in points.
20   pub y: f64,
21}
22
23/// A two-dimensional size in Freeform points.
24#[derive(Debug, Clone, Copy, PartialEq, Default)]
25#[cfg_attr(
26   feature = "serde",
27   derive(serde::Serialize, serde::Deserialize),
28   serde(rename_all = "camelCase")
29)]
30pub struct FreeformSize {
31   /// Horizontal extent in points.
32   pub width:  f64,
33   /// Vertical extent in points.
34   pub height: f64,
35}
36
37/// A complete affine transform using Core Graphics component order.
38#[derive(Debug, Clone, Copy, PartialEq)]
39#[cfg_attr(
40   feature = "serde",
41   derive(serde::Serialize, serde::Deserialize),
42   serde(rename_all = "camelCase")
43)]
44pub struct FreeformTransform {
45   /// Horizontal scale/rotation component.
46   pub a:  f64,
47   /// Vertical shear/rotation component.
48   pub b:  f64,
49   /// Horizontal shear/rotation component.
50   pub c:  f64,
51   /// Vertical scale/rotation component.
52   pub d:  f64,
53   /// Horizontal translation.
54   pub tx: f64,
55   /// Vertical translation.
56   pub ty: f64,
57}
58
59impl Default for FreeformTransform {
60   fn default() -> Self {
61      Self { a: 1.0, b: 0.0, c: 0.0, d: 1.0, tx: 0.0, ty: 0.0 }
62   }
63}
64
65/// Canvas-space rectangle in Freeform points.
66#[derive(Debug, Clone, Copy, PartialEq, Default)]
67#[cfg_attr(
68   feature = "serde",
69   derive(serde::Serialize, serde::Deserialize),
70   serde(rename_all = "camelCase")
71)]
72pub struct FreeformFrame {
73   /// Horizontal origin.
74   pub x:        f64,
75   /// Vertical origin.
76   pub y:        f64,
77   /// Width in points.
78   pub w:        f64,
79   /// Height in points.
80   pub h:        f64,
81   /// Rotation in radians when the archive exposes a decomposed angle.
82   pub rotation: f64,
83}
84
85/// Local bounds and transform for a native board item.
86#[derive(Debug, Clone, PartialEq, Default)]
87#[cfg_attr(
88   feature = "serde",
89   derive(serde::Serialize, serde::Deserialize),
90   serde(rename_all = "camelCase")
91)]
92pub struct FreeformGeometry {
93   /// Archive frame, when decoded without invention.
94   pub frame:           Option<FreeformFrame>,
95   /// Full affine transform, when present in the archive.
96   pub transform:       Option<FreeformTransform>,
97   /// Transform anchor in local coordinates.
98   pub anchor:          Option<FreeformPoint>,
99   /// Explicit horizontal flip state.
100   pub horizontal_flip: Option<bool>,
101   /// Explicit vertical flip state.
102   pub vertical_flip:   Option<bool>,
103   /// Whether the archived width is authoritative.
104   pub width_valid:     Option<bool>,
105   /// Whether the archived height is authoritative.
106   pub height_valid:    Option<bool>,
107}
108
109/// A color with its source color-space name and normalized sRGB channels.
110#[derive(Debug, Clone, PartialEq)]
111#[cfg_attr(
112   feature = "serde",
113   derive(serde::Serialize, serde::Deserialize),
114   serde(rename_all = "camelCase")
115)]
116pub struct FreeformColor {
117   /// Source color-space identifier.
118   pub color_space: String,
119   /// Normalized sRGB red channel.
120   pub red:         f64,
121   /// Normalized sRGB green channel.
122   pub green:       f64,
123   /// Normalized sRGB blue channel.
124   pub blue:        f64,
125   /// Alpha channel.
126   pub alpha:       f64,
127   /// Rounded CSS representation retained for convenient consumers.
128   pub hex:         String,
129}
130
131/// One stop in a native gradient fill.
132#[derive(Debug, Clone, PartialEq)]
133#[cfg_attr(
134   feature = "serde",
135   derive(serde::Serialize, serde::Deserialize),
136   serde(rename_all = "camelCase")
137)]
138pub struct FreeformGradientStop {
139   /// Position along the gradient from zero to one.
140   pub fraction:   f64,
141   /// Native midpoint/inflection value.
142   pub inflection: Option<f64>,
143   /// Stop color.
144   pub color:      FreeformColor,
145}
146
147/// A native fill or stroke paint.
148#[derive(Debug, Clone, PartialEq)]
149#[cfg_attr(
150   feature = "serde",
151   derive(serde::Serialize, serde::Deserialize),
152   serde(tag = "kind", rename_all = "camelCase", rename_all_fields = "camelCase")
153)]
154pub enum FreeformPaint {
155   /// A solid color.
156   Solid { color: FreeformColor },
157   /// A linear gradient.
158   LinearGradient {
159      /// Gradient start in local coordinates.
160      start: FreeformPoint,
161      /// Gradient end in local coordinates.
162      end:   FreeformPoint,
163      /// Ordered color stops.
164      stops: Vec<FreeformGradientStop>,
165   },
166   /// A radial gradient.
167   RadialGradient {
168      /// Gradient center in local coordinates.
169      center: FreeformPoint,
170      /// Gradient radius.
171      radius: f64,
172      /// Ordered color stops.
173      stops:  Vec<FreeformGradientStop>,
174   },
175   /// An image-backed fill.
176   Image {
177      /// Referenced asset identifier.
178      asset_id:  String,
179      /// Native scaling technique when known.
180      technique: Option<String>,
181   },
182   /// An unsupported paint whose archive bytes remain available.
183   Unknown { raw_data: Vec<u8> },
184}
185
186/// Stroke styling for shapes and connectors.
187#[derive(Debug, Clone, PartialEq)]
188#[cfg_attr(
189   feature = "serde",
190   derive(serde::Serialize, serde::Deserialize),
191   serde(rename_all = "camelCase")
192)]
193pub struct FreeformStrokeStyle {
194   /// Stroke paint.
195   pub paint:       FreeformPaint,
196   /// Stroke width in local points.
197   pub width:       f64,
198   /// Dash lengths in local points.
199   pub dash:        Vec<f64>,
200   /// Dash phase.
201   pub dash_offset: Option<f64>,
202   /// Native cap name.
203   pub cap:         Option<String>,
204   /// Native join name.
205   pub join:        Option<String>,
206   /// Miter limit.
207   pub miter_limit: Option<f64>,
208   /// Tail decoration.
209   pub tail_end:    Option<String>,
210   /// Head decoration.
211   pub head_end:    Option<String>,
212}
213
214/// One native shadow effect.
215#[derive(Debug, Clone, PartialEq)]
216#[cfg_attr(
217   feature = "serde",
218   derive(serde::Serialize, serde::Deserialize),
219   serde(rename_all = "camelCase")
220)]
221pub struct FreeformShadow {
222   /// Shadow kind such as drop, contact, or curved.
223   pub kind:     String,
224   /// Shadow color.
225   pub color:    FreeformColor,
226   /// Horizontal offset.
227   pub offset_x: f64,
228   /// Vertical offset.
229   pub offset_y: f64,
230   /// Blur radius.
231   pub radius:   f64,
232   /// Shadow opacity when distinct from the color alpha.
233   pub opacity:  Option<f64>,
234   /// Native angle when present.
235   pub angle:    Option<f64>,
236}
237
238/// Shared visual styling for a native board item.
239#[derive(Debug, Clone, PartialEq, Default)]
240#[cfg_attr(
241   feature = "serde",
242   derive(serde::Serialize, serde::Deserialize),
243   serde(rename_all = "camelCase")
244)]
245pub struct FreeformStyle {
246   /// Whole-item opacity.
247   pub opacity: Option<f64>,
248   /// Fill paint.
249   pub fill:    Option<FreeformPaint>,
250   /// Stroke style.
251   pub stroke:  Option<FreeformStrokeStyle>,
252   /// Ordered shadow effects.
253   pub shadows: Vec<FreeformShadow>,
254}
255
256/// Whether decoded ink points are spline controls or rendered samples.
257#[derive(Debug, Clone, Copy, PartialEq, Eq)]
258#[cfg_attr(
259   feature = "serde",
260   derive(serde::Serialize, serde::Deserialize),
261   serde(rename_all = "camelCase")
262)]
263pub enum FreeformInkPointRole {
264   /// Native uniform-cubic-B-spline control records.
265   SplineControl,
266   /// Points sampled from the rendered curve.
267   RenderedSample,
268}
269
270/// A `PencilKit` point with every channel the decoder could identify.
271#[derive(Debug, Clone, Copy, PartialEq, Default)]
272#[cfg_attr(
273   feature = "serde",
274   derive(serde::Serialize, serde::Deserialize),
275   serde(rename_all = "camelCase")
276)]
277pub struct FreeformInkPoint {
278   /// Local horizontal coordinate unless the stroke role documents otherwise.
279   pub x:               f64,
280   /// Local vertical coordinate unless the stroke role documents otherwise.
281   pub y:               f64,
282   /// Time offset from the beginning of the stroke.
283   pub time_offset:     Option<f64>,
284   /// Nib width.
285   pub width:           Option<f64>,
286   /// Nib height.
287   pub height:          Option<f64>,
288   /// Per-point opacity.
289   pub opacity:         Option<f64>,
290   /// Per-point pressure.
291   pub force:           Option<f64>,
292   /// Stylus azimuth in radians.
293   pub azimuth:         Option<f64>,
294   /// Stylus altitude in radians.
295   pub altitude:        Option<f64>,
296   /// Versioned secondary scale/radius channel.
297   pub secondary_scale: Option<f64>,
298   /// Versioned clipping threshold.
299   pub threshold:       Option<f64>,
300}
301
302/// A visible interval of a masked `PencilKit` path.
303#[derive(Debug, Clone, Copy, PartialEq)]
304#[cfg_attr(
305   feature = "serde",
306   derive(serde::Serialize, serde::Deserialize),
307   serde(rename_all = "camelCase")
308)]
309pub struct FreeformInkRange {
310   /// Inclusive parametric start.
311   pub start: f64,
312   /// Exclusive parametric end.
313   pub end:   f64,
314}
315
316/// One freehand stroke from `PencilKit` or native item storage.
317#[derive(Debug, Clone, PartialEq)]
318#[cfg_attr(
319   feature = "serde",
320   derive(serde::Serialize, serde::Deserialize),
321   serde(rename_all = "camelCase")
322)]
323pub struct FreeformInkStroke {
324   /// Normalized ink family.
325   pub ink_type:       String,
326   /// Exact archived ink identifier.
327   pub ink_identifier: String,
328   /// Ink color when decoded.
329   pub color:          Option<FreeformColor>,
330   /// Stroke-local to canvas transform.
331   pub transform:      FreeformTransform,
332   /// Meaning of entries in `points`.
333   pub point_role:     FreeformInkPointRole,
334   /// Ordered point/control records.
335   pub points:         Vec<FreeformInkPoint>,
336   /// Visible path intervals after applying a `PencilKit` mask.
337   pub visible_ranges: Option<Vec<FreeformInkRange>>,
338   /// Seed used by randomized ink textures.
339   pub random_seed:    Option<u32>,
340   /// Original stroke record for unsupported channels or future replay.
341   pub raw_data:       Vec<u8>,
342}
343
344/// Everything recovered from a `com.apple.drawing` `PKDrawing` blob.
345#[derive(Debug, Clone, PartialEq, Default)]
346#[cfg_attr(
347   feature = "serde",
348   derive(serde::Serialize, serde::Deserialize),
349   serde(rename_all = "camelCase")
350)]
351pub struct FreeformDrawing {
352   /// `PencilKit` content version when decoded.
353   pub required_content_version: Option<u32>,
354   /// Drawing bounds in canvas coordinates.
355   pub bounds:                   Option<FreeformFrame>,
356   /// Ordered strokes.
357   pub strokes:                  Vec<FreeformInkStroke>,
358}
359
360/// A command in a local shape path.
361#[derive(Debug, Clone, PartialEq)]
362#[cfg_attr(
363   feature = "serde",
364   derive(serde::Serialize, serde::Deserialize),
365   serde(tag = "kind", rename_all = "camelCase", rename_all_fields = "camelCase")
366)]
367pub enum FreeformPathCommand {
368   /// Starts a new subpath.
369   Move { point: FreeformPoint },
370   /// Adds a straight segment.
371   Line { point: FreeformPoint },
372   /// Adds a quadratic Bézier segment.
373   Quadratic { control: FreeformPoint, point: FreeformPoint },
374   /// Adds a cubic Bézier segment.
375   Cubic {
376      /// First control point.
377      control_1: FreeformPoint,
378      /// Second control point.
379      control_2: FreeformPoint,
380      /// Segment endpoint.
381      point:     FreeformPoint,
382   },
383   /// Closes the current subpath.
384   Close,
385}
386
387/// An ordered local shape path with explicit subpaths and curves.
388#[derive(Debug, Clone, PartialEq, Default)]
389#[cfg_attr(
390   feature = "serde",
391   derive(serde::Serialize, serde::Deserialize),
392   serde(rename_all = "camelCase")
393)]
394pub struct FreeformPath {
395   /// Path commands in archive order.
396   pub commands:     Vec<FreeformPathCommand>,
397   /// Natural source size used to scale preset paths.
398   pub natural_size: Option<FreeformSize>,
399   /// Original path payload.
400   pub raw_data:     Vec<u8>,
401}
402
403/// Recursive value retained from a `TSUDescription` routing hint.
404#[derive(Debug, Clone, PartialEq)]
405#[cfg_attr(
406   feature = "serde",
407   derive(serde::Serialize, serde::Deserialize),
408   serde(tag = "kind", content = "value", rename_all = "camelCase")
409)]
410pub enum TsuValue {
411   /// Null plist value.
412   Null,
413   /// Boolean plist value.
414   Bool(bool),
415   /// Signed integer plist value.
416   Integer(i64),
417   /// Floating-point plist value.
418   Real(f64),
419   /// String plist value.
420   String(String),
421   /// Raw data plist value.
422   Data(Vec<u8>),
423   /// Ordered array plist value.
424   Array(Vec<Self>),
425   /// Dictionary plist value.
426   Dictionary(BTreeMap<String, Self>),
427}
428
429/// One `TSUDescription` board-item entry.
430#[derive(Debug, Clone, PartialEq)]
431#[cfg_attr(
432   feature = "serde",
433   derive(serde::Serialize, serde::Deserialize),
434   serde(rename_all = "camelCase")
435)]
436pub struct TsuEntry {
437   /// Exact class name with only a leading `Freeform.` namespace removed.
438   pub class_name: String,
439   /// Structured routing hints.
440   pub hints:      BTreeMap<String, TsuValue>,
441}
442
443/// One styled range in a native text storage.
444#[derive(Debug, Clone, PartialEq, Default)]
445#[cfg_attr(
446   feature = "serde",
447   derive(serde::Serialize, serde::Deserialize),
448   serde(rename_all = "camelCase")
449)]
450pub struct FreeformTextRun {
451   /// Byte start in the plain UTF-8 string.
452   pub start:               usize,
453   /// Byte end in the plain UTF-8 string.
454   pub end:                 usize,
455   /// Font postscript identifier.
456   pub font_name:           Option<String>,
457   /// Font size in points.
458   pub font_size:           Option<f64>,
459   /// Bold state.
460   pub bold:                Option<bool>,
461   /// Italic state.
462   pub italic:              Option<bool>,
463   /// Underline style.
464   pub underline:           Option<String>,
465   /// Strikethrough style.
466   pub strikethrough:       Option<String>,
467   /// Character fill.
468   pub fill:                Option<FreeformPaint>,
469   /// Paragraph alignment.
470   pub paragraph_alignment: Option<String>,
471   /// Base writing direction.
472   pub writing_direction:   Option<String>,
473   /// Hyperlink target.
474   pub hyperlink:           Option<String>,
475   /// Native list-style name.
476   pub list_style:          Option<String>,
477}
478
479/// Native text with preserved style runs.
480#[derive(Debug, Clone, PartialEq, Default)]
481#[cfg_attr(
482   feature = "serde",
483   derive(serde::Serialize, serde::Deserialize),
484   serde(rename_all = "camelCase")
485)]
486pub struct FreeformText {
487   /// Complete UTF-8 text.
488   pub plain:              String,
489   /// Character and paragraph style runs.
490   pub runs:               Vec<FreeformTextRun>,
491   /// Text inset in local points.
492   pub inset:              Option<FreeformFrame>,
493   /// Native vertical-alignment name.
494   pub vertical_alignment: Option<String>,
495   /// Whether the container shrinks text to fit.
496   pub shrink_to_fit:      Option<bool>,
497}
498
499/// One endpoint of a native connector.
500#[derive(Debug, Clone, PartialEq)]
501#[cfg_attr(
502   feature = "serde",
503   derive(serde::Serialize, serde::Deserialize),
504   serde(rename_all = "camelCase")
505)]
506pub struct FreeformConnectorEndpoint {
507   /// Connected item UUID.
508   pub item_id:             Option<String>,
509   /// Native magnet name.
510   pub magnet:              Option<String>,
511   /// Normalized magnet position.
512   pub normalized_position: Option<FreeformPoint>,
513   /// Unattached local endpoint.
514   pub point:               Option<FreeformPoint>,
515   /// Line-end decoration.
516   pub line_end:            Option<String>,
517}
518
519/// One native table cell.
520#[derive(Debug, Clone, PartialEq, Default)]
521#[cfg_attr(
522   feature = "serde",
523   derive(serde::Serialize, serde::Deserialize),
524   serde(rename_all = "camelCase")
525)]
526pub struct FreeformTableCell {
527   /// Stable cell identifier.
528   pub id:                Option<String>,
529   /// Zero-based row index.
530   pub row:               Option<usize>,
531   /// Zero-based column index.
532   pub column:            Option<usize>,
533   /// Number of spanned rows.
534   pub row_span:          Option<usize>,
535   /// Number of spanned columns.
536   pub column_span:       Option<usize>,
537   /// Cell fill/style.
538   pub style:             FreeformStyle,
539   /// Cell text.
540   pub text:              Option<FreeformText>,
541   /// UUIDs of shapes anchored inside the cell.
542   pub anchored_item_ids: Vec<String>,
543}
544
545/// A referenced native asset and any captured bytes.
546#[derive(Debug, Clone, PartialEq)]
547#[cfg_attr(
548   feature = "serde",
549   derive(serde::Serialize, serde::Deserialize),
550   serde(rename_all = "camelCase")
551)]
552pub struct FreeformAsset {
553   /// Archive asset identifier.
554   pub id:             String,
555   /// Original filename when present.
556   pub filename:       Option<String>,
557   /// Pasteboard UTI carrying the bytes.
558   pub uti:            Option<String>,
559   /// Captured bytes; absent for premium or incomplete transfers.
560   pub bytes:          Option<Vec<u8>>,
561   /// Whether the item is premium/stock media.
562   pub premium:        Option<bool>,
563   /// Intrinsic media size.
564   pub intrinsic_size: Option<FreeformSize>,
565   /// Unsupported raw asset descriptor.
566   pub raw_descriptor: Vec<u8>,
567}
568
569/// Item-specific native payload.
570#[derive(Debug, Clone, PartialEq)]
571#[cfg_attr(
572   feature = "serde",
573   derive(serde::Serialize, serde::Deserialize),
574   serde(tag = "kind", rename_all = "camelCase", rename_all_fields = "camelCase")
575)]
576pub enum FreeformItemKind {
577   /// A geometric or library shape.
578   Shape { preset: Option<String>, path: Option<FreeformPath> },
579   /// A standalone text box.
580   TextBox { text: Option<FreeformText> },
581   /// A sticky note.
582   StickyNote { text: Option<FreeformText> },
583   /// A connector or arrow.
584   Connector {
585      /// Tail endpoint.
586      tail:    FreeformConnectorEndpoint,
587      /// Head endpoint.
588      head:    FreeformConnectorEndpoint,
589      /// Routing kind such as straight, corner, or curved.
590      routing: Option<String>,
591      /// Explicit routed path.
592      path:    Option<FreeformPath>,
593   },
594   /// A native table.
595   Table {
596      /// Row heights in points.
597      row_heights:   Vec<f64>,
598      /// Column widths in points.
599      column_widths: Vec<f64>,
600      /// Cells in archive order.
601      cells:         Vec<FreeformTableCell>,
602   },
603   /// An image item.
604   Image {
605      /// Referenced asset ID.
606      asset_id: Option<String>,
607      /// Crop rectangle in source coordinates.
608      crop:     Option<FreeformFrame>,
609      /// Optional mask path.
610      mask:     Option<FreeformPath>,
611   },
612   /// A movie or other media item.
613   Media { asset_id: Option<String>, media_type: Option<String> },
614   /// A file attachment.
615   File { asset_id: Option<String>, filename: Option<String> },
616   /// A URL/link-preview item.
617   Url { url: Option<String>, title: Option<String> },
618   /// A USDZ spatial item.
619   Usdz { asset_id: Option<String>, spatial_transform: Option<Vec<f64>> },
620   /// A group container.
621   Group {
622      /// Child UUIDs in group-local z-order.
623      child_ids:         Vec<String>,
624      /// Transform that preserves child placement when leaving the group.
625      counter_transform: Option<FreeformTransform>,
626   },
627   /// Native freehand ink used only when `PKDrawing` is unavailable.
628   Ink { strokes: Vec<FreeformInkStroke> },
629   /// An unsupported item whose payload remains available.
630   Unknown,
631}
632
633/// Byte range of one structurally owned record in
634/// `FreeformNative::raw_archive`.
635#[derive(Debug, Clone, Copy, PartialEq, Eq)]
636#[cfg_attr(
637   feature = "serde",
638   derive(serde::Serialize, serde::Deserialize),
639   serde(rename_all = "camelCase")
640)]
641pub struct FreeformRecordRange {
642   /// Offset from the beginning of the native object archive.
643   pub offset: usize,
644   /// Record payload length.
645   pub length: usize,
646}
647
648/// One entry of the native object graph's ordered board-item list.
649#[derive(Debug, Clone, PartialEq)]
650#[cfg_attr(
651   feature = "serde",
652   derive(serde::Serialize, serde::Deserialize),
653   serde(rename_all = "camelCase")
654)]
655pub struct FreeformBoardItem {
656   /// Position in the paste's top-level item order.
657   pub index:         usize,
658   /// Item UUID from the typed native index.
659   pub uuid:          String,
660   /// Parent group UUID for nested items.
661   pub parent_id:     Option<String>,
662   /// Class from `TSUDescription` when safely correlated.
663   pub class_name:    Option<String>,
664   /// Structured TSU routing hints.
665   pub hints:         BTreeMap<String, TsuValue>,
666   /// Item geometry.
667   pub geometry:      FreeformGeometry,
668   /// Item style.
669   pub style:         FreeformStyle,
670   /// Item-specific payload.
671   pub kind:          FreeformItemKind,
672   /// Exact bounded owner-record ranges in archive order.
673   pub record_ranges: Vec<FreeformRecordRange>,
674   /// Primary common-record bytes retained for unsupported fields.
675   ///
676   /// See [`FreeformBoardItem::record_ranges`] for every owned record.
677   pub raw_data:      Vec<u8>,
678}
679
680/// Compatibility of a decoded native archive.
681#[derive(Debug, Clone, PartialEq, Eq, Default)]
682#[cfg_attr(
683   feature = "serde",
684   derive(serde::Serialize, serde::Deserialize),
685   serde(tag = "kind", rename_all = "camelCase", rename_all_fields = "camelCase")
686)]
687pub enum FreeformCompatibility {
688   /// Version metadata was not recoverable.
689   #[default]
690   Unknown,
691   /// The archive version is fixture-verified.
692   Supported { version: u64 },
693   /// The archive requires a newer decoder.
694   Unsupported { minimum_version: u64 },
695}
696
697/// Everything recovered from `CRLNativeData` and its correlated flavors.
698#[derive(Debug, Clone, PartialEq, Default)]
699#[cfg_attr(
700   feature = "serde",
701   derive(serde::Serialize, serde::Deserialize),
702   serde(rename_all = "camelCase")
703)]
704pub struct FreeformNative {
705   /// Paste UUID from the typed native index.
706   pub paste_id:      String,
707   /// Native archive compatibility status.
708   pub compatibility: FreeformCompatibility,
709   /// Top-level items in native order.
710   pub items:         Vec<FreeformBoardItem>,
711   /// Asset descriptors recovered from native item records, keyed by asset ID.
712   pub assets:        BTreeMap<String, FreeformAsset>,
713   /// Original manifest protobuf.
714   pub raw_manifest:  Vec<u8>,
715   /// Original index plist.
716   pub raw_index:     Vec<u8>,
717   /// Original object archive for unsupported records.
718   pub raw_archive:   Vec<u8>,
719}
720
721/// Decoding failure category retained by pasteboard assembly.
722#[derive(Debug, Clone, Copy, PartialEq, Eq)]
723#[cfg_attr(
724   feature = "serde",
725   derive(serde::Serialize, serde::Deserialize),
726   serde(rename_all = "camelCase")
727)]
728pub enum FreeformDecodeErrorKind {
729   /// Structurally invalid input.
730   Invalid,
731   /// Truncated or incomplete transfer.
732   Incomplete,
733   /// Recognized but unsupported format version.
734   UnsupportedVersion,
735   /// Flavors from different selections could not be correlated.
736   CorrelationMismatch,
737}
738
739/// A structured decode error with enough context for fallback policy.
740#[derive(Debug, Clone, PartialEq, Eq)]
741#[cfg_attr(
742   feature = "serde",
743   derive(serde::Serialize, serde::Deserialize),
744   serde(rename_all = "camelCase")
745)]
746pub struct FreeformDecodeError {
747   /// Failure category.
748   pub kind:    FreeformDecodeErrorKind,
749   /// Human-readable detail.
750   pub message: String,
751}
752
753impl FreeformDecodeError {
754   /// Creates a structural invalid-input error.
755   pub fn invalid(message: impl Into<String>) -> Self {
756      Self { kind: FreeformDecodeErrorKind::Invalid, message: message.into() }
757   }
758
759   /// Creates a truncated-transfer error.
760   pub fn incomplete(message: impl Into<String>) -> Self {
761      Self { kind: FreeformDecodeErrorKind::Incomplete, message: message.into() }
762   }
763
764   /// Creates an unsupported-version error.
765   pub fn unsupported(message: impl Into<String>) -> Self {
766      Self { kind: FreeformDecodeErrorKind::UnsupportedVersion, message: message.into() }
767   }
768
769   /// Creates a flavor-correlation error.
770   pub fn correlation(message: impl Into<String>) -> Self {
771      Self { kind: FreeformDecodeErrorKind::CorrelationMismatch, message: message.into() }
772   }
773}
774
775impl std::fmt::Display for FreeformDecodeError {
776   fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
777      write!(f, "freeform decode: {}", self.message)
778   }
779}
780
781impl std::error::Error for FreeformDecodeError {}
782
783/// Outcome of decoding one independent pasteboard tier.
784#[derive(Debug, Clone, PartialEq, Eq)]
785#[cfg_attr(
786   feature = "serde",
787   derive(serde::Serialize, serde::Deserialize),
788   serde(tag = "status", content = "value", rename_all = "camelCase")
789)]
790#[derive(Default)]
791pub enum FreeformTier<T> {
792   /// The flavor was not supplied.
793   #[default]
794   Absent,
795   /// The flavor decoded successfully.
796   Decoded(T),
797   /// The flavor was retained but could not be decoded.
798   Failed(FreeformDecodeError),
799}
800
801/// Raw bytes for one exact pasteboard flavor.
802#[derive(Debug, Clone, PartialEq, Eq)]
803#[cfg_attr(
804   feature = "serde",
805   derive(serde::Serialize, serde::Deserialize),
806   serde(rename_all = "camelCase")
807)]
808pub struct FreeformFlavor {
809   /// Exact UTI or captured pasteboard type.
810   pub uti:   String,
811   /// Flavor bytes.
812   pub bytes: Vec<u8>,
813}
814
815/// Atomic set of raw flavors captured from one pasteboard change count.
816#[derive(Debug, Clone, PartialEq, Eq, Default)]
817#[cfg_attr(
818   feature = "serde",
819   derive(serde::Serialize, serde::Deserialize),
820   serde(rename_all = "camelCase")
821)]
822pub struct FreeformBlobs {
823   /// `NSPasteboard` change count observed before and after capture.
824   pub change_count: Option<i64>,
825   /// Exact flavors in pasteboard order.
826   pub flavors:      Vec<FreeformFlavor>,
827}
828
829/// Correlation metadata from `CRLNativeMetadata`.
830#[derive(Debug, Clone, PartialEq, Eq, Default)]
831#[cfg_attr(
832   feature = "serde",
833   derive(serde::Serialize, serde::Deserialize),
834   serde(rename_all = "camelCase")
835)]
836pub struct FreeformNativeMetadata {
837   /// Paste UUID shared with the CRL native index.
838   pub paste_id: Option<String>,
839   /// Original metadata protobuf.
840   pub raw_data: Vec<u8>,
841}
842
843/// A rendered fallback flavor.
844#[derive(Debug, Clone, PartialEq, Eq)]
845#[cfg_attr(
846   feature = "serde",
847   derive(serde::Serialize, serde::Deserialize),
848   serde(rename_all = "camelCase")
849)]
850pub struct FreeformRender {
851   /// Exact render UTI.
852   pub uti:   String,
853   /// Encoded PNG/TIFF/PDF bytes.
854   pub bytes: Vec<u8>,
855}
856
857/// A non-fatal assembly diagnostic.
858#[derive(Debug, Clone, PartialEq, Eq)]
859#[cfg_attr(
860   feature = "serde",
861   derive(serde::Serialize, serde::Deserialize),
862   serde(rename_all = "camelCase")
863)]
864pub struct FreeformDiagnostic {
865   /// Flavor or subsystem producing the diagnostic.
866   pub source:  String,
867   /// Diagnostic detail.
868   pub message: String,
869}
870
871/// A decoded Freeform selection assembled without losing supplied flavors.
872#[derive(Debug, Clone, PartialEq, Default)]
873#[cfg_attr(
874   feature = "serde",
875   derive(serde::Serialize, serde::Deserialize),
876   serde(rename_all = "camelCase")
877)]
878pub struct FreeformPasteboard {
879   /// `PencilKit` tier outcome.
880   pub drawing:         FreeformTier<FreeformDrawing>,
881   /// Native graph tier outcome.
882   pub native:          FreeformTier<FreeformNative>,
883   /// Standalone TSU manifest tier outcome.
884   pub manifest:        FreeformTier<Vec<TsuEntry>>,
885   /// Native correlation metadata.
886   pub metadata:        FreeformTier<FreeformNativeMetadata>,
887   /// Captured asset payloads keyed by asset ID.
888   pub assets:          BTreeMap<String, FreeformAsset>,
889   /// Render fallbacks in pasteboard order.
890   pub renders:         Vec<FreeformRender>,
891   /// Exact pasteboard-state values.
892   pub state:           BTreeMap<String, Vec<u8>>,
893   /// Style-only pasteboard flavors.
894   pub styles:          Vec<FreeformFlavor>,
895   /// Plain-text and rich-text selection flavors.
896   pub text:            Vec<FreeformFlavor>,
897   /// Unknown flavors retained for future decoders.
898   pub unknown_flavors: Vec<FreeformFlavor>,
899   /// Non-fatal correlation and partial-fidelity diagnostics.
900   pub diagnostics:     Vec<FreeformDiagnostic>,
901}