1use std::collections::HashMap;
19
20use lopdf::{Dictionary, Document, Object};
21use tiny_skia::Pixmap;
22
23use super::color::{to_u8, CmykCache, ColorSpace};
24use super::jpx;
25use super::objects::{as_stream, get2, get_bool, get_bool2, get_int, get_int2, nums};
26use crate::raster::{fax, filters, jpeg};
27
28pub struct DecodedImage {
29 pub pixmap: Pixmap,
30 pub src_width: u32,
32 pub src_height: u32,
33}
34
35struct Samples {
37 width: usize,
38 height: usize,
39 bpc: u32,
40 ncomp: usize,
41 data: Vec<u8>,
42 stride: usize,
43 adobe: bool,
45 inline_alpha: Option<Vec<u8>>,
48}
49
50impl Samples {
51 fn get(&self, x: usize, y: usize, c: usize) -> u32 {
52 let row = &self.data[y * self.stride..];
53 match self.bpc {
54 8 => u32::from(*row.get(x * self.ncomp + c).unwrap_or(&0)),
55 16 => {
56 let i = (x * self.ncomp + c) * 2;
57 (u32::from(*row.get(i).unwrap_or(&0)) << 8)
58 | u32::from(*row.get(i + 1).unwrap_or(&0))
59 }
60 bpc => {
61 let bit = (x * self.ncomp + c) * bpc as usize;
62 let byte = *row.get(bit / 8).unwrap_or(&0);
63 u32::from((byte >> (8 - bpc as usize - bit % 8)) & ((1u8 << bpc) - 1))
64 }
65 }
66 }
67
68 fn max(&self) -> f64 {
69 ((1u64 << self.bpc) - 1) as f64
70 }
71}
72
73enum Loaded {
74 Samples(Samples),
75 Placeholder,
76}
77
78pub struct LoadedImage {
80 width: usize,
81 height: usize,
82 is_mask: bool,
83 decode_arr: Option<Vec<f64>>,
84 samples: Loaded,
85 cs: Option<ColorSpace>,
87 alpha: Option<Vec<u8>>,
89}
90
91impl LoadedImage {
92 pub fn bytes(&self) -> usize {
94 let s = match &self.samples {
95 Loaded::Samples(s) => s.data.len(),
96 Loaded::Placeholder => 0,
97 };
98 s + self.alpha.as_ref().map_or(0, Vec::len)
99 }
100}
101
102pub fn declared_size(doc: &Document, d: &Dictionary) -> Option<(i64, i64)> {
107 Some((
108 get_int2(doc, d, b"Width", b"W")?,
109 get_int2(doc, d, b"Height", b"H")?,
110 ))
111}
112
113pub fn codec_reduction_shift(
125 drawn_width_units: f64,
126 drawn_height_units: f64,
127 source_width: i64,
128 source_height: i64,
129 target_pixels_per_unit: f64,
130) -> u32 {
131 const MAX_SHIFT: u32 = 3;
132 const MIN_SOURCE_EXTENT: i64 = 64;
133 if target_pixels_per_unit <= 0.0
134 || source_width <= 0
135 || source_height <= 0
136 || drawn_width_units <= 0.0
137 || drawn_height_units <= 0.0
138 {
139 return 0;
140 }
141 let target_width = drawn_width_units * target_pixels_per_unit;
142 let target_height = drawn_height_units * target_pixels_per_unit;
143 let mut shift = 0u32;
144 while shift < MAX_SHIFT {
145 let next_width = source_width >> (shift + 1);
146 let next_height = source_height >> (shift + 1);
147 if next_width < MIN_SOURCE_EXTENT || next_height < MIN_SOURCE_EXTENT {
148 break;
149 }
150 if (next_width as f64) < target_width || (next_height as f64) < target_height {
151 break;
152 }
153 shift += 1;
154 }
155 shift
156}
157
158fn may_reduce_decode(doc: &Document, d: &Dictionary, res: Option<&Dictionary>) -> bool {
164 if get_bool2(doc, d, b"ImageMask", b"IM").unwrap_or(false) {
165 return false;
166 }
167 if d.has(b"SMask") || d.has(b"Mask") {
168 return false;
169 }
170 !matches!(
171 get2(doc, d, b"ColorSpace", b"CS").and_then(|o| ColorSpace::parse(doc, o, res)),
172 Some(ColorSpace::Indexed { .. })
173 )
174}
175
176pub fn load(
181 doc: &Document,
182 stream: &lopdf::Stream,
183 res: Option<&Dictionary>,
184 reduction_shift: u32,
185) -> Result<LoadedImage, String> {
186 let d = &stream.dict;
187 let (width, height) = declared_size(doc, d).ok_or("Width")?;
188 if width <= 0
189 || height <= 0
190 || width > 1 << 16
191 || height > 1 << 16
192 || width * height > 80_000_000
193 {
194 return Err("image size".into());
195 }
196 let is_mask = get_bool2(doc, d, b"ImageMask", b"IM").unwrap_or(false);
197 let decode_arr: Option<Vec<f64>> = get2(doc, d, b"Decode", b"D").and_then(|o| nums(doc, o));
198 let shift = if reduction_shift > 0 && may_reduce_decode(doc, d, res) {
199 reduction_shift
200 } else {
201 0
202 };
203 let mut samples = load_samples(doc, stream, res, is_mask, shift)?;
204 let (w, h) = match &samples {
207 Loaded::Samples(s) => (s.width, s.height),
208 Loaded::Placeholder => (width as usize, height as usize),
209 };
210 if is_mask {
211 return Ok(LoadedImage {
212 width: w,
213 height: h,
214 is_mask,
215 decode_arr,
216 samples,
217 cs: None,
218 alpha: None,
219 });
220 }
221 let (cs, alpha) = match &mut samples {
222 Loaded::Placeholder => (None, None),
223 Loaded::Samples(s) => {
224 let cs_obj = get2(doc, d, b"ColorSpace", b"CS");
225 let guess = || match s.ncomp {
226 3 => ColorSpace::DeviceRGB,
227 4 => ColorSpace::DeviceCMYK,
228 _ => ColorSpace::DeviceGray,
229 };
230 let cs = match cs_obj {
231 Some(o) => ColorSpace::parse(doc, o, res).unwrap_or_else(guess),
232 None => guess(),
233 };
234 let cs = if s.ncomp == 4 && cs.components() != 4 {
240 if s.adobe {
241 for v in &mut s.data {
242 *v = 255 - *v;
243 }
244 }
245 ColorSpace::DeviceCMYK
246 } else {
247 cs
248 };
249 let alpha = match alpha_plane(doc, d, res, s, w, h)? {
252 Some(a) => Some(a),
253 None => s.inline_alpha.take(),
254 };
255 (Some(cs), alpha)
256 }
257 };
258 Ok(LoadedImage {
259 width: w,
260 height: h,
261 is_mask,
262 decode_arr,
263 samples,
264 cs,
265 alpha,
266 })
267}
268
269pub fn rasterize(
273 img: &LoadedImage,
274 fill: [u8; 3],
275 target: Option<(u32, u32)>,
276 cmyk: &mut CmykCache,
277) -> Result<DecodedImage, String> {
278 let (w, h) = (img.width, img.height);
279 let (fx, fy) = match target {
281 Some((tw, th)) if tw > 0 && th > 0 => (
282 ((w as u32) / tw).max(1) as usize,
283 ((h as u32) / th).max(1) as usize,
284 ),
285 _ => (1, 1),
286 };
287 let out_w = (w / fx).max(1);
288 let out_h = (h / fy).max(1);
289 let mut pixmap = Pixmap::new(out_w as u32, out_h as u32).ok_or("pixmap")?;
290 let done = |pixmap| {
291 Ok(DecodedImage {
292 pixmap,
293 src_width: w as u32,
294 src_height: h as u32,
295 })
296 };
297
298 if img.is_mask {
299 let paint_on_one = img
301 .decode_arr
302 .as_ref()
303 .is_some_and(|v| v.first().is_some_and(|x| *x >= 0.5));
304 if let Loaded::Samples(s) = &img.samples {
305 let px = pixmap.pixels_mut();
306 for oy in 0..out_h {
307 for ox in 0..out_w {
308 let mut cov = 0u32;
309 let mut n = 0u32;
310 for y in oy * fy..((oy + 1) * fy).min(h) {
311 for x in ox * fx..((ox + 1) * fx).min(w) {
312 let v = s.get(x, y, 0);
313 let on = (v != 0) == paint_on_one;
314 cov += u32::from(on);
315 n += 1;
316 }
317 }
318 let a = (cov * 255).checked_div(n).unwrap_or(0) as u8;
319 px[oy * out_w + ox] = premul(fill, a);
320 }
321 }
322 }
323 return done(pixmap);
324 }
325
326 let (Loaded::Samples(s), Some(cs)) = (&img.samples, &img.cs) else {
327 for p in pixmap.pixels_mut() {
329 *p = premul([128, 128, 128], 255);
330 }
331 return done(pixmap);
332 };
333
334 let ncomp = s.ncomp.min(cs.components()).max(1);
335 let max = s.max();
336 let default_decode = cs.default_decode(s.bpc);
337 let decode = match &img.decode_arr {
338 Some(v) if v.len() >= 2 * ncomp => v.clone(),
339 _ => default_decode.clone(),
340 };
341 let is_default_decode = decode
342 .iter()
343 .zip(default_decode.iter())
344 .all(|(a, b)| (a - b).abs() < 1e-9);
345 let alpha = &img.alpha;
346
347 let direct = alpha.is_none()
350 && s.bpc == 8
351 && is_default_decode
352 && matches!(
353 (cs, s.ncomp),
354 (ColorSpace::DeviceGray, 1) | (ColorSpace::DeviceRGB, 3)
355 );
356 if direct {
357 let px = pixmap.pixels_mut();
358 let nc = s.ncomp;
359 for oy in 0..out_h {
360 for ox in 0..out_w {
361 let (mut r, mut g, mut b, mut n) = (0u32, 0u32, 0u32, 0u32);
362 for y in oy * fy..((oy + 1) * fy).min(h) {
363 let row = &s.data[y * s.stride..];
364 for x in ox * fx..((ox + 1) * fx).min(w) {
365 let i = x * nc;
366 if nc == 1 {
367 let v = u32::from(row[i]);
368 r += v;
369 g += v;
370 b += v;
371 } else {
372 r += u32::from(row[i]);
373 g += u32::from(row[i + 1]);
374 b += u32::from(row[i + 2]);
375 }
376 n += 1;
377 }
378 }
379 let n = n.max(1);
380 px[oy * out_w + ox] = tiny_skia::PremultipliedColorU8::from_rgba(
381 (r / n) as u8,
382 (g / n) as u8,
383 (b / n) as u8,
384 255,
385 )
386 .unwrap_or(tiny_skia::PremultipliedColorU8::TRANSPARENT);
387 }
388 }
389 return done(pixmap);
390 }
391
392 let mut memo: HashMap<u64, [u8; 3]> = HashMap::new();
394 let mut convert = |vals: &[u32]| -> [u8; 3] {
395 let key = vals
396 .iter()
397 .take(4)
398 .fold(0u64, |a, &v| (a << 16) | u64::from(v & 0xFFFF));
399 if let Some(c) = memo.get(&key) {
400 return *c;
401 }
402 let rgb = match (cs, s.bpc, is_default_decode) {
403 (ColorSpace::DeviceRGB, 8, true) => [vals[0] as u8, vals[1] as u8, vals[2] as u8],
404 (ColorSpace::DeviceGray, 8, true) => [vals[0] as u8; 3],
405 (ColorSpace::DeviceCMYK, 8, true) => {
406 cmyk.rgb8(vals[0] as u8, vals[1] as u8, vals[2] as u8, vals[3] as u8)
407 }
408 _ => {
409 let comps: Vec<f64> = (0..ncomp)
410 .map(|i| {
411 let (dmin, dmax) = (decode[2 * i], decode[2 * i + 1]);
412 dmin + f64::from(vals[i]) * (dmax - dmin) / max
413 })
414 .collect();
415 match cs.to_rgb(&comps) {
416 Some(c) => [to_u8(c[0]), to_u8(c[1]), to_u8(c[2])],
417 None => [255, 255, 255],
418 }
419 }
420 };
421 if memo.len() < 1 << 18 {
422 memo.insert(key, rgb);
423 }
424 rgb
425 };
426
427 let px = pixmap.pixels_mut();
428 let mut vals = vec![0u32; ncomp.max(4)];
429 for oy in 0..out_h {
430 for ox in 0..out_w {
431 let (mut r, mut g, mut b, mut a, mut n) = (0u32, 0u32, 0u32, 0u32, 0u32);
432 for y in oy * fy..((oy + 1) * fy).min(h) {
433 for x in ox * fx..((ox + 1) * fx).min(w) {
434 for (c, v) in vals.iter_mut().enumerate().take(ncomp) {
435 *v = s.get(x, y, c);
436 }
437 let rgb = convert(&vals[..ncomp.max(1)]);
438 let al = match alpha {
439 Some(pl) => u32::from(pl[y * w + x]),
440 None => 255,
441 };
442 r += u32::from(rgb[0]) * al / 255;
444 g += u32::from(rgb[1]) * al / 255;
445 b += u32::from(rgb[2]) * al / 255;
446 a += al;
447 n += 1;
448 }
449 }
450 let n = n.max(1);
451 px[oy * out_w + ox] = tiny_skia::PremultipliedColorU8::from_rgba(
452 (r / n).min(255) as u8,
453 (g / n).min(255) as u8,
454 (b / n).min(255) as u8,
455 (a / n).min(255) as u8,
456 )
457 .unwrap_or(tiny_skia::PremultipliedColorU8::TRANSPARENT);
458 }
459 }
460 done(pixmap)
461}
462
463pub fn decode(
465 doc: &Document,
466 stream: &lopdf::Stream,
467 res: Option<&Dictionary>,
468 fill: [u8; 3],
469 target: Option<(u32, u32)>,
470 cmyk: &mut CmykCache,
471) -> Result<DecodedImage, String> {
472 let img = load(doc, stream, res, 0)?;
473 rasterize(&img, fill, target, cmyk)
474}
475
476fn premul(rgb: [u8; 3], a: u8) -> tiny_skia::PremultipliedColorU8 {
477 let m = |c: u8| ((u32::from(c) * u32::from(a) + 127) / 255) as u8;
478 tiny_skia::PremultipliedColorU8::from_rgba(m(rgb[0]), m(rgb[1]), m(rgb[2]), a)
479 .unwrap_or(tiny_skia::PremultipliedColorU8::TRANSPARENT)
480}
481
482fn load_samples(
484 doc: &Document,
485 stream: &lopdf::Stream,
486 res: Option<&Dictionary>,
487 is_mask: bool,
488 reduction_shift: u32,
489) -> Result<Loaded, String> {
490 let d = &stream.dict;
491 let w = get_int2(doc, d, b"Width", b"W").unwrap_or(0).max(0) as usize;
492 let h = get_int2(doc, d, b"Height", b"H").unwrap_or(0).max(0) as usize;
493 let chain = filters::filters(doc, d);
494 let (data, codec) =
495 filters::apply(doc, &stream.content, &chain).map_err(|e| format!("filter {e:?}"))?;
496 let bpc_dict = if is_mask {
497 1
498 } else {
499 get_int2(doc, d, b"BitsPerComponent", b"BPC")
500 .unwrap_or(8)
501 .clamp(1, 16) as u32
502 };
503 let cs_ncomp = if is_mask {
504 1
505 } else {
506 get2(doc, d, b"ColorSpace", b"CS")
507 .and_then(|o| ColorSpace::parse(doc, o, res))
508 .map(|c| c.components())
509 .unwrap_or(1)
510 };
511 match codec {
512 None => {
513 let bpc = match bpc_dict {
514 1 | 2 | 4 | 8 | 16 => bpc_dict,
515 _ => 8,
516 };
517 let stride = (w * cs_ncomp * bpc as usize).div_ceil(8);
518 let mut data = data;
519 if data.len() < stride * h {
520 data.resize(stride * h, 0);
522 }
523 Ok(Loaded::Samples(Samples {
524 width: w,
525 height: h,
526 bpc,
527 ncomp: cs_ncomp,
528 data,
529 stride,
530 adobe: false,
531 inline_alpha: None,
532 }))
533 }
534 Some(codec) if codec.name == "DCTDecode" => {
535 let transform = codec
536 .parms
537 .as_ref()
538 .and_then(|p| get_int(doc, p, b"ColorTransform"))
539 .unwrap_or(1)
540 != 0;
541 match jpeg::decode(&data, transform, 1u32 << reduction_shift.min(3)) {
542 Ok(img) => {
543 let ncomp = img.channels;
544 Ok(Loaded::Samples(Samples {
546 width: img.width,
547 height: img.height,
548 bpc: 8,
549 ncomp,
550 stride: img.width * ncomp,
551 adobe: img.adobe_inverted,
552 inline_alpha: None,
553 data: img.data,
554 }))
555 }
556 Err(e) => {
557 docling_core::debug_log!("docling-pdf render: JPEG not decoded ({e:?})");
558 Ok(Loaded::Placeholder)
559 }
560 }
561 }
562 Some(codec) if codec.name == "CCITTFaxDecode" => {
563 let p = codec.parms.as_ref();
564 let pi = |k: &[u8], default: i64| p.and_then(|p| get_int(doc, p, k)).unwrap_or(default);
565 let pb = |k: &[u8]| p.and_then(|p| get_bool(doc, p, k)).unwrap_or(false);
566 let mut rows = pi(b"Rows", 0);
567 if rows > i64::from(u16::MAX) {
568 rows = 0;
569 }
570 let params = fax::Params {
571 k: pi(b"K", 0) as i32,
572 end_of_line: pb(b"EndOfLine"),
573 byte_align: pb(b"EncodedByteAlign"),
574 black_is_1: pb(b"BlackIs1"),
575 columns: pi(b"Columns", 1728).clamp(1, 65535) as usize,
576 rows: rows.max(0) as usize,
577 };
578 let lines = fax::decode(&data, ¶ms, h);
579 let stride = w.div_ceil(8);
580 let mut out = vec![0u8; stride * h];
581 for (y, row) in out.chunks_exact_mut(stride).enumerate() {
582 match lines.get(y) {
583 Some(Some(line)) => {
584 let n = stride.min(line.len());
585 row[..n].copy_from_slice(&line[..n]);
586 }
587 _ => row.fill(if params.black_is_1 { 0x00 } else { 0xff }),
589 }
590 }
591 Ok(Loaded::Samples(Samples {
592 width: w,
593 height: h,
594 bpc: 1,
595 ncomp: 1,
596 data: out,
597 stride,
598 adobe: false,
599 inline_alpha: None,
600 }))
601 }
602 Some(codec) if codec.name == "JPXDecode" => {
603 let indexed = !is_mask
608 && matches!(
609 get2(doc, d, b"ColorSpace", b"CS").and_then(|o| ColorSpace::parse(doc, o, res)),
610 Some(ColorSpace::Indexed { .. })
611 );
612 let smask_in_data = get_int(doc, d, b"SMaskInData").unwrap_or(0);
613 let target = (reduction_shift > 0 && w > 0 && h > 0).then(|| {
614 let shift = reduction_shift.min(3);
615 ((w >> shift).max(1) as u32, (h >> shift).max(1) as u32)
616 });
617 match jpx::decode(&data, indexed, target) {
618 Ok(img) => Ok(Loaded::Samples(Samples {
619 width: img.width,
620 height: img.height,
621 bpc: 8,
622 ncomp: img.ncomp,
623 stride: img.width * img.ncomp,
624 adobe: false,
625 inline_alpha: if smask_in_data > 0 { img.alpha } else { None },
626 data: img.data,
627 })),
628 Err(e) => {
629 docling_core::debug_log!("docling-pdf render: JPX not decoded ({e})");
630 Ok(Loaded::Placeholder)
631 }
632 }
633 }
634 Some(codec) => {
635 docling_core::debug_log!(
636 "docling-pdf render: no {} decoder, drawing a placeholder",
637 codec.name
638 );
639 Ok(Loaded::Placeholder)
640 }
641 }
642}
643
644fn alpha_plane(
648 doc: &Document,
649 d: &Dictionary,
650 res: Option<&Dictionary>,
651 s: &Samples,
652 w: usize,
653 h: usize,
654) -> Result<Option<Vec<u8>>, String> {
655 if let Some(sm) = d.get(b"SMask").ok().and_then(|o| as_stream(doc, o)) {
657 let smw = get_int(doc, &sm.dict, b"Width").unwrap_or(0).max(0) as usize;
658 let smh = get_int(doc, &sm.dict, b"Height").unwrap_or(0).max(0) as usize;
659 if smw > 0 && smh > 0 && smw * smh <= 80_000_000 {
660 if let Ok(Loaded::Samples(ms)) = load_samples(doc, sm, res, false, 0) {
661 let decode = get2(doc, &sm.dict, b"Decode", b"D").and_then(|o| nums(doc, o));
662 let invert = decode.is_some_and(|v| v.first().is_some_and(|x| *x >= 0.5));
663 let mmax = ms.max();
664 let mut plane = vec![255u8; w * h];
665 for y in 0..h {
666 let my = (y * ms.height / h.max(1)).min(ms.height.saturating_sub(1));
667 for x in 0..w {
668 let mx = (x * ms.width / w.max(1)).min(ms.width.saturating_sub(1));
669 let v = f64::from(ms.get(mx, my, 0)) / mmax;
670 let v = if invert { 1.0 - v } else { v };
671 plane[y * w + x] = (v * 255.0).round() as u8;
672 }
673 }
674 return Ok(Some(plane));
675 }
676 }
677 }
678 match d.get(b"Mask").ok().map(|o| super::objects::deref(doc, o)) {
679 Some(Object::Stream(ms)) => {
681 let mw = get_int(doc, &ms.dict, b"Width").unwrap_or(0).max(0) as usize;
682 let mh = get_int(doc, &ms.dict, b"Height").unwrap_or(0).max(0) as usize;
683 if mw > 0 && mh > 0 && mw * mh <= 80_000_000 {
684 if let Ok(Loaded::Samples(bits)) = load_samples(doc, ms, res, true, 0) {
685 let decode = get2(doc, &ms.dict, b"Decode", b"D").and_then(|o| nums(doc, o));
686 let one_paints = decode.is_some_and(|v| v.first().is_some_and(|x| *x >= 0.5));
687 let mut plane = vec![255u8; w * h];
688 for y in 0..h {
689 let my = (y * bits.height / h.max(1)).min(bits.height.saturating_sub(1));
690 for x in 0..w {
691 let mx = (x * bits.width / w.max(1)).min(bits.width.saturating_sub(1));
692 let v = bits.get(mx, my, 0) != 0;
693 let masked = if one_paints { !v } else { v };
694 if masked {
695 plane[y * w + x] = 0;
696 }
697 }
698 }
699 return Ok(Some(plane));
700 }
701 }
702 Ok(None)
703 }
704 Some(Object::Array(ranges)) => {
706 let r: Vec<i64> = ranges
707 .iter()
708 .filter_map(|o| {
709 super::objects::num(super::objects::deref(doc, o)).map(|v| v as i64)
710 })
711 .collect();
712 let n = s.ncomp;
713 if r.len() < 2 * n {
714 return Ok(None);
715 }
716 let mut plane = vec![255u8; w * h];
717 for y in 0..h.min(s.height) {
718 for x in 0..w.min(s.width) {
719 let masked = (0..n).all(|c| {
720 let v = i64::from(s.get(x, y, c));
721 v >= r[2 * c] && v <= r[2 * c + 1]
722 });
723 if masked {
724 plane[y * w + x] = 0;
725 }
726 }
727 }
728 Ok(Some(plane))
729 }
730 _ => Ok(None),
731 }
732}