在PDF.js中实现前端水印

lxf2023-05-03 00:41:15

PDF.js

地址

PDF.js is a Portable Document Format (PDF) viewer that is built with HTML5.

PDF.js is community-driven and supported by Mozilla. Our goal is to create a general-purpose, web standards-based platform for parsing and rendering PDFs.

PDF.js 是一种使用 HTML5 构建的便携式文档格式 (PDF) 查看器。 PDF.js 由社区驱动并由 Mozilla 提供支持。我们的目标是创建一个通用的、基于 Web 标准的平台来解析和呈现 PDF。

我们的项目中涉及文件预览,并且有增加前端水印的需求(缓解服务器压力)。我们得预览服务采用了PDF.js,版本是2.4.x。

准备

PDF.js大体目录为:

在PDF.js中实现前端水印

我们修改的文件为:

src/display/canvas.js

web/base_viewer.js

web/app.js

新增:

src/display/xx_watermark.js

增加水印

// src/display/xx_watermark.js

// 获取水印矩形的长,取最大长度
const getRectWidth = (ctx, content) => {
  const widthArr = content.map((item) => ctx.measureText(item).width);
  const maxWidth = Math.max.apply(null, widthArr);
  return maxWidth;
};

// 获取水印矩形的宽
const getRectHeight = (ctx, content, lineY) => {
  const len = content.length;
  const height = ctx.measureText("口").width;
  return height * len + lineY * (len - 1);
};

const WATER_NUM = 30;
class Watermark {
  constructor(option) {
    this.init(option);
  }

  init(option) {
    const { ctx, commonObjs, canvasExtraState, isThumbnail, viewport } = option;
    const { alpha, angle, color, size, watermarkText, rotation, scale } =
      window.watermarkParamObj || {};
    if (!watermarkText) {
      return;
    }
    this.content = watermarkText.split("/");
    this.opacity = 1 - alpha;
    this.color = color;
    this.size = size;
    this.offsetX = 30;
    this.offsetY = 30;
    this.lineY = 10;
    this.ctx = ctx;
    this.rotate = Number(angle) + 360 + Number(rotation || 0);
    this.scale = scale;
    this.commonObjs = commonObjs;
    this.canvasExtraState = canvasExtraState;
    this.isThumbnail = isThumbnail;
    this.viewport = viewport;
    this.render(option);
  }

  render() {
    let unitWidth = "";
    let unitHeight = "";
    const {
      content,
      size,
      ctx,
      color,
      opacity,
      lineY,
      rotate,
      scale,
      offsetY,
      offsetX,
      isThumbnail,
    } = this;
    const len = content.length;
    if (len === 0) {
      return;
    }
    ctx.save();
    if (isThumbnail) {
      ctx.setTransform(1 * scale, 0, 0, 1 * scale, 0, 0);
    } else {
      ctx.setTransform(1 * 0.1, 0, 0, 1 * 0.1, 0, 0);
    }
    const transformFontSize = size * 2;
    ctx.font = `${transformFontSize}pt Arial, "PingFangSC-Regular", "microsoft yahei", "微软雅黑", "Hiragino Sans GB", sans-serif`;
    ctx.textAlign = "center";
    ctx.textBaseline = "top";
    ctx.fillStyle = color;
    ctx.globalAlpha = opacity;
    ctx.rotate((rotate * Math.PI) / 180);
    unitWidth = getRectWidth(ctx, content);
    unitHeight = getRectHeight(ctx, content, lineY);
    const singleHeight = ctx.measureText("口").width;
    const columnNum = WATER_NUM * 2;
    const rowNum = WATER_NUM * 2;

    for (let i = 0; i < rowNum; i++) {
      const y =
        i > WATER_NUM
          ? (unitHeight + offsetY) * (i - WATER_NUM) * -1
          : (unitHeight + offsetY) * i;
      for (let j = 0; j < columnNum; j++) {
        const x =
          j > WATER_NUM
            ? (unitWidth + offsetX) * (j - WATER_NUM) * -1
            : (unitWidth + offsetX) * j;
        for (let index = 0; index < len; index++) {
          const txtY = (index + 1) * (singleHeight + lineY + y);
          ctx.fillText(content[index], x, txtY);
        }
      }
    }

    ctx.rotate((-rotate * Math.PI) / 180);
    ctx.restore();
  }
}

export { Watermark };

这里会根据isThumbnail 处理缩略图和正文两种场景。

大家测试的时候可以简单写一点canvas,比如就一行字就行,这里的代码是我们要求的水印样式。

