最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

前端實(shí)現(xiàn)PDF與圖片添加自定義中文水印的詳細(xì)流程

 更新時(shí)間:2025年09月26日 08:50:05   作者:puhaha  
這篇文章主要介紹了為PDF和圖片添加中文水印的兩種方案,方案一因字體庫(kù)問(wèn)題被棄用,方案二采用canvas生成水印并結(jié)合pdf-lib實(shí)現(xiàn),支持自定義參數(shù)及高分辨率、自動(dòng)間隔、旋轉(zhuǎn)適配等關(guān)鍵技術(shù),需要的朋友可以參考下

前言

在日常開發(fā)中,為文檔和圖片添加版權(quán)水印是一項(xiàng)常見需求。本文將詳細(xì)介紹如何為PDF和圖片添加自定義中文水印的思路。

如圖所示,水印需要兩行,第一行是動(dòng)態(tài)變化的,第二行是固定文案。所以還需要考慮文字長(zhǎng)短從而改變每個(gè)水印之間的間距。

功能概述

這個(gè)方案提供了兩個(gè)核心函數(shù):

  • addWatermarkToPDF: 為PDF文件添加傾斜水印
  • addWatermarkToImage: 為圖片文件添加網(wǎng)格狀水印

兩個(gè)函數(shù)都支持自定義文本內(nèi)容、字體大小、透明度、旋轉(zhuǎn)角度等參數(shù),特別優(yōu)化了對(duì)中文文本的處理。

核心實(shí)現(xiàn)原理

方案一:采用pdf-lib實(shí)現(xiàn)水印,但是它要支持中文水印的話,還需借助和@pdf-lib/fontkit庫(kù)來(lái)加載一個(gè)中文字體集實(shí)現(xiàn)。一般小一點(diǎn)的中文字體集都有5M左右,并且這種方案生成的水印可以復(fù)制,不是我想要的效果,于是pass了。

方案二:采用canvas生成水印,再結(jié)合pdf-lib繪制圖片的方式,嵌入到pdf中。這種方案不用導(dǎo)入字體庫(kù),減去了加載字體的時(shí)間,所以速度比方案一快很多。

1. PDF水印實(shí)現(xiàn)

export async function addWatermarkToPDF(pdfUrl, companyName, personName, options = {}) {
  // 參數(shù)配置
  const {
    fontSize = 30, // PDF點(diǎn)單位
    opacity = 0.18,
    angle = -45,
    color = 'rgba(120,120,120,1)',
    pixelRatio = Math.max(window.devicePixelRatio || 1, 2),
    baseGap = 500, // 最小間隔
    lineHeight = 1.4 // 行距
  } = options;

  // 構(gòu)建水印文本
  const firstLine = companyName && personName ? `${companyName}-${personName}` : companyName || personName || '';
  const secondLine = '僅用于xxxx使用,他用無(wú)效';
  const lines = [firstLine, secondLine];

  // 單位轉(zhuǎn)換函數(shù)
  const ptToPx = pt => (pt * 96) / 72;
  const pxToPt = px => (px * 72) / 96;
  
  // 測(cè)量文本寬度
  const cssFontSize = ptToPx(fontSize);
  const measureCanvas = document.createElement('canvas');
  const mctx = measureCanvas.getContext('2d');
  mctx.font = `${cssFontSize}px sans-serif`;
  const firstLineWidthCss = mctx.measureText(firstLine).width;
  const firstLineWidthPt = pxToPt(firstLineWidthCss);

  // 動(dòng)態(tài)計(jì)算水印間隔
  const xGap = Math.max(baseGap, firstLineWidthPt * 1.2);
  const yGap = Math.max(250, fontSize * (lines.length + 1));

  // 創(chuàng)建水印圖片
  const padding = 10;
  const lineHeighCss = cssFontSize * lineHeight;
  const cssWidth = Math.max(...lines.map(l => mctx.measureText(l).width)) + padding * 2;
  const cssHeight = lines.length * lineHeighCss + padding * 2;

  // 避免生成的水印模糊
  const canvas = document.createElement('canvas');
  canvas.width = cssWidth * pixelRatio;
  canvas.height = cssHeight * pixelRatio;
  
  const ctx = canvas.getContext('2d');
  ctx.scale(pixelRatio, pixelRatio);
  ctx.font = `${cssFontSize}px sans-serif`;
  ctx.fillStyle = color;
  ctx.textBaseline = 'top';

  // 繪制文本行
  lines.forEach((line, i) => {
    ctx.fillText(line, padding, padding + i * lineHeighCss);
  });

  const dataUrl = canvas.toDataURL('image/png');

  // 使用pdf-lib處理PDF
  const existingPdfBytes = await fetch(pdfUrl).then(r => r.arrayBuffer());
  const pdfDoc = await PDFDocument.load(existingPdfBytes);
  const pngImage = await pdfDoc.embedPng(dataUrl);

  const pages = pdfDoc.getPages();
  const imgWPt = pxToPt(cssWidth);
  const imgHPt = pxToPt(cssHeight);

  // 為每頁(yè)添加水印
  for (const page of pages) {
    const { width: pageW, height: pageH } = page.getSize();
    const rotation = page.getRotation().angle || 0;

    // 根據(jù)頁(yè)面旋轉(zhuǎn)調(diào)整水印角度
    let finalAngle = angle;
    if (rotation === 90) finalAngle = angle - 90;
    else if (rotation === 270) finalAngle = angle + 90;
    else if (rotation === 180) finalAngle = angle + 180;

    // 平鋪水印
    for (let x = -pageW; x < pageW * 2; x += xGap) {
      for (let y = -pageH; y < pageH * 2; y += yGap) {
        page.drawImage(pngImage, {
          x,
          y,
          width: imgWPt,
          height: imgHPt,
          rotate: degrees(finalAngle),
          opacity
        });
      }
    }
  }

  // 導(dǎo)出并下載
  const pdfBytes = await pdfDoc.save();
  const blob = new Blob([pdfBytes], { type: 'application/pdf' });
  const link = document.createElement('a');
  link.href = URL.createObjectURL(blob);
  link.download = pdfUrl.split('/').pop() || 'watermarked.pdf';
  link.click();
  URL.revokeObjectURL(link.href);
}

