likes
comments
collection
share

JQuery插件秀:生成PDF文件(文本+上传图片+电子签名)

作者站长头像
站长
· 阅读数 34

前言

  • 需求如下:根据 docx 模板形成页面,让用户直接填写相关信息,在线生成 PDF 文件,无需用户下载 docx 模板填完信息再转为 PDF。
  • 填写信息包括普通文本、上传图片、在线电子签名。

方案确定

  • 文本和图片直接生成 PDF 可能有中文乱码、样式错乱等问题。
    • 所以考虑直接生成 PDF 内容的图片,再用插件把图片放到 PDF 文件里面,触发下载保存。
  • 如果直接将用户填写上传的区域生成 canvas,会把边框、虚线、按钮等提升用户体验的样式也一起生成。
    • 所以需要把用户输入的信息提取出来,额外按照 docx 模板渲染纯 html 片段,即上传区域直接显示上传后的图片,输入框直接显示输入的文本,电子签名区域直接显示生成的签名图片。
  • 使用 JQuery 配合以下插件实现。
  • 插件官网:

个人主页

yuziikuko.gitee.io/articles/00…

在线尝试

CodeSandbox

仓库地址

效果预览

(一)基础页面结构

🍩 按照 docx 模板编写基础 HTML

<div class="title" style="margin-top: 50px">Create</div>
<!-- 初始,渲染带装饰的html,供用户输入文本、上传图片、生成电子签名 -->
<div id="container">
  <!-- logo -->
  <div class="row">
    <!-- TODO Dropzone上传图片 -->
  </div>
  <!-- Date -->
  <div class="row" style="margin-top: 50px">
    <div class="row-input">
      <span>Date:</span>
      <input id="date_input" type="text" placeholder="XX March, 2023" />
    </div>
  </div>
  <!-- some text -->
  <div class="row" style="margin-bottom: 50px">
    <p>
      Lorem ipsum dolor sit amet, consectetur adipisicing elit. Officia autem
      nostrum delectus voluptatem. Magnam enim quis aut, maiores id nemo vel!
      <input
        id="some_input"
        type="text"
        placeholder="Type anything you need here."
      />
      Omnis, reprehenderit a? Dolore nesciunt omnis laudantium maxime tenetur.
    </p>
    <p>
      Lorem ipsum, dolor sit amet consectetur adipisicing elit. Cumque impedit
      perferendis deserunt minima fuga. Sunt, repellat. Repudiandae, fugit hic
      nam molestias magni animi itaque sapiente possimus voluptates, eius
      officia aliquid.
    </p>
  </div>
  <!-- signature -->
  <div class="row">
    <!-- TODO jSignature生成电子签名 -->
  </div>
</div>
<button class="button-upload button-convert" onclick="convertHtml()">
  Preview your file
</button>

🍩 全局样式

* {
  margin: 0;
  padding: 0;
  border: 0;
}

body {
  background-color: #303030;
  display: flex;
  flex-direction: column;
  align-items: center;
  margin-bottom: 50px;
}

#container {
  width: 80%;
  margin: 30px auto;
  padding: 30px;
  border: 2px dashed #dddddd;
  text-align: justify;
  font-size: 16px;
  line-height: 1.5;
  color: #ffffff;
}

#container_preview {
  width: 80%;
  margin: 30px auto;
  padding: 30px;
  background-color: #ffffff;
  text-align: justify;
  font-size: 16px;
  line-height: 1.5;
  display: none;
}

.title {
  font-size: 50px;
  color: #ffffff;
  text-align: center;
  margin: 100px 0 10px;
}

.row {
  width: 100%;
  margin-bottom: 15px;
}

.row p {
  margin-bottom: 10px;
}

/* 信息输入 */
.row-input {
  display: flex;
  align-items: center;
}

.row input {
  width: 300px;
  height: fit-content;
  display: inline;
  margin: 0 10px;
  padding: 0 5px;
  font-size: 16px;
  outline: unset;
  border-width: 0 0 1px 0;
  border-style: dashed;
  border-color: #ffffff;
  border-radius: unset;
  background: transparent;
  color: #ffffff;
  caret-color: #ffffff; /* 光标颜色 */
}

(二)使用 Dropzone 上传图片

🍩 页面结构

<!-- logo -->
<div class="row">
  <div class="drop-div">
    <div id="dropzone" class="dropzone dropzone-div">
      <div class="dz-message needsclick" style="height: 100%">
        <div id="preview" style="display: none" class="preview-wrap"></div>
        <div id="hide_preview" class="drop-no-img">
          <div style="font-size: 16px; color: #d8152a">Logo</div>
          <div class="button-upload" onclick="uploadImage()">Upload</div>
        </div>
      </div>
    </div>
  </div>
</div>

🍩 相关配置

  • 首行代码用于解决 Uncaught Error: Dropzone already attached. 报错。
    • 如加上首行代码后,点击 Upload 有时正常调起文件选择,有时依旧报错,此时多刷新几次就可以了。
  • url 应替换为后端 API 接口地址,如:/api/upload
