-- Copyright 2026 Open-Guji (https://github.com/open-guji) -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. -- You may obtain a copy of the License at -- -- http://www.apache.org/licenses/LICENSE-2.0 -- -- Unless required by applicable law or agreed to in writing, software -- distributed under the License is distributed on an "AS IS" BASIS, -- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -- See the License for the specific language governing permissions and -- limitations under the License. -- render_page.lua - 坐标应用与视觉渲染(第三阶段主模块) -- ============================================================================ -- 文件名: render_page.lua (原 render.lua) -- 层级: 第三阶段 - 渲染层 (Stage 3: Render Layer) -- -- 【模块功能 / Module Purpose】 -- 本模块负责排版流水线的第三阶段,将虚拟坐标应用到实际节点并绘制视觉元素: -- 1. 根据 layout_map 为每个节点设置 xoffset/yoffset(文字)或 kern/shift(块) -- 2. 插入负 kern 以抵消 TLT 方向盒子的水平推进 -- 3. 调用子模块绘制边框、版心、背景 -- 4. 文本框(Textbox)块由其内部逻辑渲染好后,在此模块仅作为整体块进行定位 -- 5. 按页拆分节点流,生成多个独立的页面盒子 -- 6. 可选绘制调试网格(蓝色框显示字符位置,红色框显示 textbox 块) -- -- 【术语对照 / Terminology】 -- apply_positions - 应用坐标位置(将虚拟坐标转为实际节点属性) -- xoffset/yoffset - 字形偏移(glyph 专用定位属性) -- kern - 字距调整(用于水平定位块级节点) -- shift - 盒子垂直偏移(box.shift 属性) -- RTL - 从右到左(Right-To-Left,竖排时列序) -- page_nodes - 页面节点分组(按页分组的节点列表) -- p_head - 页面头节点(当前页的节点链头部) -- outer_shift - 外边框偏移(外边框厚度+间距) -- -- 【注意事项】 -- • Glyph 节点使用 xoffset/yoffset 定位,块级节点(HLIST/VLIST)使用 Kern+Shift -- • RTL 列序转换:物理列号 = total_cols - 1 - 逻辑列号 -- • 绘制顺序严格控制:背景最底层 → 边框 → 版心 → 文字(通过 insert_before 实现) -- • 所有 PDF 绘图指令使用 pdf_literal 节点(mode=0,用户坐标系) -- • Kern 的 subtype=1 表示"显式 kern",不会被后续清零(用于保护版心等特殊位置) -- • 【重要】如果 xoffset/yoffset 计算错误(如 0 或 超出页面范围),文字将不可见 -- • 【重要】PDF literal 语法错误(如缺少 q/Q 对,或非法颜色值)会破坏整页渲染 -- -- 【整体架构 / Architecture】 -- 输入: 节点流 + layout_map + 渲染参数(颜色、边框、页边距等) -- ↓ -- apply_positions() -- ├─ 按页分组节点(遍历 layout_map,根据 page 分组) -- ├─ 对每一页: -- │ ├─ 绘制背景色(render_background.draw_background) -- │ ├─ 设置字体颜色(render_background.set_font_color) -- │ ├─ 绘制外边框(render_border.draw_outer_border) -- │ ├─ 绘制列边框(render_border.draw_column_borders,跳过版心列) -- │ ├─ 绘制版心列(render_banxin.draw_banxin_column,含分隔线和文字) -- │ ├─ 应用节点坐标 -- │ │ ├─ Glyph: 调用 render_position.calc_grid_position() -- │ │ └─ Block: 使用 Kern 包裹 + Shift -- │ └─ 可选:绘制调试网格 -- └─ 返回 result_pages[{head, cols}] -- ↓ -- 输出: 多个渲染好的页面(每页是一个 HLIST,dir=TLT) -- -- ============================================================================ -- Load dependencies local constants = package.loaded['core.luatex-cn-constants'] or require('core.luatex-cn-constants') local D = constants.D local utils = package.loaded['util.luatex-cn-utils'] or require('util.luatex-cn-utils') local content = package.loaded['core.luatex-cn-core-content'] or require('core.luatex-cn-core-content') local debug = package.loaded['debug.luatex-cn-debug'] or require('debug.luatex-cn-debug') local dbg = debug.get_debugger('render') local page_mod = package.loaded['core.luatex-cn-core-page'] or require('core.luatex-cn-core-page') local textbox_mod = package.loaded['core.luatex-cn-core-textbox'] or require('core.luatex-cn-core-textbox') local render_border = package.loaded['core.luatex-cn-core-render-border'] or require('core.luatex-cn-core-render-border') local linemark_mod = package.loaded['decorate.luatex-cn-linemark'] or require('decorate.luatex-cn-linemark') local page_process = package.loaded['core.luatex-cn-core-render-page-process'] or require('core.luatex-cn-core-render-page-process') -- Internal functions for unit testing (delegated to page_process module) local _internal = { handle_glyph_node = page_process.handle_glyph_node, handle_block_node = page_process.handle_block_node, handle_debug_drawing = page_process.handle_debug_drawing, handle_decorate_node = page_process.handle_decorate_node, process_page_nodes = page_process.process_page_nodes, } -- 辅助函数:计算渲染上下文(尺寸、偏移、列数等) -- All values come from ctx (populated by main.lua) local function calculate_render_context(ctx) -- Unpack nested contexts local engine = ctx.engine local grid = ctx.grid local page = ctx.page local visual = ctx.visual -- All values from ctx (main.lua populates these from style stack) local border_thickness = engine.border_thickness local half_thickness = engine.half_thickness local ob_thickness_val = page.ob_thickness local ob_sep_val = page.ob_sep local c_padding_top = engine.c_padding_top local c_padding_bottom = engine.c_padding_bottom local grid_width = grid.width local grid_height = grid.height local banxin_width = grid.banxin_width or 0 local body_font_size = grid.body_font_size or grid_width -- Dynamic values from ctx (calculated per-invocation in main.lua) local outer_shift = engine.outer_shift local shift_x = engine.shift_x local shift_y = engine.shift_y local interval = grid.n_column local p_cols = grid.cols local line_limit = grid.line_limit -- Visual params from ctx.visual (populated by main.lua) local vertical_align = visual.vertical_align or "center" local background_rgb_str = utils.normalize_rgb(visual.bg_rgb) -- Fallback to document-level font_color if current style has no font_color local text_rgb_str = utils.normalize_rgb(visual.font_rgb) if not text_rgb_str then _G.document = _G.document or {} text_rgb_str = utils.normalize_rgb(_G.document.font_color) end -- Border shape decoration parameters local border_shape = visual.border_shape or "none" local border_color_str = utils.normalize_rgb(visual.border_color) or "0 0 0" local border_width = constants.to_dimen(visual.border_width) or (65536 * 0.4) local border_margin = constants.resolve_dimen(visual.border_margin, grid_height) or (65536 * 1) -- Separate x/y margins: if border_margin_x/y set, use them; else fallback to border_margin local border_margin_x = (visual.border_margin_x and visual.border_margin_x ~= "") and constants.resolve_dimen(visual.border_margin_x, grid_width) or border_margin local border_margin_y = (visual.border_margin_y and visual.border_margin_y ~= "") and constants.resolve_dimen(visual.border_margin_y, grid_height) or border_margin local border_offset = (visual.border_offset and visual.border_offset ~= "") and constants.resolve_dimen(visual.border_offset, grid_width) or 0 local outer_margin = constants.resolve_dimen(visual.outer_margin, grid_height) or 0 -- Textbox outer border (drawn around decorative shape, not via body text mechanism) local textbox_outer_border = visual.textbox_outer_border or false local textbox_ob_thickness = constants.to_dimen(visual.textbox_ob_thickness) or (65536 * 1) local textbox_ob_sep = constants.to_dimen(visual.textbox_ob_sep) or (65536 * 2) -- Available width for glyph squeeze inside bordered TextBox -- When border_margin_x > 0, chars wider than (grid_width - 2*margin_x) get h-scaled local border_avail_width = 0 if border_shape ~= "none" and border_margin_x > 0 and grid_width > 0 then local avail = grid_width - 2 * border_margin_x if avail > 0 and avail < grid_width then border_avail_width = avail end end -- Colors: border from engine (already normalized in main.lua) local b_rgb_str = engine.border_rgb_str -- Bundle column geometry triple for position functions local col_geom = { grid_width = grid_width, banxin_width = banxin_width, interval = interval, -- Phase 2.4: Free Mode column widths (page -> col -> width_sp) col_widths_sp = grid.col_widths_sp, -- Column spacing (right/left margins per column) col_spacing_top_sp = grid.col_spacing_top_sp, col_spacing_bottom_sp = grid.col_spacing_bottom_sp, } return { border_thickness = border_thickness, half_thickness = half_thickness, ob_thickness_val = ob_thickness_val, ob_sep_val = ob_sep_val, outer_shift = outer_shift, shift_x = shift_x, shift_y = shift_y, interval = interval, p_cols = p_cols, line_limit = line_limit, c_padding_top = c_padding_top, c_padding_bottom = c_padding_bottom, b_rgb_str = b_rgb_str, background_rgb_str = background_rgb_str, text_rgb_str = text_rgb_str, grid_height = grid_height, banxin_width = banxin_width, col_geom = col_geom, body_font_size = body_font_size, vertical_align = vertical_align, -- TextFlow align default (per-node align from style stack takes precedence) textflow_align = visual.textflow_align or "outward", -- Judou parameters from visual context (populated by core-main from judou plugin context) judou_pos = visual.judou_pos or "right-bottom", judou_size = visual.judou_size or "1em", judou_color = visual.judou_color or "red", -- Border shape decoration parameters (textbox only) border_shape = border_shape, border_color_str = border_color_str, border_width = border_width, border_margin = border_margin, border_margin_x = border_margin_x, border_margin_y = border_margin_y, border_offset = border_offset, outer_margin = outer_margin, border_avail_width = border_avail_width, -- Textbox outer border params textbox_outer_border = textbox_outer_border, textbox_ob_thickness = textbox_ob_thickness, textbox_ob_sep = textbox_ob_sep, -- Band (分栏) border params n_bands = grid.n_bands or 1, band_gap_sp = grid.band_gap_sp or 0, band_heights_sp = grid.band_heights_sp, band_y_offsets_sp = grid.band_y_offsets_sp, -- Content area width for RTL→LTR coordinate conversion content_width = grid.content_width or 0, } end _internal.calculate_render_context = calculate_render_context -- 辅助函数:将节点按页分组 local function group_nodes_by_page(d_head, layout_map, total_pages) local page_nodes = {} for p = 0, total_pages - 1 do page_nodes[p] = { head = nil, tail = nil, max_col = 0, max_y_sp = 0 } end local t = d_head while t do local next_node = D.getnext(t) local pos = layout_map[t] D.setnext(t, nil) if pos and pos.mode ~= "floating" then local p = pos.page or 0 if page_nodes[p] then if not page_nodes[p].head then page_nodes[p].head = t else D.setnext(page_nodes[p].tail, t) end page_nodes[p].tail = t if pos.col > page_nodes[p].max_col then page_nodes[p].max_col = pos.col end -- Track max_y_sp (bottom of furthest cell) for sp-based height if pos.y_sp then local y_bottom = pos.y_sp + (pos.cell_height or 0) if y_bottom > page_nodes[p].max_y_sp then page_nodes[p].max_y_sp = y_bottom end end else node.flush_node(D.tonode(t)) end else node.flush_node(D.tonode(t)) end t = next_node end return page_nodes end _internal.group_nodes_by_page = group_nodes_by_page -- 辅助函数:定位浮动文本框 _internal.position_floating_box = textbox_mod.render_floating_box -- Local reference to process_page_nodes from the process module local process_page_nodes = page_process.process_page_nodes -- 辅助函数:渲染单个页面 local function render_single_page(p_head, p_max_col, p, layout_map, params, ctx, p_max_y_sp) if not p_head then return nil, 0 end local p_total_cols = p_max_col + 1 local p_cols = ctx.p_cols -- Always enforce full page width to ensure correct RTL/SplitPage absolute positioning if p_cols and p_cols > 0 and p_total_cols < p_cols then p_total_cols = p_cols end -- Actual content dimensions (for special border shapes) local actual_cols = p_max_col + 1 local actual_height_sp = (p_max_y_sp and p_max_y_sp > 0) and p_max_y_sp or ctx.grid_height local grid_width = ctx.col_geom.grid_width local grid_height = ctx.grid_height local border_thickness = ctx.border_thickness local line_limit = ctx.line_limit local c_padding_top = ctx.c_padding_top local c_padding_bottom = ctx.c_padding_bottom local grid = params.grid local engine = params.engine local page = params.page -- For textbox with user-specified height, use that height for border rendering. -- user_height_sp is only set when user explicitly specifies height (e.g., height=24cm). -- Subtract 2*border_margin_y so that user height describes the outer border extent, -- since render_borders adds border_margin_y back: final = shape_height + 2*border_m_y. if page.is_textbox and engine.user_height_sp then local border_m_y = ctx.border_margin_y or ctx.border_margin or 0 local target = engine.user_height_sp - 2 * border_m_y if target > actual_height_sp then actual_height_sp = target end end local plugins = params.plugins local draw_border = engine.draw_border local draw_column_border = engine.draw_column_border local draw_band_border = engine.draw_band_border local draw_outer_border = page.is_outer_border -- Per-column column border override from style stack (插图页 / IllustrationPage): -- Scan all nodes of this page; collect which columns have column_border=false. -- If ALL columns have it, suppress globally; otherwise build no_silk_cols set. local no_silk_cols = nil -- set of column indices where 丝栏 should be suppressed if draw_column_border then local style_registry = package.loaded['util.luatex-cn-style-registry'] if style_registry then -- Per-column silk classification: -- col_has_no_silk[col] = true → first node carries column_border=false (\插图页 marker) -- col_has_silk[col] = true → any glyph in this column lacks the no-silk style -- A column is "silk" if it carries real text content not under \插图页 scope. -- Floating-textbox anchors (whatsit) are not real text and don't promote silk. local col_has_no_silk = {} local col_has_silk = {} local col_seen = {} local scan_t = p_head while scan_t do local pos = layout_map[scan_t] if pos then local col = pos.col -- First-node-per-column: check for no-silk marker if not col_seen[col] then col_seen[col] = true local sid = D.get_attribute(scan_t, constants.ATTR_STYLE_REG_ID) if sid then local style = style_registry.get(sid) if style and style.column_border == false then col_has_no_silk[col] = true end end end -- Real glyph that isn't under \插图页: this column has actual text content. if D.getid(scan_t) == constants.GLYPH then local sid = D.get_attribute(scan_t, constants.ATTR_STYLE_REG_ID) local under_no_silk = false if sid then local style = style_registry.get(sid) if style and style.column_border == false then under_no_silk = true end end if not under_no_silk then col_has_silk[col] = true end end end scan_t = D.getnext(scan_t) end -- Expand no-silk columns to cover entire half-pages. -- In butterfly binding, interval = n_column, so: -- left half = cols 0..interval-1 -- banxin = col interval -- right half = cols interval+1..2*interval -- If any column in a half has column_border=false, the whole half is suppressed. -- Additionally: if no column on the page has real silk content (i.e. the page -- is purely \插图页 markers + floating textboxes + empty cells), promote -- suppression to the entire page so the other half (where the marker could -- not land due to layout collapsing) is also borderless. This covers the -- "every half has \插图页 but layout couldn't separate them" case without -- breaking the legitimate "half illustration + half text" layout. if next(col_has_no_silk) then local interval = ctx.interval or 0 local left_no_silk = false local right_no_silk = false if interval > 0 then for col in pairs(col_has_no_silk) do if col < interval then left_no_silk = true elseif col > interval then right_no_silk = true end end -- If no real silk content exists anywhere on the page, both halves -- are conceptually under \插图页 — extend suppression to both halves. if not next(col_has_silk) then left_no_silk = true right_no_silk = true end -- Rebuild no_silk set with full half-page ranges col_has_no_silk = {} if left_no_silk then for c = 0, interval - 1 do col_has_no_silk[c] = true end end if right_no_silk then for c = interval + 1, 2 * interval do col_has_no_silk[c] = true end end end -- Check if ALL non-banxin columns are suppressed if left_no_silk and right_no_silk then draw_column_border = false else no_silk_cols = col_has_no_silk end end end end local shift_x = ctx.shift_x local outer_shift = ctx.outer_shift local b_rgb_str = ctx.b_rgb_str -- Reserved columns (computed via engine_ctx helper function) local reserved_cols = grid.get_reserved_cols and grid.get_reserved_cols(p, p_total_cols) or {} -- Right-align columns within content area BEFORE drawing borders -- Skip TextBox (content_width = total_cols * grid_width, no gap to right-align) local page_col_widths_sp = (ctx.col_geom and ctx.col_geom.col_widths_sp and ctx.col_geom.col_widths_sp[p]) or nil -- Detect TitlePage: has legacy col_widths (from \行[width=...] in 书名页) -- but NOT from Free Mode (which exports to col_widths_sp instead) local has_free_mode_widths = page_col_widths_sp and next(page_col_widths_sp) local legacy_col_widths = not has_free_mode_widths and _G.content and _G.content.col_widths local is_titlepage = legacy_col_widths and #legacy_col_widths > 0 if not page.is_textbox and not is_titlepage then local total_cols_width_sp if has_free_mode_widths then -- Free Mode: sum variable column widths total_cols_width_sp = 0 for _, w in pairs(page_col_widths_sp) do total_cols_width_sp = total_cols_width_sp + w end else -- Fixed-width mode: calculate total width including banxin local col_gwidth = ctx.col_geom and ctx.col_geom.grid_width or 0 local col_bwidth = ctx.col_geom and ctx.col_geom.banxin_width or 0 local col_interval = ctx.col_geom and ctx.col_geom.interval or 0 if col_interval > 0 and col_bwidth > 0 and col_bwidth ~= col_gwidth then -- With banxin columns local n_banxin = math.floor(p_total_cols / (col_interval + 1)) local n_content = p_total_cols - n_banxin total_cols_width_sp = n_content * col_gwidth + n_banxin * col_bwidth else -- Uniform width total_cols_width_sp = p_total_cols * col_gwidth end end -- Right-align: add offset if columns are narrower than content area local content_width_sp = grid.content_width or 0 if content_width_sp > total_cols_width_sp then shift_x = shift_x + (content_width_sp - total_cols_width_sp) end end -- Scan layout_map for taitou columns (negative y_sp = raised border) local col_min_y_sp = {} local scan_t = p_head while scan_t do local pos = layout_map[scan_t] if pos then local col = pos.col local y = pos.y_sp or 0 if y < 0 and (not col_min_y_sp[col] or y < col_min_y_sp[col]) then col_min_y_sp[col] = y end end scan_t = D.getnext(scan_t) end -- Borders, background, and decorative frames (handled by render_border module) p_head = render_border.render_borders(p_head, { -- Grid and dimensions p_total_cols = p_total_cols, actual_cols = actual_cols, actual_height_sp = actual_height_sp, grid_width = grid_width, grid_height = grid_height, col_geom = ctx.col_geom, banxin_width = ctx.banxin_width, interval = ctx.interval, line_limit = line_limit, content_height_sp = engine.content_height_sp, -- Border params border_thickness = border_thickness, c_padding_top = c_padding_top, c_padding_bottom = c_padding_bottom, shift_x = shift_x, outer_shift = outer_shift, b_rgb_str = b_rgb_str, ob_thickness_val = ctx.ob_thickness_val, ob_sep_val = ctx.ob_sep_val, -- Flags draw_border = draw_border, draw_column_border = draw_column_border, draw_band_border = draw_band_border, draw_outer_border_flag = draw_outer_border, is_textbox = page.is_textbox, reserved_cols = reserved_cols, -- Visual params border_shape = ctx.border_shape, border_color_str = ctx.border_color_str, border_width = ctx.border_width, border_margin = ctx.border_margin, border_margin_x = ctx.border_margin_x, border_margin_y = ctx.border_margin_y, border_offset = ctx.border_offset, background_rgb_str = ctx.background_rgb_str, -- Taitou raised border col_min_y_sp = col_min_y_sp, -- Per-column silk suppression (插图页 partial page) no_silk_cols = no_silk_cols, -- Textbox outer border (drawn around decorative shape) textbox_outer_border = ctx.textbox_outer_border, textbox_ob_thickness = ctx.textbox_ob_thickness, textbox_ob_sep = ctx.textbox_ob_sep, -- Band (分栏) border params (per-page inline table overrides global) band_mode = (ctx.n_bands and ctx.n_bands > 1), n_bands = ctx.n_bands, band_heights_sp = ctx.band_heights_sp, band_y_offsets_sp = ctx.band_y_offsets_sp, band_gap_sp = ctx.band_gap_sp, -- Per-page inline table band info page_table_bands = _G.content and _G.content.page_table_bands and _G.content.page_table_bands[p] or nil, }) -- Font color p_head = content.set_font_color(p_head, ctx.text_rgb_str) -- Node positions -- Update context with page-specific total_cols and col_widths_sp local ctx_node = {} for k, v in pairs(ctx) do ctx_node[k] = v end ctx_node.p_total_cols = p_total_cols -- Phase 2.4: Pass page-specific column widths for Free Mode ctx_node.page_num = p ctx_node.page_col_widths_sp = page_col_widths_sp -- Pass page-specific column spacing for glyph offset within column ctx_node.page_col_spacing_top_sp = (ctx.col_geom and ctx.col_geom.col_spacing_top_sp and ctx.col_geom.col_spacing_top_sp[p]) or nil ctx_node.page_col_spacing_bottom_sp = (ctx.col_geom and ctx.col_geom.col_spacing_bottom_sp and ctx.col_geom.col_spacing_bottom_sp[p]) or nil -- Apply the computed right-alignment shift_x (for legacy calc_grid_position) ctx_node.shift_x = shift_x -- Original shift_x (without right-align correction) for RTL pos.x conversion ctx_node.shift_x_base = ctx.shift_x -- Per-page content_width for RTL→LTR coordinate conversion. -- TextBox: total_cols * grid_width -- Main content: grid.content_width from page geometry -- Note: pages with variable-width columns (TitlePage, Free Mode) get content_width -- from pos.content_width in layout_map (set during layout re-compute step). if page.is_textbox then ctx_node.content_width = p_total_cols * grid_width else ctx_node.content_width = grid.content_width or ctx.content_width or 0 end -- Pass is_textbox flag so process_page_nodes can skip outer col_widths fallback ctx_node.is_textbox = page.is_textbox p_head = process_page_nodes(p_head, layout_map, params, ctx_node) -- Render Line Marks (专名号/书名号) - batch PDF drawing after all glyphs are positioned if ctx_node.line_mark_entries and #ctx_node.line_mark_entries > 0 then p_head = linemark_mod.render_line_marks(p_head, ctx_node.line_mark_entries, ctx_node) end -- Render Floating TextBoxes (mode="floating" entries in layout_map) for _, pos in pairs(layout_map) do if pos.mode == "floating" and pos.page == p then p_head = textbox_mod.render_floating_box(p_head, pos, params) end end -- Debug: draw table cell coordinates if debug is enabled -- page_table_bands[p] is an array of per-table band info (one entry per table on this page) local ptbs = _G.content and _G.content.page_table_bands and _G.content.page_table_bands[p] if ptbs and debug.is_enabled("table") then for _, ptb in ipairs(ptbs) do p_head = debug.draw_table_cells_debug(p_head, ptb, { total_cols = p_total_cols, shift_x = shift_x, shift_y = ctx.shift_y, half_thickness = ctx.half_thickness, col_geom = ctx.col_geom, }) end end -- Debug: draw layout debug overlay (TextBox blocks, floating boxes, sidenotes) if debug.is_enabled("layout") then local plugin_ctxs = params.plugins and params.plugins.plugin_contexts local sidenote_map = plugin_ctxs and plugin_ctxs["sidenote"] and plugin_ctxs["sidenote"].map or nil p_head = debug.draw_layout_debug(p_head, layout_map, p, { content_width = ctx_node.content_width, shift_x = ctx.shift_x, shift_y = ctx.shift_y, half_thickness = ctx.half_thickness, grid_width = grid_width, grid_height = grid_height, col_geom = ctx.col_geom, sidenote_map = sidenote_map, p_total_cols = p_total_cols, }) end -- For TextBox: return actual content dimensions, not expanded page dimensions -- This ensures TextBox output box has correct dimensions in main document flow local return_cols = page.is_textbox and actual_cols or p_total_cols local return_height_sp = page.is_textbox and actual_height_sp or engine.content_height_sp -- Include border_margin in TextBox height only in natural layout mode -- In grid mode, height is quantized to integer rows so margin is absorbed by ceil() local is_natural = _G.content and _G.content.layout_mode == "natural" if page.is_textbox and is_natural and ctx.border_shape ~= "none" and (ctx.border_margin or 0) > 0 then return_height_sp = return_height_sp + 2 * ctx.border_margin end -- Add outer_margin for extra spacing after TextBox (independent of layout mode) if page.is_textbox and (ctx.outer_margin or 0) > 0 then return_height_sp = return_height_sp + ctx.outer_margin end return D.tonode(p_head), return_cols, return_height_sp end _internal.render_single_page = render_single_page -- @param head (node) 节点列表头部 -- @param layout_map (table) 从节点指针到 {col, row} 的映射 -- @param params (table) 渲染参数 -- @return (table) 页面信息数组 {head, cols} local function apply_positions(head, layout_map, params) local d_head = D.todirect(head) local ctx = calculate_render_context(params) dbg.log(string.format("[render] apply_positions: border=%s, font=%s", tostring(params.border_rgb), tostring(params.font_rgb))) -- Group nodes by page local page_nodes = group_nodes_by_page(d_head, layout_map, params.total_pages) local result_pages = {} -- Process each page for p = 0, params.total_pages - 1 do local p_head = page_nodes[p].head local p_max_col = page_nodes[p].max_col local p_max_y_sp = page_nodes[p].max_y_sp local rendered_head, cols, height_sp = render_single_page(p_head, p_max_col, p, layout_map, params, ctx, p_max_y_sp) if rendered_head then result_pages[p + 1] = { head = rendered_head, cols = cols, height_sp = height_sp } end end return result_pages end -- Create module table local render = { apply_positions = apply_positions, _internal = _internal } -- Register module package.loaded['core.luatex-cn-core-render-page'] = render return render