Tauri+MuPDF 实现 pdf 文件裁剪,侄子再也不用等打印试卷了🤓

基于MuPDF.js实现的 PDF 文件 A3 转 A4 小工具。(其实就是纵切分成2份🤓)

开发背景

表哥最近经常找我给我侄子的试卷 pdf 文件 A3 转 A4(因为他家只有 A4 纸,直接打印字太小了)。

WPS提供了pdf的分割工具,不过这是会员功能,我也不是总能在电脑前操作。于是我想着直接写一个小工具,拿Tauri打包成桌面应用就好了。

在掘金里刷到了柒八九大佬的文章:Rust 赋能前端:PDF 分页/关键词标注/转图片/抽取文本/抽取图片/翻转... 。发现MuPDF.js这个包有截取pdf文件的API,并且提供了编译好的wasm文件,这意味着可以在浏览器中直接体验到更高的裁切性能,于是我果断选择了基于MuPDF开发我的小工具。

项目简介

MuPDF-Crop-Kit是一个基于MuPDF.jsReactViteTauri开发的小工具,用于将 PDF 文件从 A3 纸张大小裁切为 A4 纸张大小。它具有以下特点:

  • 免费使用:无需任何费用;
  • 无需后台服务:可以直接在浏览器中运行,无需依赖服务器;
  • 高性能:利用 WebAssembly (WASM) 技术,提供高效的文件裁切性能;
  • 轻量级桌面应用:通过 Tauri 打包成桌面软件,安装包体积小,方便部署;
  • 开源项目:欢迎社区贡献代码和建议,共同改进工具。

开发过程与踩坑

  • MuPDF.js只支持ESM,官网中给出的要么使用.mjs文件,要么需要项目的type改成module

    shell 复制代码
    npm pkg set type=module

    我在我的Rsbuild搭建的项目中都没有配置成功🤷‍♂️,最后发现用 Vite 搭建的项目直接就可以用...

  • 因为没有直接提供我想要的功能,肯定是要基于现有的API手搓了。但是截取页面的API会修改原页面,那么自然想到是要复制一份出来,一个截左边一个截右边了。但是MuPDF.jscopyPage复制出来的pdf页修改之后,原pdf页居然也会被修改。于是我想到了,一开始就new两个PDFDocument对象,一个截左边一个截右边,最后再合并到一起,我很快实现了两个文档的分别截取,并通过转png图片之后再合并,完成了裁切后的文档的浏览器预览。但是在循环调用MuPDF.js提供的merge方法时,wasm运行的内存被爆了🤣。于是我考虑直接使用jspdfpng图片转pdf文件,结果2MB的原文件转换后变成了12MB,并且如果原文件是使用扫描全能王扫描出来的,生成的pdf文件会很糊。最后,终于让我在文档中发现merge方法:

    不过依赖包提供的方法很复杂:

    js 复制代码
    merge(sourcePDF, fromPage = 0, toPage = -1, startAt = -1, rotate = 0, copyLinks = true, copyAnnotations = true) {
      if (this.pointer === 0) {
          throw new Error("document closed");
      }
      if (sourcePDF.pointer === 0) {
          throw new Error("source document closed");
      }
      if (this === sourcePDF) {
          throw new Error("Cannot merge a document with itself");
      }
      const sourcePageCount = sourcePDF.countPages();
      const targetPageCount = this.countPages();
      // Normalize page numbers
      fromPage = Math.max(0, Math.min(fromPage, sourcePageCount - 1));
      toPage = toPage < 0 ? sourcePageCount - 1 : Math.min(toPage, sourcePageCount - 1);
      startAt = startAt < 0 ? targetPageCount : Math.min(startAt, targetPageCount);
      // Ensure fromPage <= toPage
      if (fromPage > toPage) {
          [fromPage, toPage] = [toPage, fromPage];
      }
      for (let i = fromPage; i <= toPage; i++) {
          const sourcePage = sourcePDF.loadPage(i);
          const pageObj = sourcePage.getObject();
          // Create a new page in the target document
          const newPageObj = this.addPage(sourcePage.getBounds(), rotate, this.newDictionary(), "");
          // Copy page contents
          const contents = pageObj.get("Contents");
          if (contents) {
              newPageObj.put("Contents", this.graftObject(contents));
          }
          // Copy page resources
          const resources = pageObj.get("Resources");
          if (resources) {
              newPageObj.put("Resources", this.graftObject(resources));
          }
          // Insert the new page at the specified position
          this.insertPage(startAt + (i - fromPage), newPageObj);
          if (copyLinks || copyAnnotations) {
              const targetPage = this.loadPage(startAt + (i - fromPage));
              if (copyLinks) {
                  this.copyPageLinks(sourcePage, targetPage);
              }
              if (copyAnnotations) {
                  this.copyPageAnnotations(sourcePage, targetPage);
              }
          }
      }
    }

    核心实现就是

    • addPage新增页面;
    • put("Resources")复制原文档页面中的内容到新页面;
    • insertPage将新增的页面插入到指定文档中。

    因为我并没有后续添加的linkannotation,所以经过设计后,决定使用一个空的pdf文档,逐页复制原文档两次到空白文档中。主要逻辑如下:

    • 加载 PDF 文件:读取并解析原始 A3 PDF 文件。
    • 复制页面:创建两个新的 PDF 文档,分别截取每页的左半部分和右半部分。
    • 合并页面:将两个新文档中的页面合并到一个新的 PDF 文档中。
    • 设置裁剪框:根据 A4 纸张尺寸设置裁剪框(CropBox)和修整框(TrimBox)。
    ts 复制代码
    export function merge(
      targetPDF: mupdfjs.PDFDocument,
      sourcePage: mupdfjs.PDFPage
    ) {
      const pageObj = sourcePage.getObject();
      const [x, y, width, height] = sourcePage.getBounds();
      // Create a new page in the target document
      const newPageObj = targetPDF.addPage(
        [x, y, width, height],
        0,
        targetPDF.newDictionary(),
        ""
      );
      // Copy page contents
      const contents = pageObj.get("Contents");
      if (contents) newPageObj.put("Contents", targetPDF.graftObject(contents));
      // Copy page resources
      const resources = pageObj.get("Resources");
      if (resources) newPageObj.put("Resources", targetPDF.graftObject(resources));
      // Insert the new page at the specified position
      targetPDF.insertPage(-1, newPageObj);
    }
    ​
    export function generateNewDoc(PDF: mupdfjs.PDFDocument) {
      const count = PDF.countPages();
      const mergedPDF = new mupdfjs.PDFDocument();
      for (let i = 0; i < count; i++) {
        const page = PDF.loadPage(i);
        merge(mergedPDF, page);
        merge(mergedPDF, page);
      }
    ​
      for (let i = 0; i < count * 2; i++) {
        const page = mergedPDF.loadPage(i); // 使用 mergedPDF 的页码
        const [x, y, width, height] = page.getBounds();
        if (i % 2 === 0)
          page.setPageBox("CropBox", [x, y, x + width / 2, y + height]);
        else page.setPageBox("CropBox", [x + width / 2, y, x + width, y + height]);
    ​
        page.setPageBox("TrimBox", [0, 0, 595.28, 841.89]);
      }
      return mergedPDF;
    }

    完成以上核心方法后,便可以成功将我侄子的试卷裁切为A4大小进行打印了✅。

