-- 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. -- ============================================================================ -- decorate_main.lua - Decorate Plugin for Vertical Engine -- ============================================================================ -- File: luatex-cn-decorate.lua -- Layer: Extension Layer - Text Decoration (circles, dots, etc.) -- -- Module Purpose: -- This module provides text decoration functionality (e.g., red circles, -- emphasis dots) for the vertical typesetting engine. -- -- 1. Registry management for decoration definitions -- 2. Creating decoration marker nodes -- 3. Rendering decorations at glyph positions -- -- ============================================================================ local constants = package.loaded['core.luatex-cn-constants'] or require('core.luatex-cn-constants') local utils = package.loaded['util.luatex-cn-utils'] or require('util.luatex-cn-utils') local text_position = package.loaded['core.luatex-cn-render-position'] or require('luatex-cn-render-position') local debug = package.loaded['debug.luatex-cn-debug'] or require('debug.luatex-cn-debug') local dbg = debug.get_debugger('decorate') local D = node.direct -- Initialize global registry _G.decorate_registry = _G.decorate_registry or {} local decorate = {} --- Register a decoration (delegates to constants.register_decorate) -- @param char_str (string) The decoration character (e.g., "。", "●") -- @param xoff_str (string) X offset (e.g., "-0.6em", "5pt") -- @param yoff_str (string) Y offset -- @param size_str (string) Font size (nil = inherit from text) -- @param color_str (string) Color (e.g., "red", "0.8 0 0") -- @param font_id (number) Font ID (nil = use current font) -- @param scale (number) Scale multiplier (default 1.0) -- @return (number) Registry ID for this decoration function decorate.register(char_str, xoff_str, yoff_str, size_str, color_str, font_id, scale) return constants.register_decorate(char_str, xoff_str, yoff_str, size_str, color_str, font_id, scale) end --- Get a decoration entry from the registry -- @param reg_id (number) Registry ID -- @return (table|nil) Decoration entry or nil if not found function decorate.get(reg_id) return _G.decorate_registry and _G.decorate_registry[reg_id] end --- Clear the decoration registry function decorate.clear_registry() _G.decorate_registry = {} end -- ============================================================================ -- Rendering Functions (moved from render-page.lua) -- ============================================================================ local color_map = constants.color_map --- Resolve font size for decoration (uses PDF scaling, no new fonts) -- @param curr (node) Current node -- @param reg (table) Registry entry -- @param params (table) Render parameters -- @param ctx (table) Render context -- @return font_id, base_size, effective_scale local function resolve_decorate_font(curr, reg, params, ctx) local attr_font_id = constants.ATTR_DECORATE_FONT and D.get_attribute(curr, constants.ATTR_DECORATE_FONT) local base_font_id = (attr_font_id and attr_font_id > 0) and attr_font_id or reg.font_id or ctx.last_font_id or params.font_id or font.current() local base_f_data = font.getfont(base_font_id) local base_size = base_f_data and base_f_data.size or 655360 local scale = reg.scale or 1.0 local font_size_sp = constants.resolve_dimen(reg.font_size, base_size) local target_size = font_size_sp if not target_size or target_size == 0 then target_size = base_size * scale else target_size = target_size * scale end local effective_scale = target_size / base_size return base_font_id, base_size, effective_scale end --- Calculate decoration position -- @param pos (table) Position {col, row} -- @param reg (table) Registry entry -- @param ctx (table) Render context -- @param base_size (number) Base font size -- @param font_id (number) Font ID -- @param char (number) Character code -- @param scale (number) Scale factor -- @param glyph_h (number) Glyph height -- @param glyph_d (number) Glyph depth -- @return x_bp, y_bp (in big points) local function calculate_decorate_position(pos, reg, ctx, base_size, font_id, char, scale, glyph_h, glyph_d) local xshift_sp = constants.resolve_dimen(reg.xshift, base_size) or 0 local yshift_sp = constants.resolve_dimen(reg.yshift, base_size) or 0 -- Fetch unscaled metrics local f_data = font.getfont(font_id) local glyph_w = 0 if f_data and f_data.characters and f_data.characters[char] then glyph_w = (f_data.characters[char].width or 0) end -- Get actual column width (may differ for banxin columns) local col_width = text_position.get_column_width(pos.col, ctx.col_geom) -- Position calculation (use previous row as decorations follow characters) local _, base_x = text_position.calculate_rtl_position(pos.col, ctx.p_total_cols, ctx.col_geom, ctx.half_thickness, ctx.shift_x) -- TextFlow sub-column support: when decoration is inside jiazhu (sub_col > 0), -- use half-column width and offset base_x to the correct sub-column. local sub_col = pos.sub_col or 0 local effective_col_width = col_width if sub_col > 0 then local sub_width = col_width / 2 effective_col_width = sub_width if sub_col == 1 then base_x = base_x + sub_width -- right sub-column end -- sub_col == 2: left sub-column stays at base_x end -- Horizontal Centering: align glyph's visual center to cell center local v_center = text_position.get_visual_center(char, font_id) or (glyph_w / 2) local scaled_v_center = v_center * scale local center_offset = (effective_col_width / 2) - scaled_v_center local dec_cell_h = pos.cell_height or ctx.grid_height or 0 local target_y_sp = math.max(0, pos.y_sp - dec_cell_h) local base_y = -target_y_sp - ctx.shift_y -- Vertical Centering: Place the glyph's ink center at cell center local cell_center_y = base_y - dec_cell_h / 2 local scaled_ink_center = ((glyph_h - glyph_d) / 2) * scale local target_baseline_y = cell_center_y - scaled_ink_center -- Apply user offsets: Positive xshift moves LEFT (flow direction), positive yshift moves DOWN local final_x = base_x + center_offset - xshift_sp local final_y = target_baseline_y - yshift_sp return final_x * utils.sp_to_bp, final_y * utils.sp_to_bp end --- Handle decoration node rendering -- @param curr (node) Current node (marker) -- @param p_head (node) Page head -- @param pos (table) Position {col, row} -- @param params (table) Render parameters -- @param ctx (table) Render context -- @param reg_id (number) Registry ID -- @return p_head (updated) function decorate.handle_node(curr, p_head, pos, params, ctx, reg_id) local reg = _G.decorate_registry and _G.decorate_registry[reg_id] if not reg then return p_head end -- Get style attributes from style_registry if available (Phase 2) local style_registry = package.loaded['util.luatex-cn-style-registry'] local style_id = style_registry and D.get_attribute(curr, constants.ATTR_STYLE_REG_ID) local style_font_color = style_id and style_registry.get_font_color(style_id) local style_font_size = style_id and style_registry.get_font_size(style_id) -- Augment reg with style registry values (priority: style_registry > reg) local effective_reg = {} for k, v in pairs(reg) do effective_reg[k] = v end if style_font_color then effective_reg.color = style_font_color end if style_font_size then effective_reg.font_size = style_font_size end -- 1. Resolve font and scale factor local font_id, base_size, scale = resolve_decorate_font(curr, effective_reg, params, ctx) local char = reg.char -- 2. Create glyph (unscaled in TeX stream) local g = D.new(constants.GLYPH) D.setfield(g, "char", char) D.setfield(g, "font", font_id) D.setfield(g, "lang", 0) -- Retrieve unscaled dimensions to set correct kerning after scaling local f_data = font.getfont(font_id) local w, h, d = 0, 0, 0 if f_data and f_data.characters and f_data.characters[char] then local c_data = f_data.characters[char] w, h, d = c_data.width or 0, c_data.height or 0, c_data.depth or 0 end D.setfield(g, "width", w) D.setfield(g, "height", h) D.setfield(g, "depth", d) -- 3. Calculate position (BP) local x_bp, y_bp = calculate_decorate_position(pos, effective_reg, ctx, base_size, font_id, char, scale, h, d) -- 4. Render with scaled PDF matrix D.setfield(g, "xoffset", 0) D.setfield(g, "yoffset", 0) local draw_rgb = (effective_reg.color and color_map[effective_reg.color]) or effective_reg.color or "0 0 0" -- Build scaled matrix: [scale 0 0 scale x y] local color_part = string.format("%s %s", utils.create_color_literal(draw_rgb, false), utils.create_color_literal(draw_rgb, true)) local matrix_part = string.format("%.4f 0 0 %.4f %.4f %.4f cm", scale, scale, x_bp, y_bp) local n_start = utils.create_pdf_literal("q " .. color_part .. " " .. matrix_part) local n_end = utils.create_pdf_literal(utils.create_graphics_state_end()) p_head = D.insert_before(p_head, curr, n_start) D.insert_after(p_head, n_start, g) -- Kern back to avoid shifting TeX's cursor local k = D.new(constants.KERN) D.setfield(k, "kern", -w) D.insert_after(p_head, g, k) D.insert_after(p_head, k, n_end) dbg.log(string.format("char=%d [c:%d, y_sp:%.0f] scale=%.2f pos_x=%.4f pos_y=%.4f", char, pos.col, pos.y_sp or 0, scale, x_bp, y_bp)) return p_head end package.loaded['decorate.luatex-cn-decorate'] = decorate return decorate