将视频图片截图后,在前端压缩图片再上传

26 篇文章 0 订阅
5 篇文章 0 订阅

要点:

  • 图片的大小由分辨率(长像素点*宽像素点)和位深度(由生成的图片是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
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值