Vue3 -- PDF展示、添加签名(带笔锋)、导出

news2024/11/16 11:11:49

文章目录

    • 笔锋签名
    • 方案一
      • 实现要点
      • 实现过程
        • 组件引用
        • 页面元素
        • 添加引用
        • 实现代码
        • 效果展示
        • 缺点
    • 方案二
      • 修改页面元素
      • 替换引用
      • 修改代码
      • 效果展示
    • 完整代码地址

实现功能的时候采用了两个方案,主要是第一个方案最后的实现效果并不太理想,但实现起来比较简单,要求不高时可以使用。
DEMO 会一次性加载并展示所有的 PDF 页面,目的是方便在手机上观看时上下滑动,如果要做成上一页下一页的效果,需要自行实现。

笔锋签名

我是用开源项目 smooth-signature 实现带笔锋签名的功能。
Gitee 地址是 https://github.com/linjc/smooth-signature

npm install --save smooth-signature

使用起来也比较简单,首先获取到需要操作的画布 canvas ,然后生成一个笔锋签名对象 SmoothSignature,optionSign 是初始化的一些简单属性。

const signature = new SmoothSignature(canvas, optionSign);

这样一来,我们的 canvas 就可以画线条了,同时我们可以通过 signature 去做一些操作,比如清空签名、撤回一步的操作等。

方案一

实现要点

  1. 读取 PDF 文件,并将 PDF 页面渲染到 Canvas 画布上,这里需要动态生成 Canvas
  2. 将每一个 Canvas 都包装成 SmoothSignature
  3. 添加一个标识,判断是否允许在 Canvas 上画线(手机滑动会和签名画线冲突,用按钮来控制什么时候允许画线)。
  4. 保存 PDF 时,先将每一个 Canvas 中的内容转化成图片格式 image/JPEG ,或者 image/PNG ,PNG格式的文件可能会比较大。
  5. 最后用生成的图片导出一个新的 PDF (实质上 PDF 每一页都是一张图片)。

实现过程

组件引用

smooth-signature笔锋签名
pdfjs-distPDF展示等功能
jspdfPDF导出相关功能
npm install --save smooth-signature
npm install --save pdfjs-dist@2.0.943
npm install --save jspdf 

页面元素

主要是读取文件、切至签名功能、切回预览功能、撤回签名、清除所有签名以及下载PDF的功能。

<template>
  <div :class="`tab-header`">
    <div id="editor">
      <Input
        :class="`button-common`"
        type="file"
        ref="fielinput"
        accept=".pdf"
        id="fielinput"
        @change="uploadFile"
      />
      <Button :class="`button-common`" v-if="isSign" @click="handleSign">切回预览</Button>
      <Button :class="`button-common`" v-else @click="handleSign">切至签名</Button>
      <Button :class="`button-common`" @click="handleUndo">撤回</Button>
      <Button :class="`button-common`" @click="handleClear">清除</Button>
      <Button :class="`button-common`" @click="savePDF">下载PDF</Button>
    </div>
    <div>
      <div id="parentDiv">
        <div ref="contentDiv" id="contentDiv"></div>
      </div>
    </div>
  </div>
</template>
<script lang="ts">
引用
...... 
实现代码
......
</script>
<style lang="less" scoped>
  .tab-header {
    background: rgb(146, 175, 138);
    padding-left: 1%;
    padding-right: 1%;
  }
  .button-common {
    margin-right: 2px;
    max-width: 200px;
  }
  #contentDiv {
    // display: inline-block;
  }
  #parentDiv {
    position: absolute;
    overflow: auto;
    top: 5%;
    bottom: 1%;
    display: inline-block;
  }
  #signShower {
    position: absolute;
    left: 50%;
    top: 5%;
    bottom: 1%;
    display: inline-block;
  }
</style>

添加引用

这里要注意的是,需要给 pdfJS 指定工作路径

  import { Button, Input } from 'ant-design-vue';
  import { defineComponent, ref } from 'vue';
  import SmoothSignature from 'smooth-signature';
  import * as pdfJS from 'pdfjs-dist';
  import * as pdfjsWorker from 'pdfjs-dist/build/pdf.worker.entry';
  import JsPDF from 'jspdf';

  pdfJS.GlobalWorkerOptions.workerSrc = pdfjsWorker;

