组合 a 标签与 canvas 实现图片资源的安全下载的方法与技巧
普通用户下载图片时只需一个「右键另存为」操作即可完成,但当我们做在线编辑器、整个 UI 都被自定义实现时,如何解决不同域问题并实现页面中图片资源的安全下载呢?本文就解决该问题过程中所涉及的正则表达式、Web API 和 canvas 操作进行记录。
本文分为以下七个部分:
- 利用 <a> 标签下载任意资源
- 解析 DOM 获取图片链接
- 分情况处理图片链接
- 工具函数中的正则表达式完善
- canvas 绘制图片资源并转 Data URLs 返回
- 实际使用与总结
- 参考资料
以下开始正文。
0. 利用 <a> 标签下载任意资源
最简单的办法,当然是利用 <a> 标签。根据 MDN 描述,<a> 标签有一个属性叫 download,此属性指示浏览器下载 URL 而不是导航到它,因此将提示用户将其保存为本地文件。如果我们再给该属性赋值,那么此值将在下载保存过程中作为预填充的文件名。
所以我们可以将需要资源链接附在一个带 download 属性的 <a> 标签上,以此实现下载的功能,例如:
<a
href="http://hijiangtao.github.io/README.md"
download="default"
>
下载 README
</a>
但需要注意的是,此属性仅适用于同源 URL,如果我们给 <a> 标签塞入一个跨域图片,那么在 chrome 中点击的效果将会是在一个页面中打开并展示这张图片,而没有下载行为。所以,面对跨域图片资源时,我们该怎么办呢?
我们都知道 <img> 加载图片资源时是不受跨域限制的,而 canvas 画布可以绘制任意图片资源,并将自身转换为 Data URLs。是的,按照这个思路,我们来一步步来解决问题。
1. 解析 DOM 获取图片链接
首先从 DOM 中找到 <img> 标签并提取图片资源链接,如果你可以通过选择器直接取到 <img> 对象,那么直接取 src 属性便可,例如:
const {src} = document.getElementById("hijiangtao");
如果你拿到的是一串 HTML 字符串,那么你将会用到如下一条正则表达式,用于匹配 <img> 标签并提取其中 src 内容:
// @Input - rawHTML
const re = /<img\s.*?src=(?:'|")([^'">]+)(?:'|")/gi;
const matchArray = re.exec(rawHTML);
const src = matchArray && matchArray[]) || '';
注:关于 <img> 标签有 <img> 和 <img /> 两种形式的讨论,本文不做讨论,详情可以移步 StackOverflow。
2. 分情况处理图片链接
拿到 src 即图片链接后我们来分情况讨论下,处理逻辑应该分这几步(本文中 Data URLs 特指 base64 形式图片 URL,以下不再额外说明):
- 同域图片或者 Data URLs 图片直接返回
- 跨域图片转 Data URLs 返回
故我们的代码应该长成这样,考虑到 img 标签完成资源下载时需要回调,我们用一个 Promise 将函数结果包住:
/**
* 获取可安全下载的图片地址
* @param src
*/
export const getDownloadSafeImgSrc = (src: string): Promise<string> => {
return new Promise(resolve => {
// 0. 无效 src 直接返回
if (!src) {
resolve(src);
} // 1. 同域或 base64 形式 src 直接返回
if (isValidDataUrl(src) || isSameOrigin(src)) {
resolve(src);
} // 2. 跨域图片转 base64 返回
getImgToBase64(src, resolve);
});
};
注:关于 base64 格式的编码和解码本文不做过多解释,Web APIs 已经有对 base64 进行编码解码的方法:,详情可移步 Base64 encoding and decoding 查看更多。
3. 工具函数中的正则表达式完善
上例中我们新增了很多处理函数,在这里我们把他们一一实现,首先来看看判断图片是否为 base64 格式的函数实现。
base64 格式是 Data URLs 的一种。Data URLs,即前缀为 data: 协议的URL,其允许内容创建者向文档中嵌入小文件。它由四个部分组成:前缀 data:、指示数据类型的MIME类型、如果非文本则为可选的base64标记、数据本身:
data:[<mediatype>][;base64],<data>
其中标记部分可选,前缀和数据必选,MIME 我们后文再继续介绍。那么,知道了 Data URLs 的组成,我们便可以把判断 URL 是否为有效 Data URLs 的正则匹配方法写成这样:
/**
* 判断给定 URL 是否为 Data URLs
* @param s
*/
export const isValidDataUrl = (s: string): boolean => {
const rg = /^\s*data:([a-z]+\/[a-z0--+.]+(;[a-z-]+=[a-z0--]+)?)?(;base64)?,([a-z0-!$&',()*+;=\-._~:@\/?%\s]*?)\s*$/i;
return rg.test(s);
};
关于跨域问题,我在文章《前端跨域请求解决方案汇总》中已有更详细的说明,这里我们直接用一个不够完美但基本可用的字符串方法来解决跨域判断:
/**
* 判断给定 URL 是否与当前页面同源
* @param s
*/
export const isSameOrigin = (s: string): boolean => {
return s.includes(location.origin)
}
这里我们再来说说 MIME,这个在我们完善 canvas 转 Data URLs 方法时会用上。MIME,全称 Multipurpose Internet Mail Extensions,我们通常说的 MIME 类型也称为媒体类型,它是一种用来表示文档、文件或字节流的性质和格式的标准。
对于图片资源来说,Web 页面中广泛支持的 MIME 类型包含以下几种:
| MIME 类型 | 图片类型 |
|---|---|
| image/gif | GIF 图片 (无损耗压缩方面被PNG所替代) |
| image/jpeg | JPEG 图片 |
| image/png | PNG 图片 |
| image/svg+xml | SVG图片 (矢量图) |
如果不考虑 webp 以及 icon 等格式,我们想要从一个资源 URL 中提取出 MIME 格式便可以这样做:
/**
* 根据资源链接地址获取 MIME 类型
* 默认返回 'image/png'
* @param src
*/
export const getImgMIMEType = (src: string): string => {
const PNG_MIME = 'image/png'; // 找到文件后缀
let type = src.replace(/.+\./, '').toLowerCase(); // 处理特殊各种对应 MIME 关系
type = type.replace(/jpg/i, 'jpeg').replace(/svg/i, 'svg+xml'); if (!type) {
return PNG_MIME;
} else {
const matchedFix = type.match(/png|jpeg|bmp|gif|svg\+xml/);
return matchedFix ? `image/${matchedFix[]}` : PNG_MIME;
}
};
启用了 CORS 的图片了解更多。
注2: 由于编码格式有所差别,Blob URL 比起 Data URLs 所占的空间资源更少,性能也更好。经网友指明,Blob URL 性能会好于 Data Urls,感兴趣的话可以尝试。
5. 实际使用与总结
以 Angular 为例,我们的 HTML 代码可能要增加这么一段:
<a
*ngIf="downloadImageUrl"
href=""
download="image"
class="context-menu-link"
>
保存图片至本地
</a>
而对于 TypeScript 脚本,除了引入 getDownloadSafeImgSrc 实现外,我们需要在某一个流更新所通知到的方法中增加如下引用:
import { getDownloadSafeImgSrc } from './utils.ts';
// ...
// 某一个流更新所通知到的方法
function updateDownloadImgState(editors: any[]) {
// 假设 editors 里面存有各类选中的 DOM HTML
const rawHTML = editors.getSelectionInnerHTML();
const re = /<img\s.*?src=(?:'|")([^'">]+)(?:'|")/gi;
const matchArray = re.exec(rawHTML);
this.downloadImageUrl = await getDownloadSafeImgSrc((matchArray && matchArray[]) || '');
}
至此,不论图片资源是否跨域,我们都可以利用 <a> + canvas 的方式将其安全地下载下来,并保留图片的原始格式。这其中涉及不少 Web API 与概念,包含 canvas, <a> download 属性, Data URLs, MIME 以及人见人爱的正则表达式,这些都是可以细细探究的方面,欢迎深入学习。
组合 a 标签与 canvas 实现图片资源的安全下载的方法与技巧的更多相关文章
- a标签点击不跳转的几种方法
a标签点击不跳转的几种方法 1.onclick事件中返回false <a href="http://www.baidu.com" onclick="return f ...
- HTML5 Canvas中绘制椭圆的几种方法
1.canvas自带的绘制椭圆的方法 ellipse(x, y, radiusX, radiusY, rotation, startAngle, endAngle, anticlockwise)是后来 ...
- PHP《将画布(canvas)图像保存成本地图片的方法》
用PHP将网页上的Canvas图像保存到服务器上的方法 2014年6月27日 歪脖骇客 发表回复 8 在几年前HTML5还没有流行的时候,我们的项目经理曾经向我提出这样一个需求:让项目评审专家们在评审 ...
- 转载:将画布(canvas)图像保存成本地图片的方法
之前我曾介绍过如何将HTML5画布(canvas)内容转变成图片形式,方法十分简单.但后来我发现只将canvas内容转变成图片输出还不够,如何能将转变后的图片保存到本地呢? 其实,这个方法也是非常简单 ...
- HTML5<canvas>标签:使用canvas元素在网页上绘制线条和圆(1)
什么是 Canvas? HTML5 的 canvas 元素使用 JavaScript 在网页上绘制图像. 画布是一个矩形区域,您可以控制其每一像素. canvas 拥有多种绘制路径.矩形.圆形.字符以 ...
- HTML5中video标签与canvas绘图的使用
video标签的使用 video标签定义视频, 它是html5中的新标签, 它的属性如下(参考自文档): domo01 <!DOCTYPE html> <html lang=&quo ...
- HTML5<canvas>标签:使用canvas元素在网页上绘制四分之一圆(3)
前几天自己做了个四分之一的圆,放到手机里面测试.效果不是很好.于是今天通过查资料,找到了canvas.自己研究了一天,发现可以使用canvas画圆.代码如下: <!doctype html> ...
- HTML5<canvas>标签:使用canvas元素在网页上绘制渐变和图像(2)
详细解释HTML5 Canvas中渐进填充的参数设置与使用,Canvas中透明度的设置与使用,结合渐进填充与透明度支持,实现图像的Mask效果. 一:渐进填充(Gradient Fill) Canva ...
- js动态新增组合Input标签
var x = 1; function addlink() { var linkdiv = document.getElementById("add1_0"); if (linkd ...
随机推荐
- java实现串中找数字
串中找数字 以下的静态方法实现了:把串s中第一个出现的数字的值返回. 如果找不到数字,返回-1 例如: s = "abc24us43" 则返回2 s = "82445ad ...
- Java实现第八届蓝桥杯魔方状态
魔方状态 题目描述 二阶魔方就是只有2层的魔方,只由8个小块组成. 如图p1.png所示. 小明很淘气,他只喜欢3种颜色,所有把家里的二阶魔方重新涂了颜色,如下: 前面:橙色 右面:绿色 上面:黄色 ...
- java代码(15) ---java8 Function 、Consumer 、Supplier
Java8 Function.Consumer.Supplier 有关JDK8新特性之前还有三篇博客: 1,java代码(1)---Java8 Lambda 2,java代码(2)---Java8 S ...
- cocos2dx Android 使用ant 批量打包
参考文章: 例子:http://www.2cto.com/kf/201305/208139.html http://blog.csdn.net/ljb_blog/article/details/127 ...
- 简谈Java语言的封装
简谈Java语言的封装 封装的定义 封装将复杂模块或系统的逻辑实现细节隐藏,让使用者只需要关心这个模块或系统怎么使用,而不用关心这个模块或系统是怎么实现的. 在面向对象的的编程中,我们一般通过接口来描 ...
- x memory pool c语言 内存池
#ifndef X_MEMORY_H #define X_MEMORY_H #include <stdlib.h> #include <stdio.h> #include &l ...
- @loj - 3120@ 「CTS2019 | CTSC2019」珍珠
目录 @description@ @solution@ @accepted code@ @details@ @description@ 有 \(n\) 个在范围 \([1, D]\) 内的整数均匀随机 ...
- Redis的持久化设计
Redis 持久化设计 持久化的功能:Redis是内存数据库,数据都是存储在内存中的,为了避免进程退出导致数据的永久丢失,要定期将Redis中的数据以某种形式从内存保存到硬盘,当下次Reids重启时, ...
- C Primer Plus(三)
重读C Primer Plus ,查漏补缺 重读C Primer Plus,记录遗漏的.未掌握的.不清楚的知识点 文件输入/输出 1.fgets函数在读取文件内容时会将换行符读入,但gets不会,fp ...
- python第三方库大全
环境管理 管理 Python 版本和环境的工具 p:非常简单的交互式 python 版本管理工具.官网 pyenv:简单的 Python 版本管理工具.官网 Vex:可以在虚拟环境中执行命令.官网 v ...