1mod symbol;
3
4#[allow(unused_imports)]
5use symbol::dictionary::{encode_symbol_dictionary_segments, plan_symbol_dictionary_layout};
6#[allow(unused_imports)]
7use symbol::text_region::{
8 compute_symbol_hash, log2up, symbol_id_from_dense_maps, uf_find, uf_union,
9};
10#[allow(unused_imports)]
11use symbol::types::*;
12
13pub use symbol::dictionary::{
16 TextRegionSymbolInstance, build_dictionary_and_get_instances, canonicalize_dict_symbols,
17 encode_symbol_dict, encode_symbol_dict_with_order,
18};
19pub use symbol::extraction::segment_symbols;
20pub use symbol::text_region::{
21 encode_page_with_symbol_dictionary, encode_text_region, encode_text_region_mapped,
22};
23pub use symbol::text_region_refine::encode_text_region_with_refinement;
24pub use symbol::types::SymbolInstance;
25
26use crate::jbig2arith::Jbig2ArithCoder;
27use crate::jbig2classify::{
28 SymbolSignature, family_bucket_key_for_symbol, family_bucket_neighbors,
29};
30use crate::jbig2comparator::Comparator;
31#[cfg(feature = "symboldict")]
33use crate::jbig2cc::analyze_page;
34use crate::jbig2structs::{
35 FileHeader, GenericRegionParams, Jbig2Config, LossySymbolMode, PageInfo, Segment, SegmentType,
36};
37
38use crate::jbig2sym::{BitImage, Rect};
39use anyhow::{Result, anyhow};
40
41#[macro_export]
43macro_rules! debug {
44 ($($arg:tt)*) => {
45 #[cfg(feature = "trace_encoder")]
46 log::debug!($($arg)*);
47
48 #[cfg(not(feature = "trace_encoder"))]
49 let _ = format_args!($($arg)*);
50 };
51}
52
53#[macro_export]
54macro_rules! trace {
55 ($($arg:tt)*) => {
56 #[cfg(feature = "trace_encoder")]
57 log::trace!($($arg)*);
58
59 #[cfg(not(feature = "trace_encoder"))]
60 let _ = format_args!($($arg)*);
61 };
62}
63
64#[allow(unused_imports)]
66use crate::{debug, trace};
67
68use ndarray::Array2;
69use rustc_hash::{FxHashMap, FxHashSet};
70use std::hash::{Hash, Hasher};
71use std::time::{Duration, Instant};
72
73#[derive(Debug, Clone, Copy, PartialEq, Eq)]
75pub struct HashKey(u64);
76
77impl Hash for HashKey {
78 fn hash<H: Hasher>(&self, state: &mut H) {
79 self.0.hash(state);
80 }
81}
82
83impl std::fmt::Display for HashKey {
84 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
85 write!(f, "HashKey({:x})", self.0)
86 }
87}
88
89const RECENT_SYMBOL_CACHE_CAP: usize = 64;
90const SYM_UNIFY_EXACT_ANCHOR_BUDGET: usize = 32;
91const SYM_UNIFY_NEIGHBOR_ANCHOR_BUDGET: usize = 16;
92const SYM_UNIFY_STRONG_ANCHOR_MIN_USAGE: usize = 8;
93const SYM_UNIFY_STRONG_ANCHOR_MIN_PAGE_SPAN: usize = 4;
94
95#[derive(Clone)]
98pub struct PageData {
99 pub image: BitImage,
100 pub symbol_instances: Vec<SymbolInstance>,
101}
102
103#[derive(Debug, Clone, Default)]
104pub struct SymbolModeStageMetrics {
105 pub cc_extraction: Duration,
106 pub matching_dedup: Duration,
107 pub clustering: Duration,
108 pub planning: Duration,
109 pub symbol_dict_encoding: Duration,
110 pub text_region_encoding: Duration,
111 pub generic_region_encoding: Duration,
112}
113
114#[derive(Debug, Clone, Default)]
115pub struct SymbolModeStats {
116 pub symbols_discovered: usize,
117 pub symbols_exported: usize,
118 pub avg_symbol_reuse: f64,
119 pub global_symbol_count: usize,
120 pub local_symbol_count: usize,
121 pub comparator_calls: usize,
122 pub comparator_hits: usize,
123 pub exact_hits: usize,
124 pub refined_hits: usize,
125 pub signature_rejects: usize,
126}
127
128#[derive(Debug, Clone, Default)]
129pub struct EncoderMetrics {
130 pub symbol_mode: SymbolModeStageMetrics,
131 pub symbol_stats: SymbolModeStats,
132}
133
134#[derive(Debug, Clone)]
135pub struct PdfSplitOutput {
136 pub global_segments: Option<Vec<u8>>,
137 pub page_streams: Vec<Vec<u8>>,
138 pub local_dict_bytes_per_page: Vec<usize>,
139 pub text_region_bytes_per_page: Vec<usize>,
140 pub generic_region_bytes_per_page: Vec<usize>,
141}
142
143#[derive(Debug, Default)]
145struct EncoderState {
146 pdf_mode: bool,
147 full_headers_remaining: bool,
148 segment: bool,
149 use_refinement: bool,
150 use_delta_encoding: bool,
151 lossy_symbol_mode_applied: bool,
152 ingest_debug_lines: Vec<String>,
153 decision_debug_lines: Vec<String>,
154}
155
156pub struct Jbig2Encoder<'a> {
161 config: &'a Jbig2Config,
163
164 state: EncoderState,
166
167 global_symbols: Vec<BitImage>,
169
170 symbol_usage: Vec<usize>,
172
173 symbol_pixel_counts: Vec<usize>,
175
176 symbol_signatures: Vec<SymbolSignature>,
178
179 symbol_page_count: Vec<usize>,
181
182 symbol_last_page_seen: Vec<Option<usize>>,
184
185 hash_map: FxHashMap<HashKey, Vec<usize>>,
187
188 pages: Vec<PageData>,
190
191 page_symbol_indices: Vec<Vec<usize>>,
193
194 next_segment_number: u32,
196
197 global_dict_segment_numbers: Vec<u32>,
199
200 metrics: EncoderMetrics,
202}
203
204impl<'a> Jbig2Encoder<'a> {
205 pub fn new(config: &'a Jbig2Config) -> Self {
210 if config.refine && !config.symbol_mode {
211 panic!("Refinement requires symbol mode to be enabled.");
212 }
213
214 Self {
215 config,
216 state: EncoderState {
217 pdf_mode: false, full_headers_remaining: config.want_full_headers,
219 segment: true, use_refinement: config.refine, use_delta_encoding: true, lossy_symbol_mode_applied: false,
223 ingest_debug_lines: Vec::new(),
224 decision_debug_lines: Vec::new(),
225 },
226 global_symbols: Vec::new(),
227 symbol_usage: Vec::new(),
228 symbol_pixel_counts: Vec::new(),
229 symbol_signatures: Vec::new(),
230 symbol_page_count: Vec::new(),
231 symbol_last_page_seen: Vec::new(),
232 hash_map: FxHashMap::default(),
233 pages: Vec::new(),
234 page_symbol_indices: Vec::new(),
235 next_segment_number: 1,
236 global_dict_segment_numbers: Vec::new(),
237 metrics: EncoderMetrics::default(),
238 }
239 }
240
241 pub fn dict_only(mut self) -> Self {
242 self.state.full_headers_remaining = false;
243 self.state.pdf_mode = true;
244 self
245 }
246
247 pub fn get_page_count(&self) -> usize {
249 self.pages.len()
250 }
251
252 pub fn metrics_snapshot(&self) -> EncoderMetrics {
253 self.metrics.clone()
254 }
255
256 pub fn decision_debug_log(&self) -> String {
257 if self.state.ingest_debug_lines.is_empty() {
258 return self.state.decision_debug_lines.join("\n");
259 }
260 if self.state.decision_debug_lines.is_empty() {
261 return self.state.ingest_debug_lines.join("\n");
262 }
263
264 let mut out = String::new();
265 out.push_str(&self.state.ingest_debug_lines.join("\n"));
266 out.push('\n');
267 out.push_str(&self.state.decision_debug_lines.join("\n"));
268 out
269 }
270
271 pub fn get_symbol_stats(&self) -> String {
273 let total_symbols = self.global_symbols.len();
274 let avg_usage = if total_symbols > 0 {
275 self.symbol_usage.iter().sum::<usize>() as f32 / total_symbols as f32
276 } else {
277 0.0
278 };
279 let low_usage_count = self.symbol_usage.iter().filter(|&&u| u < 2).count();
280
281 format!(
282 "Total symbols: {}, Average usage: {:.1}, Low usage (<2): {}",
283 total_symbols, avg_usage, low_usage_count
284 )
285 }
286
287 pub fn add_page(&mut self, image: &Array2<u8>) -> Result<()> {
288 let bitimage = crate::jbig2sym::array_to_bitimage(image);
289 self.add_page_bitimage(bitimage)
290 }
291
292 pub fn add_page_bitimage(&mut self, bitimage: BitImage) -> Result<()> {
309 let page_num = self.pages.len();
310 self.page_symbol_indices.push(Vec::new());
311 let mut symbol_instances = Vec::new();
312 let mut comparator = Comparator::default();
313 let debug_matching =
314 page_num == 0 && std::env::var("JBIG2_DEBUG").map_or(false, |v| v == "1");
315 let no_reuse = std::env::var("JBIG2_NO_REUSE").map_or(false, |v| v == "1");
316
317 let mut debug_lines: Vec<String> = Vec::new();
318 if debug_matching {
319 debug_lines.push("=== PAGE 0 MATCHING LOG ===".to_string());
320 debug_lines.push(format!("Image: {}x{}", bitimage.width, bitimage.height));
321 }
322 let mut cc_index = 0usize;
323 let mut sym_unify_anchor_map = (self.config.lossy_symbol_mode
324 == LossySymbolMode::SymbolUnify
325 && !self.global_symbols.is_empty())
326 .then(|| self.build_sym_unify_anchor_map(page_num));
327 let sym_unify_initial_anchor_count = sym_unify_anchor_map
328 .as_ref()
329 .map(|anchors| anchors.values().map(Vec::len).sum::<usize>())
330 .unwrap_or(0);
331 let sym_unify_initial_anchor_bytes = sym_unify_anchor_map
332 .as_ref()
333 .map(|anchors| anchor_map_dictionary_bytes(&self.global_symbols, anchors))
334 .unwrap_or(0);
335 let mut sym_unify_recent_hits = 0usize;
336 let mut sym_unify_anchor_hits = 0usize;
337 let mut sym_unify_bucket_hits = 0usize;
338 let mut sym_unify_new_symbols = 0usize;
339 let mut sym_unify_anchor_score_rejects = 0usize;
340 let mut sym_unify_anchor_outside_rejects = 0usize;
341 let mut sym_unify_anchor_compare_rejects = 0usize;
342 let mut sym_unify_anchor_overlap_rejects = 0usize;
343
344 if self.config.symbol_mode && self.state.segment {
346 #[cfg(feature = "symboldict")]
347 {
348 let dpi = 300; let losslevel =
350 if self.config.symbol_mode || self.config.uses_lossy_symbol_dictionary() {
351 0
352 } else if self.config.is_lossless {
353 0
354 } else {
355 1
356 };
357 let cc_start = Instant::now();
358 let cc_image = analyze_page(&bitimage, dpi, losslevel);
359 let extracted = cc_image.extract_shape_refs();
360 self.metrics.symbol_mode.cc_extraction += cc_start.elapsed();
361
362 let should_use_symbols = if extracted.len() == 1 {
366 let bbox = extracted[0].bbox;
367 !(bbox.xmin == 0
368 && bbox.ymin == 0
369 && bbox.width() as usize >= bitimage.width.saturating_sub(2)
370 && bbox.height() as usize >= bitimage.height.saturating_sub(2))
371 } else {
372 !extracted.is_empty()
373 };
374
375 if should_use_symbols {
376 let matching_start = Instant::now();
377 let mut recent_cache = RecentSymbolCache::new(RECENT_SYMBOL_CACHE_CAP);
378 let mut recent_candidates = [0usize; RECENT_SYMBOL_CACHE_CAP];
379 let mut last_y = 0u32;
380
381 for shape in extracted {
382 if Self::should_skip_symbol_candidate(
383 shape.bbox.width().max(0) as usize,
384 shape.bbox.height().max(0) as usize,
385 shape.black_pixels,
386 ) || shape.run_count == 0
387 {
388 continue;
389 }
390 let Some(symbol) = cc_image.get_bitmap_for_cc(shape.ccid) else {
391 continue;
392 };
393 let (trim_offset, trimmed) = symbol.trim();
394 let pixel_count = trimmed.count_ones();
395 if Self::should_skip_symbol_candidate(
396 trimmed.width,
397 trimmed.height,
398 pixel_count,
399 ) {
400 continue;
401 }
402
403 let rect = Rect {
408 x: shape.bbox.xmin as u32 + trim_offset.x,
409 y: shape.bbox.ymin as u32 + trim_offset.y,
410 width: trimmed.width as u32,
411 height: trimmed.height as u32,
412 };
413 if rect.y > last_y.saturating_add(24) {
414 recent_cache.clear();
415 }
416 last_y = rect.y;
417
418 let trimmed_sig = Self::compute_symbol_signature(&trimmed);
419 let mut matched = false;
420 let mut instance_bitmap = Some(symbol);
421
422 let area = (trimmed.width * trimmed.height) as u32;
424 let max_err = if self.config.text_refine {
425 (area / self.config.match_tolerance).max(3)
426 } else {
427 ((area as f32 * 0.05) as u32).max(2)
428 };
429
430 if !matched && !no_reuse {
431 let recent_len = recent_cache.copy_into(&mut recent_candidates);
432 'recent_search: for &idx in &recent_candidates[..recent_len] {
433 if let Some((err, dx, dy, needs_refinement)) = self
434 .evaluate_symbol_match(
435 &trimmed,
436 trimmed_sig,
437 pixel_count,
438 idx,
439 &mut comparator,
440 max_err,
441 )
442 {
443 if debug_matching {
444 let mode = if needs_refinement {
445 "REFINE"
446 } else if err == 0 && dx == 0 && dy == 0 {
447 "EXACT "
448 } else {
449 "LOSSY "
450 };
451 let proto = &self.global_symbols[idx];
452 debug_lines.push(format!(
453 "CC#{:04} {} pos=({},{}) {}x{} → proto#{} {}x{} err={} dx={} dy={} [recent]",
454 cc_index,
455 mode,
456 rect.x,
457 rect.y,
458 rect.width,
459 rect.height,
460 idx,
461 proto.width,
462 proto.height,
463 err,
464 dx,
465 dy
466 ));
467 }
468
469 self.symbol_usage[idx] += 1;
470 self.note_symbol_page(idx, page_num);
471 symbol_instances.push(SymbolInstance {
472 symbol_index: idx,
473 position: rect,
474 instance_bitmap: instance_bitmap.take().unwrap(),
475 needs_refinement,
476 refinement_dx: if needs_refinement { dx } else { 0 },
477 refinement_dy: if needs_refinement { dy } else { 0 },
478 });
479 recent_cache.touch(idx);
480 if self.config.lossy_symbol_mode == LossySymbolMode::SymbolUnify
481 {
482 sym_unify_recent_hits += 1;
483 }
484 matched = true;
485 break 'recent_search;
486 }
487 }
488 }
489
490 if !matched
491 && !no_reuse
492 && self.config.lossy_symbol_mode == LossySymbolMode::SymbolUnify
493 {
494 if let Some(anchor_map) = sym_unify_anchor_map.as_mut() {
495 let anchor_key =
496 family_bucket_key_for_symbol(&trimmed, &trimmed_sig);
497 let mut visited = FxHashSet::default();
498 let mut exact_examined = 0usize;
499 if let Some(bucket) = anchor_map.get(&anchor_key) {
500 'anchor_search_exact: for &idx in bucket {
501 if exact_examined >= SYM_UNIFY_EXACT_ANCHOR_BUDGET {
502 break 'anchor_search_exact;
503 }
504 exact_examined += 1;
505 if !visited.insert(idx) {
506 continue;
507 }
508 let decision = self.evaluate_symbol_unify_anchor_match(
509 &trimmed,
510 trimmed_sig,
511 pixel_count,
512 idx,
513 &mut comparator,
514 );
515 let (score, dx, dy) = match decision {
516 SymUnifyAnchorDecision::Accept { score, dx, dy } => {
517 (score, dx, dy)
518 }
519 SymUnifyAnchorDecision::RejectScore { .. } => {
520 sym_unify_anchor_score_rejects += 1;
521 continue;
522 }
523 SymUnifyAnchorDecision::RejectOutsideInk => {
524 sym_unify_anchor_outside_rejects += 1;
525 continue;
526 }
527 SymUnifyAnchorDecision::RejectCompare => {
528 sym_unify_anchor_compare_rejects += 1;
529 continue;
530 }
531 SymUnifyAnchorDecision::RejectOverlap => {
532 sym_unify_anchor_overlap_rejects += 1;
533 continue;
534 }
535 _ => continue,
536 };
537
538 if debug_matching {
539 let proto = &self.global_symbols[idx];
540 debug_lines.push(format!(
541 "CC#{:04} UNIFY pos=({},{}) {}x{} → proto#{} {}x{} score={} dx={} dy={} [anchor]",
542 cc_index,
543 rect.x,
544 rect.y,
545 rect.width,
546 rect.height,
547 idx,
548 proto.width,
549 proto.height,
550 score,
551 dx,
552 dy
553 ));
554 }
555
556 self.symbol_usage[idx] += 1;
557 self.note_symbol_page(idx, page_num);
558 self.maybe_add_sym_unify_anchor(anchor_map, idx, page_num);
559 symbol_instances.push(SymbolInstance {
560 symbol_index: idx,
561 position: rect,
562 instance_bitmap: instance_bitmap.take().unwrap(),
563 needs_refinement: false,
564 refinement_dx: 0,
565 refinement_dy: 0,
566 });
567 recent_cache.touch(idx);
568 sym_unify_anchor_hits += 1;
569 matched = true;
570 break;
571 }
572 }
573
574 if !matched {
575 let mut neighbor_examined = 0usize;
576 'anchor_search_neighbors: for neighbor in
577 family_bucket_neighbors(anchor_key)
578 {
579 if neighbor == anchor_key {
580 continue;
581 }
582 let Some(bucket) = anchor_map.get(&neighbor) else {
583 continue;
584 };
585 for &idx in bucket {
586 if neighbor_examined >= SYM_UNIFY_NEIGHBOR_ANCHOR_BUDGET
587 {
588 break 'anchor_search_neighbors;
589 }
590 neighbor_examined += 1;
591 if !visited.insert(idx) {
592 continue;
593 }
594 let decision = self.evaluate_symbol_unify_anchor_match(
595 &trimmed,
596 trimmed_sig,
597 pixel_count,
598 idx,
599 &mut comparator,
600 );
601 let (score, dx, dy) = match decision {
602 SymUnifyAnchorDecision::Accept {
603 score,
604 dx,
605 dy,
606 } => (score, dx, dy),
607 SymUnifyAnchorDecision::RejectScore { .. } => {
608 sym_unify_anchor_score_rejects += 1;
609 continue;
610 }
611 SymUnifyAnchorDecision::RejectOutsideInk => {
612 sym_unify_anchor_outside_rejects += 1;
613 continue;
614 }
615 SymUnifyAnchorDecision::RejectCompare => {
616 sym_unify_anchor_compare_rejects += 1;
617 continue;
618 }
619 SymUnifyAnchorDecision::RejectOverlap => {
620 sym_unify_anchor_overlap_rejects += 1;
621 continue;
622 }
623 _ => continue,
624 };
625
626 if debug_matching {
627 let proto = &self.global_symbols[idx];
628 debug_lines.push(format!(
629 "CC#{:04} UNIFY pos=({},{}) {}x{} → proto#{} {}x{} score={} dx={} dy={} [anchor]",
630 cc_index,
631 rect.x,
632 rect.y,
633 rect.width,
634 rect.height,
635 idx,
636 proto.width,
637 proto.height,
638 score,
639 dx,
640 dy
641 ));
642 }
643
644 self.symbol_usage[idx] += 1;
645 self.note_symbol_page(idx, page_num);
646 self.maybe_add_sym_unify_anchor(
647 anchor_map, idx, page_num,
648 );
649 symbol_instances.push(SymbolInstance {
650 symbol_index: idx,
651 position: rect,
652 instance_bitmap: instance_bitmap.take().unwrap(),
653 needs_refinement: false,
654 refinement_dx: 0,
655 refinement_dy: 0,
656 });
657 recent_cache.touch(idx);
658 sym_unify_anchor_hits += 1;
659 matched = true;
660 break 'anchor_search_neighbors;
661 }
662 }
663 }
664 }
665 }
666
667 if !matched && !no_reuse {
668 let h = trimmed.height as u64;
669 let w = trimmed.width as u64;
670 let dim_range: u64 = if self.config.text_refine { 2 } else { 0 };
671
672 'bucket_search: for dh_off in 0..=(dim_range * 2) {
673 let dh = h.wrapping_add(dh_off).wrapping_sub(dim_range);
674 if dh >= 10_000 {
675 continue;
676 }
677 for dw_off in 0..=(dim_range * 2) {
678 let dw = w.wrapping_add(dw_off).wrapping_sub(dim_range);
679 if dw >= 10_000 {
680 continue;
681 }
682
683 let nk = HashKey(dh * 10_000 + dw);
684 if let Some(bucket) = self.hash_map.get(&nk) {
685 let bucket_len = bucket.len();
686 let bucket_ptr = bucket.as_ptr();
687 for bucket_pos in 0..bucket_len {
688 let idx = unsafe { *bucket_ptr.add(bucket_pos) };
689 let Some((err, dx, dy, needs_refinement)) = self
690 .evaluate_symbol_match(
691 &trimmed,
692 trimmed_sig,
693 pixel_count,
694 idx,
695 &mut comparator,
696 max_err,
697 )
698 else {
699 continue;
700 };
701
702 if debug_matching {
703 let mode = if needs_refinement {
704 "REFINE"
705 } else if err == 0 && dx == 0 && dy == 0 {
706 "EXACT "
707 } else {
708 "LOSSY "
709 };
710 let proto = &self.global_symbols[idx];
711 debug_lines.push(format!(
712 "CC#{:04} {} pos=({},{}) {}x{} → proto#{} {}x{} err={} dx={} dy={}",
713 cc_index,
714 mode,
715 rect.x,
716 rect.y,
717 rect.width,
718 rect.height,
719 idx,
720 proto.width,
721 proto.height,
722 err,
723 dx,
724 dy
725 ));
726 }
727
728 self.symbol_usage[idx] += 1;
729 self.note_symbol_page(idx, page_num);
730 if let Some(anchor_map) = sym_unify_anchor_map.as_mut()
731 {
732 self.maybe_add_sym_unify_anchor(
733 anchor_map, idx, page_num,
734 );
735 }
736 symbol_instances.push(SymbolInstance {
737 symbol_index: idx,
738 position: rect,
739 instance_bitmap: instance_bitmap.take().unwrap(),
740 needs_refinement,
741 refinement_dx: if needs_refinement {
742 dx
743 } else {
744 0
745 },
746 refinement_dy: if needs_refinement {
747 dy
748 } else {
749 0
750 },
751 });
752 recent_cache.touch(idx);
753 if self.config.lossy_symbol_mode
754 == LossySymbolMode::SymbolUnify
755 {
756 sym_unify_bucket_hits += 1;
757 }
758 matched = true;
759 break 'bucket_search;
760 }
761 }
762 }
763 }
764 }
765
766 if !matched {
767 let idx = self.push_symbol(trimmed, pixel_count, page_num);
768 self.metrics.symbol_stats.symbols_discovered += 1;
769 if debug_matching {
770 debug_lines.push(format!(
771 "CC#{:04} NEW pos=({},{}) {}x{} trim_off=({},{}) → new proto#{} {}x{}",
772 cc_index, rect.x, rect.y, rect.width, rect.height,
773 trim_offset.x, trim_offset.y,
774 idx, self.global_symbols[idx].width, self.global_symbols[idx].height
775 ));
776 }
777 let key = hash_key(&self.global_symbols[idx]);
778 self.hash_map.entry(key).or_default().push(idx);
779 if let Some(anchor_map) = sym_unify_anchor_map.as_mut() {
780 self.maybe_add_sym_unify_anchor(anchor_map, idx, page_num);
781 }
782 symbol_instances.push(SymbolInstance {
783 symbol_index: idx,
784 position: rect,
785 instance_bitmap: instance_bitmap.take().unwrap(),
786 needs_refinement: false,
787 refinement_dx: 0,
788 refinement_dy: 0,
789 });
790 recent_cache.touch(idx);
791 if self.config.lossy_symbol_mode == LossySymbolMode::SymbolUnify {
792 sym_unify_new_symbols += 1;
793 }
794 }
795 cc_index += 1;
796 }
797 self.metrics.symbol_mode.matching_dedup += matching_start.elapsed();
798 }
799 }
800 }
801
802 if debug_matching && !debug_lines.is_empty() {
804 debug_lines.push(format!(
805 "\nTotal CCs: {}, Instances: {}",
806 cc_index,
807 symbol_instances.len()
808 ));
809 let log_path = std::path::Path::new("jbig2_debug_page0.log");
810 if let Ok(mut f) = std::fs::File::create(log_path) {
811 use std::io::Write;
812 for line in &debug_lines {
813 let _ = writeln!(f, "{}", line);
814 }
815 }
816 }
817
818 if self.config.lossy_symbol_mode == LossySymbolMode::SymbolUnify
819 && encoder_diagnostics_enabled()
820 {
821 let final_anchor_count = sym_unify_anchor_map
822 .as_ref()
823 .map(|anchors| anchors.values().map(Vec::len).sum::<usize>())
824 .unwrap_or(0);
825 let final_anchor_bytes = sym_unify_anchor_map
826 .as_ref()
827 .map(|anchors| anchor_map_dictionary_bytes(&self.global_symbols, anchors))
828 .unwrap_or(0);
829 self.state.ingest_debug_lines.push(format!(
830 "sym_unify ingest page={}: cc={} recent_hits={} anchor_hits={} bucket_hits={} new_symbols={} initial_anchors={} final_anchors={} initial_anchor_bytes={} final_anchor_bytes={} anchor_score_rejects={} anchor_outside_rejects={} anchor_compare_rejects={} anchor_overlap_rejects={}",
831 page_num + 1,
832 cc_index,
833 sym_unify_recent_hits,
834 sym_unify_anchor_hits,
835 sym_unify_bucket_hits,
836 sym_unify_new_symbols,
837 sym_unify_initial_anchor_count,
838 final_anchor_count,
839 sym_unify_initial_anchor_bytes,
840 final_anchor_bytes,
841 sym_unify_anchor_score_rejects,
842 sym_unify_anchor_outside_rejects,
843 sym_unify_anchor_compare_rejects,
844 sym_unify_anchor_overlap_rejects,
845 ));
846 }
847
848 self.pages.push(PageData {
849 image: bitimage,
850 symbol_instances,
851 });
852 Ok(())
853 }
854
855 pub fn collect_symbols(&mut self, roi: &Array2<u8>) -> Result<()> {
856 let bitimage = crate::jbig2sym::array_to_bitimage(roi);
857 let (_, trimmed) = bitimage.trim();
858 let key = hash_key(&trimmed);
859 let page_num = self.pages.len();
860 if self.page_symbol_indices.len() <= page_num {
861 self.page_symbol_indices.resize_with(page_num + 1, Vec::new);
862 }
863
864 if !self.hash_map.contains_key(&key) {
865 let pixel_count = trimmed.count_ones();
866 let idx = self.push_symbol(trimmed, pixel_count, page_num);
867 self.metrics.symbol_stats.symbols_discovered += 1;
868 self.hash_map.insert(key, vec![idx]);
869 }
870 Ok(())
871 }
872
873 pub fn flush(&mut self) -> Result<Vec<u8>> {
874 let include_header = self.state.full_headers_remaining;
875 self.state.decision_debug_lines.clear();
876 match self.config.lossy_symbol_mode {
877 LossySymbolMode::SymbolUnify => self.apply_symbol_unify()?,
878 LossySymbolMode::Off => {}
879 }
880 let plan = self.plan_document(include_header)?;
881 self.validate_plan(&plan)?;
882 let output = self.serialize_full_document(&plan)?;
883 self.state.full_headers_remaining = false;
884 self.next_segment_number = plan.next_segment_number;
885 Ok(output)
886 }
887
888 pub fn flush_pdf_split(&mut self) -> Result<PdfSplitOutput> {
889 self.state.pdf_mode = true;
890 self.state.decision_debug_lines.clear();
891 match self.config.lossy_symbol_mode {
892 LossySymbolMode::SymbolUnify => self.apply_symbol_unify()?,
893 LossySymbolMode::Off => {}
894 }
895 let plan = self.plan_document(false)?;
896 self.validate_plan(&plan)?;
897 let (
898 global_segments,
899 page_streams,
900 local_dict_bytes_per_page,
901 text_region_bytes_per_page,
902 generic_region_bytes_per_page,
903 ) = self.serialize_pdf_split(&plan)?;
904 self.next_segment_number = plan.next_segment_number;
905 Ok(PdfSplitOutput {
906 global_segments,
907 page_streams,
908 local_dict_bytes_per_page,
909 text_region_bytes_per_page,
910 generic_region_bytes_per_page,
911 })
912 }
913}
914
915pub fn encode_generic_region(img: &BitImage, cfg: &Jbig2Config) -> Result<Vec<u8>> {
918 let mut gr_cfg = GenericRegionParams::new(img.width as u32, img.height as u32, cfg.generic.dpi);
920 gr_cfg.comb_operator = cfg.generic.comb_operator;
921 gr_cfg.mmr = cfg.generic.mmr;
922 gr_cfg.tpgdon = cfg.generic.tpgdon;
923 gr_cfg.validate().map_err(|e: &'static str| anyhow!(e))?;
924
925 let coder_data =
926 Jbig2ArithCoder::encode_generic_payload(img, gr_cfg.template, &gr_cfg.at_pixels)?;
927
928 let params: GenericRegionParams = gr_cfg.clone();
929
930 let mut generic_region_payload = params.to_bytes();
931 generic_region_payload.extend_from_slice(&coder_data);
932
933 let generic_region_segment = Segment {
935 number: 1, seg_type: SegmentType::ImmediateGenericRegion,
937 deferred_non_retain: false,
938 retain_flags: 0,
939 page_association_type: 0, referred_to: Vec::new(),
941 page: Some(1), payload: generic_region_payload.clone(), };
944
945 if !cfg.want_full_headers {
947 let mut seg_bytes = Vec::new();
948 generic_region_segment.write_into(&mut seg_bytes)?;
949 return Ok(seg_bytes);
950 }
951
952 let mut out = Vec::with_capacity(generic_region_payload.len() + 64);
954
955 out.extend_from_slice(
957 &FileHeader {
958 organisation_type: false,
959 unknown_n_pages: false,
960 n_pages: 1,
961 }
962 .to_bytes(),
963 );
964
965 Segment {
967 number: 0,
968 seg_type: SegmentType::PageInformation,
969 deferred_non_retain: false,
970 retain_flags: 0,
971 page_association_type: 0,
972 referred_to: vec![],
973 page: Some(1),
974 payload: PageInfo {
975 width: img.width as u32,
976 height: img.height as u32,
977 xres: cfg.generic.dpi,
978 yres: cfg.generic.dpi,
979 is_lossless: cfg.is_lossless,
980 default_pixel: cfg.default_pixel,
981 default_operator: cfg.generic.comb_operator,
982 ..Default::default()
983 }
984 .to_bytes(),
985 }
986 .write_into(&mut out)?;
987
988 generic_region_segment.write_into(&mut out)?;
990
991 Segment {
993 number: 2,
994 seg_type: SegmentType::EndOfFile,
995 deferred_non_retain: false,
996 retain_flags: 0,
997 page_association_type: 2,
998 referred_to: vec![],
999 page: None,
1000 payload: vec![],
1001 }
1002 .write_into(&mut out)?;
1003
1004 Ok(out)
1005}
1006
1007pub fn encode_document(images: &[Array2<u8>], config: &Jbig2Config) -> Result<Vec<u8>> {
1017 let mut encoder = Jbig2Encoder::new(config);
1018 for image in images {
1019 encoder.add_page(image)?;
1020 }
1021 encoder.flush()
1022}
1023
1024pub fn get_version() -> &'static str {
1025 "0.2.0"
1026}
1027
1028#[inline]
1029pub fn hash_key(img: &BitImage) -> HashKey {
1030 let h = img.height as u64;
1035 let w = img.width as u64;
1036 HashKey(h * 10_000 + w)
1037}
1038
1039pub fn first_black_pixel(image: &BitImage) -> Option<(usize, usize)> {
1042 for y in 0..image.height {
1043 for x in 0..image.width {
1044 if image.get_usize(x, y) {
1045 return Some((x, y));
1046 }
1047 }
1048 }
1049 None
1050}