-- 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. -- ============================================================================ -- luatex-cn-core-render-border.lua - 边框渲染模块 -- ============================================================================ -- 文件名: luatex-cn-core-render-border.lua -- 层级: 第三阶段 - 渲染层 (Stage 3: Render Layer) -- -- 【模块功能 / Module Purpose】 -- 本模块负责边框和装饰边框的渲染: -- 1. draw_column_borders: 绘制普通列的边框(跳过版心列) -- 2. draw_outer_border: 绘制整个内容区域的外围边框 -- 3. render_borders: 高层协调函数,处理所有边框渲染 -- 4. 装饰边框形状(rect/octagon/circle)的渲染 -- -- ============================================================================ -- Load dependencies local utils = package.loaded['util.luatex-cn-utils'] or require('util.luatex-cn-utils') local constants = package.loaded['core.luatex-cn-constants'] or require('core.luatex-cn-constants') local drawing = package.loaded['util.luatex-cn-drawing'] or require('util.luatex-cn-drawing') local page_mod = package.loaded['core.luatex-cn-core-page'] or require('core.luatex-cn-core-page') local text_position = package.loaded['core.luatex-cn-render-position'] or require('core.luatex-cn-render-position') -- ============================================================================ -- Column Border Drawing -- ============================================================================ --- 绘制列边框(仅限普通列,不含版心列) -- 版心列应由 banxin.draw_banxin_column 单独绘制 -- @param p_head (node) 节点列表头部(直接引用) -- @param params (table) 参数表: -- - total_cols: 要绘制的总列数 -- - grid_width: 每列的宽度 (sp) -- - content_dim_h: 列边框高度 (sp), 即 line_limit * grid_height + padding -- - border_thickness: 边框厚度 (sp) -- - shift_x: 水平偏移 (sp) -- - outer_shift: 外边框偏移 (sp) -- - border_rgb_str: 归一化的 RGB 颜色字符串 -- - banxin_cols: 可选,要跳过的列索引集合(版心列) -- @return (node) 更新后的头部 local function draw_column_borders(p_head, params) local sp_to_bp = utils.sp_to_bp local total_cols = params.total_cols local grid_width = params.grid_width local border_thickness = params.border_thickness local shift_x = params.shift_x local outer_shift = params.outer_shift local border_rgb_str = params.border_rgb_str local banxin_cols = params.banxin_cols or {} -- Set of column indices to skip local col_min_y_sp = params.col_min_y_sp or {} -- Per-column min y_sp for taitou raised border local col_geom = params.col_geom or { grid_width = grid_width, banxin_width = params.banxin_width or 0, interval = params.interval or 0, } local content_dim_h = params.content_dim_h -- Pre-computed: line_limit * grid_height + padding local b_thickness_bp = border_thickness * sp_to_bp local half_thickness = math.floor(border_thickness / 2) -- Variable-width column borders: when col_widths is set, -- each column has its own width in sp. local col_widths = _G.content and _G.content.col_widths if col_widths and #col_widths > 0 then for i = 1, #col_widths do local logical_col = i - 1 local rtl_col = total_cols - 1 - logical_col local tx_bp = (text_position.get_column_x_var(rtl_col, col_widths, total_cols) + half_thickness + shift_x) * sp_to_bp local ty_bp = -(half_thickness + outer_shift) * sp_to_bp local tw_bp = col_widths[i] * sp_to_bp local th_bp = -content_dim_h * sp_to_bp local literal = utils.create_border_literal(b_thickness_bp, border_rgb_str, tx_bp, ty_bp, tw_bp, th_bp) p_head = utils.insert_pdf_literal(p_head, literal) end return p_head end for col = 0, total_cols - 1 do -- Skip banxin columns (they are drawn separately by banxin module) if not banxin_cols[col] then local rtl_col = total_cols - 1 - col local tx_bp = (text_position.get_column_x(rtl_col, col_geom) + half_thickness + shift_x) * sp_to_bp local ty_bp = -(half_thickness + outer_shift) * sp_to_bp local tw_bp = text_position.get_column_width(col, col_geom) * sp_to_bp local th_bp = -content_dim_h * sp_to_bp -- Taitou raised border: extend column upward for negative y_sp columns local min_y = col_min_y_sp[col] if min_y and min_y < 0 then local raised_sp = -min_y ty_bp = ty_bp + raised_sp * sp_to_bp th_bp = th_bp - raised_sp * sp_to_bp end -- Draw column border local literal = utils.create_border_literal(b_thickness_bp, border_rgb_str, tx_bp, ty_bp, tw_bp, th_bp) p_head = utils.insert_pdf_literal(p_head, literal) end end return p_head end -- ============================================================================ -- Outer Border Drawing -- ============================================================================ --- 在整个内容区域外围绘制外边框 -- 当有抬头列(负行号)时,绘制阶梯状外边框以包裹突出文字 -- @param p_head (node) 节点列表头部(直接引用) -- @param params (table) 参数表: -- - inner_width: 内部内容宽度 (sp) -- - inner_height: 内部内容高度 (sp) -- - outer_border_thickness: 外边框厚度 (sp) -- - outer_border_sep: 内外边框间距 (sp) -- - border_rgb_str: 归一化的 RGB 颜色字符串 -- - col_min_y_sp: (optional) 每列最小行号表 -- - total_cols: (optional) 总列数 -- - grid_width: (optional) 格子宽度 (sp) -- - grid_height: (optional) 格子高度 (sp) -- - half_thickness: (optional) 列边框半厚度 (sp) -- - shift_x: (optional) 水平偏移 (sp) -- @return (node) 更新后的头部 local function draw_outer_border(p_head, params) local sp_to_bp = utils.sp_to_bp local inner_width = params.inner_width local inner_height = params.inner_height local ob_thickness_val = params.outer_border_thickness local ob_sep_val = params.outer_border_sep local border_rgb_str = params.border_rgb_str local col_min_y_sp = params.col_min_y_sp or {} local ob_thickness_bp = ob_thickness_val * sp_to_bp local tx_bp = (ob_thickness_bp / 2) local ty_bp = -(ob_thickness_bp / 2) local tw_bp = (inner_width + ob_sep_val * 2 + ob_thickness_val) * sp_to_bp local th_bp = -(inner_height + ob_sep_val * 2 + ob_thickness_val) * sp_to_bp -- Check if any taitou columns exist local has_taitou = false for _, v in pairs(col_min_y_sp) do if v and v < 0 then has_taitou = true; break end end if not has_taitou then -- Simple rectangle (no taitou) local literal = utils.create_border_literal(ob_thickness_bp, border_rgb_str, tx_bp, ty_bp, tw_bp, th_bp) p_head = utils.insert_pdf_literal(p_head, literal) return p_head end -- Stepped outer border path for taitou columns local total_cols = params.total_cols local grid_width = params.grid_width local grid_height = params.grid_height local half_thickness = params.half_thickness or 0 local shift_x = params.shift_x or 0 local gh_bp = grid_height * sp_to_bp -- Bottom and right edges of outer border local by_bp = ty_bp + th_bp -- bottom Y local rx_bp = tx_bp + tw_bp -- right X local ob_sep_bp = ob_sep_val * sp_to_bp -- Per-column top Y (visual left to right = rtl_col 0 to n-1) local col_tops = {} for rtl_col = 0, total_cols - 1 do local col = total_cols - 1 - rtl_col local min_y = col_min_y_sp[col] if min_y and min_y < 0 then col_tops[rtl_col] = ty_bp + (-min_y) * sp_to_bp else col_tops[rtl_col] = ty_bp end end -- Column boundary X positions (supports mixed column widths) local col_geom = params.col_geom or { grid_width = grid_width, banxin_width = params.banxin_width or 0, interval = params.interval or 0, } local function cb(b) return (text_position.get_column_x(b, col_geom) + half_thickness + shift_x) * sp_to_bp end -- Construct path: counter-clockwise from bottom-left local parts = {} -- Start at bottom-left parts[#parts + 1] = string.format("%.4f %.4f m", tx_bp, by_bp) -- Left edge up to leftmost column's top parts[#parts + 1] = string.format("%.4f %.4f l", tx_bp, col_tops[0]) -- Step offset: ob_sep measured from inner edges of both borders (compensate stroke centering) local half_inner_bp = half_thickness * sp_to_bp local step_offset = ob_sep_bp + ob_thickness_bp / 2 + half_inner_bp -- Top edge with steps (left to right) for b = 1, total_cols do local rtl_col = b - 1 -- column to the left of boundary b local cur_top = col_tops[rtl_col] if b < total_cols then local next_top = col_tops[b] local has_step = math.abs(next_top - cur_top) > 0.001 if has_step then -- Shift step vertical to maintain ob_sep gap from inner column border edge local step_x if cur_top > next_top then -- Step DOWN (left taller): shift RIGHT past inner boundary step_x = cb(b) + step_offset else -- Step UP (right taller): shift LEFT before inner boundary step_x = cb(b) - step_offset end -- Horizontal to step position, then vertical step parts[#parts + 1] = string.format("%.4f %.4f l", step_x, cur_top) parts[#parts + 1] = string.format("%.4f %.4f l", step_x, next_top) else -- No step: horizontal to column boundary parts[#parts + 1] = string.format("%.4f %.4f l", cb(b), cur_top) end else -- Last column: horizontal to right edge parts[#parts + 1] = string.format("%.4f %.4f l", rx_bp, cur_top) end end -- Right edge down to bottom-right parts[#parts + 1] = string.format("%.4f %.4f l", rx_bp, by_bp) -- Close and stroke parts[#parts + 1] = "h S" local path_str = table.concat(parts, " ") local literal = string.format("q %.2f w %s RG %s Q", ob_thickness_bp, border_rgb_str, path_str) p_head = utils.insert_pdf_literal(p_head, literal) return p_head end -- ============================================================================ -- High-level Border Rendering -- ============================================================================ --- 渲染所有边框(列边框、外边框、装饰边框、背景) -- @param p_head (node) 节点列表头部 -- @param params (table) 参数表: -- -- Grid and dimensions -- - p_total_cols: 页面总列数 -- - actual_cols: 实际内容列数 -- - actual_height_sp: 实际内容高度 (sp) -- - grid_width: 每列宽度 (sp) -- - grid_height: 每行高度 (sp) -- - content_height_sp: 内容区高度 (sp), 从三层架构获取 -- -- Border params -- - border_thickness: 边框厚度 (sp) -- - b_padding_top: 顶部内边距 (sp) -- - b_padding_bottom: 底部内边距 (sp) -- - shift_x: 水平偏移 (sp) -- - outer_shift: 外边框偏移 (sp) -- - b_rgb_str: 边框颜色字符串 -- - ob_thickness_val: 外边框厚度 (sp) -- - ob_sep_val: 外边框间距 (sp) -- -- Flags -- - draw_border: 是否绘制列边框 -- - draw_outer_border: 是否绘制外边框 -- - is_textbox: 是否为文本框模式 -- - reserved_cols: 要跳过的列索引集合 -- -- Visual params -- - border_shape: 装饰边框形状 ("none", "rect", "octagon", "circle") -- - border_color_str: 装饰边框颜色 -- - border_width: 装饰边框宽度 (sp) -- - border_margin: 装饰边框外边距 (sp) -- - background_rgb_str: 背景颜色字符串 -- - text_rgb_str: 文字颜色字符串 -- @return (node) 更新后的头部 local function render_borders(p_head, params) local p_total_cols = params.p_total_cols local actual_cols = params.actual_cols local grid_width = params.grid_width local grid_height = params.grid_height local border_thickness = params.border_thickness local b_padding_top = params.b_padding_top local b_padding_bottom = params.b_padding_bottom local is_textbox = params.is_textbox local banxin_width = params.banxin_width or 0 local interval = params.interval or 0 local col_geom = params.col_geom or { grid_width = grid_width, banxin_width = banxin_width, interval = interval, } -- Calculate content dimensions (shared logic in content module) local content_mod = package.loaded['core.luatex-cn-core-content'] or require('core.luatex-cn-core-content') local content_dim_w, content_dim_h, inner_width, inner_height = content_mod.calculate_content_dimensions({ is_textbox = is_textbox, actual_cols = actual_cols, actual_height_sp = params.actual_height_sp, grid_width = grid_width, grid_height = grid_height, content_height_sp = params.content_height_sp, b_padding_top = b_padding_top, b_padding_bottom = b_padding_bottom, p_total_cols = p_total_cols, border_thickness = border_thickness, banxin_width = banxin_width, interval = interval, }) -- 1. Draw column borders if params.draw_border and p_total_cols > 0 then p_head = draw_column_borders(p_head, { total_cols = p_total_cols, grid_width = grid_width, col_geom = col_geom, content_dim_h = content_dim_h, border_thickness = border_thickness, shift_x = params.shift_x, outer_shift = params.outer_shift, border_rgb_str = params.b_rgb_str, banxin_cols = params.reserved_cols, col_min_y_sp = params.col_min_y_sp, }) end -- 2. Draw outer border if params.draw_outer_border_flag and p_total_cols > 0 then p_head = draw_outer_border(p_head, { inner_width = inner_width, inner_height = inner_height, outer_border_thickness = params.ob_thickness_val, outer_border_sep = params.ob_sep_val, border_rgb_str = params.b_rgb_str, -- Taitou stepped border params col_min_y_sp = params.col_min_y_sp, total_cols = p_total_cols, grid_width = grid_width, grid_height = grid_height, col_geom = col_geom, half_thickness = math.floor(border_thickness / 2), shift_x = params.shift_x, }) end -- 3. Draw background (shaped or rectangular) local border_shape = params.border_shape local shape_width = actual_cols * grid_width local shape_height = params.actual_height_sp if border_shape == "octagon" and params.background_rgb_str then -- Octagon-shaped background local border_m = params.border_margin or 0 p_head = drawing.draw_octagon_fill(p_head, { x = -border_m, y = border_m, width = shape_width + 2 * border_m, height = shape_height + 2 * border_m, color_str = params.background_rgb_str, }) elseif border_shape == "circle" and params.background_rgb_str then -- Circle-shaped background local border_m = params.border_margin or 0 p_head = drawing.draw_circle_fill(p_head, { cx = shape_width / 2, cy = -shape_height / 2, radius = math.max(shape_width, shape_height) / 2 + border_m, color_str = params.background_rgb_str, }) else -- Rectangular background (default) p_head = page_mod.draw_background(p_head, { bg_rgb_str = params.background_rgb_str, inner_width = inner_width, inner_height = inner_height, outer_shift = params.outer_shift, is_textbox = is_textbox, }) end -- 4. Draw border frame decoration if border_shape and border_shape ~= "none" then local border_color = params.border_color_str or params.b_rgb_str or "0 0 0" local border_w = params.border_width or (65536 * 0.4) local border_m = params.border_margin or 0 if border_shape == "rect" then p_head = drawing.draw_rect_frame(p_head, { x = -border_m, y = border_m, width = shape_width + 2 * border_m, height = shape_height + 2 * border_m, line_width = border_w, color_str = border_color, }) elseif border_shape == "octagon" then p_head = drawing.draw_octagon_frame(p_head, { x = -border_m, y = border_m, width = shape_width + 2 * border_m, height = shape_height + 2 * border_m, line_width = border_w, color_str = border_color, }) elseif border_shape == "circle" then p_head = drawing.draw_circle_frame(p_head, { cx = shape_width / 2, cy = -shape_height / 2, radius = math.max(shape_width, shape_height) / 2 + border_m, line_width = border_w, color_str = border_color, }) end end -- 5. Textbox outer border (drawn around the decorative shape frame) -- This is separate from body text outer border (section 2) to avoid coordinate conflicts if params.textbox_outer_border and border_shape and border_shape ~= "none" then local border_w = params.border_width or (65536 * 0.4) local border_m = params.border_margin or 0 local ob_t = params.textbox_ob_thickness or (65536 * 1) local ob_s = params.textbox_ob_sep or (65536 * 2) local border_color = params.border_color_str or params.b_rgb_str or "0 0 0" -- Outer border wraps the decorative shape with ob_sep gap -- Gap measured from outer edge of inner stroke to inner edge of outer stroke local gap = border_w / 2 + ob_s + ob_t / 2 local outer_x = -(border_m + gap) local outer_y = border_m + gap local outer_w = shape_width + 2 * (border_m + gap) local outer_h = shape_height + 2 * (border_m + gap) p_head = drawing.draw_rect_frame(p_head, { x = outer_x, y = outer_y, width = outer_w, height = outer_h, line_width = ob_t, color_str = border_color, }) end return p_head end -- ============================================================================ -- Module Exports -- ============================================================================ local render_border = { draw_column_borders = draw_column_borders, draw_outer_border = draw_outer_border, render_borders = render_borders, } -- Register module package.loaded['core.luatex-cn-core-render-border'] = render_border return render_border