【原创实践】Node.js 动态生成 SVG 项目规划纸模板 高仿 纸由我 PaperMe

本项目基于 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, '&lt;')
      .replace(/>/g, '&gt;');
  }

  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, '&quot;')}"`)
        .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


相关推荐
PAQQ5 小时前
ubuntu22.04 搭建 Opencv & C++ 环境
前端·webpack·node.js
程序员爱钓鱼16 小时前
Node.js 编程实战:路由处理原理与实践
后端·node.js·trae
Lucky_Turtle16 小时前
【Node】npm install报错npm error Cannot read properties of null (reading ‘matches‘)
前端·npm·node.js
聊天QQ:2769988520 小时前
基于线性自抗扰(LADRC)的无人船航向控制系统Simulink/Matlab仿真工程探索
node.js
不会写DN1 天前
JavaScript call、apply、bind 方法解析
开发语言·前端·javascript·node.js
Tiam-20161 天前
安装NVM管理多版本node
vue.js·前端框架·node.js·html·es6·angular.js
fengGer的bugs1 天前
从零到一全栈开发 | 跑腿服务系统:小程序+Vue3+Node.js
小程序·node.js·全栈开发·跑腿服务系统
老前端的功夫1 天前
Webpack打包机制与Babel转译原理深度解析
前端·javascript·vue.js·webpack·架构·前端框架·node.js
珑墨2 天前
【浏览器】页面加载原理详解
前端·javascript·c++·node.js·edge浏览器