实现代码

代码中添加了主要的注释,可以查看下述代码

  export default defineComponent({
    components: { Button, Input },
    setup() {
      const fielinput = ref(null);
      const contentDiv = ref(null);

      //签名相关
      const isSign = ref(false); //控制是否允许签名
      const canvass = ref([]); //保存所有画布元素
      const signatures = ref([]); //所有签名对象
      const historys = ref([]); //签名历史 用于回退或者清除,因为是一次性展示多个页面,会存在多个包装好的签名对象,存放历史列表方便操作

      //PDF展示相关
      const pdfData = ref(null); // PDF 内容
      const scale = ref(2); //放大比例 ,有的时候展示可能会比较模糊,可以放大展示

      //上传控件选择事件,加载选中的 PDF 文件
      const uploadFile = (e: Event) => {
        // 断言为HTMLInputElement
        const target = e.target as HTMLInputElement;
        const files = target.files;
        let reader = new FileReader();
        reader.readAsDataURL(files[0]);
        reader.onload = () => {
          let data = atob(reader.result.substring(reader.result.indexOf(',') + 1));
          loadPdfData(data);
        };
      };
      //加载PDF
      function loadPdfData(data) {
        //移除所有旧的 Canvas 画布元素
        removeChild();
        //重置对象状态
        isSign.value = false;
        canvass.value = [];
        signatures.value = [];
        // 引入pdf.js的字体,如果没有引用的话字体可能会不显示
        let CMAP_URL = 'https://unpkg.com/pdfjs-dist@2.0.943/cmaps/';
        //读取base64的pdf流文件
        pdfData.value = pdfJS.getDocument({
          data: data, // PDF base64编码
          cMapUrl: CMAP_URL,
          cMapPacked: true,
        });
        //渲染全部页面
        renderAllPages();
      }
      //移除页面上旧的元素
      function removeChild() {
        var content = contentDiv.value;
        var child = content.lastElementChild;
        while (child) {
          content.removeChild(child);
          child = content.lastElementChild;
        }
      }
      //渲染全部页面
      function renderAllPages() {
        pdfData.value.promise.then((pdf) => {
          for (let i = 1; i <= pdf.numPages; i++) {
            pdf.getPage(i).then((page) => {
              let viewport = page.getViewport(scale.value);
              //动态生成 Canvas 画布并设置宽高
              var canvas = document.createElement('canvas');
              canvas.height = viewport.height;
              canvas.width = viewport.width;

              let ctx = canvas.getContext('2d');
              let renderContext = {
                canvasContext: ctx,
                viewport: viewport,
              };
              //将 PDF 页面渲染到 Canvas 上
              page.render(renderContext).then(() => {});
              //将画布包装成 SmoothSignature
              initSignatureCanvas(canvas);
              //将画布元素放入到 div 容器中展示
              canvass.value.push(canvas);
              contentDiv.value.appendChild(canvas);
            });
          }
        });
      }
      //初始化签名对象
      const initSignatureCanvas = (canvas) => {
        const optionSign = {
          width: canvas.width,
          height: canvas.height,
          maxHistoryLength: 100, //最大历史记录
        };

        const signature = new SmoothSignature(canvas, optionSign);
        //初始化时 先移除它内部添加的监听事件,默认不能签名
        signature.removeListener();
        //签名对象 addHistory 方法做一下修改,在原来逻辑的基础上添加这一行
        // historys.value.push(signature); 方便处理历史签名记录
        signature.addHistory = function () {
          if (!signature.maxHistoryLength || !signature.canAddHistory) return;
          signature.canAddHistory = false;
          signature.historyList.push(signature.canvas.toDataURL());
          signature.historyList = signature.historyList.slice(-signature.maxHistoryLength);
          historys.value.push(signature);
        };
        signatures.value.push(signature);
      };
      /**
       * 签名预览转换
       * 允许签名:调用 signature 对象中的 addListener 方法,添加监听事件
       * 不允许签名:调用 signature 对象中的 removeListener 方法,移除监听事件
       */
      const handleSign = () => {
        isSign.value = !isSign.value;
        if (signatures.value && signatures.value.length > 0) {
          if (isSign.value) {
            for (let i = 0; i < signatures.value.length; i++) {
              signatures.value[i].addListener();
            }
          } else {
            for (let i = 0; i < signatures.value.length; i++) {
              signatures.value[i].removeListener();
            }
          }
        }
      };
      /**
       * 后退操作
       * 调用历史签名记录中的 signature 对象中的 undo 方法会撤回当前对象中的最后一次的画线记录
       * 注意:后退后不要忘记将列表中最后一个元素移除
       */
      const handleUndo = () => {
        if (historys.value && historys.value.length > 0) {
          const signatureList = historys.value;
          let signature = signatureList.pop();
          signature.undo();
          historys.value = signatureList;
        }
      };
      // 清除所有 循环把所有签名历史都处理了
      const handleClear = async () => {
        while (historys.value && historys.value.length > 0) {
          handleUndo();
        }
      };
      // 下载PDF
      const savePDF = () => {
        //生成新的 PDF
        let pdf = new JsPDF('', 'pt', 'a4');
        if (canvass.value.length > 0) {
          //将 canvas 内容转化成 JPEG
          for (let i = 0; i < canvass.value.length; i++) {
            const ccccc = canvass.value[i];
            let pageData = ccccc.toDataURL('image/JPEG');
            if (i > 0) {
              pdf.addPage();
            }
            pdf.addImage(
              pageData,
              'JPEG',
              0,
              0,
              ccccc.width / scale.value,
              ccccc.height / scale.value,
            );
          }
          //到处新的PDF 
          return pdf.save('TestPdf.pdf');
        }
      };

      return {
        fielinput,
        uploadFile,
        contentDiv,
        isSign,
        handleSign,
        handleUndo,
        handleClear,
        savePDF,
      };
    },
    mounted() {},
  });

