Z ZPI 文档 v0.3.8
介绍页

源码

packages/desktop/src/main/preview-pdf-export.ts

typescript71 行查看原文
import { basename } from "node:path";

export const maxPreviewPdfBytes = 50 * 1024 * 1024;
export const previewPdfOptions = {
  printBackground: true,
  preferCSSPageSize: true,
  margins: { top: 0, bottom: 0, left: 0, right: 0 },
};

interface PdfExportServices {
  ready(sessionId: string): Promise<boolean>;
  print(): Promise<Uint8Array>;
  chooseFile(suggestedName: string): Promise<{ canceled: boolean; filePath?: string }>;
  write(path: string, bytes: Uint8Array): Promise<void>;
}

export class PreviewPdfExporter {
  private busy = false;
  private pending: Buffer | undefined;

  private readonly services: PdfExportServices;

  constructor(services: PdfExportServices) {
    this.services = services;
  }

  async print(sessionId: string): Promise<Uint8Array> {
    if (this.busy) throw new Error("busy: 正在导出 PDF");
    this.busy = true;
    this.pending = undefined;
    try {
      if (!(await this.services.ready(sessionId))) throw new Error("invalid_input: 演示文稿打印页面尚未就绪");
      const data = await this.services.print();
      this.validate(data);
      this.pending = Buffer.from(data);
      return new Uint8Array(data);
    } finally {
      this.busy = false;
    }
  }

  async save(data: unknown, suggestedName: string): Promise<{ canceled: boolean; path?: string }> {
    if (this.busy) throw new Error("busy: 正在导出 PDF");
    this.validate(data);
    // 只允许保存该窗口刚生成的 PDF,不将打印接口变成任意文件写入能力。
    if (!this.pending?.equals(data)) throw new Error("invalid_input: PDF 与当前导出结果不匹配,请重新导出");
    if (typeof suggestedName !== "string" || /[\0\r\n]/.test(suggestedName))
      throw new Error("invalid_input: PDF 文件名无效");
    const name = basename(suggestedName.replaceAll("\\", "/")).trim().slice(0, 120);
    const filename = `${name.replace(/\.pdf$/i, "") || "presentation"}.pdf`;
    this.busy = true;
    const bytes = this.pending;
    this.pending = undefined;
    try {
      const choice = await this.services.chooseFile(filename);
      if (choice.canceled || !choice.filePath) return { canceled: true };
      await this.services.write(choice.filePath, bytes);
      return { canceled: false, path: choice.filePath };
    } finally {
      this.busy = false;
    }
  }

  private validate(data: unknown): asserts data is Uint8Array {
    if (!(data instanceof Uint8Array) || data.byteLength < 8 || data.byteLength > maxPreviewPdfBytes)
      throw new Error("invalid_input: PDF 内容无效或超过 50 MiB");
    if (Buffer.from(data.buffer, data.byteOffset, 5).toString("ascii") !== "%PDF-")
      throw new Error("invalid_input: 只允许保存 PDF");
  }
}