前端 PDF 水印方案


場景:前端下載 pdf 文件的時候,需要加上水印,再反給用戶下載
用到的庫pdf-lib (文檔) @pdf-lib/fontkit
字體github
方案目標:logo圖 + 中文 + 英文 + 數字 => 透明水印


首先安裝 pdf-lib: 它是前端創建和修改 PDF 文檔的一個工具(默認_不支持中文_,需要加載自定義字體文件)

npm install --save pdf-lib

安裝 @pdf-lib/fontkit:為 pdf-lib 加載自定義字體的工具

npm install --save @pdf-lib/fontkit

沒有使用pdf.js的原因是因為:

  1. 會將 PDF 轉成圖片,無法選中
  2. 操作后 PDF 會變模糊
  3. 文檔體積會變得異常大


實現:

首先我們的目標是在 PDF 文檔中,加上一個帶 logo 的,同時包含中文、英文、數字字符的透明水印,所以我們先來嘗試着從本地加載一個文件,一步步搭建。

1. 獲取 PDF 文件

本地:

// <input type="file" name="pdf" id="pdf-input">

let input = document.querySelector('#pdf-input');
input.onchange = onFileUpload;

// 上傳文件
function onFileUpload(e) {
  let event = window.event || e;

  let file = event.target.files[0];
}

除了本地上傳文件之外,我們也可以通過網絡請求一個 PDF 回來,注意響應格式為 **blob **。
網絡:

var x = new XMLHttpRequest();
x.open("GET", url, true);
x.responseType = 'blob';
x.onload = function (e) {
  let file = x.response;
}
x.send();

// 獲取直接轉成 pdf-lib 需要的 arrayBuffer
// const fileBytes = await fetch(url).then(res => res.arrayBuffer())

2. 文字水印

在獲取到 PDF 文件數據之后,我們通過 pdf-lib 提供的接口來對文檔做修改。