效果展示

在这里插入图片描述
在这里插入图片描述

缺点

1、生成的新的PDF每一页都是一个图片,这就表示 PDF 中的内容无法被解析,文字再也无法被选中了。
2、因为生成的是图片,所以最终效果可能会变模糊,可以通过放大比例去优化展示效果,但是始终不是一个最优的解决方案。

方案二

方案二使用一个新的组件 pdf-lib 来处理最后生成的 PDF
方案二仍旧使用 pdfjs-distCanvas 上展示 PDF,并使用 smooth-signature 使得画布拥有笔锋签名效果。
不同的是,这一次签名画布和 PDF 展示画布并不再是同一个画布,而是上下重叠的两个分离的画布
这样一来,我们可以将签名画布上的内容生成一个透明背景的 PNG 图片,然后以水印的方式添加到原来的 PDF 文件中。

修改页面元素

需要两个 Div 容器 ,父容器的滚动条需要同步滚动,否则会出现签名在滚动,但是 PDF 页面不动的情况

<template>
  <div :class="`tab-header`">
    <div id="editor">
      <Input
        :class="`button-common`"
        type="file"
        ref="fielinput"
        accept=".pdf"
        id="fielinput"
        @change="uploadFile"
      />
      <Button :class="`button-common`" v-if="isSign" @click="handleSign">点击预览</Button>
      <Button :class="`button-common`" v-else @click="handleSign">点击签名</Button>
      <Button :class="`button-common`" @click="handleUndo">撤回</Button>
      <Button :class="`button-common`" @click="handleClear">清除</Button>
      <Button :class="`button-common`" @click="savePDF">下载PDF</Button>
    </div>
    <div>
      <div id="parentDiv1">
        <div ref="contentDiv" id="contentDiv"></div>
      </div>
      <div id="parentDiv2">
        <div ref="signContentDiv" id="signContentDiv"></div>
      </div>
    </div>
  </div>
</template>

替换引用

 //import JsPDF from 'jspdf';
 import { PDFDocument } from 'pdf-lib';

修改代码

文章底部附完整代码

...
const signCanvass = ref([]); //保存所有签名画布
const base64 = ref(null);	//读取的pdf的base64数据

上传文件的方法中添加一行保存PDF base64 ,生成新的 PDF 时使用

const uploadFile = (e: Event) => {
  ...
  reader.onload = () => {
    base64.value = reader.result;
    ...
  };
};

加载 PDF 时,我们要重置的对象增加了,而且加载完之后我们要让两个父容器滚动同步

