Skip to main content

ag_psd/
helpers.rs

1/*
2File: crates/ag-psd/src/helpers.rs
3
4Purpose:
5общие хелперы крейта: числовые/цветовые утилиты, таблицы соответствия blend mode
6<-> 4-символьный ключ, layerColors, упаковка/распаковка данных каналов
7(raw / RLE / zip-without-prediction), а также заглушки canvas-уровня.
8
9Source compatibility:
10- порт upstream-файла `test/ag-psd/src/helpers.ts` (разбиение 1:1).
11
12Main responsibilities:
13- зеркалировать соответствующий upstream-модуль при портировании;
14- держать публичный контракт этого участка в одном месте.
15
16PORT STATUS: ported except browser-canvas concerns
17  (create_canvas / create_image_data / image_data_to_canvas / create_canvas_from_data /
18   initialize_canvas стабированы под модель PixelData, см. пометки ниже).
19*/
20
21use std::collections::HashMap;
22
23use flate2::{write::ZlibEncoder, Compression as FlateCompression};
24use std::io::Write as _;
25
26use crate::psd::{BlendMode, ChannelId, Compression, Layer, LayerColor, PixelData};
27
28/// upstream: `export const MOCK_HANDLERS = false;`
29pub const MOCK_HANDLERS: bool = false;
30/// upstream: `export const RAW_IMAGE_DATA = false;`
31pub const RAW_IMAGE_DATA: bool = false;
32
33// ===========================================================================
34// Blend mode <-> 4-char key mapping tables
35// ===========================================================================
36
37/// upstream `toBlendMode`: 4-символьный ключ -> BlendMode.
38/// Сохранены ВСЕ записи и точные ключи (включая пробелы), это критично для формата.
39pub fn to_blend_mode(key: &str) -> Option<BlendMode> {
40    Some(match key {
41        "pass" => BlendMode::PassThrough,
42        "norm" => BlendMode::Normal,
43        "diss" => BlendMode::Dissolve,
44        "dark" => BlendMode::Darken,
45        "mul " => BlendMode::Multiply,
46        "idiv" => BlendMode::ColorBurn,
47        "lbrn" => BlendMode::LinearBurn,
48        "dkCl" => BlendMode::DarkerColor,
49        "lite" => BlendMode::Lighten,
50        "scrn" => BlendMode::Screen,
51        "div " => BlendMode::ColorDodge,
52        "lddg" => BlendMode::LinearDodge,
53        "lgCl" => BlendMode::LighterColor,
54        "over" => BlendMode::Overlay,
55        "sLit" => BlendMode::SoftLight,
56        "hLit" => BlendMode::HardLight,
57        "vLit" => BlendMode::VividLight,
58        "lLit" => BlendMode::LinearLight,
59        "pLit" => BlendMode::PinLight,
60        "hMix" => BlendMode::HardMix,
61        "diff" => BlendMode::Difference,
62        "smud" => BlendMode::Exclusion,
63        "fsub" => BlendMode::Subtract,
64        "fdiv" => BlendMode::Divide,
65        "hue " => BlendMode::Hue,
66        "sat " => BlendMode::Saturation,
67        "colr" => BlendMode::Color,
68        "lum " => BlendMode::Luminosity,
69        _ => return None,
70    })
71}
72
73/// upstream `fromBlendMode` (построен через
74/// `Object.keys(toBlendMode).forEach(key => fromBlendMode[toBlendMode[key]] = key)`):
75/// BlendMode -> 4-символьный ключ. Это обратное отображение `to_blend_mode`.
76pub fn from_blend_mode(mode: BlendMode) -> &'static str {
77    match mode {
78        BlendMode::PassThrough => "pass",
79        BlendMode::Normal => "norm",
80        BlendMode::Dissolve => "diss",
81        BlendMode::Darken => "dark",
82        BlendMode::Multiply => "mul ",
83        BlendMode::ColorBurn => "idiv",
84        BlendMode::LinearBurn => "lbrn",
85        BlendMode::DarkerColor => "dkCl",
86        BlendMode::Lighten => "lite",
87        BlendMode::Screen => "scrn",
88        BlendMode::ColorDodge => "div ",
89        BlendMode::LinearDodge => "lddg",
90        BlendMode::LighterColor => "lgCl",
91        BlendMode::Overlay => "over",
92        BlendMode::SoftLight => "sLit",
93        BlendMode::HardLight => "hLit",
94        BlendMode::VividLight => "vLit",
95        BlendMode::LinearLight => "lLit",
96        BlendMode::PinLight => "pLit",
97        BlendMode::HardMix => "hMix",
98        BlendMode::Difference => "diff",
99        BlendMode::Exclusion => "smud",
100        BlendMode::Subtract => "fsub",
101        BlendMode::Divide => "fdiv",
102        BlendMode::Hue => "hue ",
103        BlendMode::Saturation => "sat ",
104        BlendMode::Color => "colr",
105        BlendMode::Luminosity => "lum ",
106    }
107}
108
109/// upstream `layerColors`.
110pub const LAYER_COLORS: [LayerColor; 8] = [
111    LayerColor::None,
112    LayerColor::Red,
113    LayerColor::Orange,
114    LayerColor::Yellow,
115    LayerColor::Green,
116    LayerColor::Blue,
117    LayerColor::Violet,
118    LayerColor::Gray,
119];
120
121/// upstream `largeAdditionalInfoKeys`.
122pub const LARGE_ADDITIONAL_INFO_KEYS: [&str; 14] = [
123    // from documentation
124    "LMsk", "Lr16", "Lr32", "Layr", "Mt16", "Mt32", "Mtrn", "Alph", "FMsk", "lnk2", "FEid",
125    "FXid", "PxSD", // from guessing
126    "cinf",
127];
128
129// ===========================================================================
130// Dict / enum descriptor helpers
131// ===========================================================================
132
133/// upstream `Dict` = `{ [key: string]: string }`.
134pub type Dict = HashMap<String, String>;
135
136/// upstream `revMap`: меняет местами ключи и значения словаря.
137pub fn rev_map(map: &Dict) -> Dict {
138    let mut result = Dict::new();
139    for (key, value) in map {
140        result.insert(value.clone(), key.clone());
141    }
142    result
143}
144
145/// upstream `createEnum<T>`: возвращает пару (decode, encode) для дескрипторного
146/// enum вида `prefix.value`. Так как в Rust замыкания неудобно возвращать парой,
147/// предоставляем структуру с теми же decode/encode.
148pub struct EnumCodec {
149    prefix: String,
150    def: String,
151    map: Dict,
152    rev: Dict,
153}
154
155impl EnumCodec {
156    /// upstream `createEnum(prefix, def, map)`.
157    pub fn new(prefix: &str, def: &str, map: Dict) -> Self {
158        let rev = rev_map(&map);
159        EnumCodec {
160            prefix: prefix.to_string(),
161            def: def.to_string(),
162            map,
163            rev,
164        }
165    }
166
167    /// upstream `decode(val)`: `val.split('.')[1]` -> reverse-lookup -> def.
168    /// Бросает (Err) при нераспознанном непустом значении.
169    pub fn decode(&self, val: &str) -> Result<String, String> {
170        // val.split('.')[1] — второй сегмент (может отсутствовать => "").
171        let value = val.split('.').nth(1).unwrap_or("");
172        if !value.is_empty() && !self.rev.contains_key(value) {
173            return Err(format!("Unrecognized value for enum: '{val}'"));
174        }
175        Ok(self
176            .rev
177            .get(value)
178            .cloned()
179            .unwrap_or_else(|| self.def.clone()))
180    }
181
182    /// upstream `encode(val)`: `${prefix}.${map[val] || map[def]}`.
183    /// `val == None` зеркалирует `undefined` в TS.
184    /// Бросает (Err) при невалидном непустом значении.
185    pub fn encode(&self, val: Option<&str>) -> Result<String, String> {
186        if let Some(v) = val {
187            if !self.map.contains_key(v) {
188                return Err(format!("Invalid value for enum: '{v}'"));
189            }
190        }
191        let mapped = val
192            .and_then(|v| self.map.get(v))
193            .or_else(|| self.map.get(&self.def))
194            .cloned()
195            .unwrap_or_default();
196        Ok(format!("{}.{}", self.prefix, mapped))
197    }
198}
199
200// ===========================================================================
201// Numeric const enums (port of TS `const enum`)
202// ===========================================================================
203
204/// upstream `const enum ColorSpace`.
205#[derive(Debug, Clone, Copy, PartialEq, Eq)]
206pub enum ColorSpace {
207    Rgb = 0,
208    Hsb = 1,
209    Cmyk = 2,
210    Lab = 7,
211    Grayscale = 8,
212}
213
214/// upstream `const enum LayerMaskFlags`.
215#[derive(Debug, Clone, Copy, PartialEq, Eq)]
216pub enum LayerMaskFlags {
217    PositionRelativeToLayer = 1,
218    LayerMaskDisabled = 2,
219    /// obsolete
220    InvertLayerMaskWhenBlending = 4,
221    LayerMaskFromRenderingOtherData = 8,
222    MaskHasParametersAppliedToIt = 16,
223}
224
225/// upstream `const enum MaskParams`.
226#[derive(Debug, Clone, Copy, PartialEq, Eq)]
227pub enum MaskParams {
228    UserMaskDensity = 1,
229    UserMaskFeather = 2,
230    VectorMaskDensity = 4,
231    VectorMaskFeather = 8,
232}
233
234// ===========================================================================
235// Channel / bounds data shapes
236// ===========================================================================
237
238/// upstream `ChannelData`.
239#[derive(Debug, Clone)]
240pub struct ChannelData {
241    pub channel_id: ChannelId,
242    pub compression: Compression,
243    pub buffer: Option<Vec<u8>>,
244    pub length: usize,
245}
246
247/// upstream `Bounds`.
248#[derive(Debug, Clone, Copy, Default)]
249pub struct Bounds {
250    pub top: i32,
251    pub left: i32,
252    pub right: i32,
253    pub bottom: i32,
254}
255
256/// upstream `LayerChannelData`.
257pub struct LayerChannelData {
258    pub layer: Layer,
259    pub channels: Vec<ChannelData>,
260    pub top: i32,
261    pub left: i32,
262    pub right: i32,
263    pub bottom: i32,
264    pub mask: Option<Bounds>,
265    pub real_mask: Option<Bounds>,
266}
267
268// ===========================================================================
269// Pure numeric helpers
270// ===========================================================================
271
272/// upstream `offsetForChannel(channelId, cmyk)`.
273/// В TS работает с числовыми значениями enum; здесь повторяем арифметику через i32.
274pub fn offset_for_channel(channel_id: ChannelId, cmyk: bool) -> i32 {
275    let id = channel_id as i32;
276    match channel_id {
277        ChannelId::Color0 => 0,
278        ChannelId::Color1 => 1,
279        ChannelId::Color2 => 2,
280        ChannelId::Color3 => {
281            if cmyk {
282                3
283            } else {
284                id + 1
285            }
286        }
287        ChannelId::Transparency => {
288            if cmyk {
289                4
290            } else {
291                3
292            }
293        }
294        _ => id + 1,
295    }
296}
297
298/// upstream `clamp(value, min, max)`.
299pub fn clamp(value: f64, min: f64, max: f64) -> f64 {
300    if value < min {
301        min
302    } else if value > max {
303        max
304    } else {
305        value
306    }
307}
308
309/// upstream `hasAlpha(data)`: true, если есть пиксель с alpha != 255.
310pub fn has_alpha(data: &PixelData) -> bool {
311    let size = (data.width as usize) * (data.height as usize) * 4;
312    let mut i = 3usize;
313    while i < size {
314        if data.data[i] != 255 {
315            return true;
316        }
317        i += 4;
318    }
319    false
320}
321
322/// upstream `resetImageData({ data })`.
323/// В оригинале alpha зависит от типа массива (Float32/Uint16/Uint8); наша модель
324/// PixelData хранит байты RGBA8, поэтому alpha = 0xff.
325pub fn reset_image_data(data: &mut PixelData) {
326    let buf = &mut data.data;
327    let alpha = 0xffu8;
328    let size = buf.len();
329    let mut p = 0usize;
330    while p < size {
331        buf[p] = 0;
332        buf[p + 1] = 0;
333        buf[p + 2] = 0;
334        buf[p + 3] = alpha;
335        p += 4;
336    }
337}
338
339/// upstream `decodeBitmap(input, output, width, height)`.
340/// Распаковывает 1-битное изображение в RGBA8: бит=1 -> чёрный (0), бит=0 -> белый (255).
341pub fn decode_bitmap(input: &[u8], output: &mut [u8], width: usize, height: usize) {
342    let mut p = 0usize;
343    let mut o = 0usize;
344    for _y in 0..height {
345        let mut x = 0usize;
346        while x < width {
347            let mut b = input[o];
348            o += 1;
349            let mut i = 0;
350            while i < 8 && x < width {
351                let v: u8 = if b & 0x80 != 0 { 0 } else { 255 };
352                b <<= 1;
353                output[p] = v;
354                output[p + 1] = v;
355                output[p + 2] = v;
356                output[p + 3] = 255;
357                i += 1;
358                x += 1;
359                p += 4;
360            }
361        }
362    }
363}
364
365// ===========================================================================
366// Channel data writers (raw / RLE / zip)
367// ===========================================================================
368
369/// upstream `writeDataRaw(data, offset, width, height)`.
370/// Извлекает один канал (по offset) в плотный массив длиной width*height.
371pub fn write_data_raw(data: &PixelData, offset: usize, width: usize, height: usize) -> Option<Vec<u8>> {
372    if width == 0 || height == 0 {
373        return None;
374    }
375    let mut array = vec![0u8; width * height];
376    for (i, slot) in array.iter_mut().enumerate() {
377        *slot = data.data[i * 4 + offset];
378    }
379    Some(array)
380}
381
382/// upstream `writeDataRLE(buffer, { data, width, height }, offsets, large)`.
383/// Сжимает каналы по PackBits, как в оригинале (включая раскладку length-таблицы
384/// в начале буфера). Возвращает срез использованной части буфера.
385pub fn write_data_rle(
386    buffer: &mut [u8],
387    data_pixels: &PixelData,
388    offsets: &[usize],
389    large: bool,
390) -> Option<Vec<u8>> {
391    let width = data_pixels.width as i64;
392    let height = data_pixels.height as i64;
393    if width == 0 || height == 0 {
394        return None;
395    }
396    let data = &data_pixels.data;
397    let stride = 4 * width;
398
399    let mut ol: i64 = 0;
400    let mut o: i64 = (offsets.len() as i64) * (if large { 4 } else { 2 }) * height;
401
402    let get = |idx: i64| -> i64 { data[idx as usize] as i64 };
403
404    // upstream writes into a `Uint8Array`; writing past its end is a silent
405    // no-op in JS (the value is simply dropped), and `buffer.slice(0, o)` later
406    // returns only the bytes that fit. The buffer is sized by an estimate that
407    // can be too small for tiny images (e.g. 1x1 with alpha), so faithfully
408    // mirror the TypedArray semantics by ignoring out-of-bounds writes instead
409    // of panicking. `o`/`ol` still advance so the returned length matches TS.
410    macro_rules! set {
411        ($buf:expr, $idx:expr, $val:expr) => {{
412            let idx = $idx as usize;
413            if idx < $buf.len() {
414                $buf[idx] = $val;
415            }
416        }};
417    }
418
419    for &offset in offsets {
420        let offset = offset as i64;
421        for y in 0..height {
422            let stride_start = y * stride;
423            let stride_end = stride_start + stride;
424            let last_index = stride_end + offset - 4;
425            let last_index2 = last_index - 4;
426            let start_offset = o;
427
428            let mut p = stride_start + offset;
429            while p < stride_end {
430                if p < last_index2 {
431                    let mut value1 = get(p);
432                    p += 4;
433                    let mut value2 = get(p);
434                    p += 4;
435                    let mut value3 = get(p);
436
437                    if value1 == value2 && value1 == value3 {
438                        let mut count: i64 = 3;
439                        while count < 128 && p < last_index && get(p + 4) == value1 {
440                            count += 1;
441                            p += 4;
442                        }
443                        set!(buffer, o, (1 - count) as u8);
444                        o += 1;
445                        set!(buffer, o, value1 as u8);
446                        o += 1;
447                    } else {
448                        let count_index = o;
449                        let mut write_last = true;
450                        let mut count: i64 = 1;
451                        set!(buffer, o, 0);
452                        o += 1;
453                        set!(buffer, o, value1 as u8);
454                        o += 1;
455
456                        while p < last_index && count < 128 {
457                            p += 4;
458                            value1 = value2;
459                            value2 = value3;
460                            value3 = get(p);
461
462                            if value1 == value2 && value1 == value3 {
463                                p -= 12;
464                                write_last = false;
465                                break;
466                            } else {
467                                count += 1;
468                                set!(buffer, o, value1 as u8);
469                                o += 1;
470                            }
471                        }
472
473                        if write_last {
474                            if count < 127 {
475                                set!(buffer, o, value2 as u8);
476                                o += 1;
477                                set!(buffer, o, value3 as u8);
478                                o += 1;
479                                count += 2;
480                            } else if count < 128 {
481                                set!(buffer, o, value2 as u8);
482                                o += 1;
483                                count += 1;
484                                p -= 4;
485                            } else {
486                                p -= 8;
487                            }
488                        }
489
490                        set!(buffer, count_index, (count - 1) as u8);
491                    }
492                } else if p == last_index {
493                    set!(buffer, o, 0);
494                    o += 1;
495                    set!(buffer, o, get(p) as u8);
496                    o += 1;
497                } else {
498                    // p === lastIndex2
499                    set!(buffer, o, 1);
500                    o += 1;
501                    set!(buffer, o, get(p) as u8);
502                    o += 1;
503                    p += 4;
504                    set!(buffer, o, get(p) as u8);
505                    o += 1;
506                }
507
508                p += 4;
509            }
510
511            let length = o - start_offset;
512
513            if large {
514                set!(buffer, ol, ((length >> 24) & 0xff) as u8);
515                ol += 1;
516                set!(buffer, ol, ((length >> 16) & 0xff) as u8);
517                ol += 1;
518            }
519
520            set!(buffer, ol, ((length >> 8) & 0xff) as u8);
521            ol += 1;
522            set!(buffer, ol, (length & 0xff) as u8);
523            ol += 1;
524        }
525    }
526
527    // mirror `buffer.slice(0, o)`: clamp to the buffer length so we never read
528    // past the end when the size estimate fell short (out-of-bounds writes above
529    // were dropped, so those positions hold zero/stale bytes which TS omits too).
530    let end = (o as usize).min(buffer.len());
531    Some(buffer[..end].to_vec())
532}
533
534/// upstream `writeDataZipWithoutPrediction({ data, width, height }, offsets)`.
535/// Извлекает каждый канал и сжимает zlib/deflate, конкатенируя результаты.
536pub fn write_data_zip_without_prediction(data_pixels: &PixelData, offsets: &[usize]) -> Option<Vec<u8>> {
537    let size = (data_pixels.width as usize) * (data_pixels.height as usize);
538    let data = &data_pixels.data;
539    let mut channel = vec![0u8; size];
540    let mut buffers: Vec<Vec<u8>> = Vec::new();
541    let mut total_length = 0usize;
542
543    for &offset in offsets {
544        let mut o = offset;
545        for slot in channel.iter_mut().take(size) {
546            *slot = data[o];
547            o += 4;
548        }
549
550        let buffer = deflate_sync(&channel);
551        total_length += buffer.len();
552        buffers.push(buffer);
553    }
554
555    if !buffers.is_empty() {
556        let mut buffer = Vec::with_capacity(total_length);
557        for b in &buffers {
558            buffer.extend_from_slice(b);
559        }
560        Some(buffer)
561    } else {
562        // upstream возвращает buffers[0] (undefined при пустом списке).
563        None
564    }
565}
566
567/// Эквивалент `deflate` из `pako` (zlib-обёрнутый deflate).
568fn deflate_sync(input: &[u8]) -> Vec<u8> {
569    let mut encoder = ZlibEncoder::new(Vec::new(), FlateCompression::default());
570    encoder.write_all(input).expect("zlib write");
571    encoder.finish().expect("zlib finish")
572}
573
574// ===========================================================================
575// Canvas-уровень — заглушки под модель PixelData
576// ===========================================================================
577
578/// upstream `imageDataToCanvas(pixelData)`.
579/// TODO: browser-canvas concern. Наша модель уже хранит RGBA8 в PixelData, так что
580/// "канвас" — это и есть копия PixelData. Гамма/битность-конверсии оригинала
581/// (Float32 pow(1/2.2), Uint16 >>8) не нужны для байтовой модели.
582pub fn image_data_to_canvas(pixel_data: &PixelData) -> PixelData {
583    pixel_data.clone()
584}
585
586/// upstream `createCanvasFromData(data)` — декодирование JPEG в канвас.
587/// TODO: browser-canvas concern; зависит от не-портированного `decode_jpeg`.
588/// Стаб: возвращает пустой PixelData 100x100, как и стартовый канвас в оригинале.
589pub fn create_canvas_from_data(_data: &[u8]) -> PixelData {
590    create_canvas(100, 100)
591}
592
593/// upstream `createCanvas(width, height)`.
594/// TODO: browser-canvas concern, not needed for byte IO. Возвращаем нулевой
595/// RGBA8-буфер нужного размера вместо HTMLCanvasElement.
596pub fn create_canvas(width: u32, height: u32) -> PixelData {
597    PixelData {
598        width,
599        height,
600        data: vec![0u8; (width as usize) * (height as usize) * 4],
601    }
602}
603
604/// upstream `createImageData(width, height)`.
605/// TODO: browser-canvas concern, not needed for byte IO.
606pub fn create_image_data(width: u32, height: u32) -> PixelData {
607    create_canvas(width, height)
608}
609
610/// upstream `initializeCanvas(createCanvasMethod, createImageDataMethod?)`.
611/// TODO: browser-canvas concern — установка глобальной фабрики канваса.
612/// В байтовой модели не требуется; намеренный no-op.
613pub fn initialize_canvas() {
614    // no-op
615}
616
617// ===========================================================================
618// Tests
619// ===========================================================================
620
621#[cfg(test)]
622mod tests {
623    use super::*;
624
625    #[test]
626    fn blend_mode_round_trip_all_entries() {
627        let all = [
628            BlendMode::PassThrough,
629            BlendMode::Normal,
630            BlendMode::Dissolve,
631            BlendMode::Darken,
632            BlendMode::Multiply,
633            BlendMode::ColorBurn,
634            BlendMode::LinearBurn,
635            BlendMode::DarkerColor,
636            BlendMode::Lighten,
637            BlendMode::Screen,
638            BlendMode::ColorDodge,
639            BlendMode::LinearDodge,
640            BlendMode::LighterColor,
641            BlendMode::Overlay,
642            BlendMode::SoftLight,
643            BlendMode::HardLight,
644            BlendMode::VividLight,
645            BlendMode::LinearLight,
646            BlendMode::PinLight,
647            BlendMode::HardMix,
648            BlendMode::Difference,
649            BlendMode::Exclusion,
650            BlendMode::Subtract,
651            BlendMode::Divide,
652            BlendMode::Hue,
653            BlendMode::Saturation,
654            BlendMode::Color,
655            BlendMode::Luminosity,
656        ];
657        for mode in all {
658            let key = from_blend_mode(mode);
659            assert_eq!(key.len(), 4, "key must be 4 chars: {key:?}");
660            assert_eq!(to_blend_mode(key), Some(mode), "round trip failed for {key:?}");
661        }
662    }
663
664    #[test]
665    fn blend_mode_spacey_keys() {
666        assert_eq!(to_blend_mode("mul "), Some(BlendMode::Multiply));
667        assert_eq!(to_blend_mode("div "), Some(BlendMode::ColorDodge));
668        assert_eq!(from_blend_mode(BlendMode::Luminosity), "lum ");
669        assert_eq!(to_blend_mode("nope"), None);
670    }
671
672    #[test]
673    fn clamp_edges() {
674        assert_eq!(clamp(-1.0, 0.0, 10.0), 0.0);
675        assert_eq!(clamp(11.0, 0.0, 10.0), 10.0);
676        assert_eq!(clamp(5.0, 0.0, 10.0), 5.0);
677        assert_eq!(clamp(0.0, 0.0, 10.0), 0.0);
678        assert_eq!(clamp(10.0, 0.0, 10.0), 10.0);
679    }
680
681    #[test]
682    fn offset_for_channel_rgb_and_cmyk() {
683        assert_eq!(offset_for_channel(ChannelId::Color0, false), 0);
684        assert_eq!(offset_for_channel(ChannelId::Color1, false), 1);
685        assert_eq!(offset_for_channel(ChannelId::Color2, false), 2);
686        // Color3 == 3: rgb branch -> id+1 == 4; cmyk -> 3.
687        assert_eq!(offset_for_channel(ChannelId::Color3, false), 4);
688        assert_eq!(offset_for_channel(ChannelId::Color3, true), 3);
689        // Transparency == -1.
690        assert_eq!(offset_for_channel(ChannelId::Transparency, false), 3);
691        assert_eq!(offset_for_channel(ChannelId::Transparency, true), 4);
692        // default: UserMask == -2 -> id+1 == -1.
693        assert_eq!(offset_for_channel(ChannelId::UserMask, false), -1);
694        assert_eq!(offset_for_channel(ChannelId::RealUserMask, true), -2);
695    }
696
697    #[test]
698    fn has_alpha_detects_non_opaque() {
699        let opaque = PixelData {
700            width: 2,
701            height: 1,
702            data: vec![1, 2, 3, 255, 4, 5, 6, 255],
703        };
704        assert!(!has_alpha(&opaque));
705
706        let translucent = PixelData {
707            width: 2,
708            height: 1,
709            data: vec![1, 2, 3, 255, 4, 5, 6, 128],
710        };
711        assert!(has_alpha(&translucent));
712    }
713
714    #[test]
715    fn reset_image_data_sets_black_opaque() {
716        let mut pd = PixelData {
717            width: 2,
718            height: 1,
719            data: vec![9, 9, 9, 9, 9, 9, 9, 9],
720        };
721        reset_image_data(&mut pd);
722        assert_eq!(pd.data, vec![0, 0, 0, 255, 0, 0, 0, 255]);
723    }
724
725    #[test]
726    fn decode_bitmap_packs_bits() {
727        // One byte 0b10100000 over width 8: bits -> black,white,black,white,...
728        let input = [0b1010_0000u8];
729        let mut output = vec![0u8; 8 * 4];
730        decode_bitmap(&input, &mut output, 8, 1);
731        // pixel0 bit set -> 0; pixel1 bit clear -> 255; pixel2 -> 0; pixel3 -> 255 ...
732        assert_eq!(output[0], 0);
733        assert_eq!(output[4], 255);
734        assert_eq!(output[8], 0);
735        assert_eq!(output[12], 255);
736        // alpha always 255
737        assert_eq!(output[3], 255);
738    }
739
740    #[test]
741    fn write_data_raw_extracts_channel() {
742        // 2x1 RGBA: [r0 g0 b0 a0, r1 g1 b1 a1]
743        let pd = PixelData {
744            width: 2,
745            height: 1,
746            data: vec![10, 20, 30, 40, 50, 60, 70, 80],
747        };
748        assert_eq!(write_data_raw(&pd, 0, 2, 1), Some(vec![10, 50])); // red
749        assert_eq!(write_data_raw(&pd, 3, 2, 1), Some(vec![40, 80])); // alpha
750        assert_eq!(write_data_raw(&pd, 0, 0, 1), None);
751    }
752
753    #[test]
754    fn zip_without_prediction_round_trips() {
755        use flate2::read::ZlibDecoder;
756        use std::io::Read;
757
758        let pd = PixelData {
759            width: 4,
760            height: 1,
761            data: vec![
762                1, 0, 0, 0, 2, 0, 0, 0, 3, 0, 0, 0, 4, 0, 0, 0,
763            ],
764        };
765        let out = write_data_zip_without_prediction(&pd, &[0]).unwrap();
766        let mut decoder = ZlibDecoder::new(&out[..]);
767        let mut decoded = Vec::new();
768        decoder.read_to_end(&mut decoded).unwrap();
769        assert_eq!(decoded, vec![1, 2, 3, 4]);
770    }
771
772    #[test]
773    fn rev_map_swaps() {
774        let mut m = Dict::new();
775        m.insert("a".into(), "1".into());
776        m.insert("b".into(), "2".into());
777        let r = rev_map(&m);
778        assert_eq!(r.get("1"), Some(&"a".to_string()));
779        assert_eq!(r.get("2"), Some(&"b".to_string()));
780    }
781
782    #[test]
783    fn enum_codec_encode_decode() {
784        let mut map = Dict::new();
785        map.insert("alpha".into(), "Alph".into());
786        map.insert("beta".into(), "Beta".into());
787        let codec = EnumCodec::new("Enum", "alpha", map);
788
789        assert_eq!(codec.encode(Some("beta")).unwrap(), "Enum.Beta");
790        assert_eq!(codec.encode(None).unwrap(), "Enum.Alph"); // falls back to def
791        assert!(codec.encode(Some("gamma")).is_err());
792
793        assert_eq!(codec.decode("Enum.Beta").unwrap(), "beta");
794        assert_eq!(codec.decode("Enum.Alph").unwrap(), "alpha");
795        // empty second segment -> default
796        assert_eq!(codec.decode("Enum").unwrap(), "alpha");
797        assert!(codec.decode("Enum.Zzzz").is_err());
798    }
799}