组合 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实现 洛谷 P1217 [USACO1.5]回文质数 Prime Palindromes
import java.util.Scanner; public class Main { private static Scanner cin; public static void main(St ...
- Java实现 蓝桥杯 算法提高最小方差生成树
1 问题描述 给定带权无向图,求出一颗方差最小的生成树. 输入格式 输入多组测试数据.第一行为N,M,依次是点数和边数.接下来M行,每行三个整数U,V,W,代表连接U,V的边,和权值W.保证图连通.n ...
- vue + elementUI开发,使用el-tabs,导致浏览器卡死问题。
第一次自己建项目,用过el-tabs,当时是正常使用的. 贴下版本信息: "element-ui": "^2.13.0", "js-md5" ...
- Promise相关学习
what? Promise对象表示一个异步操作的最终状态(完成或失败),以及其返回的值 https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/ ...
- centos7 hadoop 单机模式安装配置
前言 由于现在要用spark,而学习spark会和hdfs和hive打交道,之前在公司服务器配的分布式集群,离开公司之后,自己就不能用了,后来用ambari搭的三台虚拟机的集群太卡了,所以就上网查了一 ...
- (四)POI-设置单元格的对其方式
原文链接:https://blog.csdn.net/class157/article/details/92817149 package com.java.poi; import org.apache ...
- 如何解压bz2后缀的压缩文件
.bz2 解压1:bzip2 -d FileName.bz2 解压2:bunzip2 FileName.bz2 压缩: bzip2 -z FileName .tar.bz2 解压:tar jxvf F ...
- Android学习笔记.9.png格式图片
.9.png可以保证图片在合适的位置进行局部拉伸,避免了图片全局缩放造成的图片变形问题.AS提供了制作点9图片的便捷入口,并且会检查你的.9图是否有不合理的拉伸区域. 选中图片点击create 9-p ...
- demo项目开发(Python+flask+mysql+redis只包含后端接口)
[demo项目开发需求] 用户信息管理,可以注册.登录.添加用户.删除用户 注册:任何用户可以注册,对用户提交的注册信息进行校验,返回对应的信息,其中: 用户名:必填,唯一 密码:必填,只能6-12位 ...
- 八张图彻底了解JDK8 GC调优秘籍-附PDF下载
目录 简介 分代垃圾回收器的内存结构 JDK8中可用的GC 打印GC信息 内存调整参数 Thread配置 通用GC参数 CMS GC G1参数 总结 简介 JVM的参数有很多很多,根据我的统计JDK8 ...