use crate::NumContext;
use djvu_bitmap::Bitmap;
use djvu_zp::encoder::ZpEncoder;
use std::collections::BTreeMap;
fn encode_num(zp: &mut ZpEncoder, ctx: &mut NumContext, low: i32, high: i32, val: i32) {
let mut low = low;
let mut high = high;
let mut val_inner = val;
let mut cutoff: i32 = 0;
let mut phase: u32 = 1;
let mut range: u32 = 0xffff_ffff;
let mut node = ctx.root();
while range != 1 {
let decision = if low >= cutoff {
let child = ctx.get_right(node);
node = child;
true
} else if high >= cutoff {
let bit = val_inner >= cutoff;
let child = if bit {
ctx.get_right(node)
} else {
ctx.get_left(node)
};
zp.encode_bit(&mut ctx.ctx[node], bit);
node = child;
bit
} else {
let child = ctx.get_left(node);
node = child;
false
};
match phase {
1 => {
let negative = !decision;
if negative {
let temp = -low - 1;
low = -high - 1;
high = temp;
val_inner = -val_inner - 1;
}
phase = 2;
cutoff = 1;
}
2 => {
if !decision {
phase = 3;
range = ((cutoff + 1) / 2) as u32;
if range <= 1 {
range = 1;
cutoff = 0;
} else {
cutoff -= (range / 2) as i32;
}
} else {
cutoff = cutoff * 2 + 1;
}
}
3 => {
range /= 2;
if range == 0 {
range = 1;
}
if range != 1 {
if !decision {
cutoff -= (range / 2) as i32;
} else {
cutoff += (range / 2) as i32;
}
} else if !decision {
cutoff -= 1;
}
}
_ => unreachable!(),
}
}
}
#[allow(unsafe_code)]
fn encode_bitmap_direct(zp: &mut ZpEncoder, ctx: &mut [u8], bm: &Bitmap) {
debug_assert_eq!(ctx.len(), 1024);
let w = bm.width as usize;
let h = bm.height as usize;
let pw = w + 4;
let mut pixels = vec![0u8; (h + 2) * pw];
let stride = bm.row_stride();
let full_bytes = w / 8;
for y in 0..h {
let src = &bm.data[y * stride..y * stride + stride];
let dst = &mut pixels[(y + 2) * pw..(y + 2) * pw + w];
let (chunks, tail) = dst.as_chunks_mut::<8>();
for (&byte, chunk) in src.iter().zip(chunks) {
chunk[0] = (byte >> 7) & 1;
chunk[1] = (byte >> 6) & 1;
chunk[2] = (byte >> 5) & 1;
chunk[3] = (byte >> 4) & 1;
chunk[4] = (byte >> 3) & 1;
chunk[5] = (byte >> 2) & 1;
chunk[6] = (byte >> 1) & 1;
chunk[7] = byte & 1;
}
if !tail.is_empty() {
let byte = src[full_bytes];
for (bit, slot) in tail.iter_mut().enumerate() {
*slot = (byte >> (7 - bit)) & 1;
}
}
}
for bm_y in 0..h {
let row_p2 = &pixels[bm_y * pw..(bm_y + 1) * pw];
let row_p1 = &pixels[(bm_y + 1) * pw..(bm_y + 2) * pw];
let row_cur = &pixels[(bm_y + 2) * pw..(bm_y + 3) * pw];
let mut r2 = (row_p2[0] as u32) << 1 | row_p2[1] as u32;
let mut r1 = (row_p1[0] as u32) << 2 | (row_p1[1] as u32) << 1 | row_p1[2] as u32;
let mut r0: u32 = 0;
for col in 0..w {
let idx = ((r2 << 7) | (r1 << 2) | r0) as usize;
let bit = row_cur[col] != 0;
let ctx_byte = unsafe { ctx.get_unchecked_mut(idx) };
zp.encode_bit(ctx_byte, bit);
r2 = ((r2 << 1) & 0b111) | row_p2[col + 2] as u32;
r1 = ((r1 << 1) & 0b11111) | row_p1[col + 3] as u32;
r0 = ((r0 << 1) & 0b11) | bit as u32;
}
}
}
#[cfg(feature = "experimental")]
fn encode_bitmap_ref(zp: &mut ZpEncoder, ctx: &mut [u8], cbm: &Bitmap, mbm: &Bitmap) {
debug_assert_eq!(ctx.len(), 2048);
let cw = cbm.width as i32;
let ch = cbm.height as i32;
if cw <= 0 || ch <= 0 {
return;
}
let mw = mbm.width as i32;
let mh = mbm.height as i32;
let crow = (ch - 1) >> 1;
let ccol = (cw - 1) >> 1;
let mrow = (mh - 1) >> 1;
let mcol = (mw - 1) >> 1;
let row_shift = mrow - crow;
let col_shift = mcol - ccol;
let mbm_pix = |r: i32, x: i32| -> u32 {
if r < 0 || r >= mh || x < 0 || x >= mw {
0
} else {
mbm.get(x as u32, (mh - 1 - r) as u32) as u32
}
};
let cbm_pix = |r: i32, x: i32| -> u32 {
if r < 0 || r >= ch || x < 0 || x >= cw {
0
} else {
cbm.get(x as u32, (ch - 1 - r) as u32) as u32
}
};
for row in (0..ch).rev() {
let mr = row + row_shift;
let mut c_r1 = (cbm_pix(row + 1, 0) << 1) | cbm_pix(row + 1, 1);
let mut c_r0: u32 = 0;
let mut m_r1 = (mbm_pix(mr, col_shift - 1) << 2)
| (mbm_pix(mr, col_shift) << 1)
| mbm_pix(mr, col_shift + 1);
let mut m_r0 = (mbm_pix(mr - 1, col_shift - 1) << 2)
| (mbm_pix(mr - 1, col_shift) << 1)
| mbm_pix(mr - 1, col_shift + 1);
for col in 0..cw {
let m_r2 = mbm_pix(mr + 1, col + col_shift);
let idx = ((c_r1 << 8) | (c_r0 << 7) | (m_r2 << 6) | (m_r1 << 3) | m_r0) & 2047;
let bit = cbm_pix(row, col) != 0;
zp.encode_bit(&mut ctx[idx as usize], bit);
c_r1 = ((c_r1 << 1) & 0b111) | cbm_pix(row + 1, col + 2);
c_r0 = bit as u32;
m_r1 = ((m_r1 << 1) & 0b111) | mbm_pix(mr, col + col_shift + 2);
m_r0 = ((m_r0 << 1) & 0b111) | mbm_pix(mr - 1, col + col_shift + 2);
}
}
}
pub fn encode_jb2(bitmap: &Bitmap) -> Vec<u8> {
let w = bitmap.width as i32;
let h = bitmap.height as i32;
if w == 0 || h == 0 {
return Vec::new();
}
let mut zp = ZpEncoder::new();
let mut record_type_ctx = NumContext::new();
let mut image_size_ctx = NumContext::new();
let mut symbol_width_ctx = NumContext::new();
let mut symbol_height_ctx = NumContext::new();
let mut hoff_ctx = NumContext::new();
let mut voff_ctx = NumContext::new();
let mut direct_bitmap_ctx = vec![0u8; 1024];
let mut offset_type_ctx: u8 = 0;
let mut flag_ctx: u8 = 0;
encode_num(&mut zp, &mut record_type_ctx, 0, 11, 0);
encode_num(&mut zp, &mut image_size_ctx, 0, 262142, w);
encode_num(&mut zp, &mut image_size_ctx, 0, 262142, h);
zp.encode_bit(&mut flag_ctx, false);
const TILE: u32 = 1024;
let mut first_left: i32 = -1;
let mut first_bottom: i32 = h - 1;
let mut ty: u32 = 0;
while ty < bitmap.height {
let th = TILE.min(bitmap.height - ty);
let mut tx: u32 = 0;
while tx < bitmap.width {
let tw = TILE.min(bitmap.width - tx);
encode_num(&mut zp, &mut record_type_ctx, 0, 11, 3);
encode_num(&mut zp, &mut symbol_width_ctx, 0, 262142, tw as i32);
encode_num(&mut zp, &mut symbol_height_ctx, 0, 262142, th as i32);
let tile_bm = if tw == bitmap.width && th == bitmap.height {
bitmap.clone()
} else {
crop_bitmap(bitmap, tx, ty, tw, th)
};
encode_bitmap_direct(&mut zp, &mut direct_bitmap_ctx, &tile_bm);
let hoff = tx as i32 - first_left;
let voff = h - 1 - ty as i32 - first_bottom;
zp.encode_bit(&mut offset_type_ctx, true);
encode_num(&mut zp, &mut hoff_ctx, -262143, 262142, hoff);
encode_num(&mut zp, &mut voff_ctx, -262143, 262142, voff);
first_left = tx as i32;
first_bottom = h - th as i32 - ty as i32;
tx += tw;
}
ty += th;
}
encode_num(&mut zp, &mut record_type_ctx, 0, 11, 11);
zp.finish()
}
fn crop_bitmap(src: &Bitmap, x0: u32, y0: u32, w: u32, h: u32) -> Bitmap {
let mut out = Bitmap::new(w, h);
if x0.is_multiple_of(8) {
let src_stride = src.row_stride();
let out_stride = out.row_stride();
let src_byte0 = (x0 / 8) as usize;
let last_mask: u8 = if w.is_multiple_of(8) {
0xFF
} else {
0xFFu8 << (8 - (w % 8))
};
for y in 0..h as usize {
let s = (y0 as usize + y) * src_stride + src_byte0;
let d = y * out_stride;
out.data[d..d + out_stride].copy_from_slice(&src.data[s..s + out_stride]);
out.data[d + out_stride - 1] &= last_mask;
}
return out;
}
for y in 0..h {
for x in 0..w {
if src.get(x0 + x, y0 + y) {
out.set_black(x, y);
}
}
}
out
}
struct Cc {
x: u32,
y: u32,
bitmap: Bitmap,
pixel_count: u32,
}
#[cfg(feature = "experimental")]
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct CrossSizeRefinementStats {
pub total_ccs: usize,
pub fresh_ccs: usize,
pub eligible_fresh_ccs: usize,
pub candidate_ccs: usize,
pub near_matches: usize,
pub near_match_pixels: u64,
pub best_hamming: Vec<u32>,
pub estimated_rec1_bytes: u64,
pub estimated_cross_size_rec6_bytes: u64,
pub estimated_byte_delta: i64,
}
fn extract_ccs(bitmap: &Bitmap) -> Vec<Cc> {
let w = bitmap.width as usize;
let h = bitmap.height as usize;
if w == 0 || h == 0 {
return Vec::new();
}
let mut pix = vec![0u8; w * h];
let stride = bitmap.row_stride();
let full_bytes = w / 8;
for y in 0..h {
let src = &bitmap.data[y * stride..y * stride + stride];
let dst = &mut pix[y * w..y * w + w];
let (chunks, tail) = dst.as_chunks_mut::<8>();
for (&byte, chunk) in src.iter().zip(chunks) {
chunk[0] = (byte >> 7) & 1;
chunk[1] = (byte >> 6) & 1;
chunk[2] = (byte >> 5) & 1;
chunk[3] = (byte >> 4) & 1;
chunk[4] = (byte >> 3) & 1;
chunk[5] = (byte >> 2) & 1;
chunk[6] = (byte >> 1) & 1;
chunk[7] = byte & 1;
}
if !tail.is_empty() {
let byte = src[full_bytes];
for (bit, slot) in tail.iter_mut().enumerate() {
*slot = (byte >> (7 - bit)) & 1;
}
}
}
let mut out = Vec::new();
let mut stack: Vec<(u32, u32)> = Vec::new();
let mut cc_pixels: Vec<(u32, u32)> = Vec::new();
for y0 in 0..h {
for x0 in 0..w {
if pix[y0 * w + x0] == 0 {
continue;
}
stack.clear();
cc_pixels.clear();
stack.push((x0 as u32, y0 as u32));
pix[y0 * w + x0] = 0;
let mut min_x = x0;
let mut max_x = x0;
let mut min_y = y0;
let mut max_y = y0;
while let Some((cx, cy)) = stack.pop() {
cc_pixels.push((cx, cy));
let cxi = cx as usize;
let cyi = cy as usize;
if cxi < min_x {
min_x = cxi;
}
if cxi > max_x {
max_x = cxi;
}
if cyi < min_y {
min_y = cyi;
}
if cyi > max_y {
max_y = cyi;
}
let lo_x = cxi.saturating_sub(1);
let hi_x = (cxi + 1).min(w - 1);
let lo_y = cyi.saturating_sub(1);
let hi_y = (cyi + 1).min(h - 1);
for ny in lo_y..=hi_y {
let row_base = ny * w;
for nx in lo_x..=hi_x {
if pix[row_base + nx] != 0 {
pix[row_base + nx] = 0;
stack.push((nx as u32, ny as u32));
}
}
}
}
let cc_w = (max_x - min_x + 1) as u32;
let cc_h = (max_y - min_y + 1) as u32;
let mut cc_bm = Bitmap::new(cc_w, cc_h);
for &(px, py) in &cc_pixels {
cc_bm.set(px - min_x as u32, py - min_y as u32, true);
}
out.push(Cc {
x: min_x as u32,
y: min_y as u32,
bitmap: cc_bm,
pixel_count: cc_pixels.len() as u32,
});
}
}
out
}
fn packed_hamming(a: &[u8], b: &[u8]) -> u32 {
debug_assert_eq!(a.len(), b.len());
let mut total: u32 = 0;
for (x, y) in a.iter().zip(b.iter()) {
total += (x ^ y).count_ones();
}
total
}
const REFINEMENT_MIN_PIXELS: u64 = 32;
#[cfg(feature = "experimental")]
fn scaled_hamming(cand: &Bitmap, reference: &Bitmap) -> u32 {
let mut diff = 0u32;
for y in 0..cand.height {
let ry = (u64::from(y) * u64::from(reference.height) / u64::from(cand.height)) as u32;
for x in 0..cand.width {
let rx = (u64::from(x) * u64::from(reference.width) / u64::from(cand.width)) as u32;
if cand.get(x, y) != reference.get(rx, ry) {
diff += 1;
}
}
}
diff
}
#[cfg(feature = "experimental")]
fn packed_bytes_for_pixels(pixels: u64) -> u64 {
pixels.div_ceil(8)
}
#[inline]
fn symbol_hash(w: u32, h: u32, data: &[u8]) -> u64 {
let mut hash = 0xcbf2_9ce4_8422_2325u64;
let mut mix = |bytes: &[u8]| {
for &b in bytes {
hash = (hash ^ b as u64).wrapping_mul(0x0000_0100_0000_01b3);
}
};
mix(&w.to_le_bytes());
mix(&h.to_le_bytes());
mix(data);
hash
}
#[cfg(feature = "experimental")]
fn index_overhead_bytes(dict_len: usize) -> u64 {
let bits = usize::BITS - dict_len.max(1).leading_zeros();
u64::from(bits).div_ceil(8)
}
#[cfg(feature = "experimental")]
fn estimate_record1_symbol_bytes(symbol: &Bitmap) -> u64 {
const RECORD1_OVERHEAD_BYTES: u64 = 3; symbol.data.len() as u64 + RECORD1_OVERHEAD_BYTES
}
#[cfg(feature = "experimental")]
fn estimate_cross_size_rec6_symbol_bytes(hamming: u32, dict_len: usize) -> u64 {
const RECORD6_OVERHEAD_BYTES: u64 = 5; RECORD6_OVERHEAD_BYTES
+ index_overhead_bytes(dict_len)
+ packed_bytes_for_pixels(u64::from(hamming))
}
#[cfg(feature = "experimental")]
pub fn analyze_jb2_cross_size_refinement(
bitmap: &Bitmap,
shared_symbols: &[Bitmap],
max_dim_delta: u32,
max_hamming_fraction: f32,
) -> CrossSizeRefinementStats {
let mut stats = CrossSizeRefinementStats::default();
if bitmap.width == 0 || bitmap.height == 0 {
return stats;
}
let ccs = extract_ccs(bitmap);
let mut order: Vec<usize> = (0..ccs.len()).collect();
let bucket = (SAME_LINE_BASELINE_TOL.max(1)) as u32;
order.sort_by_key(|&i| {
let cc = &ccs[i];
let bottom = cc.y + cc.bitmap.height;
(bottom / bucket, cc.x)
});
let mut dedup: BTreeMap<(u32, u32, Vec<u8>), usize> = BTreeMap::new();
let mut dict_entries: Vec<Bitmap> = Vec::new();
for sym in shared_symbols {
let idx = dict_entries.len();
dedup.insert((sym.width, sym.height, sym.data.clone()), idx);
dict_entries.push(sym.clone());
}
let mut by_size: BTreeMap<(u32, u32), Vec<usize>> = BTreeMap::new();
for (idx, sym) in dict_entries.iter().enumerate() {
by_size
.entry((sym.width, sym.height))
.or_default()
.push(idx);
}
for &cc_idx in &order {
let cc = &ccs[cc_idx];
stats.total_ccs += 1;
let key = (cc.bitmap.width, cc.bitmap.height, cc.bitmap.data.clone());
if dedup.contains_key(&key) {
continue;
}
stats.fresh_ccs += 1;
let pixels = u64::from(cc.bitmap.width) * u64::from(cc.bitmap.height);
if pixels >= REFINEMENT_MIN_PIXELS {
stats.eligible_fresh_ccs += 1;
let mut best: Option<u32> = None;
let min_w = cc.bitmap.width.saturating_sub(max_dim_delta);
let max_w = cc.bitmap.width.saturating_add(max_dim_delta);
let min_h = cc.bitmap.height.saturating_sub(max_dim_delta);
let max_h = cc.bitmap.height.saturating_add(max_dim_delta);
for w in min_w..=max_w {
for h in min_h..=max_h {
if w == cc.bitmap.width && h == cc.bitmap.height {
continue;
}
let Some(indices) = by_size.get(&(w, h)) else {
continue;
};
for &idx in indices {
let d = scaled_hamming(&cc.bitmap, &dict_entries[idx]);
best = Some(best.map_or(d, |b| b.min(d)));
}
}
}
if let Some(best) = best {
stats.candidate_ccs += 1;
stats.best_hamming.push(best);
let max_diff = ((pixels as f64) * (max_hamming_fraction as f64)).round() as u32;
if best <= max_diff {
stats.near_matches += 1;
stats.near_match_pixels += pixels;
let rec1 = estimate_record1_symbol_bytes(&cc.bitmap);
let rec6 = estimate_cross_size_rec6_symbol_bytes(best, dict_entries.len());
stats.estimated_rec1_bytes += rec1;
stats.estimated_cross_size_rec6_bytes += rec6;
stats.estimated_byte_delta += rec6 as i64 - rec1 as i64;
}
}
}
let next_idx = dict_entries.len();
dedup.insert(key, next_idx);
by_size
.entry((cc.bitmap.width, cc.bitmap.height))
.or_default()
.push(next_idx);
dict_entries.push(cc.bitmap.clone());
}
stats
}
#[cfg(feature = "experimental")]
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct SameSizeRefinementStats {
pub total_ccs: usize,
pub fresh_ccs: usize,
pub eligible_fresh_ccs: usize,
pub candidate_ccs: usize,
pub near_le_2pct: usize,
pub near_le_5pct: usize,
pub near_le_10pct: usize,
pub near_le_5pct_pixels: u64,
pub near_le_5pct_hamming_bytes: u64,
pub best_hamming_permille: Vec<u32>,
}
#[cfg(feature = "experimental")]
pub fn analyze_jb2_same_size_refinement(
bitmap: &Bitmap,
shared_symbols: &[Bitmap],
) -> SameSizeRefinementStats {
same_size_refinement_scan(bitmap, shared_symbols, None)
}
#[cfg(feature = "experimental")]
fn same_size_refinement_scan(
bitmap: &Bitmap,
shared_symbols: &[Bitmap],
fresh_cc_limit: Option<usize>,
) -> SameSizeRefinementStats {
let mut stats = SameSizeRefinementStats::default();
if bitmap.width == 0 || bitmap.height == 0 {
return stats;
}
let ccs = extract_ccs(bitmap);
let mut order: Vec<usize> = (0..ccs.len()).collect();
let bucket = (SAME_LINE_BASELINE_TOL.max(1)) as u32;
order.sort_by_key(|&i| {
let cc = &ccs[i];
let bottom = cc.y + cc.bitmap.height;
(bottom / bucket, cc.x)
});
let mut dedup: BTreeMap<u64, Vec<usize>> = BTreeMap::new();
let mut dict_entries: Vec<&Bitmap> = Vec::new();
let mut by_size: BTreeMap<(u32, u32), Vec<usize>> = BTreeMap::new();
for sym in shared_symbols {
let idx = dict_entries.len();
dedup
.entry(symbol_hash(sym.width, sym.height, &sym.data))
.or_default()
.push(idx);
by_size
.entry((sym.width, sym.height))
.or_default()
.push(idx);
dict_entries.push(sym);
}
for &cc_idx in &order {
let cc = &ccs[cc_idx];
stats.total_ccs += 1;
let bm = &cc.bitmap;
let dkey = symbol_hash(bm.width, bm.height, &bm.data);
let exact = dedup.get(&dkey).is_some_and(|cands| {
cands.iter().copied().any(|i| {
let d = dict_entries[i];
d.width == bm.width && d.height == bm.height && d.data == bm.data
})
});
if exact {
continue;
}
stats.fresh_ccs += 1;
let pixels = u64::from(bm.width) * u64::from(bm.height);
if pixels >= REFINEMENT_MIN_PIXELS {
stats.eligible_fresh_ccs += 1;
if let Some(indices) = by_size.get(&(bm.width, bm.height)) {
let mut best: Option<u32> = None;
for &idx in indices {
let d = packed_hamming(&bm.data, &dict_entries[idx].data);
best = Some(best.map_or(d, |b| b.min(d)));
}
if let Some(best) = best {
stats.candidate_ccs += 1;
let frac_permille = ((u64::from(best) * 1000) / pixels.max(1)) as u32;
stats.best_hamming_permille.push(frac_permille);
if frac_permille <= 20 {
stats.near_le_2pct += 1;
}
if frac_permille <= 50 {
stats.near_le_5pct += 1;
stats.near_le_5pct_pixels += pixels;
stats.near_le_5pct_hamming_bytes += u64::from(best).div_ceil(8);
}
if frac_permille <= 100 {
stats.near_le_10pct += 1;
}
}
}
}
let next_idx = dict_entries.len();
dedup.entry(dkey).or_default().push(next_idx);
by_size
.entry((bm.width, bm.height))
.or_default()
.push(next_idx);
dict_entries.push(bm);
if let Some(limit) = fresh_cc_limit
&& stats.fresh_ccs >= limit
{
break;
}
}
stats
}
#[cfg(feature = "experimental")]
pub const SAME_SIZE_REC6_AUTO_SAMPLE_CCS: usize = 1000;
#[cfg(feature = "experimental")]
pub const SAME_SIZE_REC6_AUTO_DENSITY_THRESHOLD: f32 = 0.05;
#[cfg(feature = "experimental")]
pub const SAME_SIZE_REC6_AUTO_FRAC: f32 = 0.02;
#[cfg(feature = "experimental")]
pub fn probe_same_size_rec6_density(
bitmap: &Bitmap,
shared_symbols: &[Bitmap],
max_ccs: usize,
) -> f32 {
let stats = same_size_refinement_scan(bitmap, shared_symbols, Some(max_ccs.max(1)));
if stats.fresh_ccs == 0 {
return 0.0;
}
stats.near_le_5pct as f32 / stats.fresh_ccs as f32
}
fn find_lossy_copy_ref(
cand: &Bitmap,
dict_entries: &[&Bitmap],
same_size_indices: &[usize],
threshold: f32,
) -> Option<usize> {
if same_size_indices.is_empty() || threshold <= 0.0 {
return None;
}
let pixel_count = (cand.width as u64) * (cand.height as u64);
if pixel_count < REFINEMENT_MIN_PIXELS {
return None;
}
let max_diff = ((pixel_count as f64) * (threshold as f64)).round() as u32;
let mut best: Option<(usize, u32)> = None;
for &i in same_size_indices {
let ref_bm = dict_entries[i];
debug_assert_eq!(ref_bm.width, cand.width);
debug_assert_eq!(ref_bm.height, cand.height);
let d = packed_hamming(&cand.data, &ref_bm.data);
if d > max_diff {
continue;
}
match best {
None => best = Some((i, d)),
Some((_, bd)) if d < bd => best = Some((i, d)),
_ => {}
}
}
best.map(|(i, _)| i)
}
#[cfg(feature = "experimental")]
fn find_cross_size_refine_ref(
cand: &Bitmap,
dict_entries: &[&Bitmap],
by_size: &BTreeMap<(u32, u32), Vec<usize>>,
max_dim_delta: u32,
max_hamming_fraction: f32,
) -> Option<usize> {
let pixel_count = (cand.width as u64) * (cand.height as u64);
if pixel_count < REFINEMENT_MIN_PIXELS {
return None;
}
let max_diff = ((pixel_count as f64) * (max_hamming_fraction as f64)).round() as u32;
let min_w = cand.width.saturating_sub(max_dim_delta);
let max_w = cand.width.saturating_add(max_dim_delta);
let min_h = cand.height.saturating_sub(max_dim_delta);
let max_h = cand.height.saturating_add(max_dim_delta);
let mut best: Option<(usize, u32)> = None;
for w in min_w..=max_w {
for h in min_h..=max_h {
if w == cand.width && h == cand.height {
continue;
}
let Some(indices) = by_size.get(&(w, h)) else {
continue;
};
for &idx in indices {
let d = scaled_hamming(cand, dict_entries[idx]);
if d > max_diff {
continue;
}
match best {
None => best = Some((idx, d)),
Some((_, bd)) if d < bd => best = Some((idx, d)),
_ => {}
}
}
}
}
best.map(|(i, _)| i)
}
#[cfg(feature = "experimental")]
fn find_same_size_refine_ref(
cand: &Bitmap,
dict_entries: &[&Bitmap],
same_size_indices: &[usize],
max_hamming_fraction: f32,
) -> Option<usize> {
let pixel_count = (cand.width as u64) * (cand.height as u64);
if pixel_count < REFINEMENT_MIN_PIXELS {
return None;
}
let max_diff = ((pixel_count as f64) * (max_hamming_fraction as f64)).round() as u32;
let mut best: Option<(usize, u32)> = None;
for &idx in same_size_indices {
let ref_bm = dict_entries[idx];
debug_assert_eq!(ref_bm.width, cand.width);
debug_assert_eq!(ref_bm.height, cand.height);
let d = packed_hamming(&cand.data, &ref_bm.data);
if d > max_diff {
continue;
}
match best {
None => best = Some((idx, d)),
Some((_, bd)) if d < bd => best = Some((idx, d)),
_ => {}
}
}
best.map(|(i, _)| i)
}
#[cfg(feature = "experimental")]
#[derive(Debug, Clone, Copy)]
pub struct CrossSizeRec6Probe {
pub max_dim_delta: u32,
pub max_hamming_fraction: f32,
}
#[derive(Debug, Clone, Copy)]
pub struct Jb2EncodeOptions {
pub lossy_threshold: f32,
pub despeckle: Option<u32>,
#[cfg(feature = "experimental")]
pub cross_size_rec6_probe: Option<CrossSizeRec6Probe>,
#[cfg(feature = "experimental")]
pub same_size_rec6: Option<f32>,
}
impl Default for Jb2EncodeOptions {
fn default() -> Self {
Self {
lossy_threshold: 0.0,
despeckle: None,
#[cfg(feature = "experimental")]
cross_size_rec6_probe: None,
#[cfg(feature = "experimental")]
same_size_rec6: None,
}
}
}
impl Jb2EncodeOptions {
pub fn lossy_text() -> Self {
Self::with_lossy_threshold(0.02)
}
#[allow(clippy::needless_update)] pub fn with_lossy_threshold(threshold: f32) -> Self {
Self {
lossy_threshold: threshold,
..Self::default()
}
}
#[allow(clippy::needless_update)]
pub fn with_despeckle(max_px: u32) -> Self {
Self {
despeckle: Some(max_px),
..Self::default()
}
}
#[allow(clippy::needless_update)]
pub fn lossy_scan() -> Self {
Self {
despeckle: Some(8),
lossy_threshold: 0.06,
..Self::default()
}
}
#[cfg(feature = "experimental")]
pub fn same_size_rec6_auto(bitmap: &Bitmap, shared_symbols: &[Bitmap]) -> Self {
let density =
probe_same_size_rec6_density(bitmap, shared_symbols, SAME_SIZE_REC6_AUTO_SAMPLE_CCS);
Self {
same_size_rec6: if density >= SAME_SIZE_REC6_AUTO_DENSITY_THRESHOLD {
Some(SAME_SIZE_REC6_AUTO_FRAC)
} else {
None
},
..Self::default()
}
}
}
pub fn encode_jb2_dict(bitmap: &Bitmap) -> Vec<u8> {
encode_jb2_dict_with_shared(bitmap, &[])
}
pub fn encode_jb2_dict_with_shared(bitmap: &Bitmap, shared_symbols: &[Bitmap]) -> Vec<u8> {
encode_jb2_dict_with_options(bitmap, shared_symbols, &Jb2EncodeOptions::default())
}
pub fn encode_jb2_dict_with_options(
bitmap: &Bitmap,
shared_symbols: &[Bitmap],
opts: &Jb2EncodeOptions,
) -> Vec<u8> {
encode_jb2_dict_with_blits(bitmap, shared_symbols, opts).0
}
pub struct EncodedBlit {
pub x: u32,
pub y: u32,
pub bitmap: Bitmap,
}
fn extract_and_order_ccs(bitmap: &Bitmap, opts: &Jb2EncodeOptions) -> (Vec<Cc>, Vec<usize>) {
let mut ccs = extract_ccs(bitmap);
if let Some(max_speck_px) = opts.despeckle {
ccs.retain(|cc| cc.pixel_count > max_speck_px);
}
let mut order: Vec<usize> = (0..ccs.len()).collect();
let bucket = (SAME_LINE_BASELINE_TOL.max(1)) as u32;
order.sort_by_key(|&i| {
let cc = &ccs[i];
let bottom = cc.y + cc.bitmap.height;
(bottom / bucket, cc.x)
});
(ccs, order)
}
pub struct SymbolBox {
pub x: u32,
pub y: u32,
pub bitmap: Bitmap,
}
pub fn encode_jb2_dict_with_symbols(
mask_width: u32,
mask_height: u32,
symbols: Vec<SymbolBox>,
shared_symbols: &[Bitmap],
opts: &Jb2EncodeOptions,
) -> (Vec<u8>, Vec<EncodedBlit>) {
let w = mask_width as i32;
let h = mask_height as i32;
if w == 0 || h == 0 {
return (Vec::new(), Vec::new());
}
let order: Vec<usize> = (0..symbols.len()).collect();
let ccs: Vec<Cc> = symbols
.into_iter()
.map(|s| Cc {
x: s.x,
y: s.y,
bitmap: s.bitmap,
pixel_count: 0,
})
.collect();
encode_jb2_dict_with_ccs(w, h, ccs, order, shared_symbols, opts)
}
pub fn symbol_boxes_in_emission_order(bitmap: &Bitmap, opts: &Jb2EncodeOptions) -> Vec<SymbolBox> {
if bitmap.width == 0 || bitmap.height == 0 {
return Vec::new();
}
let (mut ccs, order) = extract_and_order_ccs(bitmap, opts);
order
.iter()
.map(|&i| {
let cc = &mut ccs[i];
SymbolBox {
x: cc.x,
y: cc.y,
bitmap: core::mem::replace(&mut cc.bitmap, Bitmap::new(0, 0)),
}
})
.collect()
}
pub fn encode_jb2_dict_with_blits(
bitmap: &Bitmap,
shared_symbols: &[Bitmap],
opts: &Jb2EncodeOptions,
) -> (Vec<u8>, Vec<EncodedBlit>) {
let w = bitmap.width as i32;
let h = bitmap.height as i32;
if w == 0 || h == 0 {
return (Vec::new(), Vec::new());
}
let (ccs, order) = extract_and_order_ccs(bitmap, opts);
encode_jb2_dict_with_ccs(w, h, ccs, order, shared_symbols, opts)
}
fn encode_jb2_dict_with_ccs(
w: i32,
h: i32,
mut ccs: Vec<Cc>,
order: Vec<usize>,
shared_symbols: &[Bitmap],
opts: &Jb2EncodeOptions,
) -> (Vec<u8>, Vec<EncodedBlit>) {
let mut zp = ZpEncoder::new();
let mut record_type_ctx = NumContext::new();
let mut image_size_ctx = NumContext::new();
let mut symbol_width_ctx = NumContext::new();
let mut symbol_height_ctx = NumContext::new();
let mut symbol_index_ctx = NumContext::new();
let mut inherit_dict_size_ctx = NumContext::new();
#[cfg(feature = "experimental")]
let mut symbol_width_diff_ctx = NumContext::new();
#[cfg(feature = "experimental")]
let mut symbol_height_diff_ctx = NumContext::new();
#[cfg(feature = "experimental")]
let mut refinement_bitmap_ctx = vec![0u8; 2048];
let mut hoff_ctx = NumContext::new();
let mut voff_ctx = NumContext::new();
let mut shoff_ctx = NumContext::new();
let mut svoff_ctx = NumContext::new();
let mut direct_bitmap_ctx = vec![0u8; 1024];
let mut offset_type_ctx: u8 = 0;
let mut flag_ctx: u8 = 0;
if !shared_symbols.is_empty() {
encode_num(&mut zp, &mut record_type_ctx, 0, 11, 9);
encode_num(
&mut zp,
&mut inherit_dict_size_ctx,
0,
262142,
shared_symbols.len() as i32,
);
}
encode_num(&mut zp, &mut record_type_ctx, 0, 11, 0);
encode_num(&mut zp, &mut image_size_ctx, 0, 262142, w);
encode_num(&mut zp, &mut image_size_ctx, 0, 262142, h);
zp.encode_bit(&mut flag_ctx, false);
let mut layout = EncoderLayout::new(h);
let mut dedup: BTreeMap<u64, Vec<usize>> = BTreeMap::new();
let mut dict_entries: Vec<&Bitmap> = Vec::new();
let mut by_size: BTreeMap<(u32, u32), Vec<usize>> = BTreeMap::new();
for sym in shared_symbols {
let idx = dict_entries.len();
dedup
.entry(symbol_hash(sym.width, sym.height, &sym.data))
.or_default()
.push(idx);
by_size
.entry((sym.width, sym.height))
.or_default()
.push(idx);
dict_entries.push(sym);
}
for &cc_idx in &order {
let cc = &ccs[cc_idx];
let cc_w = cc.bitmap.width as i32;
let cc_h = cc.bitmap.height as i32;
let x_jb2 = cc.x as i32;
let y_jb2 = h - cc.y as i32 - cc_h;
let dkey = symbol_hash(cc.bitmap.width, cc.bitmap.height, &cc.bitmap.data);
let exact_match = dedup.get(&dkey).and_then(|cands| {
cands.iter().copied().find(|&i| {
let d = dict_entries[i];
d.width == cc.bitmap.width
&& d.height == cc.bitmap.height
&& d.data == cc.bitmap.data
})
});
enum Action {
New,
Copy(usize),
#[cfg(feature = "experimental")]
Refine(usize),
}
let action = if let Some(idx) = exact_match {
Action::Copy(idx)
} else {
let candidates = by_size
.get(&(cc.bitmap.width, cc.bitmap.height))
.map(|v| v.as_slice())
.unwrap_or(&[]);
let lossy_copy = if opts.lossy_threshold > 0.0 {
find_lossy_copy_ref(&cc.bitmap, &dict_entries, candidates, opts.lossy_threshold)
} else {
None
};
if let Some(idx) = lossy_copy {
Action::Copy(idx)
} else {
#[cfg(feature = "experimental")]
{
let same_size = opts.same_size_rec6.and_then(|frac| {
find_same_size_refine_ref(&cc.bitmap, &dict_entries, candidates, frac)
});
if let Some(idx) = same_size {
Action::Refine(idx)
} else if let Some(probe) = opts.cross_size_rec6_probe {
match find_cross_size_refine_ref(
&cc.bitmap,
&dict_entries,
&by_size,
probe.max_dim_delta,
probe.max_hamming_fraction,
) {
Some(idx) => Action::Refine(idx),
None => Action::New,
}
} else {
Action::New
}
}
#[cfg(not(feature = "experimental"))]
{
Action::New
}
}
};
let dict_size = dict_entries.len();
match &action {
Action::New => {
encode_num(&mut zp, &mut record_type_ctx, 0, 11, 1);
encode_num(&mut zp, &mut symbol_width_ctx, 0, 262142, cc_w);
encode_num(&mut zp, &mut symbol_height_ctx, 0, 262142, cc_h);
encode_bitmap_direct(&mut zp, &mut direct_bitmap_ctx, &cc.bitmap);
}
Action::Copy(dict_idx) => {
encode_num(&mut zp, &mut record_type_ctx, 0, 11, 7);
encode_num(
&mut zp,
&mut symbol_index_ctx,
0,
(dict_size - 1) as i32,
*dict_idx as i32,
);
}
#[cfg(feature = "experimental")]
Action::Refine(dict_idx) => {
let reference = dict_entries[*dict_idx];
let wdiff = cc_w - reference.width as i32;
let hdiff = cc_h - reference.height as i32;
encode_num(&mut zp, &mut record_type_ctx, 0, 11, 6);
encode_num(
&mut zp,
&mut symbol_index_ctx,
0,
(dict_size - 1) as i32,
*dict_idx as i32,
);
encode_num(&mut zp, &mut symbol_width_diff_ctx, -262143, 262142, wdiff);
encode_num(&mut zp, &mut symbol_height_diff_ctx, -262143, 262142, hdiff);
encode_bitmap_ref(&mut zp, &mut refinement_bitmap_ctx, &cc.bitmap, reference);
}
}
let shoff = x_jb2 - layout.last_right;
let svoff = y_jb2 - layout.baseline_get();
let same_line = layout.same_line_seen
&& svoff.abs() <= SAME_LINE_BASELINE_TOL
&& (-SAME_LINE_OVERLAP_TOL..=SAME_LINE_GAP_MAX).contains(&shoff);
if same_line {
zp.encode_bit(&mut offset_type_ctx, false);
encode_num(&mut zp, &mut shoff_ctx, -262143, 262142, shoff);
encode_num(&mut zp, &mut svoff_ctx, -262143, 262142, svoff);
let nx = layout.last_right + shoff;
let ny = layout.baseline_get() + svoff;
layout.baseline_add(ny);
layout.last_right = nx + cc_w - 1;
} else {
zp.encode_bit(&mut offset_type_ctx, true);
let hoff = x_jb2 - layout.first_left;
let voff = y_jb2 + cc_h - 1 - layout.first_bottom;
encode_num(&mut zp, &mut hoff_ctx, -262143, 262142, hoff);
encode_num(&mut zp, &mut voff_ctx, -262143, 262142, voff);
let nx = layout.first_left + hoff;
let ny = layout.first_bottom + voff - cc_h + 1;
layout.first_left = nx;
layout.first_bottom = ny;
layout.baseline_fill(ny);
layout.baseline_add(ny);
layout.last_right = nx + cc_w - 1;
layout.same_line_seen = true;
}
if matches!(action, Action::New) {
let next_idx = dict_entries.len();
dedup.entry(dkey).or_default().push(next_idx);
by_size
.entry((cc.bitmap.width, cc.bitmap.height))
.or_default()
.push(next_idx);
dict_entries.push(&cc.bitmap);
}
}
encode_num(&mut zp, &mut record_type_ctx, 0, 11, 11);
let bytes = zp.finish();
drop(dict_entries);
let blits = order
.iter()
.map(|&i| {
let cc = &mut ccs[i];
EncodedBlit {
x: cc.x,
y: cc.y,
bitmap: core::mem::replace(&mut cc.bitmap, Bitmap::new(0, 0)),
}
})
.collect();
(bytes, blits)
}
const SAME_LINE_BASELINE_TOL: i32 = 16;
const SAME_LINE_OVERLAP_TOL: i32 = 16;
const SAME_LINE_GAP_MAX: i32 = 1000;
struct EncoderLayout {
first_left: i32,
first_bottom: i32,
last_right: i32,
baseline: [i32; 3],
baseline_idx: i32,
same_line_seen: bool,
}
impl EncoderLayout {
fn new(image_height: i32) -> Self {
Self {
first_left: -1,
first_bottom: image_height - 1,
last_right: 0,
baseline: [0, 0, 0],
baseline_idx: -1,
same_line_seen: false,
}
}
fn baseline_fill(&mut self, val: i32) {
self.baseline = [val, val, val];
}
fn baseline_add(&mut self, val: i32) {
self.baseline_idx += 1;
if self.baseline_idx == 3 {
self.baseline_idx = 0;
}
self.baseline[self.baseline_idx as usize] = val;
}
fn baseline_get(&self) -> i32 {
let (a, b, c) = (self.baseline[0], self.baseline[1], self.baseline[2]);
if (a >= b && a <= c) || (a <= b && a >= c) {
a
} else if (b >= a && b <= c) || (b <= a && b >= c) {
b
} else {
c
}
}
}
const SHARED_DICT_PIXEL_BUDGET: usize = 4 * 1024 * 1024;
pub fn encode_jb2_djbz(symbols: &[Bitmap]) -> Vec<u8> {
let mut zp = ZpEncoder::new();
let mut record_type_ctx = NumContext::new();
let mut image_size_ctx = NumContext::new();
let mut symbol_width_ctx = NumContext::new();
let mut symbol_height_ctx = NumContext::new();
let mut direct_bitmap_ctx = vec![0u8; 1024];
let mut flag_ctx: u8 = 0;
encode_num(&mut zp, &mut record_type_ctx, 0, 11, 0);
encode_num(&mut zp, &mut image_size_ctx, 0, 262142, 0);
encode_num(&mut zp, &mut image_size_ctx, 0, 262142, 0);
zp.encode_bit(&mut flag_ctx, false);
for sym in symbols {
encode_num(&mut zp, &mut record_type_ctx, 0, 11, 2);
encode_num(&mut zp, &mut symbol_width_ctx, 0, 262142, sym.width as i32);
encode_num(
&mut zp,
&mut symbol_height_ctx,
0,
262142,
sym.height as i32,
);
encode_bitmap_direct(&mut zp, &mut direct_bitmap_ctx, sym);
}
encode_num(&mut zp, &mut record_type_ctx, 0, 11, 11);
zp.finish()
}
pub fn cluster_shared_symbols(pages: &[Bitmap], page_threshold: usize) -> Vec<Bitmap> {
cluster_shared_symbols_tunable(pages, page_threshold, 0)
}
pub fn cluster_shared_symbols_from_refs(pages: &[&Bitmap], page_threshold: usize) -> Vec<Bitmap> {
cluster_impl(pages, page_threshold)
}
pub fn cluster_shared_symbols_tunable(
pages: &[Bitmap],
page_threshold: usize,
_diff_fraction: u32,
) -> Vec<Bitmap> {
let refs: Vec<&Bitmap> = pages.iter().collect();
cluster_impl(&refs, page_threshold)
}
fn cluster_impl(pages: &[&Bitmap], page_threshold: usize) -> Vec<Bitmap> {
if page_threshold < 2 || pages.len() < page_threshold {
return Vec::new();
}
struct Cluster {
rep: Bitmap,
pages_seen: Vec<usize>,
first_seen: (usize, usize),
}
#[derive(Default)]
struct SizeBucket {
clusters: Vec<Cluster>,
by_hash: BTreeMap<u64, Vec<usize>>,
}
fn bucket_page_ccs(
buckets: &mut BTreeMap<(u32, u32), SizeBucket>,
ccs: &[Cc],
page_idx: usize,
) {
for (cc_idx, cc) in ccs.iter().enumerate() {
let bm = &cc.bitmap;
let bucket = buckets.entry((bm.width, bm.height)).or_default();
let hash = symbol_hash(bm.width, bm.height, &bm.data);
let hit = bucket.by_hash.get(&hash).and_then(|cands| {
cands
.iter()
.copied()
.find(|&i| bucket.clusters[i].rep.data == bm.data)
});
match hit {
Some(i) => {
if bucket.clusters[i].pages_seen.last() != Some(&page_idx) {
bucket.clusters[i].pages_seen.push(page_idx);
}
}
None => {
let idx = bucket.clusters.len();
bucket.clusters.push(Cluster {
rep: bm.clone(),
pages_seen: vec![page_idx],
first_seen: (page_idx, cc_idx),
});
bucket.by_hash.entry(hash).or_default().push(idx);
}
}
}
}
let mut buckets: BTreeMap<(u32, u32), SizeBucket> = BTreeMap::new();
const BATCH: usize = 32;
let mut page_idx = 0usize;
for chunk in pages.chunks(BATCH) {
#[cfg(feature = "parallel")]
let ccs_batch: Vec<Vec<Cc>> = {
use rayon::prelude::*;
chunk.par_iter().map(|bm| extract_ccs(bm)).collect()
};
#[cfg(not(feature = "parallel"))]
let ccs_batch: Vec<Vec<Cc>> = chunk.iter().map(|bm| extract_ccs(bm)).collect();
for ccs in &ccs_batch {
bucket_page_ccs(&mut buckets, ccs, page_idx);
page_idx += 1;
}
}
let mut promoted: Vec<Cluster> = buckets
.into_values()
.flat_map(|b| b.clusters)
.filter(|c| c.pages_seen.len() >= page_threshold)
.collect();
let mut total_pixels: u64 = 0;
let cap = SHARED_DICT_PIXEL_BUDGET as u64;
let any_over_budget = promoted.iter().fold(0u64, |acc, c| {
acc + (c.rep.width as u64) * (c.rep.height as u64)
}) > cap;
if any_over_budget {
let mut by_value: Vec<usize> = (0..promoted.len()).collect();
by_value.sort_by(|&a, &b| {
promoted[b]
.pages_seen
.len()
.cmp(&promoted[a].pages_seen.len())
.then_with(|| {
let pa = (promoted[a].rep.width as u64) * (promoted[a].rep.height as u64);
let pb = (promoted[b].rep.width as u64) * (promoted[b].rep.height as u64);
pa.cmp(&pb)
})
});
let mut keep = vec![false; promoted.len()];
for &i in &by_value {
let pix = (promoted[i].rep.width as u64) * (promoted[i].rep.height as u64);
if total_pixels + pix > cap {
continue;
}
keep[i] = true;
total_pixels += pix;
}
let mut idx = 0;
promoted.retain(|_| {
let k = keep[idx];
idx += 1;
k
});
}
promoted.sort_by_key(|c| c.first_seen);
promoted.into_iter().map(|c| c.rep).collect()
}
#[cfg(feature = "experimental")]
pub struct DictOrderVariants {
pub baseline: Vec<Bitmap>,
pub by_frequency: Vec<Bitmap>,
pub by_bucket: Vec<Bitmap>,
}
#[cfg(feature = "experimental")]
pub fn cluster_shared_symbols_order_variants(
pages: &[Bitmap],
page_threshold: usize,
) -> DictOrderVariants {
if page_threshold < 2 || pages.len() < page_threshold {
return DictOrderVariants {
baseline: Vec::new(),
by_frequency: Vec::new(),
by_bucket: Vec::new(),
};
}
struct Cluster {
rep: Bitmap,
pages_seen: Vec<usize>,
first_seen: (usize, usize),
}
#[derive(Default)]
struct SizeBucket {
clusters: Vec<Cluster>,
by_hash: BTreeMap<u64, Vec<usize>>,
}
fn bucket_page_ccs(
buckets: &mut BTreeMap<(u32, u32), SizeBucket>,
ccs: &[Cc],
page_idx: usize,
) {
for (cc_idx, cc) in ccs.iter().enumerate() {
let bm = &cc.bitmap;
let bucket = buckets.entry((bm.width, bm.height)).or_default();
let hash = symbol_hash(bm.width, bm.height, &bm.data);
let hit = bucket.by_hash.get(&hash).and_then(|cands| {
cands
.iter()
.copied()
.find(|&i| bucket.clusters[i].rep.data == bm.data)
});
match hit {
Some(i) => {
if bucket.clusters[i].pages_seen.last() != Some(&page_idx) {
bucket.clusters[i].pages_seen.push(page_idx);
}
}
None => {
let idx = bucket.clusters.len();
bucket.clusters.push(Cluster {
rep: bm.clone(),
pages_seen: vec![page_idx],
first_seen: (page_idx, cc_idx),
});
bucket.by_hash.entry(hash).or_default().push(idx);
}
}
}
}
let mut buckets: BTreeMap<(u32, u32), SizeBucket> = BTreeMap::new();
const BATCH: usize = 32;
let mut page_idx = 0usize;
for chunk in pages.chunks(BATCH) {
#[cfg(feature = "parallel")]
let ccs_batch: Vec<Vec<Cc>> = {
use rayon::prelude::*;
chunk.par_iter().map(extract_ccs).collect()
};
#[cfg(not(feature = "parallel"))]
let ccs_batch: Vec<Vec<Cc>> = chunk.iter().map(extract_ccs).collect();
for ccs in &ccs_batch {
bucket_page_ccs(&mut buckets, ccs, page_idx);
page_idx += 1;
}
}
let mut promoted: Vec<Cluster> = buckets
.into_values()
.flat_map(|b| b.clusters)
.filter(|c| c.pages_seen.len() >= page_threshold)
.collect();
let mut total_pixels: u64 = 0;
let cap = SHARED_DICT_PIXEL_BUDGET as u64;
let any_over_budget = promoted.iter().fold(0u64, |acc, c| {
acc + (c.rep.width as u64) * (c.rep.height as u64)
}) > cap;
if any_over_budget {
let mut by_value: Vec<usize> = (0..promoted.len()).collect();
by_value.sort_by(|&a, &b| {
promoted[b]
.pages_seen
.len()
.cmp(&promoted[a].pages_seen.len())
.then_with(|| {
let pa = (promoted[a].rep.width as u64) * (promoted[a].rep.height as u64);
let pb = (promoted[b].rep.width as u64) * (promoted[b].rep.height as u64);
pa.cmp(&pb)
})
});
let mut keep = vec![false; promoted.len()];
for &i in &by_value {
let pix = (promoted[i].rep.width as u64) * (promoted[i].rep.height as u64);
if total_pixels + pix > cap {
continue;
}
keep[i] = true;
total_pixels += pix;
}
let mut idx = 0;
promoted.retain(|_| {
let k = keep[idx];
idx += 1;
k
});
}
let by_bucket: Vec<Bitmap> = promoted.iter().map(|c| c.rep.clone()).collect();
let mut baseline_idx: Vec<usize> = (0..promoted.len()).collect();
baseline_idx.sort_by_key(|&i| promoted[i].first_seen);
let baseline: Vec<Bitmap> = baseline_idx
.iter()
.map(|&i| promoted[i].rep.clone())
.collect();
let mut freq_idx: Vec<usize> = (0..promoted.len()).collect();
freq_idx.sort_by(|&a, &b| {
promoted[b]
.pages_seen
.len()
.cmp(&promoted[a].pages_seen.len())
.then_with(|| promoted[a].first_seen.cmp(&promoted[b].first_seen))
});
let by_frequency: Vec<Bitmap> = freq_idx.iter().map(|&i| promoted[i].rep.clone()).collect();
DictOrderVariants {
baseline,
by_frequency,
by_bucket,
}
}
#[derive(Debug, Default, Clone)]
pub struct CcStats {
pub total_ccs: usize,
pub rec_7_exact: usize,
pub rec_6_refine_shared: usize,
pub rec_6_refine_local: usize,
pub rec_1_new: usize,
pub rec_6_hamming: Vec<u32>,
pub pixels_rec_1: u64,
pub pixels_rec_6: u64,
pub pixels_rec_7: u64,
}
pub fn analyze_jb2_cc_stats(page: &Bitmap, shared_symbols: &[Bitmap]) -> CcStats {
let mut stats = CcStats::default();
if page.width == 0 || page.height == 0 {
return stats;
}
let ccs = extract_ccs(page);
let mut order: Vec<usize> = (0..ccs.len()).collect();
let bucket = (SAME_LINE_BASELINE_TOL.max(1)) as u32;
order.sort_by_key(|&i| {
let cc = &ccs[i];
let bottom = cc.y + cc.bitmap.height;
(bottom / bucket, cc.x)
});
let mut dedup: BTreeMap<(u32, u32, Vec<u8>), usize> = BTreeMap::new();
let mut dict_entries: Vec<Bitmap> = Vec::new();
let mut by_size: BTreeMap<(u32, u32), Vec<usize>> = BTreeMap::new();
for sym in shared_symbols {
let idx = dict_entries.len();
dedup.insert((sym.width, sym.height, sym.data.clone()), idx);
by_size
.entry((sym.width, sym.height))
.or_default()
.push(idx);
dict_entries.push(sym.clone());
}
for &cc_idx in &order {
let cc = &ccs[cc_idx];
let pixels = (cc.bitmap.width as u64) * (cc.bitmap.height as u64);
stats.total_ccs += 1;
let key = (cc.bitmap.width, cc.bitmap.height, cc.bitmap.data.clone());
if let Some(idx) = dedup.get(&key).copied() {
stats.rec_7_exact += 1;
stats.pixels_rec_7 += pixels;
let _ = idx;
continue;
}
stats.rec_1_new += 1;
stats.pixels_rec_1 += pixels;
let idx = dict_entries.len();
dedup.insert(key, idx);
by_size
.entry((cc.bitmap.width, cc.bitmap.height))
.or_default()
.push(idx);
dict_entries.push(cc.bitmap.clone());
}
stats
}
#[cfg(test)]
mod tests {
use super::*;
use crate as jb2;
use djvu_bitmap::Bitmap;
fn make_bitmap(w: u32, h: u32, f: impl Fn(u32, u32) -> bool) -> Bitmap {
let mut bm = Bitmap::new(w, h);
for y in 0..h {
for x in 0..w {
bm.set(x, y, f(x, y));
}
}
bm
}
fn roundtrip(bm: &Bitmap) -> Bitmap {
let encoded = encode_jb2(bm);
jb2::decode(&encoded, None).expect("decode failed")
}
#[test]
fn all_white_roundtrip() {
let src = Bitmap::new(32, 32);
let decoded = roundtrip(&src);
assert_eq!(decoded.width, 32);
assert_eq!(decoded.height, 32);
for y in 0..32u32 {
for x in 0..32u32 {
assert!(!decoded.get(x, y), "expected white at ({x},{y})");
}
}
}
#[test]
fn all_black_roundtrip() {
let src = make_bitmap(32, 32, |_, _| true);
let decoded = roundtrip(&src);
for y in 0..32u32 {
for x in 0..32u32 {
assert!(decoded.get(x, y), "expected black at ({x},{y})");
}
}
}
#[test]
fn checkerboard_roundtrip() {
let src = make_bitmap(16, 16, |x, y| (x + y) % 2 == 0);
let decoded = roundtrip(&src);
for y in 0..16u32 {
for x in 0..16u32 {
assert_eq!(decoded.get(x, y), (x + y) % 2 == 0, "mismatch at ({x},{y})");
}
}
}
#[test]
fn single_pixel_roundtrip() {
let src = make_bitmap(1, 1, |_, _| true);
let decoded = roundtrip(&src);
assert_eq!(decoded.width, 1);
assert_eq!(decoded.height, 1);
assert!(decoded.get(0, 0));
}
#[test]
fn larger_image_roundtrip() {
let src = make_bitmap(64, 64, |x, y| (x * 17 + y * 31) % 5 != 0);
let decoded = roundtrip(&src);
assert_eq!(decoded.width, 64);
assert_eq!(decoded.height, 64);
let mut mismatches = 0u32;
for y in 0..64u32 {
for x in 0..64u32 {
if decoded.get(x, y) != src.get(x, y) {
mismatches += 1;
}
}
}
assert_eq!(
mismatches, 0,
"{mismatches} pixel mismatches in 64×64 roundtrip"
);
}
#[test]
fn encoded_is_nonempty() {
let src = Bitmap::new(8, 8);
let encoded = encode_jb2(&src);
assert!(!encoded.is_empty());
}
#[test]
fn large_symbol_at_eof_not_wrongly_truncated() {
for &h in &[2100u32, 3100] {
let w = 200u32;
let src = make_bitmap(w, h, |x, y| {
if y < 1024 {
let mut s = x
.wrapping_mul(374761393)
.wrapping_add(y.wrapping_mul(668265263));
s = (s ^ (s >> 13)).wrapping_mul(1274126177);
(s ^ (s >> 16)) & 1 == 0
} else {
true
}
});
let encoded = encode_jb2(&src);
let decoded = jb2::decode(&encoded, None)
.unwrap_or_else(|e| panic!("{w}x{h} valid page wrongly rejected: {e:?}"));
assert_eq!((decoded.width, decoded.height), (w, h));
for y in 0..h {
for x in 0..w {
assert_eq!(
decoded.get(x, y),
src.get(x, y),
"{w}x{h} pixel mismatch at ({x},{y})"
);
}
}
}
}
#[test]
fn zero_dimension_returns_empty() {
assert!(encode_jb2(&Bitmap::new(0, 0)).is_empty());
assert!(encode_jb2(&Bitmap::new(8, 0)).is_empty());
assert!(encode_jb2(&Bitmap::new(0, 8)).is_empty());
}
#[test]
fn tiled_2048x2048_roundtrip() {
let src = make_bitmap(2048, 2048, |x, y| {
((x.wrapping_mul(2654435761)) ^ y.wrapping_mul(40503)) & 7 == 0
});
let encoded = encode_jb2(&src);
let decoded = jb2::decode(&encoded, None).expect("decode failed");
assert_eq!(decoded.width, 2048);
assert_eq!(decoded.height, 2048);
for y in 0..2048u32 {
for x in 0..2048u32 {
assert_eq!(decoded.get(x, y), src.get(x, y), "mismatch at ({x},{y})");
}
}
}
#[test]
fn tiled_irregular_size_roundtrip() {
let src = make_bitmap(1500, 1100, |x, y| (x * 13 + y * 7) % 11 == 0);
let encoded = encode_jb2(&src);
let decoded = jb2::decode(&encoded, None).expect("decode failed");
assert_eq!(decoded.width, 1500);
assert_eq!(decoded.height, 1100);
let mut mismatches = 0u32;
for y in 0..1100u32 {
for x in 0..1500u32 {
if decoded.get(x, y) != src.get(x, y) {
mismatches += 1;
}
}
}
assert_eq!(mismatches, 0);
}
#[test]
fn tiled_1x1_roundtrip() {
for &px in &[false, true] {
let src = make_bitmap(1, 1, |_, _| px);
let encoded = encode_jb2(&src);
let decoded = jb2::decode(&encoded, None).expect("decode failed");
assert_eq!(decoded.width, 1);
assert_eq!(decoded.height, 1);
assert_eq!(decoded.get(0, 0), px, "1x1 pixel mismatch px={px}");
}
}
#[test]
fn tiled_100x100_roundtrip() {
let src = make_bitmap(100, 100, |x, y| (x ^ y) & 1 == 0);
let encoded = encode_jb2(&src);
let decoded = jb2::decode(&encoded, None).expect("decode failed");
assert_eq!(decoded.width, 100);
assert_eq!(decoded.height, 100);
for y in 0..100u32 {
for x in 0..100u32 {
assert_eq!(decoded.get(x, y), src.get(x, y), "mismatch at ({x},{y})");
}
}
}
#[test]
#[ignore = "16 MP pixel-by-pixel verify is slow; enable with --ignored"]
fn tiled_4096x4096_roundtrip() {
let src = make_bitmap(4096, 4096, |x, y| {
((x.wrapping_mul(2654435761)) ^ y.wrapping_mul(40503)) & 31 == 0
});
let encoded = encode_jb2(&src);
let decoded = jb2::decode(&encoded, None).expect("decode failed");
assert_eq!(decoded.width, 4096);
assert_eq!(decoded.height, 4096);
for y in 0..4096u32 {
for x in 0..4096u32 {
assert_eq!(decoded.get(x, y), src.get(x, y), "mismatch at ({x},{y})");
}
}
}
fn roundtrip_dict(bm: &Bitmap) -> Bitmap {
let encoded = encode_jb2_dict(bm);
jb2::decode(&encoded, None).expect("dict decode failed")
}
fn assert_bitmaps_eq(a: &Bitmap, b: &Bitmap) {
assert_eq!(a.width, b.width, "width mismatch");
assert_eq!(a.height, b.height, "height mismatch");
let mut mismatches = Vec::new();
for y in 0..a.height {
for x in 0..a.width {
if a.get(x, y) != b.get(x, y) {
mismatches.push((x, y, a.get(x, y), b.get(x, y)));
}
}
}
assert!(
mismatches.is_empty(),
"{} pixel mismatches: {:?}",
mismatches.len(),
mismatches
);
}
#[test]
fn dict_all_white_roundtrip() {
let src = Bitmap::new(32, 32);
let decoded = roundtrip_dict(&src);
assert_bitmaps_eq(&src, &decoded);
}
#[test]
fn dict_single_pixel_roundtrip() {
let src = make_bitmap(16, 16, |x, y| x == 4 && y == 7);
let decoded = roundtrip_dict(&src);
assert_bitmaps_eq(&src, &decoded);
}
#[test]
fn dict_two_dots_dedup() {
let src = make_bitmap(32, 32, |x, y| (x == 3 && y == 5) || (x == 20 && y == 25));
let decoded = roundtrip_dict(&src);
assert_bitmaps_eq(&src, &decoded);
let ccs = extract_ccs(&src);
assert_eq!(ccs.len(), 2);
}
#[test]
fn dict_letter_like_shapes() {
let src = make_bitmap(32, 32, |x, y| {
(x < 3 && y < 5) || ((20..23).contains(&x) && (10..15).contains(&y))
});
let decoded = roundtrip_dict(&src);
assert_bitmaps_eq(&src, &decoded);
}
#[test]
fn dict_checkerboard_many_ccs() {
let src = make_bitmap(8, 8, |x, y| (x + y) % 2 == 0);
let decoded = roundtrip_dict(&src);
assert_bitmaps_eq(&src, &decoded);
}
#[test]
fn dict_two_different_shapes_multiple_occurrences() {
let src = make_bitmap(64, 64, |x, y| {
let in_a = |ax: u32, ay: u32| x >= ax && x < ax + 2 && y >= ay && y < ay + 2;
let in_b = |bx: u32, by: u32| x == bx && y >= by && y < by + 3;
in_a(0, 0)
|| in_a(30, 0)
|| in_a(0, 30)
|| in_a(30, 30)
|| in_b(10, 5)
|| in_b(40, 5)
|| in_b(10, 45)
|| in_b(40, 45)
});
let decoded = roundtrip_dict(&src);
assert_bitmaps_eq(&src, &decoded);
let ccs = extract_ccs(&src);
assert_eq!(ccs.len(), 8, "expected 4+4 CCs");
}
#[test]
fn dict_dimension_encoded_correctly() {
let src = make_bitmap(13, 7, |x, y| (x * 3 + y) % 5 == 0);
let decoded = roundtrip_dict(&src);
assert_bitmaps_eq(&src, &decoded);
}
#[test]
fn dict_zero_dimension_returns_empty() {
assert!(encode_jb2_dict(&Bitmap::new(0, 0)).is_empty());
assert!(encode_jb2_dict(&Bitmap::new(8, 0)).is_empty());
assert!(encode_jb2_dict(&Bitmap::new(0, 8)).is_empty());
}
#[test]
fn dict_extract_ccs_counts() {
let src = make_bitmap(30, 30, |x, y| {
(x < 3 && y < 3)
|| ((10..13).contains(&x) && (10..13).contains(&y))
|| ((25..28).contains(&x) && (25..28).contains(&y))
});
let ccs = extract_ccs(&src);
assert_eq!(ccs.len(), 3);
for cc in &ccs {
assert_eq!(cc.bitmap.width, 3);
assert_eq!(cc.bitmap.height, 3);
}
}
#[test]
fn dict_extract_ccs_8connected() {
let src = make_bitmap(4, 4, |x, y| (x == 0 && y == 0) || (x == 1 && y == 1));
let ccs = extract_ccs(&src);
assert_eq!(ccs.len(), 1);
assert_eq!(ccs[0].bitmap.width, 2);
assert_eq!(ccs[0].bitmap.height, 2);
}
#[test]
fn refine_near_duplicate_glyphs_roundtrip() {
let src = make_bitmap(40, 12, |x, y| {
let in_a = (2..7).contains(&x) && (2..7).contains(&y);
let in_b = (20..25).contains(&x) && (2..7).contains(&y) && !(x == 24 && y == 6);
in_a || in_b
});
let decoded = roundtrip_dict(&src);
assert_bitmaps_eq(&src, &decoded);
}
#[test]
fn refine_text_like_repeats_roundtrip() {
let src = make_bitmap(80, 12, |x, y| {
let local_x = x % 12;
let local_y = y;
let glyph_idx = x / 12;
let base = (local_x == 3 && (1..8).contains(&local_y))
|| (local_y == 4 && (1..7).contains(&local_x));
let perturbed = match glyph_idx {
1 => local_x == 0 && local_y == 0,
2 => local_x == 6 && local_y == 8,
3 => local_x == 6 && local_y == 0,
4 => local_x == 0 && local_y == 8,
_ => false,
};
base ^ perturbed
});
let decoded = roundtrip_dict(&src);
assert_bitmaps_eq(&src, &decoded);
}
#[test]
fn refine_far_glyph_falls_back_to_new() {
let src = make_bitmap(40, 12, |x, y| {
let in_block = (2..7).contains(&x) && (2..7).contains(&y);
let in_x = (20..25).contains(&x)
&& (2..7).contains(&y)
&& (x - 20 == y - 2 || x - 20 == 6 - (y - 2));
in_block || in_x
});
let decoded = roundtrip_dict(&src);
assert_bitmaps_eq(&src, &decoded);
}
#[test]
fn refine_packed_hamming_basic() {
let a = vec![0b1010_1010u8, 0b0000_1111u8];
let b = vec![0b1010_1011u8, 0b0000_1111u8];
assert_eq!(packed_hamming(&a, &b), 1);
let c = vec![0u8; 2];
let d = vec![0xff; 2];
assert_eq!(packed_hamming(&c, &d), 16);
}
#[cfg(feature = "experimental")]
const REC6_PROBE: CrossSizeRec6Probe = CrossSizeRec6Probe {
max_dim_delta: 2,
max_hamming_fraction: 0.05,
};
#[cfg(feature = "experimental")]
fn probe_opts() -> Jb2EncodeOptions {
Jb2EncodeOptions {
cross_size_rec6_probe: Some(REC6_PROBE),
..Jb2EncodeOptions::default()
}
}
#[test]
fn cross_size_rec6_probe_off_is_byte_identical() {
let src = make_bitmap(80, 40, |x, y| {
let a = (4..16).contains(&x) && (4..28).contains(&y);
let b = (40..53).contains(&x) && (4..28).contains(&y);
a || b
});
let shipped = encode_jb2_dict(&src);
let opt = encode_jb2_dict_with_options(&src, &[], &Jb2EncodeOptions::default());
assert_eq!(shipped, opt, "default options must match shipped output");
}
#[cfg(feature = "experimental")]
#[test]
fn cross_size_rec6_probe_roundtrips_solid_near_twins() {
let src = make_bitmap(80, 40, |x, y| {
let a = (4..16).contains(&x) && (4..28).contains(&y); let b = (40..53).contains(&x) && (4..28).contains(&y); a || b
});
let default_bytes = encode_jb2_dict_with_options(&src, &[], &Jb2EncodeOptions::default());
let probe_bytes = encode_jb2_dict_with_options(&src, &[], &probe_opts());
assert_ne!(
default_bytes, probe_bytes,
"probe should emit a rec-6 refinement, changing the byte stream"
);
let decoded = jb2::decode(&probe_bytes, None).expect("probe decode failed");
assert_bitmaps_eq(&src, &decoded);
}
#[cfg(feature = "experimental")]
#[test]
fn cross_size_rec6_probe_roundtrips_perturbed_glyphs() {
let mut src = Bitmap::new(64, 130);
let draw_block = |bm: &mut Bitmap, ox: u32, oy: u32, w: u32, h: u32, notch: bool| {
for y in 0..h {
for x in 0..w {
if notch && x >= w - 2 && y >= h - 2 {
continue;
}
bm.set(ox + x, oy + y, true);
}
}
};
draw_block(&mut src, 4, 2, 14, 18, false); draw_block(&mut src, 4, 24, 15, 18, false); draw_block(&mut src, 4, 46, 14, 19, true); draw_block(&mut src, 4, 70, 15, 19, true); draw_block(&mut src, 4, 94, 13, 18, false);
let default_bytes = encode_jb2_dict_with_options(&src, &[], &Jb2EncodeOptions::default());
let probe_bytes = encode_jb2_dict_with_options(&src, &[], &probe_opts());
assert_ne!(
default_bytes, probe_bytes,
"probe should fire on near twins"
);
let decoded = jb2::decode(&probe_bytes, None).expect("probe decode failed");
assert_bitmaps_eq(&src, &decoded);
}
#[cfg(feature = "experimental")]
fn same_size_opts() -> Jb2EncodeOptions {
Jb2EncodeOptions {
same_size_rec6: Some(0.05),
..Jb2EncodeOptions::default()
}
}
#[test]
fn lossy_text_preset_is_lossy_and_smaller() {
assert_eq!(Jb2EncodeOptions::lossy_text().lossy_threshold, 0.02);
assert_eq!(
Jb2EncodeOptions::with_lossy_threshold(0.07).lossy_threshold,
0.07
);
let mut src = Bitmap::new(64, 60);
for (oy, notch) in [(2u32, false), (30u32, true)] {
for y in 0..24 {
for x in 0..14 {
if notch && x >= 12 && y >= 22 {
continue;
}
src.set(4 + x, oy + y, true);
}
}
}
let lossless = encode_jb2_dict_with_options(&src, &[], &Jb2EncodeOptions::default());
let lossy = encode_jb2_dict_with_options(&src, &[], &Jb2EncodeOptions::lossy_text());
assert!(
lossy.len() < lossless.len(),
"lossy_text should shrink a near-twin page: {} vs {}",
lossy.len(),
lossless.len()
);
assert!(
jb2::decode(&lossy, None).is_ok(),
"lossy output must decode"
);
}
#[test]
fn despeckle_off_is_byte_identical() {
let mut src = make_bitmap(80, 40, |x, y| {
let a = (4..16).contains(&x) && (4..28).contains(&y);
let b = (40..53).contains(&x) && (4..28).contains(&y);
a || b
});
src.set(70, 5, true); src.set(75, 35, true);
let shipped = encode_jb2_dict(&src);
let opt = encode_jb2_dict_with_options(&src, &[], &Jb2EncodeOptions::default());
assert_eq!(shipped, opt, "default options must match shipped output");
let opt_explicit_none = encode_jb2_dict_with_options(
&src,
&[],
&Jb2EncodeOptions {
despeckle: None,
..Jb2EncodeOptions::default()
},
);
assert_eq!(shipped, opt_explicit_none);
}
#[test]
fn despeckle_removes_isolated_1px_specks_and_shrinks_output() {
let mut src = Bitmap::new(64, 40);
for y in 4..28 {
for x in 4..18 {
src.set(x, y, true);
}
}
let specks = [(30u32, 2u32), (35, 10), (40, 20), (50, 5), (55, 30)];
for &(x, y) in &specks {
src.set(x, y, true);
}
let lossless = encode_jb2_dict_with_options(&src, &[], &Jb2EncodeOptions::default());
let despeckled =
encode_jb2_dict_with_options(&src, &[], &Jb2EncodeOptions::with_despeckle(2));
assert!(
despeckled.len() < lossless.len(),
"despeckling 1px dust should shrink the stream: despeckled={} lossless={}",
despeckled.len(),
lossless.len()
);
let decoded = jb2::decode(&despeckled, None).expect("despeckled decode failed");
assert_eq!(decoded.width, src.width);
assert_eq!(decoded.height, src.height);
for &(x, y) in &specks {
assert!(!decoded.get(x, y), "speck at ({x},{y}) should be removed");
}
for y in 4..28 {
for x in 4..18 {
assert!(
decoded.get(x, y),
"glyph pixel ({x},{y}) must survive despeckle"
);
}
}
}
#[test]
fn despeckle_preserves_punctuation_and_diacritic_dots() {
let mut src = Bitmap::new(80, 50);
for y in 10..30 {
for x in 10..13 {
src.set(x, y, true);
}
}
let dot_px: Vec<(u32, u32)> = (4..8).flat_map(|y| (9..13).map(move |x| (x, y))).collect();
for &(x, y) in &dot_px {
src.set(x, y, true);
}
let period_px: Vec<(u32, u32)> = (40..44)
.flat_map(|y| (40..44).map(move |x| (x, y)))
.collect();
for &(x, y) in &period_px {
src.set(x, y, true);
}
let speck = (70u32, 45u32);
src.set(speck.0, speck.1, true);
for max_px in [2u32, 4, 8] {
let opts = Jb2EncodeOptions::with_despeckle(max_px);
let enc = encode_jb2_dict_with_options(&src, &[], &opts);
let decoded = jb2::decode(&enc, None)
.unwrap_or_else(|e| panic!("despeckle={max_px} decode failed: {e:?}"));
for &(x, y) in &dot_px {
assert!(
decoded.get(x, y),
"despeckle={max_px}: i-dot pixel ({x},{y}) must survive"
);
}
for &(x, y) in &period_px {
assert!(
decoded.get(x, y),
"despeckle={max_px}: period pixel ({x},{y}) must survive"
);
}
assert!(
!decoded.get(speck.0, speck.1),
"despeckle={max_px}: 1px dust speck must be removed"
);
}
}
#[test]
fn lossy_scan_preset_values() {
let preset = Jb2EncodeOptions::lossy_scan();
assert_eq!(preset.despeckle, Some(8));
assert_eq!(preset.lossy_threshold, 0.06);
}
#[test]
fn same_size_rec6_off_is_byte_identical() {
let src = make_bitmap(80, 40, |x, y| {
let a = (4..16).contains(&x) && (4..28).contains(&y);
let b = (40..53).contains(&x) && (4..28).contains(&y);
a || b
});
let shipped = encode_jb2_dict(&src);
let opt = encode_jb2_dict_with_options(&src, &[], &Jb2EncodeOptions::default());
assert_eq!(shipped, opt, "default options must match shipped output");
}
#[cfg(feature = "experimental")]
#[test]
fn same_size_rec6_roundtrips_near_twins() {
let mut src = Bitmap::new(64, 60);
let draw = |bm: &mut Bitmap, ox: u32, oy: u32, notch: bool| {
for y in 0..24 {
for x in 0..14 {
if notch && x >= 12 && y >= 22 {
continue;
}
bm.set(ox + x, oy + y, true);
}
}
};
draw(&mut src, 4, 2, false); draw(&mut src, 4, 30, true);
let default_bytes = encode_jb2_dict_with_options(&src, &[], &Jb2EncodeOptions::default());
let same_bytes = encode_jb2_dict_with_options(&src, &[], &same_size_opts());
assert_ne!(
default_bytes, same_bytes,
"same_size_rec6 should emit a rec-6 refinement, changing the stream"
);
let decoded = jb2::decode(&same_bytes, None).expect("same-size decode failed");
assert_bitmaps_eq(&src, &decoded);
}
#[cfg(feature = "experimental")]
fn dense_near_twin_bitmap() -> Bitmap {
let mut src = Bitmap::new(600, 400);
let draw = |bm: &mut Bitmap, ox: u32, oy: u32, w: u32, notch: bool| {
for y in 0..24 {
for x in 0..w {
if notch && x + 2 >= w && y >= 22 {
continue;
}
bm.set(ox + x, oy + y, true);
}
}
};
for row in 0..10u32 {
let w = 14 + row;
draw(&mut src, 4, 2 + row * 28, w, false); draw(&mut src, 4 + w + 6, 2 + row * 28, w, true); }
src
}
#[cfg(feature = "experimental")]
fn sparse_no_twin_bitmap() -> Bitmap {
let mut src = Bitmap::new(400, 400);
for row in 0..10u32 {
let w = 8 + row;
let h = 10 + row;
for y in 0..h {
for x in 0..w {
src.set(4 + x, 2 + row * 20 + y, true);
}
}
}
src
}
#[cfg(feature = "experimental")]
#[test]
fn same_size_rec6_auto_fires_on_dense_near_twins() {
let src = dense_near_twin_bitmap();
let density = probe_same_size_rec6_density(&src, &[], SAME_SIZE_REC6_AUTO_SAMPLE_CCS);
assert!(
density >= SAME_SIZE_REC6_AUTO_DENSITY_THRESHOLD,
"dense synthetic input should clear the auto-policy threshold: density={density}"
);
let opts = Jb2EncodeOptions::same_size_rec6_auto(&src, &[]);
assert_eq!(
opts.same_size_rec6,
Some(SAME_SIZE_REC6_AUTO_FRAC),
"auto-policy must enable same_size_rec6 on dense near-twin input"
);
let default_bytes = encode_jb2_dict_with_options(&src, &[], &Jb2EncodeOptions::default());
let auto_bytes = encode_jb2_dict_with_options(&src, &[], &opts);
assert_ne!(
default_bytes, auto_bytes,
"auto policy should divert near-twins to rec-6, changing the stream"
);
let decoded = jb2::decode(&auto_bytes, None).expect("auto-policy decode failed");
assert_bitmaps_eq(&src, &decoded);
}
#[cfg(feature = "experimental")]
#[test]
fn same_size_rec6_auto_stays_off_on_sparse_input() {
let src = sparse_no_twin_bitmap();
let density = probe_same_size_rec6_density(&src, &[], SAME_SIZE_REC6_AUTO_SAMPLE_CCS);
assert!(
density < SAME_SIZE_REC6_AUTO_DENSITY_THRESHOLD,
"sparse synthetic input should stay below the auto-policy threshold: density={density}"
);
let opts = Jb2EncodeOptions::same_size_rec6_auto(&src, &[]);
assert_eq!(
opts.same_size_rec6, None,
"auto-policy must leave same_size_rec6 off on sparse input"
);
let default_bytes = encode_jb2_dict_with_options(&src, &[], &Jb2EncodeOptions::default());
let auto_bytes = encode_jb2_dict_with_options(&src, &[], &opts);
assert_eq!(
default_bytes, auto_bytes,
"auto policy must stay off (byte-identical) on sparse input"
);
}
#[cfg(feature = "experimental")]
#[test]
fn probe_same_size_rec6_density_bounds_the_scan() {
let src = dense_near_twin_bitmap();
let full = same_size_refinement_scan(&src, &[], None);
let bounded = same_size_refinement_scan(&src, &[], Some(2));
assert!(
bounded.fresh_ccs <= 2,
"bounded scan must stop at the fresh-CC cap: got {}",
bounded.fresh_ccs
);
assert!(
full.fresh_ccs > bounded.fresh_ccs,
"unbounded scan should see strictly more fresh CCs than the capped one"
);
let full_density = full.near_le_5pct as f32 / full.fresh_ccs as f32;
let bounded_density = bounded.near_le_5pct as f32 / bounded.fresh_ccs as f32;
assert!((full_density - bounded_density).abs() < 1e-6);
}
fn render_glyph(bm: &mut Bitmap, x: u32, y: u32, glyph: &[&[u8]]) {
for (gy, row) in glyph.iter().enumerate() {
for (gx, &c) in row.iter().enumerate() {
if c == b'#' {
bm.set(x + gx as u32, y + gy as u32, true);
}
}
}
}
fn glyph_a() -> Vec<&'static [u8]> {
vec![
b" ## " as &[u8],
b"# #" as &[u8],
b"####" as &[u8],
b"# #" as &[u8],
b"# #" as &[u8],
]
}
fn glyph_b() -> Vec<&'static [u8]> {
vec![
b"### " as &[u8],
b"# #" as &[u8],
b"### " as &[u8],
b"# #" as &[u8],
b"### " as &[u8],
]
}
fn assert_decoded_eq(src: &Bitmap, decoded: &Bitmap) {
assert_eq!(src.width, decoded.width, "width mismatch");
assert_eq!(src.height, decoded.height, "height mismatch");
let mut mismatches = 0u32;
for y in 0..src.height {
for x in 0..src.width {
if src.get(x, y) != decoded.get(x, y) {
mismatches += 1;
}
}
}
assert_eq!(mismatches, 0, "{mismatches} pixel mismatches");
}
#[test]
fn djbz_roundtrip_two_glyphs() {
let mut a = Bitmap::new(4, 5);
render_glyph(&mut a, 0, 0, &glyph_a());
let mut b = Bitmap::new(4, 5);
render_glyph(&mut b, 0, 0, &glyph_b());
let djbz = encode_jb2_djbz(&[a.clone(), b.clone()]);
assert!(!djbz.is_empty());
let dict = jb2::decode_dict(&djbz, None).expect("decode_dict");
let mut page = Bitmap::new(20, 8);
render_glyph(&mut page, 2, 2, &glyph_a());
render_glyph(&mut page, 10, 2, &glyph_b());
let sjbz = encode_jb2_dict_with_shared(&page, &[a, b]);
let decoded = jb2::decode(&sjbz, Some(&dict)).expect("decode");
assert_decoded_eq(&page, &decoded);
}
#[test]
fn cluster_promotes_only_repeated_glyphs() {
let mut p1 = Bitmap::new(20, 10);
render_glyph(&mut p1, 2, 2, &glyph_a());
render_glyph(&mut p1, 10, 2, &glyph_b());
let mut p2 = Bitmap::new(20, 10);
render_glyph(&mut p2, 2, 2, &glyph_a());
let shared = cluster_shared_symbols(&[p1, p2], 2);
assert_eq!(shared.len(), 1, "only A should cross the threshold");
assert_eq!(shared[0].width, 4);
assert_eq!(shared[0].height, 5);
}
fn glyph_box8() -> Vec<&'static [u8]> {
vec![
b"########" as &[u8],
b"# #" as &[u8],
b"# #" as &[u8],
b"# #" as &[u8],
b"# #" as &[u8],
b"# #" as &[u8],
b"# #" as &[u8],
b"########" as &[u8],
]
}
#[test]
fn cluster_tunable_keeps_near_duplicate_large_glyphs_separate() {
let mut p1 = Bitmap::new(20, 20);
render_glyph(&mut p1, 4, 4, &glyph_box8());
p1.set(5, 4, false); let mut p2 = Bitmap::new(20, 20);
render_glyph(&mut p2, 4, 4, &glyph_box8());
p2.set(6, 4, false);
let shared = cluster_shared_symbols_tunable(&[p1.clone(), p2.clone()], 2, 4);
assert!(
shared.is_empty(),
"tunable clustering must not promote noisy near-dupes"
);
let shared_exact = cluster_shared_symbols(&[p1, p2], 2);
assert!(
shared_exact.is_empty(),
"byte-exact default must not promote noisy near-dupes"
);
}
#[test]
fn lossy_threshold_substitutes_near_duplicate_with_rec7() {
let base = make_bitmap(6, 6, |_, _| true);
let near_dup = make_bitmap(6, 6, |x, y| !(x == 3 && y == 3));
let another = make_bitmap(6, 6, |x, y| !(x == 1 && y == 4));
let stamp = |page: &mut Bitmap, ox: u32, oy: u32, src: &Bitmap| {
for y in 0..src.height {
for x in 0..src.width {
if src.get(x, y) {
page.set(ox + x, oy + y, true);
}
}
}
};
let mut page = make_bitmap(40, 12, |_, _| false);
stamp(&mut page, 2, 2, &base);
stamp(&mut page, 14, 2, &near_dup);
stamp(&mut page, 26, 2, &another);
let lossless = encode_jb2_dict_with_options(
&page,
&[],
&Jb2EncodeOptions {
lossy_threshold: 0.0,
..Jb2EncodeOptions::default()
},
);
let lossy = encode_jb2_dict_with_options(
&page,
&[],
&Jb2EncodeOptions {
lossy_threshold: 0.05,
..Jb2EncodeOptions::default()
},
);
assert!(
lossy.len() < lossless.len(),
"lossy should be smaller than lossless: lossy={} lossless={}",
lossy.len(),
lossless.len()
);
let decoded = jb2::decode(&lossy, None).expect("lossy decode");
assert_eq!(decoded.width, page.width);
assert_eq!(decoded.height, page.height);
assert!(
decoded.get(17, 5),
"lossy decode should fill base at (17,5)"
);
assert!(
decoded.get(27, 6),
"lossy decode should fill base at (27,6)"
);
let decoded_lossless = jb2::decode(&lossless, None).expect("lossless decode");
assert!(
!decoded_lossless.get(17, 5),
"lossless should preserve hole at (17,5)"
);
assert!(
!decoded_lossless.get(27, 6),
"lossless should preserve hole at (27,6)"
);
}
#[test]
fn analyze_jb2_cc_stats_classifies_records() {
let shared_glyph = make_bitmap(6, 6, |_, _| true);
let near_dup = make_bitmap(6, 6, |x, y| !(x == 3 && y == 3));
let unrelated = make_bitmap(5, 5, |_, _| true);
let stamp = |page: &mut Bitmap, ox: u32, oy: u32, src: &Bitmap| {
for y in 0..src.height {
for x in 0..src.width {
if src.get(x, y) {
page.set(ox + x, oy + y, true);
}
}
}
};
let mut page = make_bitmap(40, 12, |_, _| false);
stamp(&mut page, 2, 2, &shared_glyph);
stamp(&mut page, 14, 2, &near_dup);
stamp(&mut page, 26, 2, &unrelated);
let stats = analyze_jb2_cc_stats(&page, &[shared_glyph]);
assert_eq!(stats.rec_7_exact, 1, "expected one byte-exact rec-7 hit");
assert_eq!(
stats.rec_6_refine_shared, 0,
"shared-dict near matches must not use rec-6"
);
assert_eq!(stats.rec_6_refine_local, 0);
assert!(
stats.rec_1_new >= 2,
"expected near shared hit and unrelated CC to use rec-1 (got {})",
stats.rec_1_new
);
assert!(stats.rec_6_hamming.is_empty());
assert!(stats.pixels_rec_7 > 0);
assert_eq!(stats.pixels_rec_6, 0);
assert!(stats.pixels_rec_1 > 0);
assert_eq!(
stats.total_ccs,
stats.rec_1_new
+ stats.rec_6_refine_local
+ stats.rec_6_refine_shared
+ stats.rec_7_exact
);
}
#[cfg(feature = "experimental")]
#[test]
fn analyze_cross_size_refinement_counts_near_size_candidates() {
let shared_glyph = make_bitmap(6, 6, |_, _| true);
let taller_near = make_bitmap(6, 7, |_, _| true);
let unrelated = make_bitmap(12, 12, |x, y| x == y);
let stamp = |page: &mut Bitmap, ox: u32, oy: u32, src: &Bitmap| {
for y in 0..src.height {
for x in 0..src.width {
if src.get(x, y) {
page.set(ox + x, oy + y, true);
}
}
}
};
let mut page = make_bitmap(40, 16, |_, _| false);
stamp(&mut page, 2, 2, &shared_glyph);
stamp(&mut page, 14, 2, &taller_near);
stamp(&mut page, 26, 2, &unrelated);
let stats = analyze_jb2_cross_size_refinement(&page, &[shared_glyph], 1, 0.05);
assert_eq!(stats.near_matches, 1);
assert_eq!(stats.near_match_pixels, 42);
assert!(stats.estimated_rec1_bytes > 0);
assert!(stats.estimated_cross_size_rec6_bytes > 0);
assert!(
stats.candidate_ccs >= stats.near_matches,
"near matches must be a subset of cross-size candidates"
);
}
#[test]
fn cluster_shared_symbols_caps_total_pixel_budget() {
let cap = SHARED_DICT_PIXEL_BUDGET;
let glyph_w: u32 = 96;
let glyph_h: u32 = 96;
let pixels_per_glyph = (glyph_w as usize) * (glyph_h as usize);
let n_glyphs = (cap / pixels_per_glyph) + 64;
let glyphs: Vec<Bitmap> = (0..n_glyphs)
.map(|i| {
make_bitmap(glyph_w, glyph_h, |x, y| {
let v =
(x.wrapping_mul(2654435761) ^ y.wrapping_mul(40503)).wrapping_add(i as u32);
(v & 0xff) < 128
})
})
.collect();
let page_w: u32 = 1024;
let make_page = |start: usize, count: usize| -> Bitmap {
let cols = (page_w / (glyph_w + 2)).max(1) as usize;
let rows = count.div_ceil(cols);
let canvas_h = (rows as u32) * (glyph_h + 2) + 2;
let mut canvas = Bitmap::new(page_w, canvas_h);
for (i, g) in glyphs[start..start + count].iter().enumerate() {
let col = (i % cols) as u32;
let row = (i / cols) as u32;
let ox = col * (glyph_w + 2) + 1;
let oy = row * (glyph_h + 2) + 1;
for y in 0..glyph_h {
for x in 0..glyph_w {
if g.get(x, y) {
canvas.set(ox + x, oy + y, true);
}
}
}
}
canvas
};
let p1 = make_page(0, n_glyphs);
let p2 = make_page(0, n_glyphs);
let shared = cluster_shared_symbols_tunable(&[p1, p2], 2, 0);
let total: usize = shared
.iter()
.map(|s| (s.width as usize) * (s.height as usize))
.sum();
assert!(
total <= cap,
"cluster output {total} px must respect MAX_TOTAL_SYMBOL_PIXELS={cap}"
);
let djbz = encode_jb2_djbz(&shared);
crate::decode_dict(&djbz, None)
.expect("encoded shared Djbz must round-trip through decode_dict");
}
}