Skip to main content

csp_format/
offscreen.rs

1//! Raster pixels: the `Offscreen` table and its block data.
2//!
3//! An offscreen is a grid of 256×256 blocks. `Offscreen.Attribute` says how
4//! big it is and how a block's bytes are laid out; `Offscreen.BlockData` is
5//! the id of an external chunk holding the zlib-compressed blocks (spec:
6//! `docs/spec/04-raster.md`).
7//!
8//! `Attribute` (big endian): `u32 16, u32 len(Parameter), u32 len(InitColor),
9//! u32 len(BlockSize)`, then three sections, each a UTF-16 name followed by
10//! `u32` values. The values of `Parameter` and `InitColor` are, in order, the
11//! columns of the unused `VOffscreen` table in `ParamScheme`
12//! (`BlockDataChannelOrder`, `BlockDataPixelBytes`, … `InitColorDataAlphaValue`).
13//!
14//! A decompressed block is an alpha plane followed by an image plane:
15//!
16//! | `channel_order` | kind | block bytes | planes |
17//! |---|---|---|---|
18//! | 0x21 (33) | colour | 327 680 | alpha 256×256, then BGRA 256×256×4 (the 4th byte unused) |
19//! | 0x11 (17), 2 bytes/pixel | gray | 131 072 | alpha, then gray |
20//! | 0x11 (17), 1-bit | monochrome | 16 384 | alpha 1 bit/pixel, then 1 bit/pixel |
21//! | 0x01 (1) | mask / alpha only | 65 536 | one plane |
22//!
23//! The external payload (big endian): `u64 length of the rest`, one record
24//! per block (`u32 record length, name "BlockDataBeginChunk", u32 index,
25//! u32 decompressed size, u32 256, u32 256, u32 has data, [u32 4 + n,
26//! u32le n, n bytes of zlib], name "BlockDataEndChunk"`), then the tail
27//! sections `BlockStatus` and `BlockCheckSum` (`name, u32 12, u32 count,
28//! u32 4, count × u32`). The checksum is the Adler-32 of `u32le n` + the zlib
29//! bytes, 0 for an empty block. CSP compresses with zlib level 1.
30
31use std::io::Read;
32
33use flate2::read::ZlibDecoder;
34
35use crate::binary::{Reader, Writer, adler32};
36use crate::error::{Error, Result, bail};
37use crate::image::{GrayImage, RgbaImage};
38
39/// Width and height of a block in pixels.
40pub const BLOCK_SIZE: u32 = 256;
41/// Pixels per block.
42pub const BLOCK_PIXELS: usize = (BLOCK_SIZE * BLOCK_SIZE) as usize;
43const ATTRIBUTE_HEADER: u32 = 16;
44const BEGIN_NAME: &str = "BlockDataBeginChunk";
45const END_NAME: &str = "BlockDataEndChunk";
46const STATUS_NAME: &str = "BlockStatus";
47const CHECKSUM_NAME: &str = "BlockCheckSum";
48const TABLE_HEADER: u32 = 12;
49const TABLE_ENTRY: u32 = 4;
50/// Size of a block record without data (the `BlockSize` entry of empty blocks).
51pub const EMPTY_RECORD_SIZE: u32 = 104;
52/// What a stored block adds to its zlib length in its record.
53pub const RECORD_OVERHEAD: u32 = 112;
54
55/// Pixel layout of an offscreen.
56#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
57#[serde(rename_all = "snake_case")]
58#[non_exhaustive]
59pub enum PixelKind {
60    /// BGRA colour with a separate alpha plane.
61    Color,
62    /// 8-bit gray with alpha.
63    Gray,
64    /// 1-bit image with 1-bit alpha.
65    Monochrome,
66    /// One 8-bit plane (layer masks, selections, "alpha only" data).
67    Alpha,
68    /// A layout this crate does not know.
69    Unknown,
70}
71
72/// One named section of the attribute: its `u32` values.
73#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
74pub struct Section {
75    /// Section name (`Parameter`, `InitColor`, `BlockSize`).
76    pub name: String,
77    /// The values, as stored.
78    pub values: Vec<u32>,
79}
80
81/// `Offscreen.Attribute`, kept value for value so it can be written back.
82#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
83pub struct Attribute {
84    /// The sections, in stored order.
85    pub sections: Vec<Section>,
86}
87
88/// The fields of the `Parameter` section, named after `VOffscreen`.
89#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
90pub struct Parameter {
91    /// Width in pixels.
92    pub width: u32,
93    /// Height in pixels.
94    pub height: u32,
95    /// Blocks per row.
96    pub blocks_x: u32,
97    /// Block rows.
98    pub blocks_y: u32,
99    /// `BlockDataChannelOrder`: 0x21 colour (BGRA), 0x11 gray, 0x01 alpha only.
100    pub channel_order: u32,
101    /// `BlockDataChannelBytes` (1).
102    pub channel_bytes: u32,
103    /// `BlockDataChannelImageCount`: image channels besides alpha (4, 1, 0).
104    pub image_channels: u32,
105    /// `BlockDataPixelBytes`: bytes per pixel over all planes (5, 2, 1).
106    pub pixel_bytes: u32,
107    /// `BlockDataOffsetImageBlockBytes`: where the image plane starts in a block.
108    pub image_offset: u32,
109    /// `BlockDataOffsetImagePixelBytes`: bytes per pixel of the image plane.
110    pub image_pixel_bytes: u32,
111    /// `BlockDataOffsetImageRowBytes`: bytes per row of the image plane.
112    pub image_row_bytes: u32,
113    /// `BlockDataOffsetAlphaPixelBytes`.
114    pub alpha_pixel_bytes: u32,
115    /// `BlockDataOffsetAlphaRowBytes`: bytes per row of the alpha plane.
116    pub alpha_row_bytes: u32,
117    /// `BlockDataOffsetImageBlock`: pixels per block (65 536).
118    pub block_pixels: u32,
119    /// Block width in pixels (256).
120    pub block_width: u32,
121    /// Block height in pixels (256).
122    pub block_height: u32,
123    /// log2 of the block width (8).
124    pub block_shift_x: u32,
125    /// log2 of the block height (8).
126    pub block_shift_y: u32,
127}
128
129impl Parameter {
130    /// The layout CSP uses for a new offscreen of the given kind.
131    pub fn new(kind: PixelKind, width: u32, height: u32) -> Self {
132        let (channel_order, image_channels, pixel_bytes, image_offset, image_pixel_bytes, image_row_bytes, alpha_row) =
133            match kind {
134                PixelKind::Color => (0x21, 4, 5, 65536, 4, 1024, 256),
135                PixelKind::Gray => (0x11, 1, 2, 65536, 1, 256, 256),
136                PixelKind::Monochrome => (0x11, 1, 1, 8192, 1, 32, 32),
137                PixelKind::Alpha | PixelKind::Unknown => (0x01, 0, 1, 0, 0, 0, 256),
138            };
139        Parameter {
140            width,
141            height,
142            blocks_x: width.div_ceil(BLOCK_SIZE),
143            blocks_y: height.div_ceil(BLOCK_SIZE),
144            channel_order,
145            channel_bytes: 1,
146            image_channels,
147            pixel_bytes,
148            image_offset,
149            image_pixel_bytes,
150            image_row_bytes,
151            alpha_pixel_bytes: 1,
152            alpha_row_bytes: alpha_row,
153            block_pixels: 65536,
154            block_width: BLOCK_SIZE,
155            block_height: BLOCK_SIZE,
156            block_shift_x: 8,
157            block_shift_y: 8,
158        }
159    }
160
161    /// The pixel layout these values describe.
162    pub fn kind(&self) -> PixelKind {
163        match (self.channel_order, self.image_channels, self.alpha_row_bytes) {
164            (0x21, 4, _) => PixelKind::Color,
165            (0x11, 1, 32) => PixelKind::Monochrome,
166            (0x11, 1, _) => PixelKind::Gray,
167            (0x01, 0, _) => PixelKind::Alpha,
168            _ => PixelKind::Unknown,
169        }
170    }
171
172    /// Number of blocks in the grid.
173    pub fn block_count(&self) -> usize {
174        self.blocks_x as usize * self.blocks_y as usize
175    }
176
177    /// Bytes of one decompressed block.
178    pub fn block_bytes(&self) -> usize {
179        let alpha = self.alpha_row_bytes as usize * BLOCK_SIZE as usize;
180        let image = self.image_row_bytes as usize * BLOCK_SIZE as usize;
181        if self.image_channels == 0 { alpha } else { (self.image_offset as usize).max(alpha) + image }
182    }
183
184    /// Top-left pixel of block `index` (blocks are numbered row by row).
185    pub fn block_origin(&self, index: usize) -> (u32, u32) {
186        let column = index % self.blocks_x.max(1) as usize;
187        let row = index / self.blocks_x.max(1) as usize;
188        (column as u32 * BLOCK_SIZE, row as u32 * BLOCK_SIZE)
189    }
190
191    fn check(&self) -> Result<()> {
192        if self.blocks_x != self.width.div_ceil(BLOCK_SIZE) || self.blocks_y != self.height.div_ceil(BLOCK_SIZE) {
193            bail!(
194                "Block grid {}×{} does not fit the size {}×{}",
195                self.blocks_x,
196                self.blocks_y,
197                self.width,
198                self.height
199            );
200        }
201        if self.block_width != BLOCK_SIZE || self.block_height != BLOCK_SIZE {
202            bail!("Unsupported block size {}×{}", self.block_width, self.block_height);
203        }
204        if self.width > 1 << 20 || self.height > 1 << 20 {
205            bail!("Offscreen too large: {}×{}", self.width, self.height);
206        }
207        Ok(())
208    }
209}
210
211/// `InitColor`: how blocks that are not stored start out.
212#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
213pub struct InitColor {
214    /// Not stored blocks are filled (`DoInitialize`).
215    pub do_initialize: bool,
216    /// Alpha of a not stored pixel (0xFFFFFFFF = opaque, as on paper).
217    pub alpha: u32,
218    /// Colour values of a not stored pixel.
219    pub image_values: Vec<u32>,
220}
221
222impl Attribute {
223    /// Parses an `Offscreen.Attribute` blob.
224    pub fn parse(data: &[u8]) -> Result<Self> {
225        let mut reader = Reader::new(data);
226        let header = reader.be_u32()?;
227        if header != ATTRIBUTE_HEADER {
228            bail!("Offscreen attribute starts with {header}, expected 16");
229        }
230        let lengths = [reader.be_u32()?, reader.be_u32()?, reader.be_u32()?];
231        let mut sections = Vec::new();
232        for length in lengths {
233            let start = reader.position();
234            let name = reader.utf16_name()?;
235            let end = start + length as usize;
236            if end > data.len() || end < reader.position() || !(end - reader.position()).is_multiple_of(4) {
237                bail!("Offscreen attribute section {name:?} has a bad length {length}");
238            }
239            let mut values = Vec::new();
240            while reader.position() < end {
241                values.push(reader.be_u32()?);
242            }
243            sections.push(Section { name, values });
244        }
245        if !reader.is_empty() {
246            bail!("{} bytes after the offscreen attribute sections", reader.remaining());
247        }
248        Ok(Attribute { sections })
249    }
250
251    /// The blob; an unchanged attribute gives the same bytes.
252    pub fn to_bytes(&self) -> Vec<u8> {
253        let mut bodies = Vec::new();
254        for section in &self.sections {
255            let mut writer = Writer::new();
256            writer.utf16_name(&section.name);
257            for value in &section.values {
258                writer.be_u32(*value);
259            }
260            bodies.push(writer.into_inner());
261        }
262        let mut writer = Writer::new();
263        writer.be_u32(ATTRIBUTE_HEADER);
264        for body in &bodies {
265            writer.be_u32(body.len() as u32);
266        }
267        for body in &bodies {
268            writer.bytes(body);
269        }
270        writer.into_inner()
271    }
272
273    /// The section of this name.
274    pub fn section(&self, name: &str) -> Option<&Section> {
275        self.sections.iter().find(|section| section.name == name)
276    }
277
278    fn section_mut(&mut self, name: &str) -> Option<&mut Section> {
279        self.sections.iter_mut().find(|section| section.name == name)
280    }
281
282    /// The `Parameter` section, typed and checked.
283    pub fn parameter(&self) -> Result<Parameter> {
284        let values = &self
285            .section("Parameter")
286            .ok_or_else(|| Error::format("Offscreen attribute has no Parameter section"))?
287            .values;
288        if values.len() < 18 {
289            bail!("Parameter section has {} values, expected at least 18", values.len());
290        }
291        let parameter = Parameter {
292            width: values[0],
293            height: values[1],
294            blocks_x: values[2],
295            blocks_y: values[3],
296            channel_order: values[4],
297            channel_bytes: values[5],
298            image_channels: values[6],
299            pixel_bytes: values[7],
300            image_offset: values[8],
301            image_pixel_bytes: values[9],
302            image_row_bytes: values[10],
303            alpha_pixel_bytes: values[11],
304            alpha_row_bytes: values[12],
305            block_pixels: values[13],
306            block_width: values[14],
307            block_height: values[15],
308            block_shift_x: values[16],
309            block_shift_y: values[17],
310        };
311        parameter.check()?;
312        Ok(parameter)
313    }
314
315    /// The two values after the named ones (observed `0, 0`, and `1, 0` for
316    /// 1-bit offscreens).
317    pub fn parameter_extra(&self) -> &[u32] {
318        self.section("Parameter").map(|section| section.values.get(18..).unwrap_or(&[])).unwrap_or(&[])
319    }
320
321    /// The `InitColor` section, if any.
322    pub fn init_color(&self) -> Option<InitColor> {
323        // u32 20 (fixed part), DoInitialize, AlphaValue, ImageValueCount, u32 4, values.
324        let values = &self.section("InitColor")?.values;
325        let count = *values.get(3)? as usize;
326        Some(InitColor {
327            do_initialize: *values.get(1)? != 0,
328            alpha: *values.get(2)?,
329            image_values: values.get(5..5 + count)?.to_vec(),
330        })
331    }
332
333    /// `BlockSize`: the length of every block record in the payload.
334    pub fn block_sizes(&self) -> Option<&[u32]> {
335        let values = &self.section("BlockSize")?.values;
336        let count = *values.get(1)? as usize;
337        values.get(3..3 + count)
338    }
339
340    /// Replaces (or adds) the `BlockSize` section.
341    pub fn set_block_sizes(&mut self, sizes: &[u32]) {
342        let values = vec![TABLE_HEADER, sizes.len() as u32, TABLE_ENTRY].into_iter().chain(sizes.iter().copied());
343        match self.section_mut("BlockSize") {
344            Some(section) => section.values = values.collect(),
345            None => self.sections.push(Section { name: "BlockSize".into(), values: values.collect() }),
346        }
347    }
348
349    /// A new attribute as CSP writes it for an offscreen without init colour.
350    pub fn new(parameter: &Parameter, init: Option<&InitColor>) -> Self {
351        let mut values = vec![
352            parameter.width,
353            parameter.height,
354            parameter.blocks_x,
355            parameter.blocks_y,
356            parameter.channel_order,
357            parameter.channel_bytes,
358            parameter.image_channels,
359            parameter.pixel_bytes,
360            parameter.image_offset,
361            parameter.image_pixel_bytes,
362            parameter.image_row_bytes,
363            parameter.alpha_pixel_bytes,
364            parameter.alpha_row_bytes,
365            parameter.block_pixels,
366            parameter.block_width,
367            parameter.block_height,
368            parameter.block_shift_x,
369            parameter.block_shift_y,
370        ];
371        values.extend([(parameter.kind() == PixelKind::Monochrome) as u32, 0]);
372        let init_values = match init {
373            Some(init) => {
374                let mut values = vec![20, init.do_initialize as u32, init.alpha, init.image_values.len() as u32, 4];
375                values.extend(&init.image_values);
376                values
377            }
378            None => vec![20, 0, 0, 0, 4],
379        };
380        let mut attribute = Attribute {
381            sections: vec![
382                Section { name: "Parameter".into(), values },
383                Section { name: "InitColor".into(), values: init_values },
384            ],
385        };
386        attribute.set_block_sizes(&vec![EMPTY_RECORD_SIZE; parameter.block_count()]);
387        attribute
388    }
389}
390
391/// One block record of the payload.
392#[derive(Debug, Clone, PartialEq, Eq)]
393pub struct Block {
394    /// Index in the grid (row by row).
395    pub index: u32,
396    /// Decompressed size the record declares.
397    pub raw_size: u32,
398    /// Block width in pixels.
399    pub width: u32,
400    /// Block height in pixels.
401    pub height: u32,
402    /// zlib stream; None when the block is not stored (all transparent).
403    pub zlib: Option<Vec<u8>>,
404}
405
406impl Block {
407    /// Length of the block's record in the payload.
408    pub fn record_size(&self) -> u32 {
409        match &self.zlib {
410            Some(zlib) => RECORD_OVERHEAD + zlib.len() as u32,
411            None => EMPTY_RECORD_SIZE,
412        }
413    }
414
415    /// The checksum CSP stores for this block.
416    pub fn checksum(&self) -> u32 {
417        match &self.zlib {
418            Some(zlib) => {
419                let mut stored = (zlib.len() as u32).to_le_bytes().to_vec();
420                stored.extend_from_slice(zlib);
421                adler32(&stored)
422            }
423            None => 0,
424        }
425    }
426
427    /// The decompressed block; None when it is not stored.
428    pub fn inflate(&self) -> Result<Option<Vec<u8>>> {
429        let Some(zlib) = &self.zlib else { return Ok(None) };
430        let mut raw = Vec::with_capacity(self.raw_size as usize);
431        ZlibDecoder::new(zlib.as_slice())
432            .take(self.raw_size as u64 + 1)
433            .read_to_end(&mut raw)
434            .map_err(|_| Error::format(format!("Block {} is not valid zlib data", self.index)))?;
435        if raw.len() != self.raw_size as usize {
436            bail!("Block {} has {} bytes, expected {}", self.index, raw.len(), self.raw_size);
437        }
438        Ok(Some(raw))
439    }
440}
441
442/// A tail table (`BlockStatus`, `BlockCheckSum`).
443#[derive(Debug, Clone, PartialEq, Eq)]
444pub struct Table {
445    /// Table name.
446    pub name: String,
447    /// First value after the name (12).
448    pub header: u32,
449    /// Bytes per entry (4).
450    pub entry_size: u32,
451    /// One value per block.
452    pub values: Vec<u32>,
453}
454
455/// The external payload of an offscreen.
456#[derive(Debug, Clone, PartialEq, Eq)]
457pub struct BlockData {
458    /// One record per block of the grid.
459    pub blocks: Vec<Block>,
460    /// The tables after the blocks.
461    pub tables: Vec<Table>,
462}
463
464impl BlockData {
465    /// Parses an `Offscreen.BlockData` external payload.
466    pub fn parse(payload: &[u8]) -> Result<Self> {
467        let mut reader = Reader::new(payload);
468        let length = reader.be_u64()?;
469        if length != (payload.len() - 8) as u64 {
470            bail!("Block data says {length} bytes follow, {} do", payload.len() - 8);
471        }
472        let mut blocks = Vec::new();
473        let mut tables = Vec::new();
474        while !reader.is_empty() {
475            let start = reader.position();
476            let peek = Reader::at(payload, start + 4);
477            if peek.peek_utf16_name().as_deref() == Some(BEGIN_NAME) {
478                let record_length = reader.be_u32()? as usize;
479                reader.utf16_name()?;
480                let index = reader.be_u32()?;
481                let raw_size = reader.be_u32()?;
482                let width = reader.be_u32()?;
483                let height = reader.be_u32()?;
484                let has_data = reader.be_u32()?;
485                let zlib = match has_data {
486                    0 => None,
487                    1 => {
488                        let stored = reader.be_u32()? as usize;
489                        let zlib_length = reader.le_u32()? as usize;
490                        if stored != zlib_length + 4 {
491                            bail!("Block {index}: stored length {stored} vs zlib length {zlib_length}");
492                        }
493                        Some(reader.bytes(zlib_length)?.to_vec())
494                    }
495                    other => bail!("Block {index}: unknown data flag {other}"),
496                };
497                if reader.utf16_name()? != END_NAME {
498                    bail!("Block {index}: record does not end with {END_NAME}");
499                }
500                if reader.position() - start != record_length {
501                    bail!("Block {index}: record is {} bytes, says {record_length}", reader.position() - start);
502                }
503                blocks.push(Block { index, raw_size, width, height, zlib });
504            } else {
505                let name = reader.utf16_name()?;
506                let header = reader.be_u32()?;
507                let count = reader.be_u32()? as usize;
508                let entry_size = reader.be_u32()?;
509                if entry_size != 4 || count > reader.remaining() / 4 {
510                    bail!("Table {name:?} has entries of {entry_size} bytes × {count}");
511                }
512                let mut values = Vec::with_capacity(count);
513                for _ in 0..count {
514                    values.push(reader.be_u32()?);
515                }
516                tables.push(Table { name, header, entry_size, values });
517            }
518        }
519        Ok(BlockData { blocks, tables })
520    }
521
522    /// The payload; unchanged data gives the same bytes.
523    pub fn to_bytes(&self) -> Vec<u8> {
524        let mut writer = Writer::new();
525        writer.be_u64(0);
526        for block in &self.blocks {
527            writer.be_u32(block.record_size()).utf16_name(BEGIN_NAME);
528            writer
529                .be_u32(block.index)
530                .be_u32(block.raw_size)
531                .be_u32(block.width)
532                .be_u32(block.height)
533                .be_u32(block.zlib.is_some() as u32);
534            if let Some(zlib) = &block.zlib {
535                writer.be_u32(zlib.len() as u32 + 4).le_u32(zlib.len() as u32).bytes(zlib);
536            }
537            writer.utf16_name(END_NAME);
538        }
539        for table in &self.tables {
540            writer.utf16_name(&table.name).be_u32(table.header).be_u32(table.values.len() as u32);
541            writer.be_u32(table.entry_size);
542            for value in &table.values {
543                writer.be_u32(*value);
544            }
545        }
546        let mut data = writer.into_inner();
547        let length = (data.len() - 8) as u64;
548        data[..8].copy_from_slice(&length.to_be_bytes());
549        data
550    }
551
552    /// The table of this name.
553    pub fn table(&self, name: &str) -> Option<&Table> {
554        self.tables.iter().find(|table| table.name == name)
555    }
556
557    /// Block indexes whose stored checksum does not match their data.
558    pub fn checksum_mismatches(&self) -> Vec<u32> {
559        let Some(table) = self.table(CHECKSUM_NAME) else { return Vec::new() };
560        self.blocks
561            .iter()
562            .zip(&table.values)
563            .filter(|(block, stored)| block.checksum() != **stored)
564            .map(|(block, _)| block.index)
565            .collect()
566    }
567
568    /// Build from decompressed blocks (None = not stored), compressing as CSP does.
569    pub fn from_raw_blocks(raw_blocks: &[Option<Vec<u8>>], raw_size: usize) -> Result<Self> {
570        let mut blocks = Vec::with_capacity(raw_blocks.len());
571        for (index, raw) in raw_blocks.iter().enumerate() {
572            if let Some(raw) = raw
573                && raw.len() != raw_size
574            {
575                bail!("Block {index} has {} bytes, expected {raw_size}", raw.len());
576            }
577            blocks.push(Block {
578                index: index as u32,
579                raw_size: raw_size as u32,
580                width: BLOCK_SIZE,
581                height: BLOCK_SIZE,
582                zlib: raw.as_ref().map(|raw| compress(raw)),
583            });
584        }
585        let checksums = blocks.iter().map(Block::checksum).collect();
586        Ok(BlockData {
587            tables: vec![
588                Table {
589                    name: STATUS_NAME.into(),
590                    header: TABLE_HEADER,
591                    entry_size: TABLE_ENTRY,
592                    values: vec![1; blocks.len()],
593                },
594                Table { name: CHECKSUM_NAME.into(), header: TABLE_HEADER, entry_size: TABLE_ENTRY, values: checksums },
595            ],
596            blocks,
597        })
598    }
599
600    /// Record length of every block, for the `BlockSize` section.
601    pub fn record_sizes(&self) -> Vec<u32> {
602        self.blocks.iter().map(Block::record_size).collect()
603    }
604}
605
606/// zlib with CSP's settings: the bundled C zlib at level 1, window bits 15,
607/// memory level 9 (gives CSP's bytes exactly).
608pub fn compress(raw: &[u8]) -> Vec<u8> {
609    crate::zlib::compress(raw).expect("compressing in memory")
610}
611
612/// A decoded offscreen.
613#[derive(Debug, Clone)]
614pub struct Offscreen {
615    /// The attribute.
616    pub attribute: Attribute,
617    /// Its `Parameter` section.
618    pub parameter: Parameter,
619    /// The block data, when the payload is stored.
620    pub data: Option<BlockData>,
621}
622
623impl Offscreen {
624    /// Parses an attribute and, if given, its block data payload, and checks they agree.
625    pub fn parse(attribute: &[u8], payload: Option<&[u8]>) -> Result<Self> {
626        let attribute = Attribute::parse(attribute)?;
627        let parameter = attribute.parameter()?;
628        let data = payload.map(BlockData::parse).transpose()?;
629        if let Some(data) = &data
630            && data.blocks.len() != parameter.block_count()
631        {
632            bail!("Block data has {} blocks, the grid {}", data.blocks.len(), parameter.block_count());
633        }
634        Ok(Offscreen { attribute, parameter, data })
635    }
636
637    /// The pixel layout.
638    pub fn kind(&self) -> PixelKind {
639        self.parameter.kind()
640    }
641
642    /// Visit every stored block decompressed: (block index, raw bytes).
643    pub fn for_each_block(&self, mut visit: impl FnMut(usize, &[u8]) -> Result<()>) -> Result<()> {
644        let Some(data) = &self.data else { return Ok(()) };
645        let expected = self.parameter.block_bytes();
646        for (index, block) in data.blocks.iter().enumerate() {
647            if let Some(raw) = block.inflate()? {
648                if raw.len() < expected {
649                    bail!("Block {index} has {} bytes, the layout needs {expected}", raw.len());
650                }
651                visit(index, &raw)?;
652            }
653        }
654        Ok(())
655    }
656
657    /// Colour, gray and monochrome offscreens as RGBA (not premultiplied).
658    /// Blocks that are not stored are transparent (or the init colour).
659    pub fn to_rgba(&self) -> Result<RgbaImage> {
660        let p = self.parameter;
661        let kind = p.kind();
662        if !matches!(kind, PixelKind::Color | PixelKind::Gray | PixelKind::Monochrome | PixelKind::Alpha) {
663            bail!("Unknown pixel layout (channel order {:#x})", p.channel_order);
664        }
665        let mut image = RgbaImage::new(p.width, p.height);
666        if let Some(init) = self.attribute.init_color()
667            && init.do_initialize
668        {
669            let alpha = (init.alpha & 0xff) as u8;
670            image.fill([255, 255, 255, alpha]);
671        }
672        self.for_each_block(|index, raw| {
673            let (left, top) = p.block_origin(index);
674            let width = BLOCK_SIZE.min(p.width - left) as usize;
675            let height = BLOCK_SIZE.min(p.height - top) as usize;
676            for y in 0..height {
677                let row = image.row_mut(top + y as u32);
678                for x in 0..width {
679                    let alpha = plane_value(raw, 0, p.alpha_row_bytes, x, y);
680                    let target = (left as usize + x) * 4;
681                    let rgb = match kind {
682                        PixelKind::Color => {
683                            let at = p.image_offset as usize + y * p.image_row_bytes as usize + x * 4;
684                            [raw[at + 2], raw[at + 1], raw[at]]
685                        }
686                        PixelKind::Gray | PixelKind::Monochrome => {
687                            let value = plane_value(raw, p.image_offset as usize, p.image_row_bytes, x, y);
688                            [value; 3]
689                        }
690                        _ => [0, 0, 0],
691                    };
692                    row[target..target + 3].copy_from_slice(&rgb);
693                    row[target + 3] = alpha;
694                }
695            }
696            Ok(())
697        })?;
698        Ok(image)
699    }
700
701    /// Boxes `(x0, y0, x1, y1)` in offscreen pixels of the blocks that store
702    /// data. Nothing is decompressed; edge blocks end at the offscreen's size.
703    pub fn stored_boxes(&self) -> Vec<(u32, u32, u32, u32)> {
704        let p = self.parameter;
705        let Some(data) = &self.data else { return Vec::new() };
706        data.blocks
707            .iter()
708            .enumerate()
709            .filter(|(_, block)| block.zlib.is_some())
710            .map(|(index, _)| {
711                let (left, top) = p.block_origin(index);
712                (left, top, (left + BLOCK_SIZE).min(p.width), (top + BLOCK_SIZE).min(p.height))
713            })
714            .collect()
715    }
716
717    /// [`Offscreen::to_rgba`] of the box `(x0, y0, x1, y1)` in offscreen
718    /// pixels, which may reach outside the offscreen (that part is
719    /// transparent). Only the blocks the box overlaps are decompressed, so
720    /// part of a large sheet can be read without decoding all of it.
721    pub fn to_rgba_region(&self, region: (i64, i64, i64, i64)) -> Result<RgbaImage> {
722        let p = self.parameter;
723        if p.kind() == PixelKind::Unknown {
724            bail!("Unknown pixel layout (channel order {:#x})", p.channel_order);
725        }
726        let (x0, y0, x1, y1) = region;
727        let mut image = RgbaImage::new((x1 - x0).max(0) as u32, (y1 - y0).max(0) as u32);
728        // The part of the box inside the offscreen.
729        let inside = (x0.max(0), y0.max(0), x1.min(p.width as i64), y1.min(p.height as i64));
730        if inside.0 >= inside.2 || inside.1 >= inside.3 {
731            return Ok(image);
732        }
733        // The init colour covers the offscreen only.
734        if let Some(init) = self.attribute.init_color()
735            && init.do_initialize
736        {
737            let mut fill = RgbaImage::new((inside.2 - inside.0) as u32, (inside.3 - inside.1) as u32);
738            fill.fill([255, 255, 255, (init.alpha & 0xff) as u8]);
739            image.paste(&fill, inside.0 - x0, inside.1 - y0);
740        }
741        let Some(data) = &self.data else { return Ok(image) };
742        let expected = p.block_bytes();
743        let size = BLOCK_SIZE as i64;
744        let columns = (inside.0 / size)..(inside.2 - 1) / size + 1;
745        for row in (inside.1 / size)..(inside.3 - 1) / size + 1 {
746            for column in columns.clone() {
747                let index = (row * p.blocks_x as i64 + column) as usize;
748                let Some(raw) = data.blocks.get(index).map(Block::inflate).transpose()?.flatten() else { continue };
749                if raw.len() < expected {
750                    bail!("Block {index} has {} bytes, the layout needs {expected}", raw.len());
751                }
752                let (left, top) = p.block_origin(index);
753                let tile = block_to_rgba(&p, BLOCK_SIZE.min(p.width - left), BLOCK_SIZE.min(p.height - top), &raw);
754                image.paste(&tile, left as i64 - x0, top as i64 - y0);
755            }
756        }
757        Ok(image)
758    }
759
760    /// [`Offscreen::to_rgba`] halved `halvings` times with
761    /// [`RgbaImage::halve`], one block at a time, so a large offscreen read
762    /// at a small scale never exists at full size. Blocks are 256 pixels and
763    /// aligned, so up to 8 halvings give exactly the result of halving the
764    /// whole image; more are done on the reduced image.
765    pub fn to_rgba_halved(&self, halvings: u32) -> Result<RgbaImage> {
766        self.halved(halvings, false)
767    }
768
769    /// The mask plane as opaque grey pixels (`v, v, v, 255`), halved like
770    /// [`Offscreen::to_rgba_halved`].
771    pub fn to_mask_rgba_halved(&self, halvings: u32) -> Result<RgbaImage> {
772        self.halved(halvings, true)
773    }
774
775    fn halved(&self, halvings: u32, mask: bool) -> Result<RgbaImage> {
776        let p = self.parameter;
777        let within = halvings.min(BLOCK_SIZE.trailing_zeros());
778        let reduce = |mut v: u32| {
779            for _ in 0..within {
780                v = (v / 2).max(1);
781            }
782            v
783        };
784        let mut image = RgbaImage::new(reduce(p.width), reduce(p.height));
785        let init = self.attribute.init_color().filter(|init| init.do_initialize).map(|init| (init.alpha & 0xff) as u8);
786        match (init, mask) {
787            (Some(alpha), false) => image.fill([255, 255, 255, alpha]),
788            (alpha, true) => image.fill([alpha.unwrap_or(0), alpha.unwrap_or(0), alpha.unwrap_or(0), 255]),
789            (None, false) => {}
790        }
791        self.for_each_block(|index, raw| {
792            let (left, top) = p.block_origin(index);
793            let (width, height) = (BLOCK_SIZE.min(p.width - left), BLOCK_SIZE.min(p.height - top));
794            let mut tile =
795                if mask { block_to_mask(&p, width, height, raw) } else { block_to_rgba(&p, width, height, raw) };
796            for _ in 0..within {
797                tile = tile.halve();
798            }
799            image.paste(&tile, (left >> within) as i64, (top >> within) as i64);
800            Ok(())
801        })?;
802        for _ in within..halvings {
803            image = image.halve();
804        }
805        Ok(image)
806    }
807
808    /// The first plane as an 8-bit image (masks: 0 = hidden, 255 = shown).
809    pub fn to_alpha(&self) -> Result<GrayImage> {
810        let p = self.parameter;
811        let mut image = GrayImage::new(p.width, p.height);
812        if let Some(init) = self.attribute.init_color()
813            && init.do_initialize
814        {
815            image.pixels.fill((init.alpha & 0xff) as u8);
816        }
817        self.for_each_block(|index, raw| {
818            let (left, top) = p.block_origin(index);
819            let width = BLOCK_SIZE.min(p.width - left) as usize;
820            let height = BLOCK_SIZE.min(p.height - top) as usize;
821            for y in 0..height {
822                let start = ((top as usize + y) * p.width as usize) + left as usize;
823                for x in 0..width {
824                    image.pixels[start + x] = plane_value(raw, 0, p.alpha_row_bytes, x, y);
825                }
826            }
827            Ok(())
828        })?;
829        Ok(image)
830    }
831
832    /// Re-encode the decoded pixels and compare with the stored blocks:
833    /// (blocks equal byte for byte, blocks with equal pixels, stored blocks).
834    pub fn reencode_check(&self) -> Result<(usize, usize, usize)> {
835        let Some(data) = &self.data else { return Ok((0, 0, 0)) };
836        let (mut same_bytes, mut same_pixels, mut stored) = (0, 0, 0);
837        for block in &data.blocks {
838            let Some(raw) = block.inflate()? else { continue };
839            stored += 1;
840            let zlib = compress(&raw);
841            if Some(&zlib) == block.zlib.as_ref() {
842                same_bytes += 1;
843            }
844            let again = Block { zlib: Some(zlib), ..block.clone() }.inflate()?;
845            if again.as_deref() == Some(raw.as_slice()) {
846                same_pixels += 1;
847            }
848        }
849        Ok((same_bytes, same_pixels, stored))
850    }
851}
852
853/// One decompressed block (`width × height` of it) as an image.
854fn block_to_rgba(p: &Parameter, width: u32, height: u32, raw: &[u8]) -> RgbaImage {
855    let kind = p.kind();
856    let mut image = RgbaImage::new(width, height);
857    for y in 0..height as usize {
858        let row = image.row_mut(y as u32);
859        for x in 0..width as usize {
860            let rgb = match kind {
861                PixelKind::Color => {
862                    let at = p.image_offset as usize + y * p.image_row_bytes as usize + x * 4;
863                    [raw[at + 2], raw[at + 1], raw[at]]
864                }
865                PixelKind::Gray | PixelKind::Monochrome => {
866                    [plane_value(raw, p.image_offset as usize, p.image_row_bytes, x, y); 3]
867                }
868                _ => [0, 0, 0],
869            };
870            row[x * 4..x * 4 + 3].copy_from_slice(&rgb);
871            row[x * 4 + 3] = plane_value(raw, 0, p.alpha_row_bytes, x, y);
872        }
873    }
874    image
875}
876
877/// The first plane of a block as opaque grey pixels.
878fn block_to_mask(p: &Parameter, width: u32, height: u32, raw: &[u8]) -> RgbaImage {
879    let mut image = RgbaImage::new(width, height);
880    for y in 0..height as usize {
881        let row = image.row_mut(y as u32);
882        for x in 0..width as usize {
883            let v = plane_value(raw, 0, p.alpha_row_bytes, x, y);
884            row[x * 4..x * 4 + 4].copy_from_slice(&[v, v, v, 255]);
885        }
886    }
887    image
888}
889
890/// Value of pixel (x, y) of a plane starting at `offset`: 8-bit planes
891/// directly, 1-bit planes (row bytes = 32) as 0 or 255, most significant bit first.
892fn plane_value(raw: &[u8], offset: usize, row_bytes: u32, x: usize, y: usize) -> u8 {
893    if row_bytes == BLOCK_SIZE / 8 {
894        let byte = raw[offset + y * row_bytes as usize + x / 8];
895        if byte & (0x80 >> (x % 8)) != 0 { 255 } else { 0 }
896    } else {
897        raw[offset + y * row_bytes as usize + x]
898    }
899}
900
901/// Encode an image into blocks for `parameter` (colour, gray or alpha).
902/// Blocks whose pixels are all transparent are not stored, as CSP does.
903pub fn encode(parameter: &Parameter, rgba: Option<&RgbaImage>, alpha: Option<&GrayImage>) -> Result<BlockData> {
904    let kind = parameter.kind();
905    let raw_size = parameter.block_bytes();
906    let mut raw_blocks = Vec::with_capacity(parameter.block_count());
907    for index in 0..parameter.block_count() {
908        let (left, top) = parameter.block_origin(index);
909        let mut raw = vec![0u8; raw_size];
910        let mut visible = false;
911        for y in 0..BLOCK_SIZE {
912            let source_y = top + y;
913            if source_y >= parameter.height {
914                break;
915            }
916            for x in 0..BLOCK_SIZE {
917                let source_x = left + x;
918                if source_x >= parameter.width {
919                    break;
920                }
921                let at = (y * BLOCK_SIZE + x) as usize;
922                match (kind, rgba, alpha) {
923                    (PixelKind::Color, Some(image), _) => {
924                        let pixel = image.pixel(source_x, source_y);
925                        raw[at] = pixel[3];
926                        let base = parameter.image_offset as usize + at * 4;
927                        raw[base..base + 4].copy_from_slice(&[pixel[2], pixel[1], pixel[0], 0]);
928                        visible |= pixel[3] != 0;
929                    }
930                    (PixelKind::Gray, Some(image), _) => {
931                        let pixel = image.pixel(source_x, source_y);
932                        raw[at] = pixel[3];
933                        raw[parameter.image_offset as usize + at] = crate::image::luma(pixel[0], pixel[1], pixel[2]);
934                        visible |= pixel[3] != 0;
935                    }
936                    (PixelKind::Alpha, _, Some(image)) => {
937                        let value = image.pixels[(source_y * image.width + source_x) as usize];
938                        raw[at] = value;
939                        visible |= value != 0;
940                    }
941                    _ => bail!("Encoding {kind:?} offscreens from this input is not supported"),
942                }
943            }
944        }
945        raw_blocks.push(visible.then_some(raw));
946    }
947    BlockData::from_raw_blocks(&raw_blocks, raw_size)
948}
949
950#[cfg(test)]
951mod tests {
952    use super::*;
953
954    #[test]
955    fn colour_offscreens_round_trip() {
956        let mut image = RgbaImage::new(300, 10);
957        image.set_pixel(299, 9, [10, 20, 30, 40]);
958        image.set_pixel(0, 0, [200, 100, 50, 255]);
959        let parameter = Parameter::new(PixelKind::Color, 300, 10);
960        let data = encode(&parameter, Some(&image), None).unwrap();
961        assert_eq!(data.blocks.len(), 2);
962        assert!(data.checksum_mismatches().is_empty());
963        let bytes = data.to_bytes();
964        assert_eq!(BlockData::parse(&bytes).unwrap(), data);
965        let mut attribute = Attribute::new(&parameter, None);
966        attribute.set_block_sizes(&data.record_sizes());
967        let attribute_bytes = attribute.to_bytes();
968        assert_eq!(Attribute::parse(&attribute_bytes).unwrap(), attribute);
969        let offscreen = Offscreen::parse(&attribute_bytes, Some(&bytes)).unwrap();
970        assert_eq!(offscreen.kind(), PixelKind::Color);
971        let decoded = offscreen.to_rgba().unwrap();
972        assert_eq!(decoded.pixel(299, 9), [10, 20, 30, 40]);
973        assert_eq!(decoded.pixel(0, 0), [200, 100, 50, 255]);
974        assert_eq!(decoded.pixel(1, 1)[3], 0);
975        assert_eq!(offscreen.reencode_check().unwrap(), (2, 2, 2));
976    }
977
978    #[test]
979    fn regions_decode_only_what_they_cover() {
980        // 600×300: blocks of 256 px, the last column and row cut short.
981        let mut image = RgbaImage::new(600, 300);
982        for (x, y) in [(0, 0), (255, 255), (256, 0), (599, 299), (300, 280)] {
983            image.set_pixel(x, y, [x as u8, y as u8, 7, 200]);
984        }
985        let parameter = Parameter::new(PixelKind::Color, 600, 300);
986        let data = encode(&parameter, Some(&image), None).unwrap();
987        let mut attribute = Attribute::new(&parameter, None);
988        attribute.set_block_sizes(&data.record_sizes());
989        let offscreen = Offscreen::parse(&attribute.to_bytes(), Some(&data.to_bytes())).unwrap();
990        // Blocks 0, 1, 4 and 5 store pixels; 2 (512..600, 0..256) and 3 do not.
991        assert_eq!(
992            offscreen.stored_boxes(),
993            [(0, 0, 256, 256), (256, 0, 512, 256), (256, 256, 512, 300), (512, 256, 600, 300)]
994        );
995        let whole = offscreen.to_rgba().unwrap();
996        for region in
997            [(0, 0, 600, 300), (250, 250, 310, 290), (-10, -20, 5, 5), (590, 290, 700, 400), (700, 0, 710, 10)]
998        {
999            let part = offscreen.to_rgba_region(region).unwrap();
1000            assert_eq!((part.width as i64, part.height as i64), (region.2 - region.0, region.3 - region.1));
1001            for y in 0..part.height as i64 {
1002                for x in 0..part.width as i64 {
1003                    let (cx, cy) = (x + region.0, y + region.1);
1004                    let expected = if (0..600).contains(&cx) && (0..300).contains(&cy) {
1005                        whole.pixel(cx as u32, cy as u32)
1006                    } else {
1007                        [0; 4]
1008                    };
1009                    assert_eq!(part.pixel(x as u32, y as u32), expected, "{region:?} at {cx},{cy}");
1010                }
1011            }
1012        }
1013    }
1014
1015    #[test]
1016    fn regions_keep_the_init_colour_inside_only() {
1017        let parameter = Parameter::new(PixelKind::Color, 100, 100);
1018        let init = InitColor { do_initialize: true, alpha: 0xffff_ffff, image_values: vec![0xffff_ffff; 4] };
1019        let attribute = Attribute::new(&parameter, Some(&init));
1020        let offscreen = Offscreen::parse(&attribute.to_bytes(), None).unwrap();
1021        let part = offscreen.to_rgba_region((90, 90, 110, 110)).unwrap();
1022        assert_eq!(part.pixel(0, 0), [255, 255, 255, 255]);
1023        assert_eq!(part.pixel(9, 9), [255, 255, 255, 255]);
1024        assert_eq!(part.pixel(10, 10), [0; 4]);
1025        assert!(offscreen.stored_boxes().is_empty());
1026    }
1027
1028    #[test]
1029    fn masks_round_trip() {
1030        let mut mask = GrayImage::new(256, 256);
1031        mask.pixels[5] = 128;
1032        let parameter = Parameter::new(PixelKind::Alpha, 256, 256);
1033        let data = encode(&parameter, None, Some(&mask)).unwrap();
1034        let offscreen = Offscreen { attribute: Attribute::new(&parameter, None), parameter, data: Some(data) };
1035        assert_eq!(offscreen.to_alpha().unwrap().pixels, mask.pixels);
1036    }
1037
1038    #[test]
1039    fn halving_by_blocks_equals_halving_the_image() {
1040        let (width, height) = (601, 300);
1041        let mut image = RgbaImage::new(width, height);
1042        for y in 0..height {
1043            for x in 0..width {
1044                let v = (x * 7 + y * 13) as u8;
1045                let alpha = if (x / 37 + y / 23) % 3 == 0 { 0 } else { v / 2 + 100 };
1046                image.set_pixel(x, y, [v, 255 - v, (x ^ y) as u8, alpha]);
1047            }
1048        }
1049        let parameter = Parameter::new(PixelKind::Color, width, height);
1050        let data = encode(&parameter, Some(&image), None).unwrap();
1051        let mut attribute = Attribute::new(&parameter, None);
1052        attribute.set_block_sizes(&data.record_sizes());
1053        let offscreen = Offscreen::parse(&attribute.to_bytes(), Some(&data.to_bytes())).unwrap();
1054        let mut expected = offscreen.to_rgba().unwrap();
1055        for halvings in 1..=10 {
1056            expected = expected.halve();
1057            assert_eq!(offscreen.to_rgba_halved(halvings).unwrap(), expected, "{halvings}");
1058        }
1059    }
1060
1061    #[test]
1062    fn broken_payloads_are_errors() {
1063        let parameter = Parameter::new(PixelKind::Color, 16, 16);
1064        let mut image = RgbaImage::new(16, 16);
1065        image.fill([1, 2, 3, 4]);
1066        let bytes = encode(&parameter, Some(&image), None).unwrap().to_bytes();
1067        for cut in [4, 20, bytes.len() / 2, bytes.len() - 1] {
1068            assert!(BlockData::parse(&bytes[..cut]).is_err(), "cut {cut}");
1069        }
1070    }
1071}