// 修改文檔
async function modifyPdf(file) {
  const pdfDoc = await PDFDocument.load(await file.arrayBuffer());

  // 加載內置字體
  const helveticaFont = await pdfDoc.embedFont(StandardFonts.Courier);

  // 獲取文檔所有頁
  const pages = pdfDoc.getPages();

  // 文字渲染配置
  const drawTextParams = {
    lineHeight: 50,
    font: helveticaFont,
    size: 12,
    color: rgb(0.08, 0.08, 0.2),
    rotate: degrees(15),
    opacity: 0.5,
  };

  for (let i = 0; i < pages.length; i++) {
    const page = pages[i];

    // 獲取當前頁寬高
    const { width, height } = page.getSize();

    // 要渲染的文字內容
    let text = "water 121314";

    for (let ix = 1; ix < width; ix += 230) { // 水印橫向間隔
      let lineNum = 0; 
      for (let iy = 50; iy <= height; iy += 110) { // 水印縱向間隔
        lineNum++;
        
        page.drawText(text, {
          x: lineNum & 1 ? ix : ix + 70,
          y: iy,
          ...drawTextParams,
        });
      }
    }
  }

來看一下現在的效果
文字水印

在加載圖片這塊,我們最終想要的其實是圖片的 Blob 數據,獲取網圖的話,這里就不做介紹了,下邊主要着重介紹一下,如何通過 js 從本地加載一張圖。
先貼上代碼:

//  加載 logo blob 數據
~(function loadImg() {
  let img = new Image();
  img.src = "./water-logo.png";

  let canvas = document.createElement("canvas");
  let ctx = canvas.getContext("2d");

  img.crossOrigin = "";
  img.onload = function () {
    canvas.width = this.width;
    canvas.height = this.height;

    ctx.fillStyle = "rgba(255, 255, 255, 1)"; 
    ctx.fillRect(0, 0, canvas.width, canvas.height);

    ctx.drawImage(this, 0, 0, this.width, this.height);
    canvas.toBlob(
      function (blob) {
        imgBytes = blob;  // 保存數據到 imgBytes 中
      },
      "image/jpeg",
      1
    ); // 參數為輸出質量
  };
})();

首先通過一個自執行函數,在初期就自動加載 logo 數據,當然我們也可以根據實際情況做相應的優化。
整體的思路就是,首先通過 image 元素來加載本地資源,再將 img 渲染到 canvas 中,再通過 canvas 的 toBlob 來得到我們想要的數據。

在這塊我們需要注意兩行代碼:

ctx.fillStyle = "rgba(255, 255, 255, 1)"; 
ctx.fillRect(0, 0, canvas.width, canvas.height);

如果我們不加這兩行代碼的話,同時本地圖片還是透明圖,最后我們得到的數據將會是一個黑色的方塊。所以我們需要在 drawImage 之前,用白色填充一下 canvas 。

在渲染 logo 圖片到 PDF 文檔上之前,我們還需要和加載字體類似的,把圖片數據也掛載到 pdf-lib 創建的文檔對象上(pdfDoc),其中 imgBytes 是我們已經加載好的圖片數據。

let _img = await pdfDoc.embedJpg(await imgBytes.arrayBuffer());

掛載完之后,做一些個性化的配置

page.drawImage(_img, {
  x: lineNum & 1 ? ix - 18 : ix + 70 - 18, // 奇偶行的坐標
  y: iy - 8,
  width: 15,
  height: 15,
  opacity: 0.5,
});

5. 查看文檔

這一步的思路就是先通過 pdf-lib 提供的 save 方法,得到最后的文檔數據,將數據轉成 Blob,最后通過 a 標簽打開查看。

// 保存文檔 Serialize the PDFDocument to bytes (a Uint8Array)
  const pdfBytes = await pdfDoc.save();

  let blobData = new Blob([pdfBytes], { type: "application/pdf;Base64" });

  // 新標簽頁預覽
  let a = document.createElement("a");
  a.target = "_blank";
  a.href = window.URL.createObjectURL(blobData);
  document.body.appendChild(a);
  a.click();
  document.body.removeChild(a);

到目前的效果
logo水印

6. 中文字體

由於默認的 pdf-lib 是不支持渲染中文的
Uncaught (in promise) Error: WinAnsi cannot encode "水" (0x6c34)
WinAnsi cannot encode

所以我們需要加載自定義字體,但是常規的字體文件都會很大,為了使用,需要將字體文件壓縮一下,壓縮好的字體在文檔頭部,包含空格和基礎的3500字符。
壓縮字體用到的是 gulp-fontmin 命令行工具,不是客戶端。具體壓縮方法,可自行搜索。

在拿到字體之后(ttf文件),將字體文件上傳到網上,再拿到其 arrayBuffer 數據。之后再結合 pdf-lib 的文檔對象,對字體進行注冊和掛載。同時記得將文字渲染的字體配置改過來。

// 加載自定義字體
const url = 'https://xxx.xxx/xxxx';
const fontBytes = await fetch(url).then((res) => res.arrayBuffer());

// 自定義字體掛載
pdfDoc.registerFontkit(fontkit)
const customFont = await pdfDoc.embedFont(fontBytes)

// 文字渲染配置
  const drawTextParams = {
    lineHeight: 50,
    font: customFont,  // 改字體配置
    size: 12,
    color: rgb(0.08, 0.08, 0.2),
    rotate: degrees(15),
    opacity: 0.5,
  };

所以到現在的效果
完整效果

7. 完整代碼

import { PDFDocument, StandardFonts, rgb, degrees } from "pdf-lib";
import fontkit from "@pdf-lib/fontkit";

let input = document.querySelector("#pdf-input");
let imgBytes;

input.onchange = onFileUpload;

// 上傳文件
function onFileUpload(e) {
  let event = window.event || e;

  let file = event.target.files[0];
  console.log(file);
  if (file.size) {
    modifyPdf(file);
  }
}

// 修改文檔
async function modifyPdf(file) {
  const pdfDoc = await PDFDocument.load(await file.arrayBuffer());

  // 加載內置字體
  const helveticaFont = await pdfDoc.embedFont(StandardFonts.Courier);

  // 加載自定義字體
  const url = 'pttps://xxx.xxx/xxx';
  const fontBytes = await fetch(url).then((res) => res.arrayBuffer());

  // 自定義字體掛載
  pdfDoc.registerFontkit(fontkit)
  const customFont = await pdfDoc.embedFont(fontBytes)

  // 獲取文檔所有頁
  const pages = pdfDoc.getPages();

  // 文字渲染配置
  const drawTextParams = {
    lineHeight: 50,
    font: customFont,
    size: 12,
    color: rgb(0.08, 0.08, 0.2),
    rotate: degrees(15),
    opacity: 0.5,
  };

  let _img = await pdfDoc.embedJpg(await imgBytes.arrayBuffer());

  for (let i = 0; i < pages.length; i++) {
    const page = pages[i];

    // 獲取當前頁寬高
    const { width, height } = page.getSize();

    // 要渲染的文字內容
    let text = "水印 water 121314";

    for (let ix = 1; ix < width; ix += 230) { // 水印橫向間隔
      let lineNum = 0; 
      for (let iy = 50; iy <= height; iy += 110) { // 水印縱向間隔
        lineNum++;
        page.drawImage(_img, {
          x: lineNum & 1 ? ix - 18 : ix + 70 - 18,
          y: iy - 8,
          width: 15,
          height: 15,
          opacity: 0.7,
        });
        page.drawText(text, {
          x: lineNum & 1 ? ix : ix + 70,
          y: iy,
          ...drawTextParams,
        });
      }
    }
  }

  // 保存文檔 Serialize the PDFDocument to bytes (a Uint8Array)
  const pdfBytes = await pdfDoc.save();

  let blobData = new Blob([pdfBytes], { type: "application/pdf;Base64" });

  // 新標簽頁預覽
  let a = document.createElement("a");
  a.target = "_blank";
  a.href = window.URL.createObjectURL(blobData);
  document.body.appendChild(a);
  a.click();
  document.body.removeChild(a);
}

//  加載 logo blob 數據
~(function loadImg() {
  let img = new Image();
  img.src = "./water-logo.png";

  let canvas = document.createElement("canvas");
  let ctx = canvas.getContext("2d");

  img.crossOrigin = "";
  img.onload = function () {
    canvas.width = this.width;
    canvas.height = this.height;

    ctx.fillStyle = "rgba(255, 255, 255, 1)";
    ctx.fillRect(0, 0, canvas.width, canvas.height);

    ctx.drawImage(this, 0, 0, this.width, this.height);
    canvas.toBlob(
      function (blob) {
        imgBytes = blob;
      },
      "image/jpeg",
      1
    ); // 參數為輸出質量
  };
})();

8. 不完美的地方

當前方案雖然可以實現在前端為 PDF 加水印,但是由於時間關系,有些瑕疵還需要再進一步探索解決 💪:

  1. 水印是浮在原文本之上的,可以被選中
  2. logo 的背景雖然不注意看不到,但是實際上還未完全透明 🤔


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM