Skip to main content

ag_psd/additional_info/
mod.rs

1/*
2File: crates/ag-psd/src/additional_info/mod.rs
3
4Purpose:
5Дополнительная информация слоёв PSD (8BIM/8B64-секции "additional layer
6information"). Порт upstream-файла `test/ag-psd/src/additionalInfo.ts`.
7
8DELIBERATE DIVERGENCE FROM UPSTREAM LAYOUT:
9upstream держит всё в одном файле `additionalInfo.ts` (~5445 строк). Здесь это
10осознанно разбито на DIRECTORY MODULE: `mod.rs` (фреймворк + диспетчер) плюс
11по одному файлу на логическую группу ключей (`metadata_keys.rs`,
12`text_keys.rs`, ...). Причина — размер файла и то, что группы будут
13заполняться НЕСКОЛЬКИМИ параллельными воркерами независимо друг от друга.
14mod.rs владеет каноническим порядком ключей (важен для записи — Photoshop
15пишет ключи в порядке регистрации хэндлеров, а не в порядке данных) и
16маршрутизирует чтение/запись в group-модули.
17
18Main responsibilities:
19- CANONICAL ORDERED key registry (зеркало порядка вызовов `addHandler`);
20- публичная точка входа чтения `read_additional_info_key`;
21- публичная точка входа записи `write_additional_info`;
22- GROUP-MODULE CONTRACT, который реализует каждый group-модуль.
23
24Source compatibility:
25- зеркало `test/ag-psd/src/additionalInfo.ts` (инфраструктура `infoHandlers` /
26  `infoHandlersMap`, `addHandler` / `addHandlerAlias`, framing в
27  `readAdditionalLayerInfo` / `writeAdditionalLayerInfo`).
28*/
29
30use crate::helpers::LARGE_ADDITIONAL_INFO_KEYS;
31use crate::psd::{LayerAdditionalInfo, ReadOptions, WriteOptions};
32use crate::reader::{PsdReader, ReadResult};
33use crate::writer::{write_section, write_signature, PsdWriter};
34
35pub mod metadata_keys;
36
37// Stub group modules (filled by follow-up parallel workers; see GROUP-MODULE
38// CONTRACT below). They currently report "not mine" for every key.
39pub mod adjustment_keys;
40pub mod effects_keys;
41pub mod misc_keys;
42pub mod smart_object_keys;
43pub mod text_keys;
44pub mod vector_keys;
45
46// ===========================================================================
47// Group tags
48// ===========================================================================
49
50/// Логическая группа ключа. Диспетчер маршрутизирует чтение/запись в
51/// соответствующий group-модуль по этому тегу.
52#[derive(Debug, Clone, Copy, PartialEq, Eq)]
53pub enum Group {
54    /// Простые скалярные/строковые/enum-ключи (реализовано: `metadata_keys.rs`).
55    Metadata,
56    /// Текстовые слои (`TySh`, `Txt2`) — `text_keys.rs`.
57    Text,
58    /// Эффекты слоя (`lmfx`, `lrFX`, `lfxs`, `lfx2`) — `effects_keys.rs`.
59    Effects,
60    /// Smart objects / linked / placed (`PlLd`, `SoLd`, `SoLE`, `lnk2`, `lnkE`,
61    /// `PxSc`, `Patt`, ...) — `smart_object_keys.rs`.
62    SmartObject,
63    /// Векторные маски / заливки / штрихи (`vmsk`, `vsms`, `vscg`, `vstk`,
64    /// `vogk`, `vowv`, `SoCo`, `GdFl`, `PtFl`, `pths`) — `vector_keys.rs`.
65    Vector,
66    /// Корректирующие слои (`brit`, `levl`, `curv`, ...) — `adjustment_keys.rs`.
67    Adjustment,
68    /// Всё прочее, что не вписалось в группы выше (`Lr16`, `Lr32`, `LMsk`,
69    /// `FMsk`, `FEid`, `Anno`, `shmd`, `artb`, `artd`, `cinf`, `extn`, `CAI `,
70    /// `OCIO`, `GenI`, `sn2P`, `lfxs`-сосед, ...) — `misc_keys.rs`.
71    Misc,
72}
73
74// ===========================================================================
75// CANONICAL ORDERED key registry
76// ===========================================================================
77
78/// Запись канонического реестра ключей.
79#[derive(Debug, Clone, Copy)]
80pub struct KeyHandler {
81    /// 4-символьный ключ секции (как в файле; например `"luni"`, `"CAI "`).
82    pub key: &'static str,
83    /// Логическая группа (определяет group-модуль).
84    pub group: Group,
85    /// `writeSection` round для записи: некоторые ключи пишут 4-байтовую
86    /// длину секции (round=4) вместо 2-байтовой (round=2). Зеркало `fourBytes`
87    /// в `writeAdditionalLayerInfo`.
88    pub four_bytes: bool,
89    /// `writeTotalLength` для `writeSection` — зеркало одноимённого флага.
90    /// Для большинства ключей `true`; `false` для `Txt2`/`cinf`/`extn`/`CAI `/`OCIO`.
91    pub write_total_length: bool,
92}
93
94const fn h(key: &'static str, group: Group) -> KeyHandler {
95    KeyHandler { key, group, four_bytes: false, write_total_length: true }
96}
97
98const fn h4(key: &'static str, group: Group) -> KeyHandler {
99    KeyHandler { key, group, four_bytes: true, write_total_length: true }
100}
101
102const fn h4_no_total(key: &'static str, group: Group) -> KeyHandler {
103    KeyHandler { key, group, four_bytes: true, write_total_length: false }
104}
105
106const fn h_no_total(key: &'static str, group: Group) -> KeyHandler {
107    KeyHandler { key, group, four_bytes: false, write_total_length: false }
108}
109
110/// CANONICAL ORDERED registry — ТОТ ЖЕ ПОРЯДОК, что и вызовы `addHandler` в
111/// upstream (важно для записи: PS пишет ключи в порядке регистрации хэндлеров).
112///
113/// Алиасы (`addHandlerAlias`) НЕ добавляют новых записей в порядок записи —
114/// они лишь делают ключ распознаваемым при чтении (см. [`alias_target`]).
115///
116/// `fourBytes` / `writeTotalLength` повторяют флаги из upstream
117/// `writeAdditionalLayerInfo`.
118pub const HANDLERS: &[KeyHandler] = &[
119    h("TySh", Group::Text), // NOT in fourBytes list upstream
120    h("SoCo", Group::Vector),
121    h4("GdFl", Group::Vector),
122    h("PtFl", Group::Vector),
123    h4("vscg", Group::Vector),
124    h4("vmsk", Group::Vector),
125    h("vowv", Group::Vector), // NOT in fourBytes list upstream
126    h4("vogk", Group::Vector),
127    h4("lmfx", Group::Effects),
128    h4("lrFX", Group::Effects),
129    h4("luni", Group::Metadata),
130    h("lnsr", Group::Metadata),
131    h("lyid", Group::Metadata),
132    h("lsct", Group::Metadata),
133    h("clbl", Group::Metadata),
134    h("infx", Group::Metadata),
135    h("knko", Group::Metadata),
136    h("lmgm", Group::Metadata),
137    h("lspf", Group::Metadata),
138    h("lclr", Group::Metadata),
139    h("shmd", Group::Misc),
140    h("PxSc", Group::SmartObject),
141    h("vstk", Group::Vector),
142    h4("artb", Group::Misc),
143    h("sn2P", Group::Misc),
144    h4("PlLd", Group::SmartObject),
145    h4("SoLd", Group::SmartObject),
146    h("fxrp", Group::Metadata),
147    h("Lr16", Group::Misc),
148    h("Lr32", Group::Misc),
149    h("LMsk", Group::Misc),
150    h("Patt", Group::SmartObject),
151    h("Patt", Group::SmartObject), // second Patt handler (upstream registers twice)
152    h4_no_total("CAI ", Group::Misc),
153    h4_no_total("CAI ", Group::Misc), // second CAI handler
154    h4_no_total("OCIO", Group::Misc),
155    h4("GenI", Group::Misc),
156    h4("Anno", Group::Misc),
157    h4("lnk2", Group::SmartObject), // createLnkHandler('lnk2') (in fourBytes list)
158    h("lnkE", Group::SmartObject), // createLnkHandler('lnkE') (NOT in fourBytes list upstream)
159    h("pths", Group::Vector),
160    h("lyvr", Group::Metadata),
161    h("lfxs", Group::Effects),
162    h("brit", Group::Adjustment),
163    h("levl", Group::Adjustment),
164    h4("curv", Group::Adjustment),
165    h("expA", Group::Adjustment),
166    h4("vibA", Group::Adjustment),
167    h("hue2", Group::Adjustment),
168    h("blnc", Group::Adjustment),
169    h4("blwh", Group::Adjustment),
170    h("phfl", Group::Adjustment),
171    h("mixr", Group::Adjustment),
172    h("clrL", Group::Adjustment),
173    h("nvrt", Group::Adjustment),
174    h("post", Group::Adjustment),
175    h("thrs", Group::Adjustment),
176    h4("grdm", Group::Adjustment),
177    h("selc", Group::Adjustment),
178    h4("CgEd", Group::Adjustment),
179    h4_no_total("Txt2", Group::Text), // four_bytes + writeTotalLength=false
180    h4("FEid", Group::Misc),
181    h("FMsk", Group::Misc),
182    h("artd", Group::Misc),
183    h("lfx2", Group::Effects),
184    h4_no_total("cinf", Group::Misc),
185    h_no_total("extn", Group::Misc),
186    h("iOpa", Group::Metadata),
187    h("brst", Group::Metadata),
188    h("tsly", Group::Metadata),
189];
190
191/// Алиасы чтения (`addHandlerAlias(key, target)`): ключ в файле → ключ-цель,
192/// чей хэндлер обрабатывает данные. Запись по этим ключам НЕ производится.
193pub const ALIASES: &[(&str, &str)] = &[
194    ("vsms", "vmsk"),
195    ("vmsk", "vsms"), // upstream registers both directions
196    ("lsdk", "lsct"),
197    ("SoLE", "SoLd"),
198    ("Pat2", "Patt"),
199    ("Pat3", "Patt"),
200    ("lnkD", "lnk2"),
201    ("lnk3", "lnk2"),
202    ("FXid", "FEid"),
203];
204
205/// Возвращает ключ-цель для алиаса (или сам ключ, если не алиас).
206///
207/// Используется только при ЧТЕНИИ для маршрутизации: данные ключа-алиаса
208/// читаются хэндлером ключа-цели. ВНИМАНИЕ: при чтении `lsdk`/`lsct` группа
209/// определяется по ключу-цели, но фактический ключ передаётся в group-модуль
210/// без изменений (group-модули, обрабатывающие алиасы, должны учитывать оба).
211pub fn alias_target(key: &str) -> &str {
212    for (alias, target) in ALIASES {
213        if *alias == key {
214            return target;
215        }
216    }
217    key
218}
219
220/// Группа, к которой относится ключ (учитывая алиасы). `None`, если ключ
221/// неизвестен.
222pub fn group_for_key(key: &str) -> Option<Group> {
223    let target = alias_target(key);
224    HANDLERS.iter().find(|h| h.key == target).map(|h| h.group)
225}
226
227/// Использует ли ключ "большой" (8-байтовый) размер секции. Зеркало
228/// `largeAdditionalInfoKeys.indexOf(key) !== -1`.
229pub fn is_large_key(key: &str) -> bool {
230    LARGE_ADDITIONAL_INFO_KEYS.contains(&key)
231}
232
233// ===========================================================================
234// Context structs (orchestration glue)
235// ===========================================================================
236
237/// Контекст чтения, прокидываемый в group-модули. Зеркало хвостовых аргументов
238/// upstream `ReadMethod` (`psd`, `imageResources`) плюс опции ридера.
239///
240/// Поля сделаны опциональными ссылками, чтобы метаданные-группа (которой
241/// контекст не нужен) могла вызываться и без полностью собранного `Psd`.
242/// Группы, которым нужен `psd`/ресурсы (smart objects, linked files), будут
243/// требовать соответствующие поля — это часть GROUP-MODULE CONTRACT.
244pub struct ReadCtx<'a> {
245    pub options: &'a ReadOptions,
246    pub large: bool,
247}
248
249/// Контекст записи. Зеркало `ExtendedWriteOptions` (опции + `layerIds` /
250/// `layerToId` для дедупликации id в `lyid`).
251pub struct WriteCtx<'a> {
252    pub options: &'a WriteOptions,
253    pub psb: bool,
254    /// уже использованные id слоёв (см. `lyid`-хэндлер: дубликаты сдвигаются +100).
255    pub layer_ids: std::collections::HashSet<u32>,
256}
257
258impl<'a> WriteCtx<'a> {
259    pub fn new(options: &'a WriteOptions, psb: bool) -> Self {
260        WriteCtx { options, psb, layer_ids: std::collections::HashSet::new() }
261    }
262}
263
264// ===========================================================================
265// PUBLIC READ ENTRY POINT
266// ===========================================================================
267
268/// Прочитать тело одного additional-info ключа В ПРЕДЕЛАХ уже открытой секции.
269///
270/// Зеркало внутренностей `readAdditionalLayerInfo`: вызывающая оркестрация
271/// (`readAdditionalLayerInfo` в reader.rs, ещё не портирована) сама читает
272/// подпись `8BIM`/`8B64` и открывает `readSection`, затем зовёт эту функцию
273/// с `key` и замыканием `left` (сколько байт осталось в секции).
274///
275/// Возвращает `Ok(true)`, если ключ распознан и обработан; `Ok(false)`, если
276/// ключ неизвестен (вызывающий должен `skip_bytes(reader, left())`).
277///
278/// СТАБИЛЬНАЯ СИГНАТУРА — на неё опирается оркестрация и group-модули.
279pub fn read_additional_info_key(
280    key: &str,
281    reader: &mut PsdReader,
282    info: &mut LayerAdditionalInfo,
283    left: &dyn Fn(&PsdReader) -> usize,
284    ctx: &mut ReadCtx,
285) -> ReadResult<bool> {
286    let group = match group_for_key(key) {
287        Some(g) => g,
288        None => return Ok(false),
289    };
290
291    let result = match group {
292        Group::Metadata => metadata_keys::read(key, reader, info, left, ctx)?,
293        Group::Text => text_keys::read(key, reader, info, left, ctx)?,
294        Group::Effects => effects_keys::read(key, reader, info, left, ctx)?,
295        Group::SmartObject => smart_object_keys::read(key, reader, info, left, ctx)?,
296        Group::Vector => vector_keys::read(key, reader, info, left, ctx)?,
297        Group::Adjustment => adjustment_keys::read(key, reader, info, left, ctx)?,
298        Group::Misc => misc_keys::read(key, reader, info, left, ctx)?,
299    };
300
301    Ok(result.is_some())
302}
303
304// ===========================================================================
305// PUBLIC WRITE ENTRY POINT
306// ===========================================================================
307
308/// Записать все additional-info секции слоя в каноническом порядке.
309///
310/// Зеркало `writeAdditionalLayerInfo`: итерирует [`HANDLERS`] по порядку,
311/// для каждого ключа спрашивает у его group-модуля `has(key, info)`; если
312/// `Some(true)` — пишет подпись (`8BIM`/`8B64`) + ключ + секцию, делегируя
313/// тело записи `write(key, ...)` group-модуля.
314///
315/// Особые случаи из upstream:
316/// - `Txt2` пропускается при `options.invalidate_text_layers`;
317/// - `vmsk` при `psb` пишется как `vsms`;
318/// - `large` (8B64 / 8-байтовая длина) включается при `psb && is_large_key`.
319///
320/// СТАБИЛЬНАЯ СИГНАТУРА.
321pub fn write_additional_info(
322    writer: &mut PsdWriter,
323    info: &LayerAdditionalInfo,
324    ctx: &mut WriteCtx,
325) {
326    for handler in HANDLERS {
327        let mut key = handler.key;
328
329        // upstream: skip Txt2 when invalidating text layers.
330        if key == "Txt2" && ctx.options.invalidate_text_layers == Some(true) {
331            continue;
332        }
333        // upstream: vmsk -> vsms in PSB.
334        if key == "vmsk" && ctx.psb {
335            key = "vsms";
336        }
337
338        let has = group_has(handler.group, key, info);
339        if has != Some(true) {
340            continue;
341        }
342
343        let large = ctx.psb && is_large_key(key);
344        let round = if handler.four_bytes { 4 } else { 2 };
345
346        write_signature(writer, if large { "8B64" } else { "8BIM" });
347        write_signature(writer, key);
348        let write_total_length = handler.write_total_length;
349        write_section(
350            writer,
351            round,
352            |w| {
353                // Ошибки записи в Rust-порте отражаются как паника/в group-модуле;
354                // write-контракт group-модуля не возвращает Result (см. контракт).
355                group_write(handler.group, key, w, info, ctx);
356            },
357            write_total_length,
358            large,
359        );
360    }
361}
362
363// --- internal routing helpers ---------------------------------------------
364
365fn group_has(group: Group, key: &str, info: &LayerAdditionalInfo) -> Option<bool> {
366    match group {
367        Group::Metadata => metadata_keys::has(key, info),
368        Group::Text => text_keys::has(key, info),
369        Group::Effects => effects_keys::has(key, info),
370        Group::SmartObject => smart_object_keys::has(key, info),
371        Group::Vector => vector_keys::has(key, info),
372        Group::Adjustment => adjustment_keys::has(key, info),
373        Group::Misc => misc_keys::has(key, info),
374    }
375}
376
377fn group_write(
378    group: Group,
379    key: &str,
380    writer: &mut PsdWriter,
381    info: &LayerAdditionalInfo,
382    ctx: &mut WriteCtx,
383) {
384    let handled = match group {
385        Group::Metadata => metadata_keys::write(key, writer, info, ctx),
386        Group::Text => text_keys::write(key, writer, info, ctx),
387        Group::Effects => effects_keys::write(key, writer, info, ctx),
388        Group::SmartObject => smart_object_keys::write(key, writer, info, ctx),
389        Group::Vector => vector_keys::write(key, writer, info, ctx),
390        Group::Adjustment => adjustment_keys::write(key, writer, info, ctx),
391        Group::Misc => misc_keys::write(key, writer, info, ctx),
392    };
393    debug_assert!(handled.is_some(), "group did not handle write for key {key}");
394}