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线程中进行来防止堵塞主线程。

替代方案

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

相关推荐
vayy5 分钟前
uniapp中 ios端 scroll-view 组件内部子元素z-index失效问题
前端·ios·微信小程序·uni-app
专注API从业者25 分钟前
基于 Node.js 的淘宝 API 接口开发:快速构建异步数据采集服务
大数据·前端·数据库·数据挖掘·node.js
前端无冕之王27 分钟前
一份兼容多端的HTML邮件模板实践与详解
前端·css·数据库·html
啃火龙果的兔子32 分钟前
js获取html元素并设置高度为100vh-键盘高度
javascript·html·计算机外设
再学一点就睡2 小时前
深入理解 Redux:从手写核心到现代实践(附 RTK 衔接)
前端·redux
天天进步20153 小时前
从零到一:现代化充电桩App的React前端参考
前端·react.js·前端框架
柯南二号3 小时前
【大前端】React Native Flex 布局详解
前端·react native·react.js
龙在天4 小时前
npm run dev 做了什么❓小白也能看懂
前端
hellokai4 小时前
React Native新架构源码分析
android·前端·react native
li理5 小时前
鸿蒙应用开发完全指南:深度解析UIAbility、页面与导航的生命周期
前端·harmonyos