Skip to main content

pdf_syntax/
page.rs

1//! Reading the pages of a PDF document.
2
3use crate::content::{TypedIter, UntypedIter};
4use crate::object::Array;
5use crate::object::Dict;
6use crate::object::Name;
7use crate::object::Rect;
8use crate::object::Stream;
9use crate::object::dict::keys::*;
10use crate::object::{Object, ObjectIdentifier, ObjectLike};
11use crate::reader::ReaderContext;
12use crate::sync::OnceLock;
13use crate::util::FloatExt;
14use crate::xref::XRef;
15use alloc::boxed::Box;
16use alloc::collections::BTreeSet;
17use alloc::vec;
18use alloc::vec::Vec;
19use core::ops::Deref;
20use log::warn;
21
22/// Attributes that can be inherited.
23#[derive(Debug, Clone)]
24struct PagesContext {
25    media_box: Option<Rect>,
26    crop_box: Option<Rect>,
27    rotate: Option<u32>,
28}
29
30impl PagesContext {
31    fn new() -> Self {
32        Self {
33            media_box: None,
34            crop_box: None,
35            rotate: None,
36        }
37    }
38}
39
40/// A structure holding the pages of a PDF document.
41pub struct Pages<'a> {
42    pages: Vec<Page<'a>>,
43    xref: &'a XRef,
44}
45
46impl<'a> Pages<'a> {
47    /// Create a new `Pages` object.
48    pub(crate) fn new(
49        pages_dict: &Dict<'a>,
50        ctx: &ReaderContext<'a>,
51        xref: &'a XRef,
52    ) -> Option<Self> {
53        let mut pages = vec![];
54        let pages_ctx = PagesContext::new();
55        resolve_pages(
56            pages_dict,
57            &mut pages,
58            pages_ctx,
59            Resources::new(Dict::empty(), None, ctx),
60        )?;
61
62        Some(Self { pages, xref })
63    }
64
65    /// Create a new `Pages` object by bruteforce-searching.
66    ///
67    /// Of course this could result in the order of pages being messed up, but
68    /// this is still better than nothing.
69    pub(crate) fn new_brute_force(ctx: &ReaderContext<'a>, xref: &'a XRef) -> Option<Self> {
70        let mut pages = vec![];
71
72        for object in xref.objects() {
73            if let Some(dict) = object.into_dict()
74                && let Some(page) = Page::new(
75                    &dict,
76                    &PagesContext::new(),
77                    Resources::new(Dict::empty(), None, ctx),
78                    true,
79                )
80            {
81                pages.push(page);
82            }
83        }
84
85        if pages.is_empty() {
86            return None;
87        }
88
89        Some(Self { pages, xref })
90    }
91
92    /// Return the xref table (of the document the pages belong to).   
93    pub fn xref(&self) -> &'a XRef {
94        self.xref
95    }
96}
97
98impl<'a> Deref for Pages<'a> {
99    type Target = [Page<'a>];
100
101    fn deref(&self) -> &Self::Target {
102        &self.pages
103    }
104}
105
106/// Maximum depth for recursive page tree traversal.
107/// Prevents stack overflow on malformed PDFs with deeply nested or circular page trees.
108const MAX_PAGE_TREE_DEPTH: usize = 256;
109
110/// Maximum number of pages to collect from the page tree.
111/// Prevents exponential blowup from "page tree bombs" where shared Kids nodes
112/// cause the same subtrees to be visited multiple times (e.g., 8 KB PDF → 2^27 pages).
113const MAX_PAGE_COUNT: usize = 100_000;
114
115fn resolve_pages<'a>(
116    pages_dict: &Dict<'a>,
117    entries: &mut Vec<Page<'a>>,
118    ctx: PagesContext,
119    resources: Resources<'a>,
120) -> Option<()> {
121    let max_depth = resources
122        .ctx
123        .load_limits()
124        .object_depth_limit()
125        .map(|d| d as usize)
126        .unwrap_or(MAX_PAGE_TREE_DEPTH);
127
128    let mut visited = BTreeSet::new();
129    resolve_pages_depth(
130        pages_dict,
131        entries,
132        ctx,
133        resources,
134        0,
135        max_depth,
136        &mut visited,
137    )
138}
139
140fn resolve_pages_depth<'a>(
141    pages_dict: &Dict<'a>,
142    entries: &mut Vec<Page<'a>>,
143    mut ctx: PagesContext,
144    resources: Resources<'a>,
145    depth: usize,
146    max_depth: usize,
147    visited: &mut BTreeSet<ObjectIdentifier>,
148) -> Option<()> {
149    if depth > max_depth {
150        log::warn!("Page tree depth exceeds {max_depth}, stopping traversal");
151        return None;
152    }
153
154    // Cycle protection: a /Pages node reachable from itself (directly or via a
155    // /Kids back-reference) would otherwise be re-walked, duplicating pages and
156    // wasting work up to the depth cap. Stop the first time a node is re-entered.
157    // Inline (non-indirect) nodes have no object id and are simply not tracked.
158    if let Some(node_id) = pages_dict.obj_id()
159        && !visited.insert(node_id)
160    {
161        log::warn!("Page tree cycle detected at {node_id:?}, stopping traversal");
162        return Some(());
163    }
164
165    if let Some(media_box) = pages_dict.get::<Rect>(MEDIA_BOX) {
166        ctx.media_box = Some(media_box);
167    }
168
169    if let Some(crop_box) = pages_dict.get::<Rect>(CROP_BOX) {
170        ctx.crop_box = Some(crop_box);
171    }
172
173    // /Rotate may be negative (e.g. -90), which is valid per the PDF spec.
174    // Normalise to [0, 360) via Euclidean remainder so u32 storage is safe.
175    if let Some(rotate) = pages_dict.get::<i32>(ROTATE) {
176        ctx.rotate = Some(rotate.rem_euclid(360) as u32);
177    }
178
179    let resources = Resources::from_parent(
180        pages_dict.get::<Dict<'_>>(RESOURCES).unwrap_or_default(),
181        resources.clone(),
182    );
183
184    let kids = pages_dict.get::<Array<'a>>(KIDS)?;
185
186    for dict in kids.iter::<Dict<'_>>() {
187        if entries.len() >= MAX_PAGE_COUNT {
188            log::warn!("Page count exceeds {MAX_PAGE_COUNT}, stopping page tree traversal");
189            return Some(());
190        }
191
192        match dict.get::<Name>(TYPE).as_deref() {
193            Some(PAGES) => {
194                resolve_pages_depth(
195                    &dict,
196                    entries,
197                    ctx.clone(),
198                    resources.clone(),
199                    depth + 1,
200                    max_depth,
201                    visited,
202                );
203            }
204            // Let's be lenient and assume it's a `Page` in case it's `None` or something else
205            // (see corpus test case 0083781).
206            _ => {
207                if let Some(page) = Page::new(&dict, &ctx, resources.clone(), false) {
208                    entries.push(page);
209                }
210            }
211        }
212    }
213
214    Some(())
215}
216
217/// The rotation of the page.
218#[derive(Debug, Copy, Clone)]
219pub enum Rotation {
220    /// No rotation.
221    None,
222    /// A rotation of 90 degrees.
223    Horizontal,
224    /// A rotation of 180 degrees.
225    Flipped,
226    /// A rotation of 270 degrees.
227    FlippedHorizontal,
228}
229
230/// A PDF page.
231pub struct Page<'a> {
232    inner: Dict<'a>,
233    media_box: Rect,
234    crop_box: Rect,
235    rotation: Rotation,
236    page_streams: OnceLock<Option<Vec<u8>>>,
237    resources: Resources<'a>,
238    ctx: ReaderContext<'a>,
239}
240
241impl<'a> Page<'a> {
242    fn new(
243        dict: &Dict<'a>,
244        ctx: &PagesContext,
245        resources: Resources<'a>,
246        brute_force: bool,
247    ) -> Option<Self> {
248        // In general, pages without content are allowed, but in case we are brute-forcing
249        // we ignore them.
250        if brute_force && !dict.contains_key(CONTENTS) {
251            return None;
252        }
253
254        let media_box = dict
255            .get::<Rect>(MEDIA_BOX)
256            .or(ctx.media_box)
257            .unwrap_or(US_LETTER);
258
259        let crop_box = dict
260            .get::<Rect>(CROP_BOX)
261            .or(ctx.crop_box)
262            .unwrap_or(media_box);
263
264        let rotation = match dict
265            .get::<i32>(ROTATE)
266            .map(|r| r.rem_euclid(360) as u32)
267            .or(ctx.rotate)
268            .unwrap_or(0)
269        {
270            0 => Rotation::None,
271            90 => Rotation::Horizontal,
272            180 => Rotation::Flipped,
273            270 => Rotation::FlippedHorizontal,
274            _ => Rotation::None,
275        };
276
277        let ctx = resources.ctx.clone();
278        let resources = Resources::from_parent(
279            dict.get::<Dict<'_>>(RESOURCES).unwrap_or_default(),
280            resources,
281        );
282
283        Some(Self {
284            inner: dict.clone(),
285            media_box,
286            crop_box,
287            rotation,
288            page_streams: OnceLock::new(),
289            resources,
290            ctx,
291        })
292    }
293
294    fn operations_impl(&self) -> Option<UntypedIter<'_>> {
295        let stream = self.page_stream()?;
296        let iter = UntypedIter::new(stream);
297
298        Some(iter)
299    }
300
301    /// Return the decoded content stream of the page.
302    pub fn page_stream(&self) -> Option<&[u8]> {
303        let convert_single = |s: Stream<'_>| {
304            let data = s.decoded().ok()?;
305            Some(data.to_vec())
306        };
307
308        self.page_streams
309            .get_or_init(|| {
310                if let Some(stream) = self.inner.get::<Stream<'_>>(CONTENTS) {
311                    convert_single(stream)
312                } else if let Some(array) = self.inner.get::<Array<'_>>(CONTENTS) {
313                    let streams = array.iter::<Stream<'_>>().flat_map(convert_single);
314
315                    let mut collected = vec![];
316
317                    for stream in streams {
318                        collected.extend(stream);
319                        // Streams must have at least one whitespace in-between.
320                        collected.push(b' ');
321                    }
322
323                    Some(collected)
324                } else {
325                    warn!("contents entry of page was neither stream nor array of streams");
326
327                    None
328                }
329            })
330            .as_ref()
331            .map(|d| d.as_slice())
332    }
333
334    /// Get the resources of the page.
335    pub fn resources(&self) -> &Resources<'a> {
336        &self.resources
337    }
338
339    /// Get the media box of the page.
340    pub fn media_box(&self) -> Rect {
341        self.media_box
342    }
343
344    /// Get the rotation of the page.
345    pub fn rotation(&self) -> Rotation {
346        self.rotation
347    }
348
349    /// Get the crop box of the page.
350    pub fn crop_box(&self) -> Rect {
351        self.crop_box
352    }
353
354    /// Return the intersection of crop box and media box.
355    pub fn intersected_crop_box(&self) -> Rect {
356        self.crop_box().intersect(self.media_box())
357    }
358
359    /// Return the base dimensions of the page used for the canvas size.
360    ///
361    /// When the CropBox origin is within the MediaBox (i.e. CropBox.x0 >=
362    /// MediaBox.x0 and CropBox.y0 >= MediaBox.y0), the canvas is sized to
363    /// intersect(CropBox, MediaBox).  This matches MuPDF's behaviour for
364    /// spec-violating PDFs where CropBox.y1 > MediaBox.y1 (e.g. gen-271:
365    /// CropBox=[0,0,595,793.7] vs MediaBox=[0,0,612,792] — using raw
366    /// CropBox gives 1654px height, a 4px vertical content offset, and
367    /// SSIM 0.49; intersecting gives 1650px matching MuPDF exactly).
368    ///
369    /// When CropBox extends below the MediaBox origin (gen-802 style:
370    /// CropBox=[0,0,684,864] vs MediaBox=[36,36,648,828]), MuPDF still uses
371    /// the full CropBox dimensions, so we do too. (#544, #558, gen-271)
372    pub fn base_dimensions(&self) -> (f32, f32) {
373        let crop_box = self.crop_box();
374        let media_box = self.media_box();
375
376        // Clip to MediaBox only when the CropBox origin lies within the
377        // MediaBox (both axes).  When the CropBox extends below the MediaBox
378        // origin MuPDF uses the raw CropBox, so preserve that behaviour.
379        let effective = if crop_box.x0 >= media_box.x0 && crop_box.y0 >= media_box.y0 {
380            crop_box.intersect(media_box)
381        } else {
382            crop_box
383        };
384
385        if (effective.width() as f32).is_nearly_zero()
386            || (effective.height() as f32).is_nearly_zero()
387        {
388            (US_LETTER.width() as f32, US_LETTER.height() as f32)
389        } else {
390            (
391                effective.width().max(1.0) as f32,
392                effective.height().max(1.0) as f32,
393            )
394        }
395    }
396
397    /// Return the with and height of the page that should be assumed when rendering the page.
398    ///
399    /// Depending on the document, it is either based on the media box or the crop box
400    /// of the page. In addition to that, it also takes the rotation of the page into account.
401    pub fn render_dimensions(&self) -> (f32, f32) {
402        let (mut base_width, mut base_height) = self.base_dimensions();
403
404        if matches!(
405            self.rotation(),
406            Rotation::Horizontal | Rotation::FlippedHorizontal
407        ) {
408            core::mem::swap(&mut base_width, &mut base_height);
409        }
410
411        (base_width, base_height)
412    }
413
414    /// Return an untyped iterator over the operators of the page's content stream.
415    pub fn operations(&self) -> UntypedIter<'_> {
416        self.operations_impl().unwrap_or(UntypedIter::empty())
417    }
418
419    /// Get the raw dictionary of the page.
420    pub fn raw(&self) -> &Dict<'a> {
421        &self.inner
422    }
423
424    /// Get the xref table (of the document the page belongs to).
425    pub fn xref(&self) -> &'a XRef {
426        self.ctx.xref()
427    }
428
429    /// Return a typed iterator over the operators of the page's content stream.
430    pub fn typed_operations(&self) -> TypedIter<'_> {
431        TypedIter::from_untyped(self.operations())
432    }
433
434    /// Return the annotation dictionaries for this page, if any.
435    pub fn annots(&self) -> Vec<Dict<'a>> {
436        self.inner
437            .get::<Array<'_>>(crate::object::dict::keys::ANNOTS)
438            .map(|arr| arr.iter::<Dict<'_>>().collect())
439            .unwrap_or_default()
440    }
441}
442
443/// A structure keeping track of the resources of a page.
444#[derive(Clone, Debug)]
445pub struct Resources<'a> {
446    parent: Option<Box<Self>>,
447    ctx: ReaderContext<'a>,
448    /// The raw dictionary of external graphics states.
449    pub ext_g_states: Dict<'a>,
450    /// The raw dictionary of fonts.
451    pub fonts: Dict<'a>,
452    /// The raw dictionary of properties.
453    pub properties: Dict<'a>,
454    /// The raw dictionary of color spaces.
455    pub color_spaces: Dict<'a>,
456    /// The raw dictionary of x objects.
457    pub x_objects: Dict<'a>,
458    /// The raw dictionary of patterns.
459    pub patterns: Dict<'a>,
460    /// The raw dictionary of shadings.
461    pub shadings: Dict<'a>,
462}
463
464impl<'a> Resources<'a> {
465    /// Create a new `Resources` object from a dictionary with a parent.
466    pub fn from_parent(resources: Dict<'a>, parent: Self) -> Self {
467        let ctx = parent.ctx.clone();
468
469        Self::new(resources, Some(parent), &ctx)
470    }
471
472    /// Create a new `Resources` object.
473    pub(crate) fn new(resources: Dict<'a>, parent: Option<Self>, ctx: &ReaderContext<'a>) -> Self {
474        let ext_g_states = resources.get::<Dict<'_>>(EXT_G_STATE).unwrap_or_default();
475        let fonts = resources.get::<Dict<'_>>(FONT).unwrap_or_default();
476        let color_spaces = resources.get::<Dict<'_>>(COLORSPACE).unwrap_or_default();
477        let x_objects = resources.get::<Dict<'_>>(XOBJECT).unwrap_or_default();
478        let patterns = resources.get::<Dict<'_>>(PATTERN).unwrap_or_default();
479        let shadings = resources.get::<Dict<'_>>(SHADING).unwrap_or_default();
480        let properties = resources.get::<Dict<'_>>(PROPERTIES).unwrap_or_default();
481
482        let parent = parent.map(Box::new);
483
484        Self {
485            parent,
486            ext_g_states,
487            fonts,
488            color_spaces,
489            properties,
490            x_objects,
491            patterns,
492            shadings,
493            ctx: ctx.clone(),
494        }
495    }
496
497    fn get_resource<T: ObjectLike<'a>>(&self, name: Name, dict: &Dict<'a>) -> Option<T> {
498        dict.get::<T>(name.deref())
499    }
500
501    /// Get the parent in the resource, chain, if available.
502    pub fn parent(&self) -> Option<&Self> {
503        self.parent.as_deref()
504    }
505
506    /// Get an external graphics state by name.
507    pub fn get_ext_g_state(&self, name: Name) -> Option<Dict<'a>> {
508        self.get_resource::<Dict<'_>>(name.clone(), &self.ext_g_states)
509            .or_else(|| self.parent.as_ref().and_then(|p| p.get_ext_g_state(name)))
510    }
511
512    /// Get a color space by name.
513    pub fn get_color_space(&self, name: Name) -> Option<Object<'a>> {
514        self.get_resource::<Object<'_>>(name.clone(), &self.color_spaces)
515            .or_else(|| self.parent.as_ref().and_then(|p| p.get_color_space(name)))
516    }
517
518    /// Get a font by name.
519    pub fn get_font(&self, name: Name) -> Option<Dict<'a>> {
520        self.get_resource::<Dict<'_>>(name.clone(), &self.fonts)
521            .or_else(|| self.parent.as_ref().and_then(|p| p.get_font(name)))
522    }
523
524    /// Get a pattern by name.
525    pub fn get_pattern(&self, name: Name) -> Option<Object<'a>> {
526        self.get_resource::<Object<'_>>(name.clone(), &self.patterns)
527            .or_else(|| self.parent.as_ref().and_then(|p| p.get_pattern(name)))
528    }
529
530    /// Get an x object by name.
531    pub fn get_x_object(&self, name: Name) -> Option<Stream<'a>> {
532        self.get_resource::<Stream<'_>>(name.clone(), &self.x_objects)
533            .or_else(|| self.parent.as_ref().and_then(|p| p.get_x_object(name)))
534    }
535
536    /// Get a shading by name.
537    pub fn get_shading(&self, name: Name) -> Option<Object<'a>> {
538        self.get_resource::<Object<'_>>(name.clone(), &self.shadings)
539            .or_else(|| self.parent.as_ref().and_then(|p| p.get_shading(name)))
540    }
541}
542
543// <https://github.com/apache/pdfbox/blob/a53a70db16ea3133994120bcf1e216b9e760c05b/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/common/PDRectangle.java#L38>
544const POINTS_PER_INCH: f64 = 72.0;
545const POINTS_PER_MM: f64 = 1.0 / (10.0 * 2.54) * POINTS_PER_INCH;
546
547/// The dimension of an A4 page (kept for completeness).
548pub const A4: Rect = Rect {
549    x0: 0.0,
550    y0: 0.0,
551    x1: 210.0 * POINTS_PER_MM,
552    y1: 297.0 * POINTS_PER_MM,
553};
554
555/// US Letter (8.5×11 in) — used as fallback when no MediaBox is present.
556///
557/// Old PDF 1.0/1.1 documents (especially US-government publications from the
558/// 1990s) omit MediaBox entirely and assume a US-Letter canvas, which matches
559/// the PostScript default and MuPDF's behaviour.  Using A4 here causes a
560/// ~5 % scale mismatch that tanks SSIM against MuPDF's oracle renders.
561/// See render_mupdf_oracle failures gen-059, gen-069, …  (#544)
562const US_LETTER: Rect = Rect {
563    x0: 0.0,
564    y0: 0.0,
565    x1: 8.5 * POINTS_PER_INCH,
566    y1: 11.0 * POINTS_PER_INCH,
567};
568
569pub(crate) mod cached {
570    use crate::page::Pages;
571    use crate::reader::ReaderContext;
572    use crate::xref::XRef;
573    use core::ops::Deref;
574
575    // Keep in sync with the implementation in `sync`. We duplicate it here
576    // to make it more visible since we have unsafe code here.
577    #[cfg(feature = "std")]
578    pub(crate) use std::sync::Arc;
579
580    #[cfg(not(feature = "std"))]
581    pub(crate) use alloc::rc::Rc as Arc;
582
583    pub(crate) struct CachedPages {
584        pages: Pages<'static>,
585        // NOTE: `pages` references the data in `xref`, so it's important that `xref`
586        // appears after `pages` in the struct definition to ensure correct drop order.
587        _xref: Arc<XRef>,
588        /// `true` if the normal page-tree walk failed and pages were recovered
589        /// via brute-force scan (page order may differ from the source).
590        page_tree_rebuilt: bool,
591    }
592
593    impl CachedPages {
594        pub(crate) fn new(xref: Arc<XRef>) -> Option<Self> {
595            // SAFETY:
596            // - The XRef's location is stable in memory:
597            //   - We wrapped it in a `Arc` (or `Rc` in `no_std`), which implements `StableDeref`.
598            //   - The struct owns the `Arc`, ensuring that the inner value is not dropped during the whole
599            //     duration.
600            // - The internal 'static lifetime is not leaked because its rewritten
601            //   to the self-lifetime in `pages()`.
602            let xref_reference: &'static XRef = unsafe { core::mem::transmute(xref.deref()) };
603
604            let ctx = ReaderContext::new(xref_reference, false);
605            // Detect whether the normal page-tree walk succeeded: if it returns
606            // `None` and we fall back to a brute-force scan, the page tree was
607            // rebuilt (recovery is still always attempted).
608            let normal = xref_reference
609                .get_with(xref.trailer_data().pages_ref, &ctx)
610                .and_then(|p| Pages::new(&p, &ctx, xref_reference));
611            let page_tree_rebuilt = normal.is_none();
612            let pages = normal.or_else(|| Pages::new_brute_force(&ctx, xref_reference))?;
613
614            Some(Self {
615                pages,
616                _xref: xref,
617                page_tree_rebuilt,
618            })
619        }
620
621        pub(crate) fn get(&self) -> &Pages<'_> {
622            &self.pages
623        }
624
625        /// Whether the page tree was recovered via brute-force scan.
626        pub(crate) fn page_tree_rebuilt(&self) -> bool {
627            self.page_tree_rebuilt
628        }
629    }
630}
631
632#[cfg(test)]
633mod cycle_tests {
634    use crate::pdf::Pdf;
635    use alloc::format;
636    use alloc::vec::Vec;
637
638    /// Build a minimal PDF whose page tree contains a self-cycle: the single
639    /// `/Pages` node lists a real `/Page` followed by a back-reference to
640    /// itself. Without cycle detection the walker re-enters the node up to the
641    /// depth cap, duplicating the page hundreds of times.
642    fn cyclic_pages_pdf() -> Vec<u8> {
643        let mut buf: Vec<u8> = Vec::new();
644        let mut offsets = [0usize; 4];
645        buf.extend_from_slice(b"%PDF-1.7\n");
646        offsets[1] = buf.len();
647        buf.extend_from_slice(b"1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n");
648        offsets[2] = buf.len();
649        buf.extend_from_slice(
650            b"2 0 obj\n<< /Type /Pages /Kids [3 0 R 2 0 R] /Count 1 >>\nendobj\n",
651        );
652        offsets[3] = buf.len();
653        buf.extend_from_slice(
654            b"3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>\nendobj\n",
655        );
656        let xref_off = buf.len();
657        buf.extend_from_slice(b"xref\n0 4\n0000000000 65535 f \n");
658        for off in &offsets[1..4] {
659            buf.extend_from_slice(format!("{off:010} 00000 n \n").as_bytes());
660        }
661        buf.extend_from_slice(
662            format!("trailer\n<< /Size 4 /Root 1 0 R >>\nstartxref\n{xref_off}\n%%EOF").as_bytes(),
663        );
664        buf
665    }
666
667    #[test]
668    fn cyclic_page_tree_yields_one_page_without_runaway() {
669        let pdf = Pdf::new(cyclic_pages_pdf()).expect("cyclic PDF should still load");
670        // Exactly the one real page; the /Kids self-reference is detected and
671        // skipped. Without cycle detection this would be hundreds of duplicates.
672        assert_eq!(
673            pdf.pages().len(),
674            1,
675            "page-tree cycle must not duplicate pages"
676        );
677    }
678}