// 上传图片
Dropzone.autoDiscover = false;
function uploadImage() {
  let upload_dropzone = new Dropzone("#dropzone", {
    url: "/", // TODO
    uploadMultiple: false,
    acceptedFiles: ".png,.jpg,.tiff,.jpeg",
    addedfile: function (file) {
      $("#hide_preview").hide();
      $("#preview").html('<div class="loader preview-img"></div>');
      $("#preview").show();
    },
    success: function (file, response, e) {
      if (response.code == 200) {
        $("#hide_preview").hide();
        $("#preview").html(
          `<img src="${response.data.url}" class="preview-img">`
        );
        $("#preview").show();
      } else {
        alert(response.msg);
      }
    },
    error: function (e, t) {
      alert(t);
    },
  });
}

🍩 展示样式

/* 图片上传 */
.drop-div {
  width: 150px;
  height: 150px;
  background: #ffffff;
  border-radius: 10px;
  border: 1px solid #dddddd;
  padding: 10px;
  margin-bottom: 50px;
}

.dropzone-div {
  width: 100%;
  height: 100%;
  padding: 0;
  overflow: hidden;
}

.dropzone {
  border-radius: 10px;
  border: 1px dashed #d8152a;
  text-align: center;
}

.drop-no-img {
  height: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: space-evenly;
}

.preview-wrap {
  width: 100%;
  height: 100%;
  position: relative;
}

.preview-img {
  max-width: 100%;
  max-height: 100%;
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
}

.button-upload {
  width: 80px;
  height: 25px;
  border-radius: 25px;
  border: 1px solid #d8152a;
  font-size: 12px;
  color: #d8152a;
  line-height: 25px;
  margin: 0 auto;
  text-align: center;
  cursor: pointer;
}

.button-upload:hover {
  color: #ffffff;
  background-color: #d8152a;
  border: 1px solid #d8152a;
  transition: all 0.2s linear;
}

(三)使用 jSignature 生成电子签名图片

🍩 页面结构

<!-- signature -->
<div class="row">
  <p>Generate your signature:</p>
  <div id="signature"></div>
  <div
    style="
        width: 100%;
        display: flex;
        align-items: center;
        justify-content: flex-end;
      "
  >
    <div class="button-upload signature-feature" onclick="generateSignature()">
      Generate
    </div>
    <div class="button-upload signature-feature" onclick="clearSignature()">
      Clear
    </div>
  </div>
  <p>Preview your signature:</p>
  <div id="signature_preview"></div>
</div>

🍩 初始化

  • 调用 jSignature() 初始化签名画布区域。
// 初始化
$("#signature").jSignature({
  lineWidth: 5, // 画笔粗细
  width: "100%", // 画布宽度
  height: 196, // 画布高度:div高度减去上下2px的边框
  "background-color": "#dddddd", // 画布背景
  color: "#333333", // 画笔颜色
  UndoButton: false, // 撤销上一步按钮
  willReadFrequently: true,
});

🍩 生成签名

  • 封装按钮点击事件,点击按钮时生成签名。
  • 定义全局变量 signatureSrc 保存生成的签名图片,用于后续生成完整 HTML 结构,以导出 PDF。
  • 导出的签名图片格式可以自定义,具体参考官网配置。
let signatureSrc = "";
// 生成签名
function generateSignature() {
  let res = $("#signature").jSignature("getData", "svgbase64");
  let img = new Image();
  signatureSrc = `data:${res[0]},${res[1]}`;
  img.src = signatureSrc;
  $("#signature_preview").html(img);
}

🍩 清空签名

  • 封装按钮点击事件,点击按钮时清空签名画布。
// 清空签名
function clearSignature() {
  $("#signature").jSignature("reset");
  $("#signature_preview").html("");
  signatureSrc = "";
}

🍩 展示样式

/* 电子签名 */
#signature {
  width: 100%;
  height: 200px;
  margin: 0 0 10px;
  background-color: #696969;
  border-radius: 10px;
  border: 2px dashed #dddddd;
  box-sizing: border-box;
  overflow: hidden;
}

.signature-feature {
  margin: 0 0 0 10px;
  padding: 0;
  font-weight: normal;
  display: flex;
  align-items: center;
  justify-content: center;
}

#signature_preview {
  width: 100%;
  height: 200px;
  padding: 20px;
  margin: 0 0 20px;
  background-color: #ffffff;
  border-radius: 10px;
  box-sizing: border-box;
  border: 1px solid #dddddd;
}

#logo {
  height: 150px;
}

#signature_img {
  height: 100px;
}

.button-convert {
  width: 400px;
  height: 40px;
  line-height: 1.5;
  padding: 0 40px;
  font-size: 16px;
  padding: 0 25px;
  margin: 10px auto;
}

(四)渲染 HTML 预览区域

  • 按照 docx 模板格式,提取用户输入、上传的信息渲染预览区域的 html。

🍩 页面结构

<!-- 
  预览,根据html生成图片或canvas
  【html2canvas插件必须在原html存在的情况下生成,如果预览和生成同时进行会渲染出空白横线】
