前端生成pdf水印解決方案

最近有需求需要做一個(gè)小工具:生成pdf水印的網(wǎng)頁(yè)工具毒费。一開(kāi)始是想前端傳遞文件宗雇、水印文案給后端,然后由服務(wù)器生成,然后返回前端制作好的帶水印的pdf鏈接提供下載侧甫。然后就按照這個(gè)方案開(kāi)始了行動(dòng)凶伙,后端同事一頓噼里啪啦的操作最后使用python的reportlab完成了本次需求的重中之重的工作拾因。未聯(lián)調(diào)接口之前捏萍,程序工作的挺好的。

原本以為這樣就結(jié)束了本次需求纵苛,誰(shuí)知聯(lián)調(diào)之后問(wèn)題來(lái)了剿涮,發(fā)現(xiàn)需上傳的文件可能幾十上百兆,需生成pdf的水印文案也可能多達(dá)十?dāng)?shù)個(gè)攻人,生成時(shí)間就成了一個(gè)大問(wèn)題取试。據(jù)測(cè)試單個(gè)10+M的pdf文件加完一個(gè)水印就需要30s左右,大大的降低了工具效率怀吻。然后提出方案在服務(wù)器保留該次任務(wù)瞬浓,等用戶下次再打開(kāi)該頁(yè)面再來(lái)下載之前處理好的文件,但是這樣也有效率問(wèn)題蓬坡,而且前后端需要兩次交互才能把文件下載下來(lái)猿棉,效率不高磅叛。本著效率至上的原則,pass掉了后端生成的方案萨赁。

于是便想著看是否能在前端就把pdf給處理完成弊琴,不與后端交互,效率肯定能大大的提升起來(lái)杖爽,在github搜索pdf敲董,篩選js庫(kù),可以看到很多優(yōu)秀的庫(kù)慰安,如:pdf.js(30.9k)/jsPDF(19.2k)/pdfmake(8.3k)/pdfkit(6.2k)/pdf-lib(1.1k)腋寨。仿佛看到了巨大的希望,從星星最高的開(kāi)始查閱化焕,哪個(gè)庫(kù)能夠符合我的需求萄窜。

pdf.js (官方api文檔還未完善,只有一個(gè)api.js的文檔撒桨,但閱讀起來(lái)有點(diǎn)費(fèi)勁) 確實(shí)是非常優(yōu)秀的pdf閱讀庫(kù)查刻,提供了非常多的功能,但大多數(shù)都是查看類(lèi)的元莫,沒(méi)有能夠操作的方法赖阻,但是其提供了一個(gè)在canvas中顯示的方法(默默記下了這個(gè)方法)

jsPDF/pdfmake/pdfkit都是用來(lái)生成pdf的庫(kù),但是閱讀文檔后沒(méi)有發(fā)現(xiàn)能夠在源文檔上直接操作的方法踱蠢。值得注意的是他們提供了可以從canvas中生成pdf的方法。

思考:把pdf每一頁(yè)都畫(huà)在canvas上棋电,然后通過(guò)canvas的api在pdf上層畫(huà)一層水印茎截,最后再把canvas轉(zhuǎn)成pdf下載下來(lái)不就可以了么,思路有了就開(kāi)始實(shí)現(xiàn)吧

瞎B操作:

/**html代碼省略**/