体验与安装使用

浏览器版

桌面版

使用教程

使用本工具非常简单,只需几个步骤即可完成 PDF 文件的裁切:

  1. 选择需要裁切的 A3 PDF 文件;

  2. 点击裁切按钮;

  3. 下载裁切后的 A4 PDF 文件。

不足

  • 项目所使用的wasm文件大小有10MB,本工具真正用到的并没有那么多,但是优化需要修改原始文件并重新编译;
  • 浏览器端的性能受限,并且wasm运行可以使用的内存也是有限的;
  • 没有使用Web Worker,理论上转换这种高延迟的任务应当放在Woker线程中进行来防止堵塞主线程。

替代方案

如果在使用过程中遇到问题或需要更多功能,可以尝试以下在线工具:

相关推荐
橙某人3 分钟前
🍊🍊🍊在网格中进行拖动布局-Javascript、Vue
前端·javascript·vue.js
若川5 分钟前
Taro 4 已发布:11. Taro 是如何解析入口配置 app.config.ts 和页面配置的?
前端·javascript·微信小程序
八了个戒9 分钟前
「数据可视化 D3系列」入门第一章:Hello D3.js
开发语言·前端·javascript·数据可视化·canvas
·薯条大王11 分钟前
Node.js 操作 MySQL 数据库
javascript·数据库·mysql
二川bro19 分钟前
深度解析 Vue 项目 Webpack 分包与合包 一文读懂
前端·vue.js·webpack
getapi20 分钟前
flutter底部导航代码解释
前端·javascript·flutter
nui11125 分钟前
汽配快车道解决chrome backgroud.js(Service Worker) XMLHttpRequest is not defined问题
前端·javascript·chrome
团酱40 分钟前
ESLint常见错误
开发语言·javascript·ecmascript
CodeCraft Studio1 小时前
PDF处理控件Aspose.PDF指南:使用 C# 从 PDF 文档中删除页面
前端·pdf·c#
好_快1 小时前
Lodash源码阅读-baseFill
前端·javascript·源码阅读