function loadPdfData(data) {
  removeChild();
  ...
  signCanvass.value = []; //重置
  ...
  renderAllPages();
  
  //两个DIV协同滚动
  var div1 = document.getElementById('parentDiv1');
  var div2 = document.getElementById('parentDiv2');
  div1.addEventListener('scroll', function () {
    div2.scrollLeft = div1.scrollLeft;
    div2.scrollTop = div1.scrollTop;
  });
  div2.addEventListener('scroll', function () {
    div1.scrollLeft = div2.scrollLeft;
    div1.scrollTop = div2.scrollTop;
  });
}

移除页面元素的时候,我们要将两个 div 容器中的元素都移除掉

function removeChild() {
  var content = contentDiv.value;
  var child = content.lastElementChild;
  while (child) {
    content.removeChild(child);
    child = content.lastElementChild;
  }

  var signContent = signContentDiv.value;
  var child2 = signContent.lastElementChild;
  while (child2) {
    signContent.removeChild(child2);
    child2 = signContent.lastElementChild;
  }
}

渲染 PDF 页面的时候,每一个页面都会生成两个相同大小的画布,一个用来展示,一个用来签名,两个画布是重叠的。

function renderAllPages() {
  pdfData.value.promise.then((pdf) => {
    for (let i = 1; i <= pdf.numPages; i++) {
      pdf.getPage(i).then((page) => {
        // 获取DOM中为预览PDF准备好的canvasDOM对象
        let viewport = page.getViewport(scale.value);
        var canvas = document.createElement('canvas');//用来展示
        var sighCanvas = document.createElement('canvas');//用来签名
        canvas.height = viewport.height;
        canvas.width = viewport.width;
        sighCanvas.height = viewport.height;
        sighCanvas.width = viewport.width;

        let ctx = canvas.getContext('2d');
        let renderContext = {
          canvasContext: ctx,
          viewport: viewport,
        };
        page.render(renderContext).then(() => {});
        initSignatureCanvas(sighCanvas);
		
		canvass.value.push(canvas);
        signCanvass.value.push(sighCanvas);
        contentDiv.value.appendChild(canvas);
        signContentDiv.value.appendChild(sighCanvas);
      });
    }
  });
}

主要是保存 PDF 的功能与原来完全不一样。
因为我们前面说的签名画布和 PDF 页是同步生成的,所以页码(下标)也是相对应的。
所以我们只要把签名页面转成一个透明背景的 PNG ,然后添加到 PDF 对应页码的页面上,新的 PDF 文件就是我们需要的签名文件 。

const savePDF = async () => {
  const pdfDoc = await PDFDocument.load(base64.value);
  const pages = pdfDoc.getPages();
  for (let i = 0; i < pages.length; i++) {
  	//对应下标的 签名画布中的内容生成 png图片
    const eleImgCover = await pdfDoc.embedPng(signCanvass.value[i].toDataURL('image/PNG'));
    //页面中添加水印
    pages[i].drawImage(eleImgCover, {
      x: 0,
      y: 0,
      width: eleImgCover.width / scale.value, //这里要进行缩放,因为之前的画布我们是放大过的
      height: eleImgCover.height / scale.value, //这里要进行缩放,因为之前的画布我们是放大过的
    });
  }
  //生成blob流
  const pdfBytes = await pdfDoc.save();
  saveBlob(pdfBytes, 'TestPdf');
};
//网上找的 保存 bolb流 的方法
function saveBlob(data, fileName) {
  if (typeof window.navigator.msSaveBlob !== 'undefined') {
    window.navigator.msSaveBlob(
      new Blob([data], { type: 'application/pdf' }),
      fileName + '.pdf',
    );
  } else {
    let url = window.URL.createObjectURL(new Blob([data], { type: 'application/pdf' })); //定义下载的链接
    let link = document.createElement('a'); //创建一个超链接元素
    link.style.display = 'none'; //隐藏该元素
    link.href = url; //创建下载的链接
    link.setAttribute('download', fileName + '.pdf');
    document.body.appendChild(link);
    link.click(); //点击下载
    document.body.removeChild(link); //下载完成移除元素
    window.URL.revokeObjectURL(url); //释放掉blob对象
  }
}

效果展示

