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