免费下载案例集|20+数字化领先企业人才培养实践经验 了解详情
写点什么

浏览器渲染 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:26976

评论

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

Hive 中的 GroupBy, Distinct 和 Join

tkanng

sql 大数据 hadoop hive

高仿瑞幸小程序 01 初建项目,引入Vant Weapp

曾伟@喵先森

小程序 微信小程序 大前端 vant

Kafka系列第1篇:Kafka是什么?它能干什么?

z小赵

大数据 kafka 推荐 实时计算

游戏夜读 | 2020周记(4.3-4.10)

game1night

什么是 MQ ?

itfinally

系统设计 MQ

用行动解决情绪,情绪永远是累赘

熊斌

情绪控制 团队协作

Go语言获取程序各类资源的绝对路径的方法

良少

Python 路径 动态 绿色 Go 语言

C++中glog源码剖析以及如何设计一个高效 log模块

泰伦卢

c++ 编程语言

周报 01|多点分享,少点创作

强劲九

学习 读书

这里有一个慢 SQL 查询等你来优化

程序猿石头

MySQL 数据库 性能优化 后端

爬虫(108)Python 3.8的超酷新功能(接近一万字,请耐心享用,而且建议收藏)

志学Python

python 爬虫 python3.x python升级

太极宗师与华晨宇

伯薇

水平思考力 电视剧 综艺节目 歌手

关于5G RCS的产品猜想

机器鸟

爬虫(107)Python 3.7的超酷新功能(接近一万字,请耐心享用,而且建议收藏)

志学Python

Python 最佳实践 python 爬虫 python3.7 python升级

kettle(Pentaho Data Integration) 使用"最佳"实践

稻草鸟人

Java kettle

目标:2020年学会写文章

wiflish

​成功的人,都是 “狠角色”

非著名程序员

程序员 提升认知 成功学 自律

Windows Terminal添加右键菜单

simon

Windows Terminal 右键菜单 终端 开发者工具 命令行

每日一道python面试题 - Python的函数参数传递

志学Python

Python 面试 爬虫 python 爬虫 python3.x

如何写排版优雅简洁的文章?

池建强

写作 排版

每天打卡python面试题 - 在一行中捕获多个异常(块除外)

志学Python

Python 面试 python 爬虫 python3.7

运维常见问题及排查思路

编程随想曲

运维

我愿沉迷于学习,无法自拔(二)

孙瑜

深度思考 个人成长

周日福利来了

志学Python

Python 福利 python教程 python视频教程

Kafka系列第2篇:安装测试

z小赵

大数据 kafka 推荐 实时计算

MyBatis核心功能介绍

Java收录阁

mybatis

如何优雅的接收正在运行古董代码?

冰临深渊

项目管理 架构

Flutter引擎源码解读-Flutter是如何在iOS上运行起来的

Geek_70xtik

flutter ios 移动应用 跨平台 dart

GroupBy 用法的三重境界,面试终结者

Hyun

数据库 sql 大数据 性能优化 数据分析

3NF建模&维度建模

常海峰

初步了解MyBatis

Java收录阁

mybatis

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