效果展示

2. 圖片水印實(shí)現(xiàn)

思路和pdf的類似,只不過(guò)不需要pdf-lib庫(kù)了,先生成和上面一樣的水印圖,再創(chuàng)建canvas加載原圖,遍歷添加水印圖即可。

export async function addWatermarkToImage(imageUrl, companyName, personName, options = {}) {
  if (!imageUrl) return '';
  const {
    opacity = 0.38,
    angle = 45,
    color = 'rgba(120,120,120,1)',
    pixelRatio = Math.max(window.devicePixelRatio || 1, 2),
    lineHeight = 1.4,
    crossOrigin = 'anonymous',
    mimeType = 'image/png',
    quality = 0.92,
    fontRatio = 0.02, // 字體比例
    gapRatio = { x: 0.25, y: 0.2 } // 間隔比例
  } = options;

  const firstLine = companyName && personName ? `${companyName}-${personName}` : companyName || personName || '';
  const secondLine = '僅用于xxxx,他用無(wú)效';
  const lines = [firstLine, secondLine];

  const loadImage = (src, needCO) =>
    new Promise((resolve, reject) => {
      const img = new Image();
      if (needCO) img.crossOrigin = crossOrigin;
      img.onload = () => resolve(img);
      img.onerror = reject;
      img.src = src;
    });

  const baseImg = await loadImage(imageUrl, true);
  const W = baseImg.naturalWidth || baseImg.width;
  const H = baseImg.naturalHeight || baseImg.height;

  const minSide = Math.min(W, H);
  const adaptiveFontSize = Math.max(12, Math.round(minSide * fontRatio));

  const measureCanvas = document.createElement('canvas');
  const mctx = measureCanvas.getContext('2d');
  mctx.font = `${adaptiveFontSize}px sans-serif`;

  const firstLineWidthPx = mctx.measureText(firstLine).width;

  // 動(dòng)態(tài)間隔(比例 + 最小間隔限制)
  let xGap = W * gapRatio.x;
  let yGap = H * gapRatio.y;

  const minXGap = Math.max(firstLineWidthPx * 3, 200); // 至少 200px
  const minYGap = Math.max(adaptiveFontSize * 4, 150); // 至少 150px

  xGap = Math.max(xGap, minXGap);
  yGap = Math.max(yGap, minYGap);

  const padding = 10;
  const lineHeightPx = adaptiveFontSize * lineHeight;
  const cssWidth = Math.max(...lines.map(l => mctx.measureText(l).width)) + padding * 2;
  const cssHeight = lines.length * lineHeightPx + padding * 2;

  const tileCanvas = document.createElement('canvas');
  tileCanvas.width = cssWidth * pixelRatio;
  tileCanvas.height = cssHeight * pixelRatio;

  const tctx = tileCanvas.getContext('2d');
  tctx.scale(pixelRatio, pixelRatio);
  tctx.font = `${adaptiveFontSize}px sans-serif`;
  tctx.fillStyle = color;
  tctx.textBaseline = 'top';
  tctx.textAlign = 'left';

  lines.forEach((line, i) => {
    tctx.fillText(line, padding, padding + i * lineHeightPx);
  });

  const tileDataUrl = tileCanvas.toDataURL('image/png');
  const tileImg = await loadImage(tileDataUrl, false);

  const outCanvas = document.createElement('canvas');
  outCanvas.width = W * pixelRatio;
  outCanvas.height = H * pixelRatio;

  const ctx = outCanvas.getContext('2d');
  ctx.scale(pixelRatio, pixelRatio);

  ctx.drawImage(baseImg, 0, 0, W, H);

  ctx.globalAlpha = opacity;
  const rad = (angle * Math.PI) / 180;

  for (let x = -W; x < W * 2; x += xGap) {
    for (let y = -H; y < H * 2; y += yGap) {
      ctx.save();
      ctx.translate(x + cssWidth / 2, y + cssHeight / 2);
      ctx.rotate(rad);
      ctx.drawImage(tileImg, -cssWidth / 2, -cssHeight / 2, cssWidth, cssHeight);
      ctx.restore();
    }
  }

  ctx.globalAlpha = 1;
  return outCanvas.toDataURL(mimeType, quality);
}