$('#upload-input').change(async function () {
        var files = $(this)[0].files;
        if (files.length) {
          $('#file-name').html(files[0].name);
          showPdf(await files[0].arrayBuffer(),files[0].name)
        }
      })

      function showPdf(file,filename) {
        var size = 80;
        var text = '我是';
        pdfjsLib.workerSrc = '<?php echo CDN; ?>js/pdfjs/pdf.worker.js';
        pdfjsLib.getDocument(file).promise.then(function (pdf) {
          var pageNum = 1;
          var time = 0;
          var timer = setInterval(() => time += 10, 10)
          var new_pdf;
          setCanvas()
          function setCanvas() {
            pdf.getPage(pageNum).then(function (page) {
              var scale = 1.5;
              var viewport = page.getViewport({scale: scale,});
              var {offsetX, offsetY, width, height} = viewport;
              var orientation = width <= height ? 'p' : 'l'
              var canvas = document.getElementById('the-canvas');
              var wm_canvas = document.getElementById('watermarking-canvas');

              var context = canvas.getContext('2d');
              var wm_context = wm_canvas.getContext('2d');
              wm_context.height = canvas.height = height;
              wm_context.width = canvas.width = width;

              var renderContext = {
                canvasContext: context,
                viewport: viewport
              };
              if (pageNum == 1) {
                new_pdf = new jsPDF({
                  orientation,
                  unit: 'px',
                  format: [width/scale, height/scale],
                })
              }
              // console.log(pdf,page,viewport)
              page.render(renderContext).promise.then(function () {
                context.fillStyle = 'rgba(0,0,0,0.1)';
                context.font = `300 ${size}px "Helvetica Neue", Helvetica, Arial, "PingFang SC", "Hiragino Sans GB", "Heiti SC", "Microsoft YaHei", "WenQuanYi Micro Hei", sans-serif`;
                var textW = context.measureText(text).width
                for (let i = 0; i < width + size; i += textW) {
                  for (let j = size; j < height + size; j += size) {
                    context.fillText(text, i, j);
                  }
                }
                // var pageData = canvas.toDataURL('image/jpeg', 1.0);

              }).finally(function () {
                pageNum++
                new_pdf.addImage(canvas, 'JPEG', offsetX, offsetY, new_pdf.internal.pageSize.getWidth(), new_pdf.internal.pageSize.getHeight());
                if (pageNum <= pdf.numPages) {
                  new_pdf.addPage([width/scale, height/scale])
                  setCanvas()
                } else {
                  clearInterval(timer)
                  console.log(time)
                  new_pdf.save('水印' + filename)
                }
              })
            })
          }
        });
      }

最后,運(yùn)行一下程序赶盔,真的生成了

WX20200608-172347@2x.png

可是問(wèn)題又來(lái)了企锌!

WX20200608-172421@2x.png
  1. 生成出來(lái)的pdf變成了圖片,里面的文字不能被選中
  2. 生成的pdf會(huì)模糊于未,先放大再縮小之后質(zhì)量上去了撕攒,但是文件大小也上去了,源文件2.1M但是現(xiàn)在生成之后是180M,這不能忍昂嫫帧抖坪!

方案再次被pass!

最后抱著試一試的態(tài)度試了一下pdf-lib這個(gè)庫(kù)(星星也是少的可憐)闷叉,但是它很強(qiáng)大擦俐,支持直接修改pdf源文檔,同時(shí)也提供了畫(huà)水印文字的方法握侧,感覺(jué)非常符合我目前的需求呀蚯瞧!然后是一頓閱讀+操作嘿期,最后按照官方給的一個(gè)demo試試

import { degrees, PDFDocument, rgb, StandardFonts } from 'pdf-lib';

async function modifyPdf() {
  const url = 'https://pdf-lib.js.org/assets/with_update_sections.pdf'
  const existingPdfBytes = await fetch(url).then(res => res.arrayBuffer())

  const pdfDoc = await PDFDocument.load(existingPdfBytes)
  const helveticaFont = await pdfDoc.embedFont(StandardFonts.Helvetica)

  const pages = pdfDoc.getPages()
  const firstPage = pages[0]
  const { width, height } = firstPage.getSize()
  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(-45),
  })

  const pdfBytes = await pdfDoc.save()
}

很好,非常好埋合,勝利就在前方了备徐!

但是,這時(shí)候又出問(wèn)題了甚颂,畫(huà)上去的水印文字不支持設(shè)置透明度坦喘。查閱github的issue之后發(fā)現(xiàn)作者說(shuō),透明度是個(gè)很好的功能呀西设,可惜我不支持瓣铣,你們誰(shuí)想出來(lái)了給我發(fā)個(gè)pr。贷揽。棠笑。(吐血...)