文字内容可以解析、能够被选中
在这里插入图片描述
在这里插入图片描述

完整代码地址

方案一
方案二

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/375842.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

JavaScript手写题

一、防抖 function debounce(fn, delay200) {let timeout null; // 定时器控制return function(...args) {if (timeout) { // 定时器存在&#xff0c;表示某个动作之前触发过了clearTimeout(timeout); // 清除定时器timeout null;} else {// 对第一次输入立即执行fn.apply…

【Leetcode 剑指Offer】第 5 天 查找算法(中等)

查找算法剑指 Offer 04. 二维数组中的查找剑指 Offer 11. 旋转数组的最小数字剑指 Offer 50. 第一个只出现一次的字符Python字典基础哈希表&#xff08;python中是dict()&#xff09;有序哈希表第一个中等&#xff0c;后两个简单题。剑指 Offer 04. 二维数组中的查找 题&#…

Node.js的研究和应用

首先我们要知道什么是node.js? Node.js采用谷歌的V8引擎&#xff0c;是一个服务器端的、非阻断式I/O的、事件驱动的 JavaScript运行环境。 一、Node.js架构具体包含什么呢&#xff1f;咱们从一个图来看看会更加明白。 natives modules&#xff1a; 1.当前层内容由js实现 。…

不使用implements关键字实现实现类(类似于mapper)

首先&#xff0c;说明一下功能需求&#xff0c;平时定义一个接口&#xff0c;就要使用implements关键字来实现接口。那么&#xff0c;当不使用此关键字的时候&#xff0c;是否也能使相关接口也能够绑定实现类呢&#xff1f; 答案是肯定的。 此篇文章的主要功能有两个&#xf…

知识类问答数据集资源对外开放:百万级百度知道、社区问答及六大领域级小规模语料概述

随着chatgpt的火热&#xff0c;中文知识类问答数据集由收到诸多关注&#xff0c;其作为高质量的QA数据&#xff0c;可以用于SFT阶段以及pretrain预训练阶段。 本文主要介绍目前开源可下载的两个较大规模的知识类数据集&#xff0c;包括147万百度知道知识类数据集、425万社区问…

IOS逆向前期环境准备笔记

ios系统由于效验问题&#xff0c;只能升级不能降级&#xff0c;需要特别注意&#xff0c; 刷系统可以在爱思上搞定&#xff1b; 越狱推荐使用u盘镜像及本地启动盘制作&#xff1a; 注意&#xff0c;要进去bios,关闭安全启动&#xff0c;不然直接失败&#xff1a; Checkra1n镜…

linux升级gcc版本详细教程

0.前言一般linux操作系统默认的gcc版本都比较低&#xff0c;例如centos7系统默认的gcc版本为4.8.5。gcc是从4.7版本开始支持C11的&#xff0c;4.8版本对C11新特性的编译支持还不够完善&#xff0c;因此如果需要更好的体验C11以及以上版本的新特性&#xff0c;需要升级gcc到一个…

九、Vben之可拖拽穿梭框和水印背景如何添加

近期在开发的过程中遇到了两个难题&#xff0c;一个是目前的穿梭框不支持产品的要求&#xff0c;不能够上下拖拽&#xff0c;二是vben没有水印的api&#xff0c;需要我们自己来开发。 一、可拖拽穿梭框 做成功的效果如下&#xff1a; 思路&#xff1a; 将table放入transfer的…

操作系统权限提升(十七)之绕过UAC提权-Windows令牌概述和令牌窃取攻击

系列文章 操作系统权限提升(十二)之绕过UAC提权-Windows UAC概述 操作系统权限提升(十三)之绕过UAC提权-MSF和CS绕过UAC提权 操作系统权限提升(十四)之绕过UAC提权-基于白名单AutoElevate绕过UAC提权 操作系统权限提升(十五)之绕过UAC提权-基于白名单DLL劫持绕过UAC提权 操作系…

android EditText设置后缀

有两种实现方案。 方案一&#xff1a;是自己写一个TextWatcher。 方案二&#xff1a;是重写TextView的getOffsetForPosition方法&#xff0c;返回一个计算好的offset。 我在工作时&#xff0c;使用的是方案一。在离职之后&#xff0c;我还是对这个问题耿耿于怀&#xff0c;所以…

git在工作中的正常使用

开发A和B功能后进行发版。。 一、拉取代码 git clone http://ntc.ntsvars.com:8090/lvweijie/test.git二、开发功能A任务 创建A任务本地分支 #创建A分支&#xff0c;并切换A分支 git checkout -b A三、开发A任务 四、提交A功能文件到本地分支 git add .五、添加提交A功能备…

Tina_Linux打包流程说明指南_new

OpenRemoved_Tina_Linux_打包流程_说明指南_new 1 概述 1.1 编写目的 介绍Allwinner 平台上打包流程。 1.2 适用范围 Allwinner 软件平台Tina v3.0 版本以上。 1.3 相关人员 适用Tina 平台的广大客户&#xff0c;想了解Tina 打包流程的开发人员。 2 固件打包简介 固件…

Jenkins+Gitlab实现代码自动构建部署

一、环境准备 主机名ip安装软件jenkins192.168.75.149jenkinsgitlab192.168.75.147gitlabweb192.168.75.155部署应用 二、jenkins服务器配置 1、生产公钥 [rootjenkins ~]# ssh-keygen &#xff08;2&#xff09;获取公钥信息 公钥信息在配置 Gitlab SSH Keys 时用到。 &am…

Tcpdump抓包验证zookeeper的心跳机制

一、背景 在分布式系统中&#xff0c;zookeeper可以作为服务注册中心&#xff0c;所有提供服务的节点都可以在zookeeper上面注册&#xff0c;并作为一个node被组织起来&#xff0c;如下图&#xff1a; 在RPC框架中&#xff0c;这些服务提供者就是RPC服务的提供者。zookeeper注…

【测试】Python手机自动化测试库uiautomator2和weditor的详细使用

1.说明 我们之前在电脑操作手机进行自动化测试&#xff0c;基本上都是通过Appium的&#xff0c;这个工具确实强大&#xff0c;搭配谷歌官方的UiAutomator基本上可以完成各种测试&#xff0c;但缺点也很明显&#xff0c;配置环境太麻烦了&#xff0c;需要jdk、sdk等&#xff0c…

利用较新版本的IDEA 2022.3.2 创建Java Web的maven项目

1.创建项目 正常三步走&#xff0c;没什么可说的 2.用模板创建项目&#xff08;重要&#xff09; 第一步&#xff0c;一定要选Jakarta EE。这个模板是基于JavaWeb的一个标准模板&#xff0c;如果选了maven中的JavaWeb模板&#xff0c;那就变成了web目录在根目录下&#xff0c;…

测试跟踪模块UX交互升级,多个X-Pack功能开放至开源版,MeterSphere开源持续测试平台v2.7.0发布

2023年2月24日&#xff0c;MeterSphere一站式开源持续测试平台正式发布v2.7.0版本。 在这一版本中&#xff0c;MeterSphere在测试跟踪模块进行了UX交互升级&#xff0c;整个页面采用轻量化设计进行整体降噪&#xff0c;页面信息更加清晰易懂&#xff0c;操作流程更顺畅&#x…

【学习笔记】深入理解JVM之类加载机制

【学习笔记】深入理解JVM之类加载机制 以后基本上都在语雀上面更新&#xff0c;大家有兴趣可以看看嗷&#xff01; 首发地址&#xff1a; 知识库 文章流程图&#xff1a; 1、概述 首先我们先来看看一个 Class 文件所需要经过的一个流程图&#xff1a; 而我们今天要重点需讲的…

如何保护阿里云、政采云等云市场三方账号安全?

什么是云市场&#xff1f;根据百度百科释义&#xff0c;云市场是指物联网中分布在不同地点的海量的商品生产者和消费者之间各种经济关系的集合体&#xff0c;是通过相对集中的云平台资源联合物联网各个感知节点信息资源的方式&#xff0c;以运行分布在不同地点的海量的经济交换…

Java——数组

目录 前言 一、数组的定义 二、数组声明和创建 三、三种初始化及内存分析 Java内存分析 三种初始化 静态初始化 动态初始化 数组的默认初始化 数组的四个基本特点 四、下标越界及小结 五、数组的使用 For-Each循环 数组作方法入参 数组作返回值 六、二维数组 七…