【QCon】精华内容上线92%,全面覆盖“人工智能+”的典型案例!>>> 了解详情
写点什么

浏览器渲染 PDF 文件成图引发的故事

  • 2019-09-25
  • 本文字数:4888 字

    阅读完需:约 16 分钟

浏览器渲染PDF文件成图引发的故事

1 起因

某天从产品那里接到需求,要把 pdf 文件在移动端展示并保存在手机相册里。保存到手机相册中,那就要把 pdf 文件变成图片。网上搜索一下,Mozilla 的 pdf.js 库刚好可以。于是开始看官方文档,但文档都是由源代码的注释生成的,也就看到一部分源代码。pdf.js 源码里有一个重要的方法 getDocument,在这个方法上的注释写到:


@param{string|TypedArray|DocumentInitParameters|PDFDataRangeTransport} src Can be a url to where a PDF is located, a typed array (Uint8Array)

already populated with data or parameter object.


可以是 URL 也可以是 Uint8Array?Uint8Array 是什么?正是这里的 Uint8Array 才有接下来关于 Uint8Array(TypedArray)的一系列知识点的学习。

2 历史

JavaScript 在设计时只是简单运行于网页的脚本语言,远没有预想到会发展到今时今日的地步。起初 JavaScript 在业务场景中不会处理到复杂的数据和交互逻辑,但随着互联网的发展,网页已经不仅仅局限简单的文字图片展示等基础功能,逐步需要囊括视频播放、音频播放、在线绘画等功能。在此业务场景下,JavaScript 亟需更多能力以处理音视频等二进制数据。但 JavaScript 的基本类型 Boolean、Function、String 等都无法处理音视频等二进制数据(早期由 Flash 插件代为处理),也不存在某个对象拥有处理二进制流数据的能力。随着时间推移,Node 的出现让 JavaScript 第一次能够处理文件(二进制流数据)的能力——Buffer。但 Node 依旧只能让运行在 server 端的 JavaScript 具有处理二进制数据的能力,client 端依旧无法处理二进制数据。直到 2015 年的新规范 ES2015(ES6)中,才定义了具有处理二进制数据能力的对象 TypedArray。先来看看早于 TypedArray 出现的 Buffer 是什么:

2.1 Buffer

在 Node 文档中描写 Buffer 道:


在引入 TypedArray 之前,JavaScript 并没有读取或者操作流或二进制数据数据的机制。而 Buffer 正是因此被引入 Node.js API 中,使得 JavaScript 能够介入 TCP 字节流、文件操作系统和其他场景并能处理其中的内容。随着 TypedArray 的普及,Buffer 的地位变成更优化和更适合的 Node 端 Uint8Array 类型的 TypedArray 实现。


显然 Buffer 是为在 HTTP 和文件系统的场景下给予 JavaScript 处理数据的能力而诞生的,并且概念上 Buffer 属于 TypedArray。那么 Buffer 是如何读取和操作数据内容的呢?


Buffer 提供以下几个 API:


  • 创建缓存区:Buffer.from、Buffer.alloc;(废弃 new Buffer()确保新建的 Buffer 实例的内容不会包含敏感数据)

  • Buffer.concat 像数组似的链接两个 Buffer;

  • Buffer.compare 对比两个缓存区。


下面是关于 Node 中转文件的简单例子:


Node 文件中转-Buffer


 1async function nodeBuffer(){ 2        let query = ctx.request.query; 3        let downloadPath = query.path; 4        let instance = axios.create({ 5            headers: { 6                'content-type': 'application/octet-stream' 7            }, 8        }) 9        delete query.path;10        await instance.get(downloadPath, query)11            .then(res => {12                ctx.attachment(query.name)13                ctx.set('Encoding','binary');  \14                ctx.set('Content-Type', 'application/octet-stream');15                ctx.set("content-disposition", `attachment;filename=${query.name}`);16                ctx.body = Buffer.from(res.data, 'binary');17                ctx.status = 200;18            })19            .catch(res => {20                console.log("catch:", res)21            })22}
复制代码

2.2 TypedArray