// src/display/canvas.js
...
import { Watermark } from "./xxx_watermark.js";
...
endDrawing: function CanvasGraphics_endDrawing() {
// Finishing all opened operations such as SMask group painting.
  if (this.current.activeSMask !== null) {
    this.endSMaskGroup();
  }
  this.ctx.restore();

  if (this.transparentCanvas) {
    this.ctx = this.compositeCtx;
    this.ctx.save();
    this.ctx.setTransform(1, 0, 0, 1, 0, 0); // Avoid apply transform twice
    this.ctx.drawImage(this.transparentCanvas, 0, 0);
    this.ctx.restore();
    this.transparentCanvas = null;
  }
  // ========= 新增 draw水印 ===========
  if (window.watermarkParamObj && window.watermarkParamObj.addWatermark) {
    const isThumbnail = this.ctx.canvas.hasAttribute("aria-label");
    var watermarkCanvas = this.cachedCanvases.getCanvas(
      "watermark",
      this.ctx.canvas.width,
      this.ctx.canvas.height,
      true
    );
    this.compositeCtx = this.ctx;
    this.watermarkCanvas = watermarkCanvas.canvas;
    this.ctx = watermarkCanvas.context;
    this.ctx.save();
    this.beginDrawWatermark({
      ctx: this.ctx,
      commonObjs: this.commonObjs,
      canvasExtraState: this.current,
      isThumbnail,
      viewport: this.viewport,
    });

    this.ctx = this.compositeCtx;
    this.ctx.save();
    this.ctx.drawImage(this.watermarkCanvas, 0, 0);
    this.ctx.restore();
    this.watermarkCanvas = null;
  }
  // ========= 新增end ===========

  this.cachedCanvases.clear();
  this.webGLContext.clear();

  if (this.imageLayer) {
    this.imageLayer.endLayout();
  }
}
...
getCanvasPosition: function CanvasGraphics_getCanvasPosition(x, y) {
  var transform = this.ctx.mozCurrentTransform;
  return [
    transform[0] * x + transform[2] * y + transform[4],
    transform[1] * x + transform[3] * y + transform[5],
  ];
},
// ========= 新增 draw水印 ===========
beginDrawWatermark: function CanvasGraphics_beginDrawWatermark(param) {
  if (window.watermarkParamObj && window.watermarkParamObj.addWatermark) {
    // eslint-disable-next-line no-new
    new Watermark(param);
  }
},
...

这里增加水印逻辑。

利用了PDF.js也是画canvas实现pdf预览的原理,所以在其画完文件内容的时候,继续画水印。


//web/app.js
//该文件找到 
...
load(pdfDocument) {

...
// ========= 新增 获取水印参数 ===========
const getQueryStringParamByKey = (key, qryString) => {
  const tempObj = {};
  const qryStringArr = (qryString || "").split("&");
  qryStringArr.forEach((item) => {
    const [itemKey, itemValue] = item.split("=");
    tempObj[itemKey] = itemValue;
  });
  return tempObj[key] || "";
};
const urlFileParam = decodeURIComponent(
  getQueryStringParamByKey(
    "file",
    new URL(window.location).search.substring(1)
  )
);
const webShowJson =
  decodeURIComponent(
    getQueryStringParamByKey(
      "webShowJson",
      urlFileParam.split("?")[1]
    )
  ) || "{}";
window.watermarkParamObj = JSON.parse(webShowJson);
// ========= 新增 获取水印参数 ===========
...

}
...

本段代码,目的就是获取url中的水印参数。

// web/base_viewer.js
// 文件中找到
...
set pagesRotation(rotation) {
// =========新增==========
    if (window.watermarkParamObj && window.watermarkParamObj.addWatermark) {
      window.watermarkParamObj.rotation = rotation;
    }
 // =========end==========
...
}

...
_setScale(value, noScroll = false) {
let scale = parseFloat(value);
// =========新增==========
    if (window.watermarkParamObj && window.watermarkParamObj.addWatermark) {
      window.watermarkParamObj.scale = scale;
    }
// =========end==========
}
...

这里处理的是旋转和缩放问题,因为水印画的时候会根据文件页的长宽来计算、铺满整个页,所以旋转缩放会导致水印异常,所以在用户旋转缩放的时候触发改变参数数值。

问题

  1. 性能还是有损耗;
  2. 前后端生成的水印样式有差异,没有100%复刻;
  3. 旋转、缩放操作可能会导致水印样式不合理,需要调试;
  4. PDF.js新版本代码完全重构,这些代码移植或许会有问题;
  5. 我们维护的是2.4版本,目前这个版本有些不能满足我们的需求了,但是我们要兼容的端别比较多,版本也需要兼容的比较低,所以可能采用多版本并行的方案,这样维护起来成本也会增加;

其余的想不到了,酒浆!

微信公众号:王大锤学前端

本网站是一个以CSS、JavaScript、Vue、HTML为核心的前端开发技术网站。我们致力于为广大前端开发者提供专业、全面、实用的前端开发知识和技术支持。 在本网站中,您可以学习到最新的前端开发技术,了解前端开发的最新趋势和最佳实践。我们提供丰富的教程和案例,让您可以快速掌握前端开发的核心技术和流程。 本网站还提供一系列实用的工具和插件,帮助您更加高效地进行前端开发工作。我们提供的工具和插件都经过精心设计和优化,可以帮助您节省时间和精力,提升开发效率。 除此之外,本网站还拥有一个活跃的社区,您可以在社区中与其他前端开发者交流技术、分享经验、解决问题。我们相信,社区的力量可以帮助您更好地成长和进步。 在本网站中,您可以找到您需要的一切前端开发资源,让您成为一名更加优秀的前端开发者。欢迎您加入我们的大家庭,一起探索前端开发的无限可能!