Skip to main content

azul_layout/solver3/
paged_layout.rs

1//! CSS Paged Media layout integration with integrated fragmentation
2//!
3//! This module provides functionality for laying out documents with pagination,
4//! such as for PDF generation. It uses the new integrated architecture where:
5//!
6//! 1. page_index is assigned to nodes DURING layout based on Y position
7//! 2. generate_display_lists_paged() creates per-page DisplayLists by filtering
8//! 3. No post-hoc fragmentation is needed
9//!
10//! **Note**: Full CSS `@page` rule parsing is not yet implemented. The `FakePageConfig`
11//! provides programmatic control over page decoration as a temporary solution.
12
13use std::collections::{BTreeMap, HashMap};
14
15use azul_core::{
16    dom::{DomId, NodeId},
17    geom::{LogicalPosition, LogicalRect, LogicalSize},
18    hit_test::ScrollPosition,
19    resources::RendererResources,
20    selection::TextSelection,
21    styled_dom::StyledDom,
22};
23use azul_css::LayoutDebugMessage;
24
25use crate::{
26    font_traits::{ParsedFontTrait, TextLayoutCache},
27    paged::FragmentationContext,
28    solver3::{
29        cache::LayoutCache,
30        display_list::DisplayList,
31        pagination::FakePageConfig,
32        LayoutContext, LayoutError, Result,
33    },
34};
35
36/// Result of `compute_layout_with_fragmentation`: contains the data
37/// needed to generate a display list afterwards. The tree and
38/// calculated_positions are stored in the `LayoutCache` that was passed in.
39pub struct FragmentationLayoutResult {
40    pub scroll_ids: HashMap<usize, u64>,
41}
42
43/// Layout a document with integrated pagination, returning one DisplayList per page.
44///
45/// +spec:positioning:a4936a - Absolutely positioned elements positioned relative to containing block ignoring page breaks
46/// Layout is performed on a continuous document; pages are split afterward by Y position,
47/// so absolutely positioned elements are positioned as if the document were continuous.
48///
49/// This function performs CSS Paged Media layout with fragmentation integrated
50/// into the layout process itself, using the new architecture where:
51///
52/// 1. The FragmentationContext is passed to layout_document via LayoutContext
53/// 2. Nodes get their page_index assigned during layout based on absolute Y position
54/// 3. DisplayLists are generated per-page by filtering items based on page bounds
55///
56/// Uses default page header/footer configuration (page numbers in footer).
57/// For custom headers/footers, use `layout_document_paged_with_config`.
58///
59/// # Arguments
60/// * `fragmentation_context` - Controls page size and fragmentation behavior
61/// * Other arguments same as `layout_document()`
62///
63/// # Returns
64/// A vector of DisplayLists, one per page. Each DisplayList contains the
65/// elements that fit on that page, with Y-coordinates relative to the page origin.
66#[cfg(feature = "text_layout")]
67pub fn layout_document_paged<T, F>(
68    cache: &mut LayoutCache,
69    text_cache: &mut TextLayoutCache,
70    fragmentation_context: FragmentationContext,
71    new_dom: &StyledDom,
72    viewport: LogicalRect,
73    font_manager: &mut crate::font_traits::FontManager<T>,
74    scroll_offsets: &BTreeMap<NodeId, ScrollPosition>,
75    debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
76    gpu_value_cache: Option<&azul_core::gpu::GpuValueCache>,
77    renderer_resources: &RendererResources,
78    id_namespace: azul_core::resources::IdNamespace,
79    dom_id: DomId,
80    font_loader: F,
81    image_cache: &azul_core::resources::ImageCache,
82    get_system_time_fn: azul_core::task::GetSystemTimeCallback,
83) -> Result<Vec<DisplayList>>
84where
85    T: ParsedFontTrait + Sync + 'static,
86    F: Fn(
87        std::sync::Arc<rust_fontconfig::FontBytes>,
88        usize,
89    ) -> std::result::Result<T, crate::text3::cache::LayoutError>,
90{
91    // Use default page config (page numbers in footer)
92    let page_config = FakePageConfig::new().with_footer_page_numbers();
93
94    layout_document_paged_with_config(
95        cache,
96        text_cache,
97        fragmentation_context,
98        new_dom,
99        viewport,
100        font_manager,
101        scroll_offsets,
102        debug_messages,
103        gpu_value_cache,
104        renderer_resources,
105        id_namespace,
106        dom_id,
107        font_loader,
108        page_config,
109        image_cache,
110        get_system_time_fn,
111        false,
112    )
113}
114
115/// Layout a document with integrated pagination and custom page configuration.
116///
117/// This function is the same as `layout_document_paged` but allows you to
118/// specify custom headers and footers via `FakePageConfig`.
119///
120/// # Arguments
121/// * `page_config` - Configuration for page headers/footers (see `FakePageConfig`)
122/// * Other arguments same as `layout_document_paged()`
123#[cfg(feature = "text_layout")]
124pub fn layout_document_paged_with_config<T, F>(
125    cache: &mut LayoutCache,
126    text_cache: &mut TextLayoutCache,
127    mut fragmentation_context: FragmentationContext,
128    new_dom: &StyledDom,
129    viewport: LogicalRect,
130    font_manager: &mut crate::font_traits::FontManager<T>,
131    scroll_offsets: &BTreeMap<NodeId, ScrollPosition>,
132    debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
133    gpu_value_cache: Option<&azul_core::gpu::GpuValueCache>,
134    renderer_resources: &RendererResources,
135    id_namespace: azul_core::resources::IdNamespace,
136    dom_id: DomId,
137    font_loader: F,
138    page_config: FakePageConfig,
139    image_cache: &azul_core::resources::ImageCache,
140    get_system_time_fn: azul_core::task::GetSystemTimeCallback,
141    print_timing: bool,
142) -> Result<Vec<DisplayList>>
143where
144    T: ParsedFontTrait + Sync + 'static,
145    F: Fn(
146        std::sync::Arc<rust_fontconfig::FontBytes>,
147        usize,
148    ) -> std::result::Result<T, crate::text3::cache::LayoutError>,
149{
150    // Font Resolution And Loading
151    {
152        use crate::solver3::getters::{
153            collect_and_resolve_font_chains_with_registration, collect_font_ids_from_chains,
154            compute_fonts_to_load, load_fonts_from_disk,
155        };
156
157        // TODO: Accept platform as parameter instead of using ::current()
158        let platform = azul_css::system::Platform::current();
159
160        let chains = collect_and_resolve_font_chains_with_registration(
161            new_dom, &font_manager.fc_cache, font_manager, &platform,
162        );
163
164        let required_fonts = collect_font_ids_from_chains(&chains);
165        let already_loaded = font_manager.get_loaded_font_ids();
166        let fonts_to_load = compute_fonts_to_load(&required_fonts, &already_loaded);
167
168        if !fonts_to_load.is_empty() {
169            let load_result =
170                load_fonts_from_disk(&fonts_to_load, &font_manager.fc_cache, &font_loader);
171
172            font_manager.insert_fonts(load_result.loaded);
173            for (font_id, error) in &load_result.failed {
174                if let Some(msgs) = debug_messages {
175                    msgs.push(LayoutDebugMessage::warning(format!(
176                        "[FontLoading] Failed to load font {:?}: {}",
177                        font_id, error
178                    )));
179                }
180            }
181        }
182        font_manager.set_font_chain_cache(chains.into_fontconfig_chains());
183    }
184
185    // Get page dimensions from fragmentation context
186    let page_content_height = fragmentation_context.page_content_height();
187
188    // Handle continuous media (no pagination)
189    if !fragmentation_context.is_paged() {
190        let _result = compute_layout_with_fragmentation(
191            cache,
192            text_cache,
193            &mut fragmentation_context,
194            new_dom,
195            viewport,
196            font_manager,
197            debug_messages,
198            image_cache,
199            get_system_time_fn,
200            print_timing,
201        )?;
202
203        // Generate display list from cached tree/positions
204        let tree = cache.tree.as_ref().ok_or(LayoutError::InvalidTree)?;
205        let mut counter_values = cache.counters.clone();
206        let empty_text_selections: BTreeMap<DomId, TextSelection> = BTreeMap::new();
207        let mut ctx = LayoutContext {
208            scrollbar_style_cache: core::cell::RefCell::new(std::collections::HashMap::new()),
209            styled_dom: new_dom,
210            font_manager: &*font_manager,
211            text_selections: &empty_text_selections,
212            debug_messages,
213            counters: &mut counter_values,
214            viewport_size: viewport.size,
215            fragmentation_context: Some(&mut fragmentation_context),
216            cursor_is_visible: true,
217            cursor_locations: Vec::new(),
218            preedit_text: None,
219            dirty_text_overrides: BTreeMap::new(),
220            cache_map: std::mem::take(&mut cache.cache_map),
221            image_cache,
222            system_style: None,
223            get_system_time_fn,
224        };
225
226        use crate::solver3::display_list::generate_display_list;
227        let display_list = generate_display_list(
228            &mut ctx,
229            tree,
230            &cache.calculated_positions,
231            scroll_offsets,
232            &cache.scroll_ids,
233            gpu_value_cache,
234            renderer_resources,
235            id_namespace,
236            dom_id,
237        )?;
238        cache.cache_map = std::mem::take(&mut ctx.cache_map);
239        return Ok(vec![display_list]);
240    }
241
242    // Paged Layout
243
244    // Perform layout with fragmentation context (layout only, no display list)
245    let _result = compute_layout_with_fragmentation(
246        cache,
247        text_cache,
248        &mut fragmentation_context,
249        new_dom,
250        viewport,
251        font_manager,
252        debug_messages,
253        image_cache,
254        get_system_time_fn,
255        print_timing,
256    )?;
257
258    // Get the layout tree and positions
259    let tree = cache.tree.as_ref().ok_or(LayoutError::InvalidTree)?;
260    let calculated_positions = &cache.calculated_positions;
261
262    // Debug: log page layout info
263    if let Some(msgs) = debug_messages {
264        msgs.push(LayoutDebugMessage::info(format!(
265            "[PagedLayout] Page content height: {}",
266            page_content_height
267        )));
268    }
269
270    // Use scroll IDs computed by compute_layout_with_fragmentation (stored in cache)
271    let scroll_ids = &cache.scroll_ids;
272
273    // Create temporary context for display list generation
274    let mut counter_values = cache.counters.clone();
275    let empty_text_selections: BTreeMap<DomId, TextSelection> = BTreeMap::new();
276    let mut ctx = LayoutContext {
277        scrollbar_style_cache: core::cell::RefCell::new(std::collections::HashMap::new()),
278        styled_dom: new_dom,
279        font_manager: &*font_manager,
280        text_selections: &empty_text_selections,
281        debug_messages,
282        counters: &mut counter_values,
283        viewport_size: viewport.size,
284        fragmentation_context: Some(&mut fragmentation_context),
285        cursor_is_visible: true, // Paged layout: cursor always visible
286        cursor_locations: Vec::new(),   // Paged layout: no cursor
287        preedit_text: None,
288        dirty_text_overrides: BTreeMap::new(),
289        cache_map: std::mem::take(&mut cache.cache_map),
290        image_cache,
291        system_style: None,
292        get_system_time_fn,
293    };
294
295    // NEW: Use the commitment-based pagination approach with CSS break properties
296    //
297    // This treats pages as viewports into a single infinite canvas:
298    // 1. Generate ONE complete display list on infinite vertical strip
299    // 2. Analyze CSS break properties (break-before, break-after, break-inside)
300    // 3. Calculate page boundaries based on break properties
301    // 4. Slice content to page boundaries (items are NEVER shifted, only clipped)
302    // 5. Headers and footers are injected per-page
303    //
304    // Benefits over the old approach:
305    // - No coordinate desynchronization between page_index and actual Y position
306    // - Backgrounds render correctly (clipped, not torn/duplicated)
307    // - Simple mental model: pages are just views into continuous content
308    // - Headers/footers with page numbers are automatically generated
309    // - CSS fragmentation properties are respected
310
311    use crate::solver3::display_list::{
312        generate_display_list, paginate_display_list_with_slicer_and_breaks,
313        SlicerConfig,
314    };
315
316    // Step 1: Generate ONE complete display list (infinite canvas)
317    let full_display_list = generate_display_list(
318        &mut ctx,
319        tree,
320        calculated_positions,
321        scroll_offsets,
322        scroll_ids,
323        gpu_value_cache,
324        renderer_resources,
325        id_namespace,
326        dom_id,
327    )?;
328
329    if let Some(msgs) = ctx.debug_messages {
330        msgs.push(LayoutDebugMessage::info(format!(
331            "[PagedLayout] Generated master display list with {} items",
332            full_display_list.items.len()
333        )));
334    }
335
336    // Step 2: Configure the slicer with page dimensions and headers/footers
337    let page_width = viewport.size.width;
338    let header_footer = page_config.to_header_footer_config();
339
340    if let Some(msgs) = ctx.debug_messages {
341        msgs.push(LayoutDebugMessage::info(format!(
342            "[PagedLayout] Page config: header={}, footer={}, skip_first={}",
343            header_footer.show_header, header_footer.show_footer, header_footer.skip_first_page
344        )));
345    }
346
347    let slicer_config = SlicerConfig {
348        page_content_height,
349        page_gap: 0.0,
350        allow_clipping: true,
351        header_footer,
352        page_width,
353        table_headers: Default::default(),
354    };
355
356    // Step 3: Paginate with CSS break property support
357    let pages = paginate_display_list_with_slicer_and_breaks(
358        full_display_list,
359        &slicer_config,
360    )?;
361
362    if let Some(msgs) = ctx.debug_messages {
363        msgs.push(LayoutDebugMessage::info(format!(
364            "[PagedLayout] Paginated into {} pages with CSS break support",
365            pages.len()
366        )));
367    }
368
369    cache.cache_map = std::mem::take(&mut ctx.cache_map);
370
371    Ok(pages)
372}
373
374/// Internal helper: Perform layout with a fragmentation context (layout only, no display list)
375///
376/// Returns a `FragmentationLayoutResult` containing the computed scroll IDs.
377/// The tree & positions are stored in `cache`. To generate a display list,
378/// call `generate_display_list` separately using the tree/positions from the cache.
379#[cfg(feature = "text_layout")]
380fn compute_layout_with_fragmentation<T: ParsedFontTrait + Sync + 'static>(
381    cache: &mut LayoutCache,
382    text_cache: &mut TextLayoutCache,
383    fragmentation_context: &mut FragmentationContext,
384    new_dom: &StyledDom,
385    viewport: LogicalRect,
386    font_manager: &crate::font_traits::FontManager<T>,
387    debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
388    image_cache: &azul_core::resources::ImageCache,
389    get_system_time_fn: azul_core::task::GetSystemTimeCallback,
390    _print_timing: bool,
391) -> Result<FragmentationLayoutResult> {
392    use crate::solver3::cache;
393
394    // Create temporary context without counters for tree generation
395    let mut counter_values = HashMap::new();
396    let empty_text_selections: BTreeMap<DomId, TextSelection> = BTreeMap::new();
397    let mut ctx_temp = LayoutContext {
398        scrollbar_style_cache: core::cell::RefCell::new(std::collections::HashMap::new()),
399        styled_dom: new_dom,
400        font_manager,
401        text_selections: &empty_text_selections,
402        debug_messages,
403        counters: &mut counter_values,
404        viewport_size: viewport.size,
405        fragmentation_context: Some(fragmentation_context),
406        cursor_is_visible: true, // Paged layout: cursor always visible
407        cursor_locations: Vec::new(),   // Paged layout: no cursor
408        preedit_text: None,
409        dirty_text_overrides: BTreeMap::new(),
410        cache_map: cache::LayoutCacheMap::default(),
411        image_cache,
412        system_style: None,
413        get_system_time_fn,
414    };
415
416    // --- Step 1: Tree Building & Invalidation ---
417    let is_fresh_dom = cache.tree.is_none();
418    let (mut new_tree, mut recon_result) = if is_fresh_dom {
419        // Fast path: no old tree to diff against — build tree directly.
420        use crate::solver3::layout_tree::generate_layout_tree;
421        let new_tree = generate_layout_tree(&mut ctx_temp)?;
422        let n = new_tree.nodes.len();
423        let mut result = cache::ReconciliationResult::default();
424        result.layout_roots.insert(new_tree.root);
425        result.intrinsic_dirty = (0..n).collect::<std::collections::BTreeSet<_>>();
426        (new_tree, result)
427    } else {
428        // Incremental path: diff old tree vs new DOM
429        cache::reconcile_and_invalidate(&mut ctx_temp, cache, viewport)?
430    };
431
432    // Step 1.2: Clear Taffy Caches for Dirty Nodes
433    for &node_idx in &recon_result.intrinsic_dirty {
434        if let Some(warm) = new_tree.warm_mut(node_idx) {
435            warm.taffy_cache.clear();
436        }
437    }
438
439    // Step 1.3: Compute CSS Counters
440    cache::compute_counters(new_dom, &new_tree, &mut counter_values);
441
442    // Step 1.4: Resize and invalidate per-node cache (Taffy-inspired 9+1 slot cache)
443    // Move cache_map out of LayoutCache for the duration of layout.
444    let mut cache_map = std::mem::take(&mut cache.cache_map);
445    cache_map.resize_to_tree(new_tree.nodes.len());
446    for &node_idx in &recon_result.intrinsic_dirty {
447        cache_map.mark_dirty(node_idx, &new_tree.nodes);
448    }
449    for &node_idx in &recon_result.layout_roots {
450        cache_map.mark_dirty(node_idx, &new_tree.nodes);
451    }
452
453    // Now create the real context with computed counters and fragmentation
454    let mut ctx = LayoutContext {
455        scrollbar_style_cache: core::cell::RefCell::new(std::collections::HashMap::new()),
456        styled_dom: new_dom,
457        font_manager,
458        text_selections: &empty_text_selections,
459        debug_messages,
460        counters: &mut counter_values,
461        viewport_size: viewport.size,
462        fragmentation_context: Some(fragmentation_context),
463        cursor_is_visible: true, // Paged layout: cursor always visible
464        cursor_locations: Vec::new(),   // Paged layout: no cursor
465        preedit_text: None,
466        dirty_text_overrides: BTreeMap::new(),
467        cache_map,
468        image_cache,
469        system_style: None,
470        get_system_time_fn,
471    };
472
473    // --- Step 1.5: Early Exit Optimization ---
474    if recon_result.is_clean() {
475        ctx.debug_log("No changes, layout cache is clean");
476        let tree = cache.tree.as_ref().ok_or(LayoutError::InvalidTree)?;
477
478        use crate::window::LayoutWindow;
479        let (scroll_ids, scroll_id_to_node_id) = LayoutWindow::compute_scroll_ids(tree, new_dom);
480        cache.scroll_ids = scroll_ids.clone();
481        cache.scroll_id_to_node_id = scroll_id_to_node_id;
482
483        return Ok(FragmentationLayoutResult {
484            scroll_ids,
485        });
486    }
487
488    // --- Step 2: Incremental Layout Loop ---
489    let mut calculated_positions = cache.calculated_positions.clone();
490    let mut loop_count = 0;
491    loop {
492        loop_count += 1;
493        if loop_count > 10 {
494            break;
495        }
496
497        calculated_positions = cache.calculated_positions.clone();
498        let mut reflow_needed_for_scrollbars = false;
499
500        crate::solver3::sizing::calculate_intrinsic_sizes(
501            &mut ctx,
502            &mut new_tree,
503            text_cache,
504            &recon_result.intrinsic_dirty,
505        )?;
506
507        for &root_idx in &recon_result.layout_roots {
508            let (cb_pos, cb_size) = get_containing_block_for_node(
509                &new_tree,
510                new_dom,
511                root_idx,
512                &calculated_positions,
513                viewport,
514            );
515
516            // For ROOT nodes (no parent), we need to account for their margin.
517            // The containing block position from viewport is (0, 0), but the root's
518            // content starts at (margin + border + padding, margin + border + padding).
519            let root_node = &new_tree.nodes[root_idx];
520            let root_bp = root_node.box_props.unpack();
521            let is_root_with_margin = root_node.parent.is_none()
522                && (root_bp.margin.left != 0.0 || root_bp.margin.top != 0.0);
523
524            let adjusted_cb_pos = if is_root_with_margin {
525                LogicalPosition::new(
526                    cb_pos.x + root_bp.margin.left,
527                    cb_pos.y + root_bp.margin.top,
528                )
529            } else {
530                cb_pos
531            };
532
533            cache::calculate_layout_for_subtree(
534                &mut ctx,
535                &mut new_tree,
536                text_cache,
537                root_idx,
538                adjusted_cb_pos,
539                cb_size,
540                &mut calculated_positions,
541                &mut reflow_needed_for_scrollbars,
542                &mut cache.float_cache,
543                cache::ComputeMode::PerformLayout,
544            )?;
545
546            // For root nodes, the position should be at (margin.left, margin.top) relative
547            // to the viewport origin, because the margin creates space between the viewport
548            // edge and the element's border-box.
549            if !super::pos_contains(&calculated_positions, root_idx) {
550                let root_position = if is_root_with_margin {
551                    adjusted_cb_pos
552                } else {
553                    cb_pos
554                };
555                super::pos_set(&mut calculated_positions, root_idx, root_position);
556            }
557        }
558
559        cache::reposition_clean_subtrees(
560            new_dom,
561            &new_tree,
562            &recon_result.layout_roots,
563            &mut calculated_positions,
564        );
565
566        if reflow_needed_for_scrollbars {
567            ctx.debug_log("Scrollbars changed container size, starting full reflow...");
568            recon_result.layout_roots.clear();
569            recon_result.layout_roots.insert(new_tree.root);
570            recon_result.intrinsic_dirty = (0..new_tree.nodes.len()).collect();
571            continue;
572        }
573
574        break;
575    }
576
577    // --- Step 3: Adjust Positions ---
578    crate::solver3::positioning::adjust_relative_positions(
579        &mut ctx,
580        &new_tree,
581        &mut calculated_positions,
582        viewport,
583    )?;
584
585    crate::solver3::positioning::position_out_of_flow_elements(
586        &mut ctx,
587        &mut new_tree,
588        &mut calculated_positions,
589        viewport,
590    )?;
591
592    // --- Step 3.75: Compute Stable Scroll IDs ---
593    use crate::window::LayoutWindow;
594    let (scroll_ids, scroll_id_to_node_id) = LayoutWindow::compute_scroll_ids(&new_tree, new_dom);
595
596    // --- Step 4: Update Cache ---
597    let cache_map_back = std::mem::take(&mut ctx.cache_map);
598
599    cache.tree = Some(new_tree);
600    cache.previous_positions = std::mem::replace(&mut cache.calculated_positions, calculated_positions);
601    cache.viewport = Some(viewport);
602    cache.scroll_ids = scroll_ids.clone();
603    cache.scroll_id_to_node_id = scroll_id_to_node_id;
604    cache.counters = counter_values;
605    cache.cache_map = cache_map_back;
606
607    Ok(FragmentationLayoutResult {
608        scroll_ids,
609    })
610}
611
612// Helper function (copy from mod.rs)
613fn get_containing_block_for_node(
614    tree: &crate::solver3::layout_tree::LayoutTree,
615    styled_dom: &StyledDom,
616    node_idx: usize,
617    calculated_positions: &super::PositionVec,
618    viewport: LogicalRect,
619) -> (LogicalPosition, LogicalSize) {
620    use crate::solver3::getters::get_writing_mode;
621
622    if let Some(parent_idx) = tree.get(node_idx).and_then(|n| n.parent) {
623        if let Some(parent_node) = tree.get(parent_idx) {
624            let pos = calculated_positions
625                .get(parent_idx)
626                .copied()
627                .unwrap_or_default();
628            let size = parent_node.used_size.unwrap_or_default();
629            let pbp = parent_node.box_props.unpack();
630            let content_pos = LogicalPosition::new(
631                pos.x + pbp.border.left + pbp.padding.left,
632                pos.y + pbp.border.top + pbp.padding.top,
633            );
634
635            if let Some(dom_id) = parent_node.dom_node_id {
636                let styled_node_state = &styled_dom
637                    .styled_nodes
638                    .as_container()
639                    .get(dom_id)
640                    .map(|n| &n.styled_node_state)
641                    .cloned()
642                    .unwrap_or_default();
643                let writing_mode =
644                    get_writing_mode(styled_dom, dom_id, styled_node_state).unwrap_or_default();
645                let content_size = pbp.inner_size(size, writing_mode);
646                return (content_pos, content_size);
647            }
648
649            return (content_pos, size);
650        }
651    }
652    
653    // For ROOT nodes: the containing block is the viewport.
654    // Do NOT subtract margin here - margins are handled in calculate_used_size().
655    (viewport.origin, viewport.size)
656}