|
41 | 41 | \usepackage{xcolor} |
42 | 42 | \usepackage{titlesec} % 标题样式 |
43 | 43 | \usepackage{fancyhdr} % 页眉页脚 |
44 | | -\setlength{\headheight}{10pt} |
45 | | -\usepackage{bookmark} % 增强型书签 |
| 44 | +\setlength{\headheight}{7pt} |
46 | 45 | \usepackage{etoolbox} % 编程逻辑 |
47 | 46 | \usepackage{metalogox} |
48 | 47 | \usepackage{tikz} |
|
128 | 127 | \begin{luacode*} |
129 | 128 | require("lualibs") |
130 | 129 |
|
131 | | - local page_map_cache = {} |
132 | | - |
133 | | - -- 解码 PDF 字符串 (UTF-16BE 或 PDFDocEncoding) |
134 | | - function decode_pdf_string(s) |
135 | | - if type(s) ~= "string" then s = tostring(s) end |
136 | | - if s:byte(1) == 254 and s:byte(2) == 255 then |
137 | | - -- UTF-16BE |
138 | | - local out = {} |
139 | | - for i = 3, #s, 2 do |
140 | | - local b1, b2 = s:byte(i, i+1) |
141 | | - if not b2 then break end |
142 | | - local code = b1 * 256 + b2 |
143 | | - if code < 0x80 then |
144 | | - table.insert(out, string.char(code)) |
145 | | - elseif code < 0x800 then |
146 | | - table.insert(out, string.char(0xc0 + math.floor(code / 64), 0x80 + (code % 64))) |
147 | | - else |
148 | | - table.insert(out, string.char(0xe0 + math.floor(code / 4096), 0x80 + (math.floor(code / 64) % 64), 0x80 + (code % 64))) |
149 | | - end |
150 | | - end |
151 | | - return table.concat(out) |
152 | | - else |
153 | | - -- 简单处理 PDFDocEncoding (大部分情况同 UTF-8) |
154 | | - return s |
155 | | - end |
156 | | - end |
157 | | - |
158 | | - -- 获取页面索引 (1-based) |
159 | | - function get_page_index(doc, page_obj) |
160 | | - if not page_map_cache[doc] then |
161 | | - page_map_cache[doc] = {} |
162 | | - local pages = pdfe.pagestotable(doc) |
163 | | - for i, p in ipairs(pages) do |
164 | | - page_map_cache[doc][p] = i |
165 | | - end |
166 | | - end |
167 | | - return page_map_cache[doc][page_obj] |
168 | | - end |
169 | | - |
170 | | - -- 解析 Destination 为页码 |
171 | | - function resolve_destination(doc, dest) |
172 | | - if not dest then return nil end |
173 | | - if type(dest) == "string" then |
174 | | - local catalog = doc.Catalog |
175 | | - local d = nil |
176 | | - if catalog.Dests then d = catalog.Dests[dest] end |
177 | | - if not d and catalog.Names and catalog.Names.Dests then |
178 | | - -- Names 树查找稍微复杂,尝试递归或直接访问 |
179 | | - -- 这里尝试一种通用的查找 |
180 | | - local function find_in_tree(node, key) |
181 | | - if not node then return nil end |
182 | | - local names = node.Names |
183 | | - if names then |
184 | | - for i = 1, #names, 2 do |
185 | | - if tostring(names[i]) == key then return names[i+1] end |
186 | | - end |
187 | | - end |
188 | | - local kids = node.Kids |
189 | | - if kids then |
190 | | - for i = 1, #kids do |
191 | | - local res = find_in_tree(kids[i], key) |
192 | | - if res then return res end |
193 | | - end |
194 | | - end |
195 | | - return nil |
196 | | - end |
197 | | - d = find_in_tree(catalog.Names.Dests, dest) |
198 | | - end |
199 | | - if d then return resolve_destination(doc, d) end |
200 | | - return nil |
201 | | - end |
202 | | - -- Array 形式: [page_ref, /Fit...] |
203 | | - if type(dest) == "table" or type(dest) == "userdata" then |
204 | | - local page_obj = dest[1] |
205 | | - return get_page_index(doc, page_obj) |
206 | | - end |
207 | | - return nil |
208 | | - end |
209 | | - |
210 | | - -- 清理调试日志 |
211 | | - function extract_pdf_bookmarks(file_path, base_level, link_prefix) |
212 | | - local doc = pdfe.open(file_path) |
213 | | - if not doc then return end |
214 | | - |
215 | | - local catalog = doc.Catalog |
216 | | - local outlines = catalog.Outlines |
217 | | - if not outlines or not outlines.First then |
218 | | - pdfe.close(doc) |
219 | | - return |
220 | | - end |
221 | | - |
222 | | - local function traverse(outline, level) |
223 | | - local current = outline.First |
224 | | - while current do |
225 | | - local dest = current.Dest |
226 | | - if not dest and current.A and current.A.S == "GoTo" then |
227 | | - dest = current.A.D |
228 | | - end |
229 | | - |
230 | | - local page = resolve_destination(doc, dest) |
231 | | - if page then |
232 | | - local title = decode_pdf_string(current.Title) |
233 | | - local open = (current.Count or 0) >= 0 and "true" or "false" |
234 | | - tex.print(string.format("\\bookmark[level=%d, dest={%s.%d}, open=%s]{%s}", |
235 | | - level + base_level, link_prefix, page, open, title)) |
236 | | - end |
237 | | - |
238 | | - if current.First then |
239 | | - traverse(current, level + 1) |
240 | | - end |
241 | | - current = current.Next |
242 | | - end |
243 | | - end |
244 | | - |
245 | | - traverse(outlines, 0) |
246 | | - pdfe.close(doc) |
247 | | - end |
248 | | - |
249 | 130 | function process_book_structure() |
250 | 131 | -- 1. 读取文件 |
251 | 132 | local f = io.open("toc.json", "rb") |
|
303 | 184 |
|
304 | 185 | for i, chapter in ipairs(_G.book_content) do |
305 | 186 | tex.print("\\chapter{" .. (chapter.title or "Chapter") .. "}") |
| 187 | + -- 兼容 sections 字段名 |
306 | 188 | local sections = chapter.sections or chapter.content |
307 | 189 | if sections then |
308 | 190 | for j, section in ipairs(sections) do |
| 191 | + -- 兼容 path 字段名 |
309 | 192 | local file_path = section.path or section.file |
| 193 | + local sec_title = section.title or "Section" |
| 194 | + |
| 195 | + -- 检查文件路径是否为空 |
310 | 196 | if file_path and file_path ~= "" then |
311 | | - local link_prefix = "pdfidx_" .. i .. "_" .. j |
312 | | - -- 插入 PDF,带 link 锚点 |
| 197 | + -- 这里如果不转义路径中的特殊字符可能会有问题,但在 Windows 路径通常斜杠处理要注意 |
| 198 | + -- 最好在 Python 端把路径都转为正斜杠 / |
313 | 199 | local options = "pages=-, width=\\paperwidth, height=\\paperheight, " .. |
314 | 200 | "pagecommand={\\thispagestyle{fancy}}, " .. |
315 | | - "link=true, linkname=" .. link_prefix |
| 201 | + "addtotoc={1, section, 1, {" .. sec_title .. "}, sec:" .. i .. "-" .. j .. "}" |
316 | 202 | tex.print("\\includepdf[" .. options .. "]{" .. file_path .. "}") |
317 | | - |
318 | | - -- 提取并插入该 PDF 的书签 |
319 | | - -- base_level 为 1 表示映射为 \section |
320 | | - extract_pdf_bookmarks(file_path, 1, link_prefix) |
321 | 203 | end |
322 | 204 | end |
323 | 205 | end |
|
412 | 294 | \end{center} |
413 | 295 | \vspace*{\fill} |
414 | 296 |
|
415 | | -\end{document} |
| 297 | +\end{document} |
0 commit comments