本项目基于 Node.js 构建了一个虚拟 DOM 元素工厂(createFakeElement),通过模板函数生成多种类型的纸张布局,并最终输出 SVG 文件,可直接在浏览器中预览或用于打印。
该项目灵感来源于 PaperMe,在分析其生成逻辑后实现了高仿功能,旨在供学习和研究用途。
目前支持的模板类型包括:
- project_planning:项目规划纸
- meeting_note:会议笔记纸
- student_note:课程笔记纸(可灵活扩展)
下面展示具体实现代码:
// restore-papers.mjs
import { writeFileSync, mkdirSync } from 'fs';
function createFakeElement(tagName) {
const attrs = {};
const children = [];
let textContent = '';
const DEFAULT_FONT_STACK =
'"Arial","Microsoft YaHei","PingFang SC","Hiragino Sans GB","SimSun","WenQuanYi Micro Hei",sans-serif';
function getCurrentDimension(attrName) {
const val = attrs[attrName];
if (val === undefined || val === null) return 0;
const num = parseFloat(val);
return isNaN(num) ? 0 : num;
}
function escapeXML(s) {
return String(s)
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>');
}
const element = {
tagName,
/* ---------------- DOM-like API ---------------- */
getAttribute(name) {
return attrs[name] ?? null;
},
setAttribute(name, value) {
attrs[name] = String(value ?? '');
},
appendChild(child) {
if (child === null || child === undefined) return;
// 支持 appendChild("文字")
if (typeof child === 'string') {
textContent += child;
return;
}
children.push(child);
},
set textContent(v) {
textContent = String(v ?? '');
},
get textContent() {
return textContent;
},
/* --------- SVG width / height 兼容 --------- */
get width() {
return { baseVal: { value: getCurrentDimension('width') } };
},
get height() {
return { baseVal: { value: getCurrentDimension('height') } };
},
/* ---------------- 序列化 ---------------- */
get outerHTML() {
const attrStrEntries = Object.entries(attrs);
// 可选:自动给 text 补字体(你之前那段是对的)
if (tagName === 'text' && !attrs['font-family']) {
attrStrEntries.push(['font-family', DEFAULT_FONT_STACK]);
}
const attrStr = attrStrEntries
.map(([k, v]) => `${k}="${v.replace(/"/g, '"')}"`)
.join(' ');
const selfClosing = new Set([
'rect', 'circle', 'ellipse', 'line', 'path', 'polygon',
'polyline', 'use', 'image', 'stop'
]);
// text 永远不能自闭合
if (selfClosing.has(tagName) && children.length === 0 && !textContent) {
return `<${tagName}${attrStr ? ' ' + attrStr : ''}/>`;
}
const startTag = `<${tagName}${attrStr ? ' ' + attrStr : ''}>`;
const endTag = `</${tagName}>`;
const inner =
escapeXML(textContent) +
children.map(c => c.outerHTML).join('\n');
return `${startTag}${inner}${endTag}`;
}
};
return element;
}
let w = (t) => createFakeElement(t);
let k = (t) => Math.round((96 / 25.4) * t);
//w = (t) => document.createElementNS("http://www.w3.org/2000/svg", t),
let j = function (t) {
let e =
arguments.length > 1 && void 0 !== arguments[1]
? arguments[1]
: 0.1;
return t && !(t <= 0) && isFinite(t) ? t : e;
},
N = function (t) {
let e =
arguments.length > 1 && void 0 !== arguments[1]
? arguments[1]
: 0.1,
i = k(j(t, e));
return i <= 0 ? k(e) : i;
};
function getThemeColors(themeName, overrideColor) {
const themes = {
default: {
name: "default",
colors: {
primary: "#3B82F6",
secondary: "#6366F1",
background: "#FFFFFF",
text: "#1F2937",
lines: "#808080",
accent: "#4F46E5",
},
fonts: { heading: "Arial, sans-serif", body: "Arial, sans-serif" },
},
night: {
name: "night",
colors: {
primary: "#6366F1",
secondary: "#8B5CF6",
background: "#111827",
text: "#F9FAFB",
lines: "#4B5563",
accent: "#EC4899",
},
fonts: { heading: "Arial, sans-serif", body: "Arial, sans-serif" },
},
sepia: {
name: "sepia",
colors: {
primary: "#9F7F63",
secondary: "#8D7B68",
background: "#F5F0E6",
text: "#4B3621",
lines: "#A67C52",
accent: "#C8A27C",
},
fonts: { heading: "Georgia, serif", body: "Georgia, serif" },
},
classic: {
name: "classic",
colors: {
primary: "#1e3a8a",
secondary: "#4b5563",
background: "#f8f8f8",
text: "#111827",
lines: "#374151",
accent: "#b91c1c",
},
fonts: { heading: "Georgia, serif", body: "Georgia, serif" },
},
vintage: {
name: "vintage",
colors: {
primary: "#7c3aed",
secondary: "#6b7280",
background: "#fff8e1",
text: "#4b5563",
lines: "#9ca3af",
accent: "#d97706",
},
fonts: { heading: "Palatino, serif", body: "Palatino, serif" },
},
dark: {
name: "dark",
colors: {
primary: "#60a5fa",
secondary: "#9ca3af",
background: "#1f2937",
text: "#f9fafb",
lines: "#6b7280",
accent: "#fbbf24",
},
fonts: { heading: "system-ui, sans-serif", body: "system-ui, sans-serif" },
},
pastel: {
name: "pastel",
colors: {
primary: "#8b5cf6",
secondary: "#9ca3af",
background: "#f0fdf4",
text: "#4b5563",
lines: "#94a3b8",
accent: "#ec4899",
},
fonts: { heading: "Avenir, system-ui, sans-serif", body: "Avenir, system-ui, sans-serif" },
},
minimalist: {
name: "minimalist",
colors: {
primary: "#000000",
secondary: "#404040",
background: "#FFFFFF",
text: "#333333",
lines: "#CCCCCC",
accent: "#808080",
},
fonts: { heading: "Helvetica, Arial, sans-serif", body: "Helvetica, Arial, sans-serif" },
},
nature: {
name: "nature",
colors: {
primary: "#059669",
secondary: "#6b7280",
background: "#ecfdf5",
text: "#1f2937",
lines: "#10b981",
accent: "#f59e0b",
},
fonts: { heading: "system-ui, sans-serif", body: "system-ui, sans-serif" },
},
custom: {
name: "custom",
colors: {
primary: "#3b82f6",
secondary: "#6b7280",
background: "#ffffff",
text: "#1f2937",
lines: "#808080",
accent: "#f59e0b",
},
fonts: { heading: "system-ui, sans-serif", body: "system-ui, sans-serif" },
},
ocean: {
name: "ocean",
colors: {
primary: "#0077B6",
secondary: "#00B4D8",
background: "#F0FAFF",
text: "#003049",
lines: "#90E0EF",
accent: "#023E8A",
},
fonts: { heading: "Georgia, serif", body: "Arial, sans-serif" },
},
forest: {
name: "forest",
colors: {
primary: "#2D6A4F",
secondary: "#40916C",
background: "#F6FFF8",
text: "#1B4332",
lines: "#95D5B2",
accent: "#52B788",
},
fonts: { heading: "Verdana, Geneva, sans-serif", body: "Verdana, Geneva, sans-serif" },
},
sunset: {
name: "sunset",
colors: {
primary: "#E76F51",
secondary: "#F4A261",
background: "#FFF8F0",
text: "#5E3023",
lines: "#FFD6BA",
accent: "#E9C46A",
},
fonts: { heading: "Trebuchet MS, sans-serif", body: "Trebuchet MS, sans-serif" },
},
tech: {
name: "tech",
colors: {
primary: "#3A0CA3",
secondary: "#4361EE",
background: "#F0F4FF",
text: "#14213D",
lines: "#4CC9F0",
accent: "#7209B7",
},
fonts: { heading: "Courier New, monospace", body: "Courier New, monospace" },
},
elegant: {
name: "elegant",
colors: {
primary: "#7D5A50",
secondary: "#B4846C",
background: "#FDF6EC",
text: "#543C32",
lines: "#E7BC91",
accent: "#A47148",
},
fonts: { heading: "Garamond, serif", body: "Garamond, serif" },
},
creative: {
name: "creative",
colors: {
primary: "#FF6B6B",
secondary: "#FFD93D",
background: "#FFFFFF",
text: "#453C67",
lines: "#4D96FF",
accent: "#6BCB77",
},
fonts: { heading: "Comic Sans MS, cursive", body: "Arial, sans-serif" },
}
};
const templates = [
{
id: "project_planning",
name: "项目规划纸",
description: "帮助规划和跟踪项目进度的专业模板",
paperType: "project_planning",
paperSize: "a4",
lineColor: "#4361EE",
lineSpacing: 7,
lineWidth: 0.5,
lineStyle: "solid",
margins: { top: 25, right: 20, bottom: 25, left: 30 },
theme: "tech",
background: {
color: "#f8fafc",
pattern: "none",
patternColor: "#CCCCCC",
patternOpacity: 0.1,
},
watermark: {
enabled: !1,
text: "",
opacity: 0.1,
angle: -45,
fontSize: 48,
color: "#888888",
},
pages: { count: 1, numberingEnabled: !0, startNumber: 1 },
},
{
id: "meeting_note",
name: "会议笔记纸",
description: "结构化的会议记录模板,包含议程和行动项",
paperType: "meeting_note",
paperSize: "a4",
lineColor: "#4b5563",
lineSpacing: 8,
lineWidth: 0.5,
lineStyle: "solid",
margins: { top: 40, right: 20, bottom: 30, left: 20 },
theme: "elegant",
background: {
color: "#ffffff",
pattern: "none",
patternColor: "#e0e0e0",
patternOpacity: 0.1,
},
watermark: {
enabled: !1,
text: "",
opacity: 0.15,
angle: -45,
fontSize: 48,
color: "#888888",
},
pages: { count: 1, numberingEnabled: !0, startNumber: 1 },
},
];
// 修复函数:替换 a[xxx] 为 getThemeColors 调用
return { line: overrideColor || (themes[themeName]?.line || "#808080") };
}
// ===============================================
const z = {
student_note: (t, e) => {
var i, r;
let {
lineSpacing: n,
lineWidth: o,
margins: l,
theme: s,
lineColor: d,
} = e;
const themeColors = getThemeColors(s, d);
let u = d || themeColors.line;
let p = parseInt(t.getAttribute("width") || "0");
let c = parseInt(t.getAttribute("height") || "0");
let b = N(n, 0.1);
let g = k(l.left);
let h = p - k(l.right);
let m = k(l.top);
let f = c - k(l.bottom);
let A = w("g");
A.setAttribute("stroke", u);
A.setAttribute("stroke-width", o.toString());
let x = 3 * b;
let S = w("rect");
S.setAttribute("x", g.toString());
S.setAttribute("y", m.toString());
S.setAttribute("width", (h - g).toString());
S.setAttribute("height", x.toString());
S.setAttribute("fill", "none");
S.setAttribute("stroke", u);
A.appendChild(S);
let y = w("text");
y.setAttribute("x", (g + 10).toString());
y.setAttribute("y", (m + x / 2 + 5).toString());
y.setAttribute("font-family", "Arial");
y.setAttribute("font-size", "12");
y.setAttribute("fill", u);
y.textContent = "课程笔记";
A.appendChild(y);
let C = w("text");
C.setAttribute("x", (h - 100).toString());
C.setAttribute("y", (m + x / 2 + 5).toString());
C.setAttribute("font-family", "Arial");
C.setAttribute("font-size", "10");
C.setAttribute("fill", u);
C.textContent = "日期: ___/___/___";
A.appendChild(C);
let v = k(25);
let j = w("line");
j.setAttribute("x1", (g + v).toString());
j.setAttribute("y1", (m + x).toString());
j.setAttribute("x2", (g + v).toString());
j.setAttribute("y2", f.toString());
A.appendChild(j);
for (let t = m + x + b; t <= f; t += b) {
let e = w("line");
e.setAttribute("x1", g.toString());
e.setAttribute("y1", t.toString());
e.setAttribute("x2", h.toString());
e.setAttribute("y2", t.toString());
A.appendChild(e);
}
let z = f + 10;
let E = w("line");
E.setAttribute("x1", g.toString());
E.setAttribute("y1", z.toString());
E.setAttribute("x2", h.toString());
E.setAttribute("y2", z.toString());
E.setAttribute("stroke-width", (0.5 * o).toString());
A.appendChild(E);
t.appendChild(A);
},
meeting_note: (t, e) => {
var i, r, n, o;
let {
lineSpacing: l,
lineWidth: s,
margins: d,
theme: u,
lineColor: p,
} = e;
const themeColors = getThemeColors(u, p);
let c = p || themeColors.line;
let b = themeColors.accent;
let g = parseInt(t.getAttribute("width") || "0");
let h = parseInt(t.getAttribute("height") || "0");
let m = N(l, 0.1);
let f = k(d.left);
let A = g - k(d.right);
let x = k(d.top);
let S = h - k(d.bottom);
let y = w("g");
y.setAttribute("stroke", c);
y.setAttribute("stroke-width", s.toString());
let C = 4 * m;
let v = w("rect");
v.setAttribute("x", f.toString());
v.setAttribute("y", x.toString());
v.setAttribute("width", (A - f).toString());
v.setAttribute("height", C.toString());
v.setAttribute("fill", b);
v.setAttribute("fill-opacity", "0.1");
v.setAttribute("stroke", c);
y.appendChild(v);
let j = w("text");
j.setAttribute("x", (f + 10).toString());
j.setAttribute("y", (x + 20).toString());
j.setAttribute("font-family", "Arial");
j.setAttribute("font-size", "14");
j.setAttribute("font-weight", "bold");
j.setAttribute("fill", c);
j.textContent = "会议记录";
y.appendChild(j);
[
{ label: "日期:", x: f + 10, y: x + 45 },
{ label: "参与者:", x: f + 10, y: x + 70 },
{ label: "主题:", x: A - 200, y: x + 45 },
].forEach((t) => {
let e = w("text");
e.setAttribute("x", t.x.toString());
e.setAttribute("y", t.y.toString());
e.setAttribute("font-family", "Arial");
e.setAttribute("font-size", "12");
e.setAttribute("fill", c);
e.textContent = t.label;
y.appendChild(e);
let i = t.y + 5;
let r = w("line");
r.setAttribute("x1", t.x.toString());
r.setAttribute("y1", i.toString());
r.setAttribute("x2", (t.x + 150).toString());
r.setAttribute("y2", i.toString());
r.setAttribute("stroke-width", (0.5 * s).toString());
y.appendChild(r);
});
let z = x + C + m;
let E = (S - z) / 3;
let _ = [
{ title: "议程", y: z },
{ title: "讨论要点", y: z + E },
{ title: "行动项", y: z + 2 * E },
];
_.forEach((t, e) => {
if (e > 0) {
let e = w("line");
e.setAttribute("x1", f.toString());
e.setAttribute("y1", t.y.toString());
e.setAttribute("x2", A.toString());
e.setAttribute("y2", t.y.toString());
e.setAttribute("stroke-width", (1.2 * s).toString());
y.appendChild(e);
}
let i = w("text");
i.setAttribute("x", (f + 10).toString());
i.setAttribute("y", (t.y + 20).toString());
i.setAttribute("font-family", "Arial");
i.setAttribute("font-size", "12");
i.setAttribute("font-weight", "bold");
i.setAttribute("fill", c);
i.textContent = t.title;
y.appendChild(i);
let r = t.y + 30;
let n = e < _.length - 1 ? t.y + E : S;
for (let t = r; t < n; t += m) {
let e = w("line");
e.setAttribute("x1", f.toString());
e.setAttribute("y1", t.toString());
e.setAttribute("x2", A.toString());
e.setAttribute("y2", t.toString());
y.appendChild(e);
}
if (2 === e)
for (let t = r; t < n; t += m) {
let e = w("rect");
e.setAttribute("x", (f + 5).toString());
e.setAttribute("y", (t - 10).toString());
e.setAttribute("width", "10");
e.setAttribute("height", "10");
e.setAttribute("fill", "none");
e.setAttribute("stroke", c);
e.setAttribute("stroke-width", (0.5 * s).toString());
y.appendChild(e);
}
});
t.appendChild(y);
},
project_planning: (t, e) => {
var i, r, n, o;
let {
lineSpacing: l,
lineWidth: s,
margins: d,
theme: u,
lineColor: p,
specialConfig: c,
} = e;
const themeColors = getThemeColors(u, p);
let b = p || themeColors.line;
let g = themeColors.accent;
let h = parseInt(t.getAttribute("width") || "0");
let m = parseInt(t.getAttribute("height") || "0");
let f = N(l, 0.1);
let A = k(d.left);
let x = h - k(d.right);
let S = k(d.top);
let y = m - k(d.bottom);
let C = w("g");
C.setAttribute("stroke", b);
C.setAttribute("stroke-width", s.toString());
let v = 3 * f;
let j = w("rect");
j.setAttribute("x", A.toString());
j.setAttribute("y", S.toString());
j.setAttribute("width", (x - A).toString());
j.setAttribute("height", v.toString());
j.setAttribute("fill", g);
j.setAttribute("fill-opacity", "0.1");
j.setAttribute("stroke", b);
j.setAttribute("rx", "3");
C.appendChild(j);
let z = w("text");
z.setAttribute("x", (A + 10).toString());
z.setAttribute("y", (S + v / 2 + 5).toString());
z.setAttribute("font-family", "Arial");
z.setAttribute("font-size", "14");
z.setAttribute("font-weight", "bold");
z.setAttribute("fill", b);
z.textContent = "项目规划";
C.appendChild(z);
let E = S + v + f;
let _ = 4 * f;
let F = w("rect");
F.setAttribute("x", A.toString());
F.setAttribute("y", E.toString());
F.setAttribute("width", (x - A).toString());
F.setAttribute("height", _.toString());
F.setAttribute("fill", "none");
F.setAttribute("stroke", b);
F.setAttribute("rx", "2");
C.appendChild(F);
[
{ label: "项目名称:", x: A + 10, y: E + 20 },
{ label: "开始日期:", x: A + 10, y: E + 50 },
{ label: "负责人:", x: (A + x) / 2 + 10, y: E + 20 },
{ label: "截止日期:", x: (A + x) / 2 + 10, y: E + 50 },
].forEach((t) => {
let e = w("text");
e.setAttribute("x", t.x.toString());
e.setAttribute("y", t.y.toString());
e.setAttribute("font-family", "Arial");
e.setAttribute("font-size", "12");
e.setAttribute("fill", b);
e.textContent = t.label;
C.appendChild(e);
let i = t.y + 5;
let r = w("line");
r.setAttribute("x1", (t.x + 70).toString());
r.setAttribute("y1", i.toString());
r.setAttribute("x2", (t.x + 150).toString());
r.setAttribute("y2", i.toString());
r.setAttribute("stroke-width", (0.5 * s).toString());
r.setAttribute("stroke-opacity", "0.7");
C.appendChild(r);
});
let M = E + _ + 2 * f;
let T = 10 * f;
let O = w("text");
O.setAttribute("x", (A + 10).toString());
O.setAttribute("y", (M - 10).toString());
O.setAttribute("font-family", "Arial");
O.setAttribute("font-size", "12");
O.setAttribute("font-weight", "bold");
O.setAttribute("fill", b);
O.textContent = "甘特图";
C.appendChild(O);
let I = w("rect");
I.setAttribute("x", A.toString());
I.setAttribute("y", M.toString());
I.setAttribute("width", (x - A).toString());
I.setAttribute("height", T.toString());
I.setAttribute("fill", "none");
I.setAttribute("stroke", b);
I.setAttribute("rx", "2");
C.appendChild(I);
let W = T / 5;
for (let t = 1; t < 5; t++) {
let e = w("line");
e.setAttribute("x1", A.toString());
e.setAttribute("y1", (M + t * W).toString());
e.setAttribute("x2", x.toString());
e.setAttribute("y2", (M + t * W).toString());
e.setAttribute("stroke-opacity", "0.7");
C.appendChild(e);
}
let P = (x - A - 80) / 14;
for (let t = 0; t <= 14; t++) {
let e = w("line");
e.setAttribute("x1", (A + 80 + t * P).toString());
e.setAttribute("y1", M.toString());
e.setAttribute("x2", (A + 80 + t * P).toString());
e.setAttribute("y2", (M + T).toString());
e.setAttribute("stroke-opacity", "0.7");
C.appendChild(e);
}
let B = w("line");
B.setAttribute("x1", (A + 80).toString());
B.setAttribute("y1", M.toString());
B.setAttribute("x2", (A + 80).toString());
B.setAttribute("y2", (M + T).toString());
B.setAttribute("stroke-width", (1.2 * s).toString());
C.appendChild(B);
for (let t = 0; t < 14; t++) {
let e = w("text");
e.setAttribute("x", (A + 80 + t * P + P / 2).toString());
e.setAttribute("y", (M - 5).toString());
e.setAttribute("font-family", "Arial");
e.setAttribute("font-size", "8");
e.setAttribute("text-anchor", "middle");
e.setAttribute("fill", b);
e.textContent = "D".concat(t + 1);
C.appendChild(e);
}
let L = ["任务1", "任务2", "任务3", "任务4", "任务5"];
for (let t = 0; t < L.length; t++) {
let e = w("text");
e.setAttribute("x", (A + 5).toString());
e.setAttribute("y", (M + t * W + W / 2 + 4).toString());
e.setAttribute("font-family", "Arial");
e.setAttribute("font-size", "10");
e.setAttribute("fill", b);
e.textContent = L[t];
C.appendChild(e);
}
let D = M + T + 2 * f;
let G = w("text");
G.setAttribute("x", (A + 10).toString());
G.setAttribute("y", (D - 10).toString());
G.setAttribute("font-family", "Arial");
G.setAttribute("font-size", "12");
G.setAttribute("font-weight", "bold");
G.setAttribute("fill", b);
G.textContent = "任务列表";
C.appendChild(G);
for (let t = D; t <= y; t += f) {
let e = w("line");
e.setAttribute("x1", A.toString());
e.setAttribute("y1", t.toString());
e.setAttribute("x2", x.toString());
e.setAttribute("y2", t.toString());
C.appendChild(e);
if (t > D) {
let e = w("rect");
e.setAttribute("x", (A + 5).toString());
e.setAttribute("y", (t - 10).toString());
e.setAttribute("width", "10");
e.setAttribute("height", "10");
e.setAttribute("fill", "none");
e.setAttribute("stroke", b);
e.setAttribute("stroke-width", (0.5 * s).toString());
C.appendChild(e);
}
}
t.appendChild(C);
}
};
// ===============================================
// ========== 【3. 模板配置】==========
const templates = [
{
id: "project_planning",
name: "项目规划纸",
description: "帮助规划和跟踪项目进度的专业模板",
paperType: "project_planning",
paperSize: "a4",
lineColor: "#4361EE",
lineSpacing: 7,
lineWidth: 0.5,
lineStyle: "solid",
margins: { top: 25, right: 20, bottom: 25, left: 30 },
theme: "tech",
background: {
color: "#f8fafc",
pattern: "none",
patternColor: "#CCCCCC",
patternOpacity: 0.1,
},
watermark: {
enabled: !1,
text: "",
opacity: 0.1,
angle: -45,
fontSize: 48,
color: "#888888",
},
pages: { count: 1, numberingEnabled: !0, startNumber: 1 },
},
{
id: "meeting_note",
name: "会议笔记纸",
description: "结构化的会议记录模板,包含议程和行动项",
paperType: "meeting_note",
paperSize: "a4",
lineColor: "#4b5563",
lineSpacing: 8,
lineWidth: 0.5,
lineStyle: "solid",
margins: { top: 40, right: 20, bottom: 30, left: 20 },
theme: "elegant",
background: {
color: "#ffffff",
pattern: "none",
patternColor: "#e0e0e0",
patternOpacity: 0.1,
},
watermark: {
enabled: !1,
text: "",
opacity: 0.15,
angle: -45,
fontSize: 48,
color: "#888888",
},
pages: { count: 1, numberingEnabled: !0, startNumber: 1 },
}
];
const folder = 'restored_papers';
mkdirSync(folder, { recursive: true });
for (const config of templates) {
try {
const W = k(210), H = k(297);
const svg = w('svg');
svg.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
svg.setAttribute('width', String(W));
svg.setAttribute('height', String(H));
const bgColor = config.background?.color || '#ffffff';
const bgRect = w('rect');
bgRect.setAttribute('x', '0');
bgRect.setAttribute('y', '0');
bgRect.setAttribute('width', String(W));
bgRect.setAttribute('height', String(H));
bgRect.setAttribute('fill', bgColor);
svg.appendChild(bgRect); // 背景必须在最底层,所以先 append
if (!z[config.paperType]) {
throw new Error(`未实现的纸张类型: ${config.paperType}`);
}
z[config.paperType](svg, config);
const svgStrTemp = svg.outerHTML;
const texts = [...svgStrTemp.matchAll(/<text[^>]*>([\s\S]*?)<\/text>/g)]
.map(m => m[1].trim());
console.log(`📄 ${config.id}: 共 ${texts.length} 个 text`);
texts.forEach((t, i) => {
console.log(` TEXT[${i}] =`, JSON.stringify(t));
});
const svgString = `<?xml version="1.0" encoding="UTF-8"?>\n${svg.outerHTML}`;
const filename = `${folder}/${config.name}.svg`;
writeFileSync(filename, svgString, 'utf8');
console.log(` 生成成功: ${filename}`);
} catch (err) {
console.error(` 生成失败 (${config.id}):`, err.message);
}
}
console.log(`\n共生成 ${templates.length} 个 SVG 文件,保存在 ./${folder}/ 目录中。`);
使用node.js>=22的版本
node restore-papers.mjs

输出效果
├── restore-papers.mjs
└── restored_papers
├── 会议笔记纸.svg
└── 项目规划纸.svg
2 directories, 4 files