ES6 规范尚未出世前,Buffer 仅仅是 server 端的一种实现。而 TypedArray 才是 JavaScript 语言真正的‘Buffer’。在 MDN 文档中描述 TypedArray 道:


TypedArray 是描述底层二进制数据缓存的一种类似数组的视图。在全局环境下并没有叫做 TypedArray 的对象,也没有叫做 TypedArray 的构造函数。相反,全局环境下有许多不同的对象,他们的 value 正是由针对特定类型的类型化数组构造函数所创建的。在接下来的描述中,你会发现有一些属性、方法在任何类型上都能使用。


从上述文字不难发现以下两点:


  • TypedArray 对象无法在全局环境中获取,应理解为全局环境下存在几种不同的 TypedArray 实例;

  • TypedArray 是在表现上一种类似数组的存在,并是用于描述二进制数据缓存区的内容的视图。


TypedArray 有多种实现,但不存在基类?听上去总有点不可能,继续翻阅 MDN 文档关于 TypedArray 的内容在一段描述中道:


当创建一个 TypedArray 实例(例如:Int8Array)时,一个数组缓冲区将被创建在内存中,如果 ArrayBuffer 对象被当作参数传给构造函数将使用传入的 ArrayBuffer 代替。缓冲区的地址被存储在实例的内部属性中,所有的 %TypedArray%.prototype 上的方法例如 set value 和 get value 等都会操作在数组缓冲区上。


从描述中能知道全局环境中并不是不存在 TypedArray,而是 TypedArray 被设置为无法通过 JavaScript 直接访问。如果 TypedArray 只是视图,那真正存储数据的究竟是什么?在 MDN 文档上关于 ArrayBuffer 的定义描述道:


ArrayBuffer 对象被用以表示通用的固定长度的原始二进制数据缓存区。你不能直接操作一个 ArrayBuffer 的内容。你应该创建一个类型化数组或是 DataView 对象,类型化数组和 DataView 会将缓冲区中的数据格式化为特定格式,来读取和写入 Buffer 的内容。


答案已经不言而喻,TypedArray 只是用以操作 ArrayBuffer 的对象或者说视图,而拥有二进制数据的则是 ArrayBuffer。到这里相信关于 TypedArray、ArrayBuffer、Buffer 的概念和关系已经有基本雏形,他们的关系总结如下图所示:



上图九种 TypedArray 中最常见和常用的是 Uint8Array 类型,正好由无符号的八位二进制数表示一个字节。下例是关于 Uinit8Array 与 base64 字符串的相互转换:


2.2.1 StringToTypedArray


