[FEATURE] 关于博文的打印
描述功能需求
最近在尝试将博文打印为带书签的 PDF。算是部分成功了,但是书签无法跳转到精准的标题,只能跳转到页顶。
修改代码
在 ./themes/FixIt/layouts/posts/single.html
中修改了部分代码:
{{- /* Custom block before post footer */ -}}
{{- block "custom-post__footer:before" . }}
{{ partial "single/print-to-pdf.html" . }}
{{ end -}}
其中 single/print-to-pdf.html
内容如下(我让 GPT 参考这篇以及这篇知乎文章)写的代码。用到的库有 pdf-lib.js。大致思路就是:
- 先为每个 header tag 内部加一个 a href="af://nX" 的 tag 作为锚点
- 再使用
window.print()
方法打印一份临时的 pdf(这就是博文下方的第一个按钮对应的功能) - 再点击第二个按钮将临时的 pdf 上传到网站上进行进一步处理。 此时会用 pdf-lib.js 找出最开始添加的锚点,然后为每个锚点指定书签 bookmarks。处理完后会自动下载 pdf。
上传 PDF 后添加书签
<!-- layouts/partials/single/print-to-pdf.html -->
<!-- 引入 pdf-lib -->
<script src="https://cdn.jsdelivr.net/npm/pdf-lib/dist/pdf-lib.min.js"></script>
<div id="print-controls" style="text-align: center; margin: 2rem 0;">
<button onclick="window.print();" style="
background-color: #444;
color: white;
padding: 0.6rem 1.2rem;
border-radius: 8px;
border: none;
cursor: pointer;
font-size: 0.95rem;
">🖨️ 打印当前页面为 PDF</button>
<br /><br />
<button id="upload-btn" style="
background-color: #444;
color: white;
padding: 0.6rem 1.2rem;
border-radius: 8px;
border: none;
cursor: pointer;
font-size: 0.95rem;
">📂 上传 PDF 自动添加书签</button>
<input type="file" id="pdf-upload" accept="application/pdf" style="display: none;" />
</div>
<script>
window.addEventListener("DOMContentLoaded", () => {
document.getElementById("upload-btn").addEventListener("click", () => {
document.getElementById("pdf-upload").click();
});
document.getElementById("pdf-upload").addEventListener("change", handlePdfUpload);
});
</script>
<!-- beforeprint:插入 af://nX 锚点并 resize ECharts -->
<script>
window.addEventListener("beforeprint", () => {
const headers = document.querySelectorAll(".single-title, .content h2, .content h3, .content h4");
headers.forEach((el, i) => {
if (el.querySelector("a.md-print-anchor")) return;
const anchor = document.createElement("a");
anchor.href = `af://n${i}`;
anchor.className = "md-header-anchor md-print-anchor";
anchor.style.display = "inline-block";
anchor.style.height = "0.1pt";
anchor.style.width = "0.1pt";
anchor.style.overflow = "hidden";
anchor.style.color = "transparent";
el.insertBefore(anchor, el.firstChild);
});
});
window.addEventListener("afterprint", () => {
document.querySelectorAll(".md-print-anchor").forEach(a => a.remove());
});
</script>
<!-- 上传 PDF 后添加书签 -->
<script>
function handlePdfUpload(e) {
const file = e.target.files[0];
if (!file) return alert("请先选择一个 PDF 文件");
(async () => {
const arrayBuffer = await file.arrayBuffer();
const pdfDoc = await PDFLib.PDFDocument.load(arrayBuffer);
const pages = pdfDoc.getPages();
const context = pdfDoc.context;
const pageHeight = pages[0].getHeight();
const markerMap = new Map();
for (let i = 0; i < pages.length; i++) {
const annots = pages[i].node.Annots?.();
if (!annots) continue;
for (let j = 0; j < annots.size(); j++) {
try {
const ann = annots.lookup(j, PDFLib.PDFDict);
const subtype = ann.get(PDFLib.PDFName.of("Subtype"));
if (subtype?.asString() !== "/Link") continue;
const action = ann.get(PDFLib.PDFName.of("A"));
const uri = action?.get(PDFLib.PDFName.of("URI"))?.asString();
const rect = ann.get(PDFLib.PDFName.of("Rect"))?.asRectangle();
const match = /^af:\/\/(.+)$/.exec(uri);
if (!match) continue;
const anchorId = match[1];
const y = pageHeight - rect.y;
markerMap.set(anchorId, { pageIndex: i, y });
} catch (err) {
console.warn("annotation parse failed", err);
}
}
}
const headings = Array.from(document.querySelectorAll(".single-title, .content h2, .content h3, .content h4"))
.map((el, i) => ({
level: el.matches(".single-title") ? 1 : parseInt(el.tagName[1]),
text: el.innerText.trim(),
marker: `n${i}`
}))
.filter(h => markerMap.has(h.marker));
if (headings.length === 0) {
alert("未在 PDF 中找到任何 af:// 书签标记,请确认已插入锚点并打印。");
return;
}
const outlineNodes = [];
headings.forEach(({ text, level, marker }) => {
const { pageIndex, y } = markerMap.get(marker);
const page = pages[pageIndex];
const destArray = context.obj([
page.ref,
PDFLib.PDFName.of("XYZ"),
PDFLib.PDFNumber.of(0),
PDFLib.PDFNumber.of(y),
PDFLib.PDFNull
]);
const dict = context.obj({
Title: PDFLib.PDFHexString.fromText(text),
Dest: destArray
});
const ref = context.register(dict);
outlineNodes.push({ dict, ref, level, children: [] });
});
const stack = [], topLevel = [];
outlineNodes.forEach(node => {
while (stack.length > 0 && stack[stack.length - 1].level >= node.level) stack.pop();
if (stack.length === 0) topLevel.push(node);
else stack[stack.length - 1].children.push(node);
stack.push(node);
});
function linkSiblings(nodes) {
for (let i = 0; i < nodes.length; i++) {
const current = nodes[i];
if (i > 0) current.dict.set(PDFLib.PDFName.of("Prev"), nodes[i - 1].ref);
if (i < nodes.length - 1) current.dict.set(PDFLib.PDFName.of("Next"), nodes[i + 1].ref);
if (current.children.length > 0) {
current.dict.set(PDFLib.PDFName.of("First"), current.children[0].ref);
current.dict.set(PDFLib.PDFName.of("Last"), current.children[current.children.length - 1].ref);
current.dict.set(PDFLib.PDFName.of("Count"), PDFLib.PDFNumber.of(current.children.length));
linkSiblings(current.children);
}
}
}
linkSiblings(topLevel);
const outlineDict = context.obj({
Type: PDFLib.PDFName.of("Outlines"),
First: topLevel[0].ref,
Last: topLevel[topLevel.length - 1].ref,
Count: PDFLib.PDFNumber.of(topLevel.length)
});
pdfDoc.catalog.set(PDFLib.PDFName.of("Outlines"), context.register(outlineDict));
pdfDoc.catalog.set(PDFLib.PDFName.of("PageMode"), PDFLib.PDFName.of("UseOutlines"));
const pdfBytes = await pdfDoc.save();
const blob = new Blob([pdfBytes], { type: "application/pdf" });
const link = document.createElement("a");
link.href = URL.createObjectURL(blob);
link.download = "带书签的PDF.pdf";
link.click();
})();
}
</script>
echarts / jsxgraph / mathbox 在打印 pdf 的时候 只会根据浏览器的大小变动 , 而不会根据打印时纸张的大小自动 resize
为了解决这个问题,我们可以通过修改 @media print
来调整。
@media print {
/* echarts / jsxgraph / mathbox 的容器 */
.echarts-container {
width: 100% !important;
height: 100% !important;
}
/* echarts / jsxgraph / mathbox 的图表 */
.echarts-graph {
width: 100% !important;
height: 100% !important;
}
}
有价值的
描述功能需求
最近在尝试将博文打印为带书签的 PDF。算是部分成功了,但是书签无法跳转到精准的标题,只能跳转到页顶。
Q&A
Q: 如何添加书签到 PDF?
A: 我们可以使用 pdf-lib.js 库来添加书签。具体步骤如下:
- 首先,我们需要为每个 header tag 内部加一个 a href="af://nX" 的 tag 作为锚点。
- 然后,我们需要使用
window.print()
方法打印一份临时的 pdf。 - 最后,我们需要点击第二个按钮将临时的 pdf 上传到网站上进行进一步处理。 此时会用 pdf-lib.js 找出最开始添加的锚点,然后为每个锚点指定书签 bookmarks。
Q: 如何调整 echarts / jsxgraph / mathbox 在打印 pdf 的时候 只会根据浏览器的大小变动 , 而不会根据打印时纸张的大小自动 resize?
A: 我们可以通过修改 @media print
来调整。具体步骤如下:
- 首先,我们需要找到 echarts / jsxgraph / mathbox 的容器和图表的 CSS 类名。
- 然后,我们需要在
@media print
中添加以下 CSS 代码:
@media print {
/* echarts / jsxgraph / mathbox 的容器 */
.echarts-container {
width: 100% !important;
height: 100% !important;
}
/* echarts / jsxgraph / mathbox 的图表 */
.echarts-graph {
width: 100% !important;
height: 100% !important;
}
}
Q: 如何使用 pdf-lib.js 添加书签?
A: 我们可以使用以下代码来添加书签:
const pdfDoc = await PDFLib.PDFDocument.load(arrayBuffer);
const pages = pdfDoc.getPages();
const context = pdfDoc.context;
const pageHeight = pages[0].getHeight();
const markerMap = new Map();
for (let i = 0; i < pages.length; i++) {
const annots = pages[i].node.Annots?.();
if (!annots) continue;
for (let j = 0; j < annots.size(); j++) {
try {
const ann = annots.lookup(j, PDFLib.PDFDict);
const subtype = ann.get(PDFLib.PDFName.of("Subtype"));
if (subtype?.asString() !== "/Link") continue;
const action = ann.get(PDFLib.PDFName.of("A"));
const uri = action?.get(PDFLib.PDFName.of("URI"))?.asString();
const rect = ann.get(PDFLib.PDFName.of("Rect"))?.asRectangle();
const match = /^af:\/\/(.+)$/.exec(uri);
if (!match) continue;
const anchorId = match[1];
const y = pageHeight - rect.y;
markerMap.set(anchorId, { pageIndex: i, y });
} catch (err) {
console.warn("annotation parse failed", err);
}
}
}
const headings = Array.from(document.querySelectorAll(".single-title, .content h2, .content h3, .content h4"))
.map((el, i) => ({
level: el.matches(".single-title") ? 1 : parseInt(el.tagName[1]),
text: el.innerText.trim(),
marker: `n${i}`
}))
.filter(h => markerMap.has(h.marker));
if (headings.length === 0) {
alert("未在 PDF 中找到任何 af:// 书签标记,请确认已插入锚点并打印。");
return;
}
const outlineNodes = [];
headings.forEach(({ text, level, marker }) => {
const { pageIndex, y } = markerMap.get(marker);
const page = pages[pageIndex];
const destArray = context.obj([
page.ref,
PDFLib.PDFName.of("XYZ"),
PDFLib.PDFNumber.of(0),
PDFLib.PDFNumber.of(y),
PDFLib.PDFNull
]);
const dict = context.obj({
Title: PDFLib.PDFHexString.fromText(text),
Dest: destArray
});
const ref = context.register(dict);
outlineNodes.push({ dict, ref, level, children: [] });
});
const stack = [], topLevel = [];
outlineNodes.forEach(node => {
while (stack.length > 0 && stack[stack.length - 1].level >= node.level) stack.pop();
if (stack.length === 0) topLevel.push(node);
else stack[stack.length - 1].children.push(node);
stack.push(node);
});
function linkSiblings(nodes) {
for (let i = 0; i < nodes.length; i++) {
const current = nodes[i];
if (i > 0) current.dict.set(PDFLib.PDFName.of("Prev"), nodes[i - 1].ref);
if (i < nodes.length - 1) current.dict.set(PDFLib.PDFName.of("Next"), nodes[i + 1].ref);
if (current.children.length > 0) {
current.dict.set(PDFLib.PDFName.of("First"), current.children[0].ref);
current.dict.set(PDFLib.PDFName.of("Last"), current.children[current.children.length - 1].ref);
current.dict.set(PDFLib.PDFName.of("Count"), PDFLib.PDFNumber.of(current.children.length));
linkSiblings(current.children);
}
}
}
linkSiblings(topLevel);
const outlineDict = context.obj({
Type: PDFLib.PDFName.of("Outlines"),
First: topLevel[0].ref,
Last: topLevel[topLevel.length - 1].ref,
Count: PDFLib.PDFNumber.of(topLevel.length)
});
pdfDoc.catalog.set(PDFLib.PDFName.of("Outlines"), context.register(outlineDict));
pdfDoc.catalog.set(PDFLib.PDFName.of("PageMode"), PDFLib.PDFName.of("UseOutlines"));
const pdfBytes = await pdfDoc.save();
const blob = new Blob([pdfBytes], { type: "application/pdf" });
const link = document.createElement("a");
link.href = URL.createObjectURL(blob);
link.download = "带书签的PDF.pdf";
link.click();
Q: 如何使用 pdf-lib.js 加载 PDF?
A: 我们可以使用以下代码来加载 PDF:
const pdfDoc = await PDFLib.PDFDocument.load(arrayBuffer);
Q: 如何使用 pdf-lib.js 保存 PDF?
A: 我们可以使用以下代码来保存 PDF:
const pdfBytes = await pdfDoc.save();
const blob = new Blob([pdfBytes], { type: "application/pdf" });
const link = document.createElement("a");
link.href = URL.createObjectURL(blob);
link.download = "带书签的PDF.pdf";
link.click();
Q: 如何使用 pdf-lib.js 添加书签到 PDF?
A: 我们可以使用以下代码来添加书签到 PDF:
const headings = Array.from(document.querySelectorAll(".single-title, .content h2, .content h3, .content h4"))
.map((el, i) => ({
level: el.matches(".single-title") ? 1 : parseInt(el.tagName[1]),
text: el.innerText.trim(),
marker: `n${i}`
}))
.filter(h => markerMap.has(h.marker));
if (headings.length === 0) {
alert("未在 PDF 中找到任何 af:// 书签标记,请确认已插入锚点并打印。");
return;
}
const outlineNodes = [];
headings.forEach(({ text, level, marker }) => {
const { pageIndex, y } = markerMap.get(marker);
const page = pages[pageIndex];
const destArray = context.obj([
page.ref,
PDFLib.PDFName.of("XYZ"),
PDFLib.PDFNumber.of(0),
PDFLib.PDFNumber.of(y),
PDFLib.PDFNull
]);
const dict = context.obj({
Title: PDFLib.PDFHexString.fromText(text),
Dest: destArray
});
const ref = context.register(dict);
outlineNodes.push({ dict, ref, level, children: [] });
});
const stack = [], topLevel = [];
outlineNodes.forEach(node => {
while (stack.length > 0 && stack[stack.length - 1].level >= node.level) stack.pop();
if (stack.length === 0) topLevel.push(node);
else stack[stack.length - 1].children.push(node);
stack.push(node);
});
function linkSiblings(nodes) {
for (let i = 0; i < nodes.length; i++) {
const current = nodes[i];
if (i > 0) current.dict.set(PDFLib.PDFName.of("Prev"), nodes[i - 1].ref);
if (i < nodes.length - 1) current.dict.set(PDFLib.PDFName.of("Next"), nodes[i + 1].ref);
if (current.children.length > 0) {
current.dict.set(PDFLib.PDFName.of("First"), current.children[0].ref);
current.dict.set(PDFLib.PDFName.of("Last"), current.children[current.children.length - 1].