效果展示

關(guān)鍵技術(shù)點(diǎn)

1. 高分辨率處理

通過(guò)pixelRatio參數(shù)確保在高DPI屏幕上水印依然清晰,這是通過(guò)將canvas尺寸放大再縮放實(shí)現(xiàn)的。

2. 自動(dòng)間隔計(jì)算

水印間隔不是固定值,而是基于文本長(zhǎng)度動(dòng)態(tài)計(jì)算:

const xGap = Math.max(baseGap, firstLineWidthPt * 1.2);

這確保了水印既不會(huì)過(guò)于密集也不會(huì)過(guò)于稀疏。

3. 頁(yè)面旋轉(zhuǎn)適配

  firstPage.drawText('This text was added with JavaScript!', {
    x: 5,
    y: height / 2 + 300,
    size: 50,
    font: helveticaFont,
    color: rgb(0.95, 0.1, 0.1),
    rotate: degrees(0),
  })

上面這行代碼,在每頁(yè)高度>寬度的情況下,這段代碼水印顯示沒(méi)什么問(wèn)題。 反之,就會(huì)出現(xiàn)下面這種情況,水印倒轉(zhuǎn)過(guò)來(lái)了

于是在添加水印前,應(yīng)先判斷pdf的方向

    const { width: pageW, height: pageH } = page.getSize();
    const rotation = page.getRotation().angle || 0;
    let finalAngle = angle;
    if (rotation === 90) finalAngle = angle - 90;
    else if (rotation === 270) finalAngle = angle + 90;
    else if (rotation === 180) finalAngle = angle + 180;

PDF處理時(shí)自動(dòng)檢測(cè)頁(yè)面旋轉(zhuǎn)角度并相應(yīng)調(diào)整水印方向,保證水印始終以正確角度顯示。

4. 跨域圖片處理

圖片水印函數(shù)支持crossOrigin參數(shù),可以正確處理需要CORS的圖片資源。

相關(guān)文章

最新評(píng)論

宁阳县| 乳山市| 博罗县| 荃湾区| 麻栗坡县| 绍兴市| 麻江县| 南岸区| 弋阳县| 巴林右旗| 视频| 拜泉县| 成安县| 郁南县| 孟州市| 莱阳市| 建平县| 林芝县| 阜南县| 竹山县| 屯门区| 永春县| 合水县| 车致| 连城县| 得荣县| 宁晋县| 嵊泗县| 海安县| 霸州市| 安宁市| 循化| 南投市| 循化| 陵水| 封丘县| 扎囊县| 紫金县| 勐海县| 都安| 二连浩特市|