Skip to main content

caj2pdf_core/jbig2/
text.rs

1// SPDX-License-Identifier: MIT
2
3//! Bounded T.88 text-region segment data headers (§§7.4.1, 7.4.3.1).
4//!
5//! This parser validates segment framing, the region information field, the
6//! Figure 36 flags, optional Huffman and refinement AT fields, and
7//! `SBNUMINSTANCES`, then exposes the remaining body as an exact source range.
8//! It never reads the body, decodes symbol instances, or composes pixels.
9
10use super::{FieldCursor, FieldFault, SegmentHeader, SegmentSpan, unsupported};
11use crate::{Cancellation, Context, Error, Limits, RangedSource, Result};
12
13/// Immediate text region segment type (T.88 §7.3).
14pub const IMMEDIATE_TEXT_REGION: u8 = 6;
15const INTERMEDIATE_TEXT_REGION: u8 = 4;
16const IMMEDIATE_LOSSLESS_TEXT_REGION: u8 = 7;
17const REGION_INFO_BYTES: usize = 17;
18const FLAGS_BYTES: usize = 2;
19/// The fixed prefix read before optional fields: region information and flags.
20const PREFIX_BYTES: usize = REGION_INFO_BYTES + FLAGS_BYTES;
21
22/// External combination operator of a region with its page (§7.4.1.5).
23#[derive(Clone, Copy, Debug, Eq, PartialEq)]
24pub enum RegionCombination {
25    Or,
26    And,
27    Xor,
28    Xnor,
29    Replace,
30}
31
32/// Region segment information field (§7.4.1).
33#[derive(Clone, Copy, Debug, Eq, PartialEq)]
34pub struct RegionInfo {
35    pub width: u32,
36    pub height: u32,
37    pub x: u32,
38    pub y: u32,
39    pub combination: RegionCombination,
40}
41
42/// Symbol instance reference corner (`REFCORNER`).
43#[derive(Clone, Copy, Debug, Eq, PartialEq)]
44pub enum ReferenceCorner {
45    BottomLeft,
46    TopLeft,
47    BottomRight,
48    TopRight,
49}
50
51/// Symbol combination operator inside the region (`SBCOMBOP`).
52#[derive(Clone, Copy, Debug, Eq, PartialEq)]
53pub enum SymbolCombination {
54    Or,
55    And,
56    Xor,
57    Xnor,
58}
59
60/// Decoded Figure 36 text region flags. `raw` keeps the encoded value.
61#[derive(Clone, Copy, Debug, Eq, PartialEq)]
62pub struct TextRegionFlags {
63    pub raw: u16,
64    /// `SBHUFF`: Huffman rather than arithmetic coding.
65    pub huffman: bool,
66    /// `SBREFINE`: symbol instances may be refined.
67    pub refine: bool,
68    /// `LOGSBSTRIPS`; `SBSTRIPS` is `1 << log_strips`.
69    pub log_strips: u8,
70    pub reference_corner: ReferenceCorner,
71    pub transposed: bool,
72    pub combination: SymbolCombination,
73    /// `SBDEFPIXEL`: initial value of every region pixel.
74    pub default_pixel: bool,
75    /// Signed five-bit `SBDSOFFSET`, in `-16..=15`.
76    pub ds_offset: i8,
77    /// `SBRTEMPLATE`; zero whenever `refine` is false.
78    pub refinement_template: u8,
79}
80
81impl TextRegionFlags {
82    /// `SBSTRIPS`: 1, 2, 4, or 8.
83    pub fn strips(self) -> u8 {
84        1 << self.log_strips
85    }
86}
87
88/// Caller-selected validation for a documented HN/C8 header defect.
89/// The default parser always uses `Strict`.
90#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
91pub enum TextHeaderPolicy {
92    #[default]
93    Strict,
94    /// Accept an unused `SBRTEMPLATE` bit in the measured arithmetic,
95    /// eight-strip, bottom-left, non-transposed OR profile with default
96    /// pixel zero and `SBREFINE=0`. Only `SBDSOFFSET` varies; it retains
97    /// its standard signed five-bit meaning. Other validation stays strict.
98    HnC8UnusedRefinementTemplate,
99}
100
101/// A nonconforming header accepted under an explicit caller policy.
102#[derive(Clone, Copy, Debug, Eq, PartialEq)]
103pub enum TextHeaderAnomaly {
104    UnusedRefinementTemplate,
105}
106
107/// Parsed text region data header. `body` is an exact absolute source range.
108#[derive(Clone, Copy, Debug, Eq, PartialEq)]
109pub struct TextRegionHeader {
110    /// The immediate text segment and its single dictionary reference.
111    pub segment: u32,
112    pub page_association: u32,
113    pub dictionary_segment: u32,
114    pub region: RegionInfo,
115    pub flags: TextRegionFlags,
116    /// `None` for strictly valid headers; retains an accepted deviation.
117    pub anomaly: Option<TextHeaderAnomaly>,
118    /// Figure 37 Huffman table selections, present only when `SBHUFF` is 1.
119    pub huffman_flags: Option<u16>,
120    /// `SBRATX1, SBRATY1, SBRATX2, SBRATY2`, present only when `SBREFINE` is 1
121    /// and `SBRTEMPLATE` is 0.
122    pub refinement_at: Option<[(i8, i8); 2]>,
123    /// `SBNUMINSTANCES`.
124    pub instances: u32,
125    pub header_bytes: u64,
126    pub body: SegmentSpan,
127}
128
129impl TextRegionHeader {
130    /// The first legal configuration outside the arithmetic, refinement
131    /// template 1 (or no refinement) profile that later decoders target.
132    pub fn unsupported_feature(&self) -> Option<(&'static str, u64)> {
133        if let Some(flags) = self.huffman_flags {
134            Some(("Huffman text region", u64::from(flags)))
135        } else if self.refinement_at.is_some() {
136            Some(("refinement template 0 with adaptive pixels", 0))
137        } else {
138            None
139        }
140    }
141}
142
143struct Cursor<'a, S, C> {
144    source: &'a mut S,
145    cancellation: &'a C,
146    segment: u32,
147    fields: FieldCursor,
148}
149
150impl<S: RangedSource, C: Cancellation> Cursor<'_, S, C> {
151    fn error_at(&self, offset: u64, error: Error) -> Error {
152        error.or_at(
153            offset,
154            Context::Jbig2 {
155                segment: Some(self.segment),
156            },
157        )
158    }
159
160    fn error(&self, error: Error) -> Error {
161        self.error_at(self.fields.at, error)
162    }
163
164    /// Read exactly `N` header bytes with bounded requests.
165    fn read<const N: usize>(&mut self, field: &'static str) -> Result<[u8; N]> {
166        let mut bytes = [0u8; N];
167        let result = self.fields.fill(self.source, self.cancellation, &mut bytes);
168        result.map_err(|fault| self.fault(fault, field))?;
169        Ok(bytes)
170    }
171
172    /// A field whose end overflows is truncated, as one past the end is.
173    fn fault(&self, fault: FieldFault, field: &'static str) -> Error {
174        let at = self.fields.at;
175        self.error(match fault {
176            FieldFault::Overflow { expected } => {
177                Error::truncated(at, expected, self.fields.end.saturating_sub(at)).because(field)
178            }
179            fault => fault.error(at, field),
180        })
181    }
182}
183
184fn be32(bytes: &[u8]) -> u32 {
185    u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]])
186}
187
188/// Parse one text region segment data header within `header.data`.
189///
190/// `dictionary` is the validated header of the single symbol dictionary this
191/// region refers to. Framing, reference, page, and span checks complete before
192/// any source read; the returned `body` range is never read or allocated.
193/// The region's pixels and its `SBNUMINSTANCES` are each bounded by
194/// `Limits::max_image_pixels`.
195pub fn read_text_region_header<S: RangedSource, C: Cancellation>(
196    source: &mut S,
197    header: &SegmentHeader,
198    dictionary: &SegmentHeader,
199    limits: &Limits,
200    cancellation: &C,
201) -> Result<TextRegionHeader> {
202    read_text_region_header_with_policy(
203        source,
204        header,
205        dictionary,
206        limits,
207        cancellation,
208        TextHeaderPolicy::Strict,
209    )
210}
211
212/// Parse a text-region header with an explicit interoperability policy.
213/// `HnC8UnusedRefinementTemplate` admits its documented profile; it does not
214/// normalize the returned flags or skip any framing, size, or body checks.
215pub fn read_text_region_header_with_policy<S: RangedSource, C: Cancellation>(
216    source: &mut S,
217    header: &SegmentHeader,
218    dictionary: &SegmentHeader,
219    limits: &Limits,
220    cancellation: &C,
221    policy: TextHeaderPolicy,
222) -> Result<TextRegionHeader> {
223    let fail = |error: Error| {
224        error.or_at(
225            header.data.offset,
226            Context::Jbig2 {
227                segment: Some(header.number),
228            },
229        )
230    };
231    if cancellation.is_cancelled() {
232        return Err(fail(Error::cancelled()));
233    }
234    match header.segment_type {
235        IMMEDIATE_TEXT_REGION => {}
236        INTERMEDIATE_TEXT_REGION | IMMEDIATE_LOSSLESS_TEXT_REGION => {
237            return Err(fail(unsupported("text region segment type")));
238        }
239        _ => {
240            return Err(fail(Error::invalid("segment type is not a text region")));
241        }
242    }
243    if header.page_association == 0 {
244        return Err(fail(Error::invalid("immediate region without a page")));
245    }
246    if header.referred_to.len() != 1 {
247        return Err(fail(unsupported("text region reference count")));
248    }
249    if header.referred_to[0] != dictionary.number {
250        return Err(fail(Error::invalid(
251            "reference differs from the supplied dictionary",
252        )));
253    }
254    if dictionary.segment_type != 0 {
255        return Err(fail(Error::invalid(
256            "referred segment is not a symbol dictionary",
257        )));
258    }
259    if dictionary.number >= header.number {
260        return Err(fail(Error::invalid("dictionary does not precede region")));
261    }
262    if dictionary.page_association != 0 && dictionary.page_association != header.page_association {
263        return Err(fail(Error::invalid("dictionary page association differs")));
264    }
265    if header.data.length > limits.max_input_bytes {
266        return Err(fail(Error::limit(
267            "text region data bytes",
268            limits.max_input_bytes,
269            header.data.length,
270        )));
271    }
272    if header.header_length > header.data.offset {
273        return Err(fail(Error::invalid("segment header start underflow")));
274    }
275    let end = header
276        .data
277        .offset
278        .checked_add(header.data.length)
279        .ok_or_else(|| fail(Error::invalid("data end overflow")))?;
280    if end > source.size() {
281        return Err(fail(Error::invalid("data outside source")));
282    }
283
284    let mut cursor = Cursor {
285        source,
286        cancellation,
287        segment: header.number,
288        fields: FieldCursor {
289            start: header.data.offset,
290            at: header.data.offset,
291            end,
292            fetched: 0,
293            request_bytes: limits.io_chunk_bytes.max(1),
294        },
295    };
296    let start = header.data.offset;
297    let prefix: [u8; PREFIX_BYTES] = cursor.read("text region header")?;
298    let region = parse_region(&prefix, &cursor, limits)?;
299    let (flags, anomaly) = parse_flags(
300        u16::from_be_bytes([prefix[REGION_INFO_BYTES], prefix[REGION_INFO_BYTES + 1]]),
301        policy,
302    )
303    .map_err(|error| cursor.error_at(start + REGION_INFO_BYTES as u64, error))?;
304
305    let huffman_flags = if flags.huffman {
306        let offset = cursor.fields.at;
307        let raw = u16::from_be_bytes(cursor.read("text region Huffman flags")?);
308        check_huffman_flags(raw, flags.refine).map_err(|error| cursor.error_at(offset, error))?;
309        Some(raw)
310    } else {
311        None
312    };
313    let refinement_at = if flags.refine && flags.refinement_template == 0 {
314        let [x1, y1, x2, y2] = cursor.read("text region refinement AT")?;
315        Some([(x1 as i8, y1 as i8), (x2 as i8, y2 as i8)])
316    } else {
317        None
318    };
319    let instances_offset = cursor.fields.at;
320    let instances = u32::from_be_bytes(cursor.read("SBNUMINSTANCES")?);
321    if u64::from(instances) > limits.max_image_pixels {
322        return Err(cursor.error_at(
323            instances_offset,
324            Error::limit(
325                "text region symbol instances",
326                limits.max_image_pixels,
327                u64::from(instances),
328            ),
329        ));
330    }
331    let body_length = end - cursor.fields.at;
332    if !flags.huffman && body_length < 2 {
333        return Err(cursor.error(
334            Error::truncated(cursor.fields.at, 2, body_length).because("MQ body terminal pair"),
335        ));
336    }
337    Ok(TextRegionHeader {
338        segment: header.number,
339        page_association: header.page_association,
340        dictionary_segment: header.referred_to[0],
341        region,
342        flags,
343        anomaly,
344        huffman_flags,
345        refinement_at,
346        instances,
347        header_bytes: cursor.fields.at - start,
348        body: SegmentSpan {
349            offset: cursor.fields.at,
350            length: body_length,
351        },
352    })
353}
354
355fn parse_region<S: RangedSource, C: Cancellation>(
356    bytes: &[u8; PREFIX_BYTES],
357    cursor: &Cursor<'_, S, C>,
358    limits: &Limits,
359) -> Result<RegionInfo> {
360    let start = cursor.fields.start;
361    let (width, height) = (be32(&bytes[0..4]), be32(&bytes[4..8]));
362    let (x, y) = (be32(&bytes[8..12]), be32(&bytes[12..16]));
363    let flags_offset = start + 16;
364    if bytes[16] & 0xf8 != 0 {
365        return Err(cursor.error_at(
366            flags_offset,
367            Error::invalid("reserved region segment flags"),
368        ));
369    }
370    let combination = match bytes[16] & 7 {
371        0 => RegionCombination::Or,
372        1 => RegionCombination::And,
373        2 => RegionCombination::Xor,
374        3 => RegionCombination::Xnor,
375        4 => RegionCombination::Replace,
376        _ => {
377            return Err(
378                cursor.error_at(flags_offset, Error::invalid("region combination operator"))
379            );
380        }
381    };
382    for (attempted, offset) in [(width, start), (height, start + 4)] {
383        if attempted == 0 {
384            return Err(cursor.error_at(offset, unsupported("empty text region")));
385        }
386    }
387    let pixels = u64::from(width) * u64::from(height);
388    if pixels > limits.max_image_pixels {
389        return Err(cursor.error_at(
390            start,
391            Error::limit("text region pixels", limits.max_image_pixels, pixels),
392        ));
393    }
394    Ok(RegionInfo {
395        width,
396        height,
397        x,
398        y,
399        combination,
400    })
401}
402
403fn parse_flags(
404    raw: u16,
405    policy: TextHeaderPolicy,
406) -> Result<(TextRegionFlags, Option<TextHeaderAnomaly>)> {
407    let refine = raw & 2 != 0;
408    let refinement_template = (raw >> 15) as u8;
409    let anomaly = if !refine && refinement_template != 0 {
410        // Bits 10..14 are the ordinary signed SBDSOFFSET, not part of the
411        // unused-template anomaly. Preserve them for instance placement.
412        if policy == TextHeaderPolicy::HnC8UnusedRefinementTemplate && raw & !0x7c00 == 0x800c {
413            Some(TextHeaderAnomaly::UnusedRefinementTemplate)
414        } else {
415            return Err(Error::invalid("SBRTEMPLATE without SBREFINE"));
416        }
417    } else {
418        None
419    };
420    // Sign-extend the five-bit SBDSOFFSET in bits 10-14.
421    let ds_offset = (((raw >> 10) & 0x1f) as i8) << 3 >> 3;
422    Ok((
423        TextRegionFlags {
424            raw,
425            huffman: raw & 1 != 0,
426            refine,
427            log_strips: ((raw >> 2) & 3) as u8,
428            reference_corner: match (raw >> 4) & 3 {
429                0 => ReferenceCorner::BottomLeft,
430                1 => ReferenceCorner::TopLeft,
431                2 => ReferenceCorner::BottomRight,
432                _ => ReferenceCorner::TopRight,
433            },
434            transposed: raw & 0x40 != 0,
435            combination: match (raw >> 7) & 3 {
436                0 => SymbolCombination::Or,
437                1 => SymbolCombination::And,
438                2 => SymbolCombination::Xor,
439                _ => SymbolCombination::Xnor,
440            },
441            default_pixel: raw & 0x200 != 0,
442            ds_offset,
443            refinement_template,
444        },
445        anomaly,
446    ))
447}
448
449/// Figure 37 constraints: bit 15 reserved, selector value 2 forbidden for
450/// `SBHUFFFS` and the refinement tables, which must be zero without refinement.
451fn check_huffman_flags(raw: u16, refine: bool) -> Result<()> {
452    let malformed = |field| Err(Error::invalid(field));
453    if raw & 0x8000 != 0 {
454        return malformed("reserved Huffman flag");
455    }
456    if [0, 6, 8, 10, 12]
457        .iter()
458        .any(|shift| (raw >> shift) & 3 == 2)
459    {
460        return malformed("reserved Huffman table selector");
461    }
462    if !refine && raw & 0x7fc0 != 0 {
463        return malformed("refinement Huffman tables without SBREFINE");
464    }
465    Ok(())
466}