還好天無(wú)絕人之路,這時(shí)看到一位老哥在另一個(gè)issue中回了一句禽绪,pdf-lib它還支持合并pdf蓖救,你可以搞個(gè)水印層的pdf和源pdf進(jìn)行合并從而曲線救國(guó),來(lái)達(dá)到實(shí)現(xiàn)pdf加水印的效果印屁。略一思量循捺,該方案可行,開(kāi)干吧雄人。

誰(shuí)知又遇到了問(wèn)題从橘,其他庫(kù)高了一圈也都不能生成帶透明度的pdf文件,這可咋整啊~(哪位大佬如果知道础钠,歡迎指導(dǎo))

最后恰力,看來(lái)還得借助后端老哥的力量了。前端把文案信息傳遞給后端旗吁,后端生成pdf水印層踩萎,然后返回文件鏈接,前端拿到連接后請(qǐng)求下來(lái)很钓,前端負(fù)責(zé)把源文件與水印層pdf文件進(jìn)行合并香府,由于讀取的只有一個(gè)單頁(yè)的水印pdf速度還是很快的,源文件在本地讀取就行了码倦,效率也不算太差企孩。經(jīng)測(cè)試給一個(gè)80M的pdf添加5個(gè)水印,耗時(shí)10S左右吧叹洲。

最最后柠硕,還有一個(gè)問(wèn)題,就是服務(wù)器上產(chǎn)生的水印pdf垃圾文件怎么辦?當(dāng)然是刪除嘍蝗柔,不然留著過(guò)年拔趴!用了promise.all來(lái)監(jiān)聽(tīng)是否全部轉(zhuǎn)換完成癣丧,完成后發(fā)送一個(gè)請(qǐng)求給后端槽畔,刪除掉之前生產(chǎn)的文件。

 let file, water_name_keys,base_url="你的api地址";
      //監(jiān)聽(tīng)上傳按鈕
      $('#upload-input').change(function () {
        const files = $(this)[0].files;
        if (files.length) {
          $('#file-name').html(files[0].name);
          file = files[0];
        }
      })

      //提交水印文案胁编,獲取打好水印的pdf
      $('#submit').click(function () {
        const water_name = $('[name=users]').val();

        if(!file){
          new_alert('請(qǐng)先上傳文件', 1500);
          return
        }else if(!water_name){
          new_alert('請(qǐng)先填寫(xiě)水印文案', 1500);
          return
        }
        let time = 0;
        let timer = setInterval(()=>{time+=100},100);
        const msg_id = new_alert('生成中...', 999999);
        $.ajax({
          url:base_url+'/tool/ExecFileWaterMark',
          data: {water_name},
          method: 'get',
          dataType: 'jsonp',
          success(res) {
            if (res.resultCode == 0) {
              let modifyPdfList = []
              water_name_keys = Object.keys(res.result.data);
              Object.values(res.result.data).map((wm_url, index) => {
                modifyPdfList.push(modifyPdf(wm_url, file, index));
              })
              Promise.all(modifyPdfList).then(() => {
                //統(tǒng)計(jì)時(shí)間
                clearInterval(timer);
                console.log(time);

                new_alert.close(msg_id);
                new_alert('生成成功', 1500);
                deleteWmFile();
              }).catch(() => {
                new_alert('生成失敗', 1500);
                clearInterval(timer);
              })
            } else {
              new_alert.close(msg_id);
              new_alert('生成失敗', 1500);
            }
          },
          error() {
            new_alert.close(msg_id);
          }
        })
      })

      //合并pdf
      function modifyPdf(url, file, index) {
        return new Promise(async (resolve, reject) => {
          try {
            const {PDFDocument} = PDFLib;
            //獲取源pdf文件文檔
            const existingPdfBytes = await file.arrayBuffer();
            const pdfDoc = await PDFDocument.load(existingPdfBytes);
            //獲取水印文件文檔
            const wmPdf = await fetch(url).then(res => res.arrayBuffer());
            //把水印pdf頁(yè)面嵌入文檔中
            const [wmPreamble] = await pdfDoc.embedPdf(wmPdf, [0]);
            //獲取源文件總頁(yè)數(shù)
            const pages = pdfDoc.getPages();
            //循環(huán)源pdf頁(yè)面添加水印
            for (let i = 0; i < pages.length; i++) {
              const page = pages[i];
              const {width, height} = page.getSize();
              //把水印頁(yè)面畫(huà)到源pdf頁(yè)面上
              page.drawPage(wmPreamble, {width, height, x: 0, y: 0});
            }
            //保存pdf頁(yè)面厢钧,返回二進(jìn)制unit8Array二進(jìn)制數(shù)組
            const pdfBytes = await pdfDoc.save();
            //二進(jìn)制數(shù)組轉(zhuǎn)為blob數(shù)據(jù)
            const blob = new Blob([pdfBytes], {type: 'application/pdf'});
            //下載
            const a = document.createElement("a");
            a.href = URL.createObjectURL(blob);
            a.download = water_name_keys[index] + '-' + file.name; // 這里填保存成的文件名
            a.click();
            URL.revokeObjectURL(a.href);
            a.remove();
            resolve();
          } catch (e) {
            reject()
          }
        })
      }

      //  刪除服務(wù)器上的水印文件
      function deleteWmFile() {
        const water_name = water_name_keys.join(',')
        $.ajax({
          url: base_url+'/tool/DeleteFileWaterMark',
          data: {water_name},
          method: 'get',
          dataType: 'jsonp'
        })
      }