-->
<div id="preview_title" class="title" style="display: none">Preview</div>
<!-- 根据输入的信息生成没有装饰、带文本图片的html片段,以此直接生成canvas -->
<div id="container_preview">
  <!-- logo -->
  <div class="row">
    <img id="logo" src="" alt="logo" />
  </div>
  <!-- Date -->
  <div class="row" style="margin-top: 50px">
    <span>Date:</span>
    <span id="date"></span>
  </div>
  <!-- some text -->
  <div class="row" style="margin-bottom: 50px">
    <p>
      Lorem ipsum dolor sit amet, consectetur adipisicing elit. Officia autem
      nostrum delectus voluptatem. Magnam enim quis aut, maiores id nemo vel!
      <span id="some"></span> Omnis, reprehenderit a? Dolore nesciunt omnis
      laudantium maxime tenetur.
    </p>
    <p>
      Lorem ipsum, dolor sit amet consectetur adipisicing elit. Cumque impedit
      perferendis deserunt minima fuga. Sunt, repellat. Repudiandae, fugit hic
      nam molestias magni animi itaque sapiente possimus voluptates, eius
      officia aliquid.
    </p>
  </div>
  <!-- signature -->
  <div class="row">
    <img id="signature_img" src="" alt="signature" />
  </div>
</div>
<button
  id="convert_canvas_btn"
  class="button-upload button-convert"
  style="display: none"
  onclick="convertCanvas()"
>
  Generate your file to PDF
</button>

🍩 上传图片成功后提取图片 url 绑定到 html 片段

// 上传图片
Dropzone.autoDiscover = false;
function uploadImage() {
  let upload_dropzone = new Dropzone("#dropzone", {
    // ...
    success: function (file, response, e) {
      if (response.code == 200) {
        // ...
        // 生成预览需要转换为base64的dataUrl
        $("#logo").prop("src", file.dataURL);
      } else {
        alert(response.msg);
      }
    },
    // ...
  });
}

🍩 电子签名生成成功后提取图片 url 绑定到 html 片段

// 生成签名
function generateSignature() {
  // ...
  $("#signature_img").prop("src", signatureSrc);
}
// 清空签名
function clearSignature() {
  // ...
  $("#signature_img").prop("src", "");
}

🍩 点击事件生成 html 预览区域

// 转换成html片段
function convertHtml() {
  // 隐藏预览区域
  const delay = 100;
  $("#preview_title").slideUp(delay);
  $("#container_preview").slideUp(delay);
  $("#convert_canvas_btn").slideUp(delay);

  // $("#logo")图片的赋值在Dropzone插件中完成
  $("#date").html($("#date_input").val());
  $("#some").html($("#some_input").val());
  // $("#signature_img")图片的赋值在jSignature插件中完成

  // 显示预览区域
  $("#preview_title").slideDown(delay);
  $("#container_preview").slideDown(delay);
  $("#convert_canvas_btn").slideDown(delay);
  // 滚动到预览区域
  $("html, body").animate(
    {
      scrollTop: $("#preview_title").offset().top,
    },
    200
  );
}

(五)使用 html2canvas 生成 HTML 预览区域的图片

// html转换成图片
function convertCanvas() {
  html2canvas(document.querySelector("#container_preview")).then((canvas) => {
    let imgUrl = canvas.toDataURL("image/png"); // 将canvas转换成img的src流
    // 预览图
    $("#canvas_preview").prop("src", imgUrl);
    $("#result_preview").slideDown(100);
    $("html, body").animate(
      {
        scrollTop: $("#preview_title").offset().top,
      },
      200
    );
  });
}

(六)使用 jsPDF 生成 PDF

// 生成PDF
function convertCanvas() {
  html2canvas(document.querySelector("#container_preview")).then((canvas) => {
    // ...

    // 创建文件
    let doc = new jsPDF();

    // 图片在文件中的边距(0.1 => 10%):左右共0.1、上下共0.1
    const margin = 0.1;

    // 获取图片宽高
    const imgWidth = canvas.width;
    const imgHeight = canvas.height;

    // 计算文件除去边距后剩余的可填充区域宽高
    const docWidth = doc.internal.pageSize.width * (1 - margin);
    const docHeight = doc.internal.pageSize.height * (1 - margin);

    // 计算可填充区域左上角坐标
    const x = doc.internal.pageSize.width * (margin / 2);
    const y = doc.internal.pageSize.height * (margin / 2);

    // 计算可填充区域和待填充图片的宽高比:找出图片的较短边
    const widthRatio = docWidth / imgWidth;
    const heightRatio = docHeight / imgHeight;

    // 按较短边比例缩放图片
    const ratio = Math.min(widthRatio, heightRatio);
    const w = imgWidth * ratio;
    const h = imgHeight * ratio;

    // 调用插件函数填充图片
    doc.addImage(imgUrl, "JPEG", x, y, w, h);

    // 触发下载保存
    doc.save("Your File.pdf");
  });
}