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大体目录为:
我们修改的文件为:
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 };
// 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==========
}
...
这里处理的是旋转和缩放问题,因为水印画的时候会根据文件页的长宽来计算、铺满整个页,所以旋转缩放会导致水印异常,所以在用户旋转缩放的时候触发改变参数数值。
问题
- 性能还是有损耗;
- 前后端生成的水印样式有差异,没有100%复刻;
- 旋转、缩放操作可能会导致水印样式不合理,需要调试;
- PDF.js新版本代码完全重构,这些代码移植或许会有问题;
- 我们维护的是2.4版本,目前这个版本有些不能满足我们的需求了,但是我们要兼容的端别比较多,版本也需要兼容的比较低,所以可能采用多版本并行的方案,这样维护起来成本也会增加;
其余的想不到了,酒浆!
微信公众号:王大锤学前端
本网站是一个以CSS、JavaScript、Vue、HTML为核心的前端开发技术网站。我们致力于为广大前端开发者提供专业、全面、实用的前端开发知识和技术支持。 在本网站中,您可以学习到最新的前端开发技术,了解前端开发的最新趋势和最佳实践。我们提供丰富的教程和案例,让您可以快速掌握前端开发的核心技术和流程。 本网站还提供一系列实用的工具和插件,帮助您更加高效地进行前端开发工作。我们提供的工具和插件都经过精心设计和优化,可以帮助您节省时间和精力,提升开发效率。 除此之外,本网站还拥有一个活跃的社区,您可以在社区中与其他前端开发者交流技术、分享经验、解决问题。我们相信,社区的力量可以帮助您更好地成长和进步。 在本网站中,您可以找到您需要的一切前端开发资源,让您成为一名更加优秀的前端开发者。欢迎您加入我们的大家庭,一起探索前端开发的无限可能!