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}