Skip to main content

jbig2enc_rust/
lib.rs

1//! JBIG2 Encoder in Rust
2//!
3//! This crate provides functionality to encode binary images into the JBIG2 format.
4//! It supports both standalone JBIG2 files and PDF-embedded fragments with proper
5//! global dictionary handling.
6
7#![allow(missing_docs)]
8#![allow(dead_code)]
9#![allow(unused_variables)]
10#![allow(unused_mut)]
11
12// Re-export commonly used types
13pub use ndarray::Array2;
14
15/// Errors that can occur during JBIG2 encoding
16#[derive(Debug)]
17pub enum Jbig2Error {
18    /// Input buffer size mismatch
19    BufferSizeMismatch {
20        expected: usize,
21        actual: usize,
22        width: u32,
23        height: u32,
24        ratio: f64,
25    },
26
27    /// Buffer too small for operation
28    BufferTooSmall { expected: usize, actual: usize },
29
30    /// Array shape error during conversion
31    ArrayShapeError { source: ndarray::ShapeError },
32
33    /// Encoding failed
34    EncodingFailed { message: String },
35
36    /// Dictionary creation failed  
37    DictionaryFailed { message: String },
38
39    /// Packed binary data detected when unpacked expected
40    PackedDataDetected,
41
42    /// Stream count mismatch
43    StreamCountMismatch { expected: usize, actual: usize },
44
45    /// Segment writing failed
46    SegmentWriteFailed {
47        segment_type: String,
48        message: String,
49    },
50}
51
52impl std::fmt::Display for Jbig2Error {
53    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
54        match self {
55            Jbig2Error::BufferSizeMismatch {
56                expected,
57                actual,
58                width,
59                height,
60                ratio,
61            } => {
62                write!(
63                    f,
64                    "Input buffer size mismatch: expected {}, got {} for {}x{} image (ratio: {:.3})",
65                    expected, actual, width, height, ratio
66                )
67            }
68            Jbig2Error::BufferTooSmall { expected, actual } => {
69                write!(f, "Buffer too small: expected {}, got {}", expected, actual)
70            }
71            Jbig2Error::ArrayShapeError { source } => {
72                write!(f, "Array shape error: {}", source)
73            }
74            Jbig2Error::EncodingFailed { message } => {
75                write!(f, "Encoding failed: {}", message)
76            }
77            Jbig2Error::DictionaryFailed { message } => {
78                write!(f, "Dictionary creation failed: {}", message)
79            }
80            Jbig2Error::PackedDataDetected => {
81                write!(
82                    f,
83                    "Input appears to be packed binary data (1 bit per pixel), but JBIG2 encoder expects unpacked data (1 byte per pixel)"
84                )
85            }
86            Jbig2Error::StreamCountMismatch { expected, actual } => {
87                write!(
88                    f,
89                    "Expected {} stream(s) for single image encoding, got {}",
90                    expected, actual
91                )
92            }
93            Jbig2Error::SegmentWriteFailed {
94                segment_type,
95                message,
96            } => {
97                write!(f, "Failed to write {} segment: {}", segment_type, message)
98            }
99        }
100    }
101}
102
103impl std::error::Error for Jbig2Error {
104    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
105        match self {
106            Jbig2Error::ArrayShapeError { source } => Some(source),
107            _ => None,
108        }
109    }
110}
111
112impl From<ndarray::ShapeError> for Jbig2Error {
113    fn from(source: ndarray::ShapeError) -> Self {
114        Jbig2Error::ArrayShapeError { source }
115    }
116}
117
118// Module declarations
119pub mod jbig2;
120pub mod jbig2arith;
121#[cfg(feature = "symboldict")]
122pub mod jbig2cc;
123pub mod jbig2classify;
124pub mod jbig2comparator;
125pub mod jbig2context;
126pub mod jbig2cost;
127pub mod jbig2enc;
128pub mod jbig2halftone;
129pub mod jbig2shared;
130pub(crate) mod jbig2simd;
131pub mod jbig2structs;
132pub mod jbig2sym;
133pub mod jbig2unify;
134
135// Re-export the main encode functions and config
136pub use crate::jbig2arith::Jbig2ArithCoder;
137#[cfg(feature = "symboldict")]
138pub use jbig2cc::{BBox, CC, CCImage, Run, analyze_page, extract_symbols_for_jbig2};
139pub use jbig2enc::{PdfSplitOutput, encode_document};
140pub use jbig2structs::Jbig2Config;
141
142use jbig2enc::Jbig2Encoder;
143use jbig2sym::binary_pixels_to_bitimage;
144use std::env;
145
146/// Result of JBIG2 encoding with separate global and page data for PDF embedding
147#[derive(Debug, Clone)]
148pub struct Jbig2EncodeResult {
149    /// Global dictionary data (if any) - should be stored as separate PDF object
150    pub global_data: Option<Vec<u8>>,
151    /// Page-specific data - the actual image stream
152    pub page_data: Vec<u8>,
153}
154
155/// Context for JBIG2 encoding operations
156#[derive(Debug, Clone)]
157pub struct Jbig2Context {
158    /// The underlying configuration
159    config: Jbig2Config,
160    pdf_mode: bool,
161}
162
163impl Default for Jbig2Context {
164    fn default() -> Self {
165        Self {
166            config: Jbig2Config::default(),
167            pdf_mode: false,
168        }
169    }
170}
171
172impl Jbig2Context {
173    /// Create a new context with default values
174    pub fn new() -> Self {
175        Self::default()
176    }
177
178    /// Create a new context with specified PDF mode
179    pub fn with_pdf_mode(pdf_mode: bool) -> Self {
180        Self {
181            config: Jbig2Config::default(),
182            pdf_mode,
183        }
184    }
185
186    /// Create a new context with custom configuration
187    pub fn with_config(config: Jbig2Config, pdf_mode: bool) -> Self {
188        Self { config, pdf_mode }
189    }
190
191    /// Create a new context with lossless configuration (no symbol dictionaries)
192    /// This is useful for PDF embedding when symbol dictionaries cause display issues
193    pub fn with_lossless_config(pdf_mode: bool) -> Self {
194        Self {
195            config: Jbig2Config::lossless(),
196            pdf_mode,
197        }
198    }
199
200    /// Get the PDF mode setting
201    pub fn get_pdf_mode(&self) -> bool {
202        self.pdf_mode
203    }
204
205    /// Get the symbol mode setting
206    pub fn get_symbol_mode(&self) -> bool {
207        self.config.symbol_mode
208    }
209
210    /// Get the DPI setting
211    pub fn get_dpi(&self) -> u32 {
212        if self.config.generic.dpi == 0 {
213            300
214        } else {
215            self.config.generic.dpi
216        }
217    }
218}
219
220/// Main encoding function that handles both standalone and PDF fragment modes
221///
222/// This function encodes a single binary image into JBIG2 format. When PDF mode is enabled,
223/// it returns separate global dictionary and page data that can be properly embedded in PDF.
224///
225/// # Arguments
226/// * `input` - Binary image data (0/1 values)
227/// * `width` - Image width in pixels
228/// * `height` - Image height in pixels
229/// * `pdf_mode` - Whether to create PDF fragments (true) or standalone file (false)
230///
231/// # Returns
232/// A `Jbig2EncodeResult` containing separate global and page data for PDF embedding,
233/// or combined data for standalone files.
234pub fn encode_single_image(
235    input: &[u8],
236    width: u32,
237    height: u32,
238    pdf_mode: bool,
239) -> Result<Jbig2EncodeResult, Jbig2Error> {
240    let bitimage = validate_and_build_bitimage(input, width, height)?;
241    encode_single_bitimage(bitimage, Jbig2Context::with_pdf_mode(pdf_mode))
242}
243
244/// Encodes a single binary image into JBIG2 format with custom configuration.
245///
246/// This function allows specifying custom JBIG2 configurations for symbol encoding.
247///
248/// # Arguments
249/// * `input` - Binary image data (0/1 values)
250/// * `width` - Image width in pixels
251/// * `height` - Image height in pixels
252/// * `context` - JBIG2 encoding context with configuration
253///
254/// # Returns
255/// A `Jbig2EncodeResult` containing separate global and page data for PDF embedding,
256/// or combined data for standalone files.
257pub fn encode_single_image_with_config(
258    input: &[u8],
259    width: u32,
260    height: u32,
261    context: Jbig2Context,
262) -> Result<Jbig2EncodeResult, Jbig2Error> {
263    let bitimage = validate_and_build_bitimage(input, width, height)?;
264    encode_single_bitimage(bitimage, context)
265}
266
267/// Encodes a single binary image into JBIG2 format using lossless configuration.
268///
269/// This function forces symbol_mode = false to create standalone JBIG2 streams
270/// without global dictionaries, which can resolve PDF display issues.
271///
272/// # Arguments
273/// * `input` - Binary image data (0/1 values)
274/// * `width` - Image width in pixels
275/// * `height` - Image height in pixels
276/// * `pdf_mode` - Whether to create PDF fragments (true) or standalone file (false)
277///
278/// # Returns
279/// A `Jbig2EncodeResult` containing page data without global dictionary
280pub fn encode_single_image_lossless(
281    input: &[u8],
282    width: u32,
283    height: u32,
284    pdf_mode: bool,
285) -> Result<Jbig2EncodeResult, Jbig2Error> {
286    let bitimage = validate_and_build_bitimage(input, width, height)?;
287    encode_single_bitimage(bitimage, Jbig2Context::with_lossless_config(pdf_mode))
288}
289
290/// Encodes a multi-page document for PDF embedding, sharing one symbol
291/// dictionary across every page.
292///
293/// PDF readers expect multi-page JBIG2 content as a single `JBIG2Globals`
294/// object (the shared symbol dictionary) referenced by each page's separate
295/// `/JBIG2Decode` stream. Producing that layout requires every page to be
296/// planned and serialized by the *same* encoder instance so that the symbol
297/// indices referenced by each page's text regions line up with the shared
298/// dictionary's symbol table — building the dictionary and the page streams
299/// from independent encoders silently desyncs those indices and produces
300/// undecodable pages (solid black/white, decoder errors, etc.).
301///
302/// # Arguments
303/// * `images` - the page images, in document order
304/// * `config` - encoder configuration; `symbol_mode` controls whether a
305///   shared global dictionary is produced at all (when disabled, every page
306///   is encoded as an independent generic region and `global_segments` is
307///   `None`)
308///
309/// # Returns
310/// A [`PdfSplitOutput`] with the optional global dictionary segment plus one
311/// page stream per input image, ready to embed directly.
312pub fn encode_document_pdf_split(
313    images: &[Array2<u8>],
314    config: &Jbig2Config,
315) -> Result<PdfSplitOutput, Jbig2Error> {
316    let mut enc_config = config.clone();
317    enc_config.want_full_headers = false;
318    if !enc_config.symbol_mode {
319        enc_config.refine = false;
320        enc_config.text_refine = false;
321    }
322
323    let mut encoder = Jbig2Encoder::new(&enc_config);
324    for image in images {
325        encoder
326            .add_page(image)
327            .map_err(|e| Jbig2Error::EncodingFailed {
328                message: e.to_string(),
329            })?;
330    }
331
332    encoder
333        .flush_pdf_split()
334        .map_err(|e| Jbig2Error::EncodingFailed {
335            message: e.to_string(),
336        })
337}
338
339fn validate_and_build_bitimage(
340    input: &[u8],
341    width: u32,
342    height: u32,
343) -> Result<jbig2sym::BitImage, Jbig2Error> {
344    let expected_len = width as usize * height as usize;
345    if input.len() < expected_len {
346        let packed_size = (width as usize * height as usize).div_ceil(8);
347        if input.len() == packed_size {
348            return Err(Jbig2Error::PackedDataDetected);
349        }
350
351        return Err(Jbig2Error::BufferSizeMismatch {
352            expected: expected_len,
353            actual: input.len(),
354            width,
355            height,
356            ratio: input.len() as f64 / expected_len as f64,
357        });
358    }
359
360    binary_pixels_to_bitimage(&input[..expected_len], width as usize, height as usize)
361        .map_err(|message| Jbig2Error::EncodingFailed { message })
362}
363
364fn encode_single_bitimage(
365    bitimage: jbig2sym::BitImage,
366    ctx: Jbig2Context,
367) -> Result<Jbig2EncodeResult, Jbig2Error> {
368    let mut enc_config = ctx.config.clone();
369    enc_config.want_full_headers = !ctx.get_pdf_mode();
370    if !enc_config.symbol_mode {
371        enc_config.refine = false;
372        enc_config.text_refine = false;
373    }
374
375    let mut encoder = Jbig2Encoder::new(&enc_config);
376    encoder
377        .add_page_bitimage(bitimage)
378        .map_err(|e| Jbig2Error::EncodingFailed {
379            message: e.to_string(),
380        })?;
381
382    if ctx.get_pdf_mode() {
383        // PDF embedding must use the same planning/serialization as tests/pdf_variants:
384        // one encoder, flush_pdf_split() → globals stream + page stream with matching
385        // segment references. The previous approach (dict_only().flush_dict() on one
386        // encoder + flush() on a second) paired unrelated streams and broke decoders
387        // (solid black/white pages, etc.).
388        let split = encoder
389            .flush_pdf_split()
390            .map_err(|e| Jbig2Error::EncodingFailed {
391                message: e.to_string(),
392            })?;
393        let n = split.page_streams.len();
394        if n != 1 {
395            return Err(Jbig2Error::StreamCountMismatch {
396                expected: 1,
397                actual: n,
398            });
399        }
400        let page_data = split.page_streams.into_iter().next().unwrap();
401        Ok(Jbig2EncodeResult {
402            global_data: split.global_segments,
403            page_data,
404        })
405    } else {
406        let page_data = encoder.flush().map_err(|e| Jbig2Error::EncodingFailed {
407            message: e.to_string(),
408        })?;
409        Ok(Jbig2EncodeResult {
410            global_data: None,
411            page_data,
412        })
413    }
414}
415
416/// Get the version string for the crate
417pub fn get_version() -> String {
418    let enc_version = option_env!("JBIG2ENC_VERSION").unwrap_or("unknown");
419    format!(
420        "jbig2-rs {}, jbig2enc {}",
421        env!("CARGO_PKG_VERSION"),
422        enc_version
423    )
424}
425
426/// Get the build information string
427pub fn get_build_info() -> String {
428    let build_ts = option_env!("VERGEN_BUILD_TIMESTAMP").unwrap_or("unknown");
429    let build_type = if cfg!(debug_assertions) {
430        "debug"
431    } else {
432        "release"
433    };
434    format!("{} (built with {})", build_ts, build_type)
435}