要点:
- 图片的大小由分辨率(长像素点*宽像素点)和位深度(由生成的图片是png/jpeg)决定(图像数据量大小=图像中的像素总数×图像深度 ÷ 8 ÷ 1024),图像深度决定图像能够显示的颜色种类。
- jpg是有损压缩格式,png是无损压缩格式。同一张图片,jpg的大小比png格式的大小小1.5倍以及以上。jpeg使用的一种失真压缩标准方法;PNG格式是无损数据压缩方法。
- canvas.toDataURL(type, encoderOptions);
- type 参数是决定压缩的类型,默认为 PNG 格式。图片的分辨率为96dpi。
- type的值非“image/png”,但是返回的值以“data:image/png”开头,那么该传入的类型是不支持的。
- type一般为 image/jpeg 、image/png 、image/webp,当type是image/png时,压缩质量属性设置不生效。
- encoderOptions是压缩质量只有type为 image/jpeg 或 image/webp才生效
let base64 = canvas.toDataURL('image/jpeg', 0.5) // 压缩重点'image/jpeg',可以得到大小小得多得图片
//type为image/png时,图片保留原始数据,为无损压缩,图片大小偏大,并且设置图片质量属性encoderOptions会不生效。
方案:
-
1.用canvas在同等比例的情况下改变图像的分辨率,同等比例可以确保图片不被裁切掉。
-
2.用canvas压缩成jpeg格式,超过固定大小,则循环减少质量属性encoderOptions。
-
难点:有可能会有画布污染的问题,由资源的同源策略引起的报错,解决:
//1.在html中的video上写crossorigin = "anonymous"
<canvas style="display: none"></canvas>
<video id="myvideo" controls crossorigin = "anonymous" preload="none" class="video-js vjs-default-skin vjs-big-play-centered" width="600" (play)="playAudio()" (pause)="pauseAudio()" (seeked)="seeking()" (seeking)="seeking()">
<source type="video/mp4">
</video>
<audio [src]="appAudioUrl" class="audio_box" id="app_audio"></audio>
<audio [src]="webAudioUrl" class="audio_box" id="web_audio"></audio>
//2.在js中 video.crossOrigin = "anonymous"
let video = document.getElementsByTagName("video")[document.getElementsByTagName("video").length - 1 ]; // 取视频暂停时得那一帧,也就是看到的那一帧
// let video = document.getElementsByTagName("video")[0]; // 取视频的第一帧
// console.log("视频", document.getElementsByTagName("video"));
video.crossOrigin = "anonymous";
// 3.有可能仍然不生效
解决1:服务端的请求头要设置允许跨域访问(也就是get的视频资源文件要允许跨域访问)
解决2:视频不要引入线上地址,仍然使用本地视频地址,即可解决跨域问。
完整案例
描述:视频截图后,要压缩图片大小在200-250kb之间,并且要打水印后(水印要加阴影),再上传。
takePicture(spacing: number = 0, isRecursion: boolean = false) {
this.canTake = true;
try {
let task = this.type == "current" ? this.currentTask : this.historyTask;
let currentTime = <HTMLMediaElement>(
document.getElementById("myvideo").firstChild["currentTime"]
);
this.cutTime = this.getCutTime(currentTime);
// console.log(new Date(this.cutTime));
const currentUser = JSON.parse(sessionStorage.getItem("currentUser"));
this.store.dispatch(StatusActions.setButtonDisabled(true));
const localId = task.id;
let registNo = task.registNo;
let caseId = this.currentChat.caseId;
const customerId = this.currentChat.customerId;
// let video = document.getElementsByTagName('video')[0];
let video = document.getElementsByTagName("video")[
document.getElementsByTagName("video").length - 1
]; // 取视频的最后一帧
// console.log("视频", document.getElementsByTagName("video"));
video.crossOrigin = "anonymous"; // 避免出错画布被污染的错误
let canvas = document.querySelectorAll("canvas")[0];
// let canvas1=new fabric.Canvas('canvas');
let ctx = canvas.getContext("2d");
//目标尺寸
const videoWidths = video.offsetWidth; // 视频播放尺寸
const videoHeights = video.offsetHeight;
let maxWidth: number, maxHeight: number;
if (isRecursion) {
// 判断是否是递归
// 如果是递归
this.maxWidth = this.maxWidth - spacing;
this.maxHeight = this.maxWidth - spacing;
} else {
// 如果不是递归,则是初次截图
this.maxWidth = 620;
this.maxHeight = 540;
}
maxHeight = this.maxHeight;
maxWidth = this.maxWidth;
let targetWidth = videoWidths,
targetHeight = videoHeights,
rate = 1;
// 等比缩小
// console.log("最大宽度", maxWidth, videoWidths);
if (videoWidths > maxWidth || videoHeights > maxHeight) {
//如果原始尺寸大于了设定的最大尺寸
if (videoWidths / videoHeights > maxWidth / maxHeight) {
//图片原本的宽高比例大于了设定的宽高比例
//大于规定的比例 证明 原始宽度大于高度 -》所以按照高度除以宽度的比例去缩放高度
targetWidth = maxWidth;
targetHeight = Math.round(maxWidth * (videoHeights / videoWidths));
rate = maxWidth / videoWidths;
} else {
//小于则表明 原始高度大于原始宽度 -》所以按照宽度除以高度的比例去缩放宽度
targetHeight = maxHeight;
targetWidth = Math.round(maxHeight * (videoWidths / videoHeights));
rate = maxHeight / videoHeights;
}
}
canvas.width = targetWidth;
canvas.height = targetHeight;
// console.log("宽:", targetWidth);
// console.log("高:", targetHeight);
// console.log("比率:", rate);
// canvas.width = 960;
// canvas.height = 720;
// 清除画布
ctx.clearRect(0, 0, targetWidth, targetHeight);
// 图片压缩
ctx.drawImage(video, 0, 0, targetWidth, targetHeight); // 将video中的数据绘制到canvas里
// ctx.drawImage(video, 0, 0, 850, 645);
ctx.font = this.canvasFontsize;
ctx.fillStyle = "#e34646";
ctx.textAlign = "left";
ctx.textBaseline = "middle";
ctx.shadowColor = "#fff";
ctx.shadowOffsetX = 0.5;
ctx.shadowOffsetY = 0.5;
ctx.shadowBlur = 1;
ctx.fillText(
`工号:${currentUser.account}`,
targetWidth * 0.02,
targetHeight * 0.76
);
ctx.shadowColor = "#fff";
ctx.shadowOffsetX = 0.5;
ctx.shadowOffsetY = 0.5;
ctx.shadowBlur = 1;
ctx.fillText(
`名字:${currentUser.realname}`,
targetWidth * 0.02,
targetHeight * 0.8
);
ctx.shadowColor = "#fff";
ctx.shadowOffsetX = 0.5;
ctx.shadowOffsetY = 0.5;
ctx.shadowBlur = 1;
ctx.fillText(`${registNo}`, targetWidth * 0.02, targetHeight * 0.84);
ctx.shadowColor = "#fff";
ctx.shadowOffsetX = 0.5;
ctx.shadowOffsetY = 0.5;
ctx.shadowBlur = 1;
ctx.fillText(
`${this.timeService.gmt2display(new Date(this.cutTime))}`,
targetWidth * 0.02,
targetHeight * 0.88
);
if (task.selfRecordingAddress) {
ctx.shadowColor = "#fff";
ctx.shadowOffsetX = 0.5;
ctx.shadowOffsetY = 0.5;
ctx.shadowBlur = 1;
ctx.fillText(
`位置:${task.selfRecordingAddress}`,
targetWidth * 0.02,
targetHeight * 0.92
);
}
let image: any = canvas.toDataURL("image/png"); // 可以控制压缩质量
image = this.convertBase64UrlToBlob(image);
let params = new FormData();
let imgSize = image.size / 1024;
console.log("图片大小:", imgSize);
if (imgSize > 250) {
// 如果大于250kb
this.takePicture(10, true); // 重新压缩
return;
} else if (targetWidth < 480) {
// 防止字体超出图片
if (imgSize < 200) {
// 如果图片比较小
this.takePicture(-10, true); // 增加分辨率
} else {
// 改变字体大小
this.canvasFontsize = "90% 黑体";
}
return;
}
params.append("file", image, "taskPicter.png");
this.apiService.postUploadAsync(params).then((res) => {
let data = {
path: res.path || "",
caseId: localId || "",
categoryId: this.authService.isAllGroupPropertyAuth() ? 26 : 1,
screenshotedOn: new Date(this.cutTime),
};
this.apiService.postUploadsAsync(data).then((res2) => {
if (res2 && res2.attachmentPath) {
this.langService.pop(
"success",
{ key: "提示" },
{ key: "截图成功" }
);
this.canTake = false;
let list: any = [];
list.push(res2);
const picture = _.mapKeys(list, function (value, key) {
return _.camelCase(key);
});
picture.uploadFlag = this.apiService.PICTURE_UPLOAD_FLAGS[0]; // 照片默认状态
picture.attachmentPath = this.apiService.fixFileUrl(
picture[0].attachmentPath
);
this.store.dispatch(PictureActions.addCollectPicture(picture, 1));
this.readState();
this.store.dispatch(StatusActions.setIsNewPicture(true));
}
this.store.dispatch(StatusActions.setButtonDisabled(false));
});
});
} catch (error) {
console.error(error);
this.canTake = false;
this.langService.pop("error", { key: "提示" }, { key: "截图失败" });
}
}
canvas知识点
- 是h5标签,是一个图形容器,需要通过脚本来绘制图形。默认情况下 元素没有边框和内容,并且 只有两个可选的属性 width、heigth 属性,而没有 src、alt 属性。
- 会创建一个固定大小的画布,会公开一个或多个渲染上下文,使用渲染上下文来绘制和处理要展示的内容。渲染上下文可以理解为图层。
var canvas = document.getElementById('canvas');
//获得 2d的渲染上下文(是一个对象)
var ctx = canvas.getContext('2d'); // 获取图层
- canvas坐标:画布的左上角为原点,向下是Y轴正轴,向右是x轴正轴。
1.canvas绘制矩形的函数
// 绘制一个填充的矩形。(有背景颜色的矩形,默认黑色)
fillRect(x, y, width, height)
// 绘制一个矩形的边框。(只有线条)
strokeRect(x, y, width, height)
// 清除指定的矩形区域,使其完全透明。
clearRect(x, y, width, height)
x与y是矩形在canvas画布上距离原点的坐标。
width和height是设置矩形的尺寸。
2.canvas绘制路径(轮廓)的函数
- 使用路径绘制图形步骤
- 首先,创建路径起始点。beginPath()
- 然后使用画图命令去画出路径。
- 之后把路径封闭。一旦路径生成,你就能通过描边或填充路径区域来渲染图形。
- 函数
beginPath()
新建一条路径,生成之后,图形绘制命令被指向到生成的路径上。
closePath()
// 闭合路径之后图形绘制命令又重新指向到上下文中,不是必需的
stroke()
通过线条来绘制图形轮廓。
fill()
// 通过填充路径的内容区域生成实心的图形。
- 案例:绘制一个三角形
var canvas = document.getElementById('canvas');
if (canvas.getContext) {
var ctx = canvas.getContext('2d');
//开始绘制路径
ctx.beginPath();
//从坐标x=75和y=50开始绘画
ctx.moveTo(75, 50);
// 绘制一条线,从坐标(75,50)到(100,75)
ctx.lineTo(100, 75);
// 绘制一条线,从坐标(75,50)到(100,25)
ctx.lineTo(100, 25);
// 填充绘画的区域,形成三角形
ctx.fill();
}
-
moveTo(x, y):这个函数实际上并不能画出任何东西,它是将画笔移动到指定的坐标x以及y上。
-
案例:绘制一个笑脸
var canvas = document.getElementById('canvas');
if (canvas.getContext){
var ctx = canvas.getContext('2d');
ctx.beginPath();
// 以(75,75)为原心坐标,50为半径,从0点开始,绘制一个整圆。
ctx.arc(75, 75, 50, 0, Math.PI * 2, true); // 绘制一个圆
//将画笔向左平移到x=110
ctx.moveTo(110, 75);
ctx.arc(75, 75, 35, 0, Math.PI, false); // 口(顺时针)
ctx.moveTo(65, 65);
ctx.arc(60, 65, 5, 0, Math.PI * 2, true); // 左眼
ctx.moveTo(95, 65);
ctx.arc(90, 65, 5, 0, Math.PI * 2, true); // 右眼
ctx.stroke(); // 将路径描绘成
}
+案例:给绘制的圆形颜色
var c=document.getElementById("myCanvas");
var ctx=c.getContext("2d");
ctx.beginPath();
ctx.arc(95,50,40,0,2*Math.PI); // 画圆
ctx.fillStyle="#ff0";//设置填充颜色
ctx.fill();//开始填充圆(fill()填充当前路径)
ctx.strokeStyle="red";//将圆的线条颜色设置为红色
ctx.stroke();
ctx.fillStyle = 'red'; // 设置填充色
ctx.fillRect(25,25,25,25); // 将(25,25)位置的矩形填充成红色
3.添加样式和颜色
给图形填充颜色:ctx.fillStyle = "orange";
设置图形轮廓的颜色:strokeStyle = '#FFA500';
设置透明度:ctx.globalAlpha = 0.2; ctx.fillStyle = 'rgba(255,255,255,0.5)';
- 案例:将图片绘制到canvas上
var ctx = document.getElementById('canvas').getContext('2d');
// 创建新 image 对象,用作图案
var img = new Image();
img.src = 'https://mdn.mozillademos.org/files/222/Canvas_createpattern.png';
img.onload = function() {
// 在图片加载完成后执行回调
// 创建图案
var ptrn = ctx.createPattern(img, 'repeat');
ctx.fillStyle = ptrn;
ctx.fillRect(0, 0, 150, 150);
}
- 案例:给文字加阴影
var ctx = document.getElementById('canvas').getContext('2d');
ctx.shadowOffsetX = 2; // 阴影向右延伸
ctx.shadowOffsetY = 2; // 阴影向下延伸
ctx.shadowBlur = 2; // 设定阴影的模糊程度,其数值并不跟像素数量挂钩
// 高斯模糊值。值越大,阴影边缘越模糊
ctx.shadowColor = "rgba(0, 0, 0, 0.5)"; // 设定阴影颜色
ctx.font = "20px Times New Roman";
ctx.fillStyle = "Black"; // 字体颜色
ctx.fillText("Sample String", 5, 30);//5,30文字位置
ctx.font = '12px Calibri';
ctx.fillStyle = 'black';
ctx.fillText("文字1", 40, 80);
ctx.font = '24px 黑体';
ctx.fillStyle = "green";
tx.fillText("文字2", 80, 40);
4.绘制文本
fillText(text, x, y [, maxWidth]) //实心文字
// 在(x,y)位置填充指定的文本,绘制的最大宽度是可选的
strokeText(text, x, y [, maxWidth]) //空心文字
// 在指定的(x,y)位置绘制文本边框,绘制的最大宽度是可选的.
ctx.font = "bold 48px serif"; // 用来绘制文本的样式
ctx.textAlign = "left"; // 设置文本水平方向的对齐方式
ctx.textBaseline = 'top';
// 决定文字垂直方向的对齐方式
ctx.direction = 'rtl';// 文本方向(从右到左)
5.canvas操作视频
- HTMLVideoElement 接口提供了用于操作视频对象的特殊属性和方法。它同时还继承了HTMLMediaElement 和 HTMLElement 的属性与方法。
- 压缩图片步骤
1.明确图片压缩来源(来自于图片元素/视频元素/其他canvas元素/高性能的位图,也就是地图)
2.创建绘画环境,也就是图层,获取绘图环境
var ctx = canvas.getContext('2d');
3.在图层上绘画图片源,用drawImage绘画,从(0,0)坐标开始,绘画图层为(200,300)的画面,画面分辨率为200*300
ctx.drawImage(video, 0, 0, 200, 300);
4.将画布转化成base的形式,并且控制质量
4.1将之前 canvas 生成的 base64 数据拆分后,通过 atob 方法解码
4.2将解码后的数据转换成 Uint8Array 4.3格式的无符号整形数组
4.4转换后的数组来生成一个 Blob 数据对象,通过 URL.createObjectURL(blob) 来生成一个临时的 DOM 对象
4.5之后 IE 类浏览器可以调用 window.navigator.msSaveOrOpenBlob 方法来执行下载,其他浏览器也可以继续通过 <a> 标签的 download 属性来进行下载
let base64 = canvas.toDataURL('image/jpeg', 0.5)
5. base64 图片转 blob 后下载
downloadImg() {
let parts = this.compressImg.split(';base64,');
let contentType = parts[0].split(':')[1];
let raw = window.atob(parts[1]);
let rawLength = raw.length;
let uInt8Array = new Uint8Array(rawLength);
for(let i = 0; i < rawLength; ++i) {
uInt8Array[i] = raw.charCodeAt(i);
}
const blob = new Blob([uInt8Array], {type: contentType});
this.compressImg = URL.createObjectURL(blob);
if (window.navigator.msSaveOrOpenBlob) {
// 兼容 ie 的下载方式
window.navigator.msSaveOrOpenBlob(blob, this.fileName);
}else{
const a = document.createElement('a');
a.href = this.compressImg;
a.setAttribute('download', this.fileName);
a.click();
}
}
- 图片下载
this.httpService.getDownload(`/api/file/download/${id}`).subscribe(
(res: Response) => {
console.log("download", res);
let result = res["_body"];
let url = window.URL.createObjectURL(new Blob([result])); //处理文档流
let link = document.createElement("a");
link.style.display = "none";
link.href = url;
link.download = picture.fileName;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
},
优化
1.该设置是让画布知道没有透明感,则优化绘画性能
canvas.getContext('2d', { alpha: false })
2.跨域报错的原因(或者画布污染的原因)
// Failed to execute 'toDataURL' on 'HTMLCanvasElement': Tainted canvases may not be exported.
2.1首先:设置video标签的属性 crossorigin="anonymous"
<video ref='videoView' crossorigin="anonymous" v-show="videoId" :src="videoPath" controls="controls" class="video-upload-post-preview m-box-center m-box-center-a image-wrap more-image img3"></video>
2.2其次:服务端的请求头要设置允许跨域访问
2.3解决:视频不要引入线上地址,仍然使用本地视频地址,即可解决跨域问。
3.将图片转化成base64并且压缩质量
0.5是压缩质量成0.5,默认0.92,数值0-1之间,越大越清晰
let base64 = canvas.toDataURL('image/jpeg', 0.5)
问题:
- getElementsByTagName和querySelector的区别?
场景:点击目录,页面插入video标签。插入后获取video开始播放事件,结果:能打印出dom信息,但是video元素对象的长度为0且不能绑定事件。实际问题就是,dom中成功append结构,但是浏览器还没重绘,此时绑定事件失败。除了使用setTimeout,还有其他方法吗?
Element.getElementsByTagName() 方法返回一个动态的包含所有指定标签名的元素的HTML集合HTMLCollection。指定的元素的子树会被搜索,不包括元素自己。返回的列表是动态的,这意味着它会随着DOM树的变化自动更新自身。所以,使用相同元素和相同参数时,没有必要多次的调用Element.getElementsByTagName() .划重点了啊。这意味着它会随着DOM树的变化自动更新自身,他是动态呢,那么结合console打出来的其实是快照,也是动态的。那么得出结论,你那个时候video还没有插入进去。你可以用querySelector验证一下. 传送门-去MDN看看getElementsByTagName以及HTMLCollection