來(lái)張效果圖 ^ _ ^

完結(jié)撒花!f页取早直!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市市框,隨后出現(xiàn)的幾起案子霞扬,更是在濱河造成了極大的恐慌,老刑警劉巖枫振,帶你破解...
    沈念sama閱讀 216,544評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件喻圃,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡粪滤,警方通過(guò)查閱死者的電腦和手機(jī)斧拍,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,430評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)杖小,“玉大人肆汹,你說(shuō)我怎么就攤上這事∏喜啵” “怎么了县踢?”我有些...
    開(kāi)封第一講書(shū)人閱讀 162,764評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)伟件。 經(jīng)常有香客問(wèn)我,道長(zhǎng)议经,這世上最難降的妖魔是什么斧账? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,193評(píng)論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮煞肾,結(jié)果婚禮上咧织,老公的妹妹穿的比我還像新娘。我一直安慰自己籍救,他們只是感情好习绢,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,216評(píng)論 6 388
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著,像睡著了一般闪萄。 火紅的嫁衣襯著肌膚如雪梧却。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 51,182評(píng)論 1 299
  • 那天败去,我揣著相機(jī)與錄音放航,去河邊找鬼。 笑死圆裕,一個(gè)胖子當(dāng)著我的面吹牛广鳍,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播吓妆,決...
    沈念sama閱讀 40,063評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼赊时,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了行拢?” 一聲冷哼從身側(cè)響起祖秒,我...
    開(kāi)封第一講書(shū)人閱讀 38,917評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎剂陡,沒(méi)想到半個(gè)月后狈涮,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,329評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡鸭栖,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,543評(píng)論 2 332
  • 正文 我和宋清朗相戀三年歌馍,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片晕鹊。...
    茶點(diǎn)故事閱讀 39,722評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡松却,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出溅话,到底是詐尸還是另有隱情晓锻,我是刑警寧澤,帶...
    沈念sama閱讀 35,425評(píng)論 5 343
  • 正文 年R本政府宣布飞几,位于F島的核電站砚哆,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏屑墨。R本人自食惡果不足惜躁锁,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,019評(píng)論 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望卵史。 院中可真熱鬧战转,春花似錦、人聲如沸以躯。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,671評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至刁标,卻和暖如春颠通,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背命雀。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,825評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工蒜哀, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人吏砂。 一個(gè)月前我還...
    沈念sama閱讀 47,729評(píng)論 2 368
  • 正文 我出身青樓撵儿,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親狐血。 傳聞我的和親對(duì)象是個(gè)殘疾皇子淀歇,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,614評(píng)論 2 353