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