1    function StringToTypedArray(str){2            let len  = str.length;3            let ab = new Uint8Array(len);4            for(let i =0 ;i < len ;i++){5                ab[i] = str.charCodeAt(i);6            }7            //返回buffer的引用,buffer为只读属性8            return ab.buffer 9        }
复制代码


2.2.2 TypedArrayToString


1    function TypedArrayToString(buffer){2            let ab = Uint8Array.from(buffer);3            let res = '';4            for(let i = 0; i<ab.length; i++){5                //fromCharCode通过一串Unicode创建字符串6                res += String.fromCharCode(ab[i]) 7            } 8            return res9        }
复制代码


上述两例的成功转换基于 String 都是相同的编码类型(UTF-8、UTF-16 等)。


More:JavaScript 能从 canvas.toDataURL()、FileReader.readAsDataURL()、window.btoa()等方法获取到 base64 字符串;通过 FileReader.readAsArrayBuffer()和 XHR 请求设置 XHR 属性 ResponseType 为 ArrayBuffer 等方法获取到 ArrayBuffer 类型数据。


2.2.3 实际运用于 pdf.js


pdf.js 的 getDocument 方法明确指出接受两种类型的参数:一是 URL 并且返回文件流,二是 TypedArray(Uint8Array)类型数据。这时假如后端的有个接口(URL:’/somepath/to/file’),接口在成功时返回二进制文件流在失败时返回 json 字符串,如下:


1//成功2...Binary Data3//失败4{5    errno:10004,6    error:'错误信息'7}
复制代码


那对于渲染 pdf 文件可以这样做:


 1ajax.get({ 2    url: '/somepath/to/file', 3    data:{ 4        //...some data 5    }, 6    responseType: 'arraybuffer', 7    success:(res)=>{ 8        pdf.getDocument(res) 9            .then()10            .catch()11    },12    error:(err)=>{13        toast(err.error)14    }15})
复制代码


在后端兼容’arraybuffer’类型的返回值时(通常是兼容的),我们将直接得到二进制数据的 ArrayBuffer 类型作为返回,不再需要从字符串转为 ArrayBuffer。值得注意的是失败时也会返回 ArrayBuffer 类型,此处笔者与后端约定此接口失败时将置 HTTP CODE 为 403,这样不需要再将 ArrayBuffer 转换为对象来判断接口请求成功与否。

2.3 Blob

在 HTML5 中,关于 web 应用对于文件的操作引入相应的 API,包括使用 type=file 的元素和 FileReader API。MDN 中描述 FileReader 道:


FileReader 对象允许 Web 程序异步读取用户计算机上的文件或缓冲区内容,FileReader 操作的对象是 Blob 以及继承于 Blob 的 File 对象。


其中 Blob 全称是 Binary Large Object:二进制类型的大对象。显然 Blob 也是关于操作二进制数据的对象,MDN 中描述道:


Blob 对象表示一个不可变、原始数据的类文件对象。Blob 表示的不一定是 JavaScript 原生格式的数据。File 接口基于 Blob,继承 Blob 的功能并将其扩展使其支持用户系统上的文件。


从上述内容中我们能知道以下四点:


  • 不可变(immutable);

  • 原始数据(raw data);

  • 数据不需要是 JavaScript 原生格式;

  • File 继承于 Blob。


Blob 只能通过构造函数生成,语法为 new Blob(array , options)。其中 array 是由 ArrayBuffer、ArrayBufferView(TypedArray 和 DataView)、Blob、DOMString 等对象构成的数组。options 包括 type 和 endings,type 表示文件的 MIME 类型,endings 代表结束符\n 如何被写入。显然 Blob 与 TypedArray 等关系如下图所示:



Blob 对象包含两个只读属性(size 和 type)以及一个方法 slice([start,[end,[contentType]]])。十分符合 Blob 的定义,不可变的原始数据的类文件对象。其中由 Blob 所延伸出的的 Blob URL 在实际场景中常会用到。


Blob URL: Blob URLs 是 W3C 的官方名称,在实际使用时常称为 Object-URLs。Blob URLs 只能通过 URL.createObjectURL 方法暂时生成在浏览器中,此方法将生成一个指向于 Blob 和 File 的对象的 URL,并能通过 URL.revokeObjectURL 方法释放,在页面关闭时被销毁掉并只存在于当前 session 中。


对比 Data URLs: Blob URLs 只是暂时的协议允许 Blob 和 File 对象能够利用 URL 以资源的形式被使用在图片(img 标签)、二进制数据下载链接等,而不需通过上传文件到 server 端后返回 URL 资源。而 Data URLs 定义为 data:[][;base64],,在 JavaScript 中为二进制数据需要编码为 Base64 字符串,而一个字符需要占用两字节,使得原始纯二进制数据的 Blob URLs 相比于 Data URLs 更小更快。


解决的问题: 在面对需要保存 canvas 为图片的需求中,可以通过 canvas.toDataURL 方法转数据为 base64 编码并用以下载和展示。但在面对长图时,过长的 base64 编码将消耗过多的内存,并且由于客户端能力不尽相同使得在客户端有内存溢出风险甚至引起浏览器崩溃。但 canvas 同时可以使用 canvas.toBlob 方法获得原始二进制数据的 Blob 对象,并通过 URL.createObjectURL 方法获取暂时的 URL 资源,相比于 base64 消耗更低的内存风险也更小。


下面是关于生成 Blob URLs 并下载的简单示例:


1canvas.toBlob((blob)=>{2    let url = URL.createObjectURL(blob);3    let a = documen.createElement('a');4    a.href = url;5    a.download = 'test.test';6    document.body.appendChild();7    a.click();8},'image/png',1);
复制代码


下面是通过 FileReader 读取 Blob 的简单示例:


1canvas.toBlob((blob)=>{2    let fr = new FileReader();3    fr.addEventListener('load' , (event) => {4        //dosomething to event.target.result5    })6    // readAsArrayBuffer、readAsBinaryString、readAsText7    fr.readAsDataURL(blob); 8},'image/png',1);关系总结图
复制代码

3 总结图

1)关系总结图



2)Blob 与 TyepdArray



作者介绍:


金闪闪(企业代号名),目前负责贝壳找房装修事业部前端开发工作。


本文转载自公众号贝壳产品技术(ID:gh_9afeb423f390)。


原文链接:


https://mp.weixin.qq.com/s/9o9ReXuHL8KW_IrFRoZfOg


2019-09-25 23:26863

评论

发布
暂无评论
发现更多内容

12 Prometheus之监控Kubernetes

穿过生命散发芬芳

Prometheus 1月月更

激发数字化时代权益运营新动能

鲸品堂

运营 数字化 通信运营商

Go 分布式令牌桶限流 + 兜底策略

万俊峰Kevin

Go 微服务 限流 Go 语言 令牌桶

TDengine在中节能风力发电运维系统中的落地实践

TDengine

数据库 大数据 tdengine 物联网

白话大数据 | 元宇宙来了,但是你了解元数据吗?

星环科技

元数据

一篇域名从购买到备案到解析的详细教程

冴羽

前端 vuepress 域名解析 域名 域名备案

Sophon联邦学习,让数据发挥真正的价值

星环科技

联邦学习

AI+CFD:面向空天动力的科学机器学习新方法与新范式

百度开发者中心

飞桨

便捷、高效、智能—从运维视角看星环科技大数据基础平台TDH

星环科技

大数据

反直觉投资--《香帅中国财富报告》摘录(4/100)

hackstoic

投资

热门链游GameFi项目Radio Caca 联手Hoo虎符撒空投

区块链前沿News

Hoo 虎符交易所 链游 RACA

multi-key索引和wildCard索引场景比较

MongoDB中文社区

mongodb

Web or Native 谁才是元宇宙的未来(上)?

Orillusion

开源 WebGL 元宇宙 Metaverse webgpu

Golang:定时器的终止与重置

CRMEB

Avaya大中华区CTO李农:阿里云计算巢加快Avaya应用云部署

阿里云弹性计算

弹性计算 年度峰会 计算巢

Form 表单在数栈的应用(上): 校验篇

袋鼠云数栈

大数据 前端

Sql server之sql注入篇

喀拉峻

网络安全 信息安全 SQL注入

MongoDB find getmore操作慢问题排查

MongoDB中文社区

mongodb

Hoo虎符研究院 | 币海寻珠——波卡平行链Moonbeam生态一览

区块链前沿News

虎符交易所 波卡

基于javaweb,springboot银行管理系统

叫练

银行 毕业设计 计算机毕业设计

sql审核-避免离线sql导致的db集群故障

名白

sql db sql检测 SQL审核 SQL审批

政法委社会治安防控平台建设,重点人员联防联控系统开发

a13823115807

【量化投资入门】带你通过恒有数数据接口来实战量化指标

恒生LIGHT云社区

量化策略 量化投资 量化交易 量化

【量化】量化交易入门系列5:量化交易学习书籍推荐(一)

恒生LIGHT云社区

金融科技 量化策略 量化投资 量化交易 量化

文末有惊喜| 青藤ATT&CK论文入选国家级期刊

青藤云安全

聊聊Netty那些事儿之从内核角度看IO模型

bin的技术小屋

Netty nio Linux Kenel Linux网络编程 #java

netty系列之:让TCP连接快一点,再快一点

程序那些事

Java Netty 程序那些事 1月月更

CODING 携手 Thoughtworks 助力老百姓大药房打造“自治、自决、自动”的敏捷文化

CODING DevOps

敏捷 thoughtworks CODING 项目协同 老百姓大药房

x86架构应用如何向Arm架构低成本迁移

北京好雨科技有限公司

如何帮助金融客户“用好云”?

金融 混合云 数智化

如何保证redis与数据库一致性

zdd

redis

浏览器渲染PDF文件成图引发的故事_文化 & 方法_金闪闪_InfoQ精选文章