【填坑日记4】JS this关键字 + Canvas图像格式转换

文章目录

  • 前言
  • 代码部分
    • 文件上传部分
    • 文件显示部分
  • 技术点(坑)
    • 坑1 this关键字
      • 情况1
      • 情况2
      • 回调函数事件参数
      • 一般情况
    • 坑2 数据形式转换
      • canvas数据组织形式
      • FileReader读取文件的数据组织形式
      • ArrayBuffer
  • 下一步

前言

继续跟着前面的文章走,前面的文章从第一篇开始:
快速搭建系统
后续又增加了两篇:
模块化 + 跨域问题
数据库连接池 + JS异步
今天准备继续,算上之前的三篇,今天的这篇算是第四篇了。统一加上标题:【填坑日记】,今天是第四篇,前面三篇就懒得改了。
后续文章的结构基本上是分成两个部分:
第一部分贴出项目中的成品代码,在成品代码中会写一写自己改代码的过程。
第二部分针对改代码(也就是填坑)过程中涉及到的技术点进行分析和记录。

代码部分

接第三篇,那一篇的内容还是在折腾后台,把后台的一个关键部分–数据库连接池做好了。基本流程就是通的了。
那么今天的这一篇就要开始做业务了,我们的业务是要上传一张图像,然后把这个图像的内容做一个OCR–文字识别。
我设计的上传界面很简单:就两个部分

文件上传部分


在vue对象的methods里面增加一个函数:
通过input组件上传一个文件,然后获得这个对象后,定义一个FileReader,转换成BASE-64格式,通过POST请求发送到后台保存。
代码一:
upload_img: function () {
const selectedFile = this.$refs.refFile.files[0]

  let name = selectedFile.name
  let size = selectedFile.size
  console.log('name: ' + name + 'size: ' + size)

  const reader = new FileReader()
  reader.readAsDataURL(selectedFile)
  reader.onload = function (evt) {
    let data = {'filename': name, 'content': evt.target.result, 'type': 'base64'}
    axios.post('http://localhost:3000/data/getdata', qs.stringify(data)).then(res => {
      console.log('success post')
      outputResult() //这里是伪代码,从逻辑上是将识别出来的结果显示出来。在后面的章节补充上去,放在CANVAS的图像处理那部分一起说
    })
  }

  showImg() //伪代码,显示上传的图像
  }

这里原来的设计是两个按钮的,一个按钮上传图像,显示之后使用者确认然后再点击确认,再发送到后台保存和识别。
原来打算的代码是这样的:
代码二:
data() { //VUE
return {
imgvalue_base64: ‘’
}
}

  upload_img: function () {
  const selectedFile = this.$refs.refFile.files[0]

  let name = selectedFile.name
  let size = selectedFile.size
  console.log('name: ' + name + 'size: ' + size)
  
  const reader = new FileReader()
  reader.readAsDataURL(selectedFile)
  reader.onload = function (evt) {
    let data = {'filename': name, 'content': evt.target.result, 'type': 'base64'}
    this.imgvalue_base64 = data
  }

  showImg() //伪代码,显示上传的图像
  },
  confirm: function() {
	 axios.post('http://localhost:3000/data/getdata', qs.stringify(data)).then(res => {
       console.log('success post')
       outputResult() 
    })
 }

confirm是另外一个按钮的onclick响应函数。但是fr.onload这个回调函数中的this调用,根本就不会调用到VUE对象里的data部分的变量。理由在上一篇中描述异步调用的时候已经提过了。
但是对于我自己对this这个关键字的使用起了兴趣,所以文章的下半部分就是记录一下this关键字的趟坑记录。

文件显示部分

// CANVAS用于显示上传的图像。

这里本来是考虑用元素的,后来想着后面是不是可以在原图上做一些操作,而且canvas的功能更强大,更灵活,所以就用的canvas元素
在上面的upload_img函数接上下面这段代码:
代码三:

  let canvas = this.$refs.canvas
  let ctx = canvas.getContext('2d')
  const arrayReader = new FileReader()
  arrayReader.readAsArrayBuffer(selectedFile)
  arrayReader.onload = function (evt) {
    let content = evt.target.result
    console.log('abcdefg')
    console.log(content.byteLength)
    let c = ctx.getImageData(0, 0, 480, 960)
    let c1 = new ImageData(Uint8ClampedArray.from(content), c.width, c.height)
    c1.data.buffer = content
    ctx.putImageData(c1, 100, 100)

后续补上成品代码。

技术点(坑)

坑1 this关键字

一个搞JS的大神同事告诉我,this调用的原则主要有两点:

  1. this所在的函数是哪个对象的方法,则该对象就是this所引用的对象
  2. js的变量作用域里的一条规则:“全局变量都是window对象的属性”
  3. 事件中,this 表示接收事件的元素

情况1

先说第一点,在“代码一”和“代码二”部分,第一句代码:

const selectedFile = this.$refs.refFile.files[0]

这里用到了this关键字,根据第一条原则,找到this所在的函数:upload_img(),而这个函数呢?属于VUE对象。因此,这里的this引用的对象就是VUE对象,就可以通过代码$refs来引用前面的插件。

情况2

在代码二中的

this.imgvalue_base64

我原本以为这里的这个this和情况一里面的this一样,引用的是VUE对象。但是从输出结果来看不是的。所以肯定是分析错了,我们根据原则一看一下,这一句代码的调用者是匿名函数

function () {
	let data = {'filename': name, 'content': evt.target.result, 'type': 'base64'}
    this.imgvalue_base64 = data
}

然后把这个函数作为回调函数赋值给FileReader。所以这个调用this的是回调这个函数的某个对象,在带事件参数的回调函数中,this 表示接收事件的元素。

回调函数事件参数

回到上面的代码中:

reader.onload = function (evt) {
    let data = {'filename': name, 'content': evt.target.result, 'type': 'base64'}
    this.imgvalue_base64 = data
  }

我们获取filerender读取的文件内容用的是: evt.target.result。或者可以用this.result。那么为什么不是this.target.result呢?我们可以用console.log(evt)输出这个对象的结构来看一下:
【填坑日记4】JS this关键字 + Canvas图像格式转换_第1张图片
可以看到evt对象是一个ProgressEvent对象(现在不知道这个对象具体是啥情况,暂时不管吧),这里面有一个target的成员,这个成员就是FileReader类型的。
根据原则3,this就表示接受事件的对象,应该就是这个target了,所以就可以用this.result来表示了。
这里我自己的理解是:BOM或者浏览器框架或者是某个环境(现在不清楚)组装了一个ProgressEvent的对象,用于调用回调函数之间传递参数,在这个对象中把接受这个事件的对象也塞进去了(虽然不知道为什么要这么做),所以this就可以表示event.target了。 现在就理解到这个程度了,后续再来补这个坑。

一般情况

看下面的这一段代码:


    
        ex_canvas
    
    
        

这里的输出结果是:
【填坑日记4】JS this关键字 + Canvas图像格式转换_第2张图片
定义在script元素中的变量x(全局变量),直接作为了window顶层对象(BOM)的成员了。
这里的一个问题是:
都是在函数imgload()中,代码:

var x = 'x2'
let canvas = document.getElementById('canvas')
let ctx = canvas.getContext('2d')
let img = document.getElementById('img')
console.log(this.x)
var f = function () {
       console.log(this.x)
}
f()

在定义的匿名函数中,this.x还是script中定义的全局变量x,而代码

var x = 'x2'

只是定义了一个imgload方法的方法变量。
而代码:

console.log(this.y)
var obj = {
      y: 'y2',
      func: function () {
           console.log(this.y)
      }
}
obj.func()

则是输出了y2,也就是说浏览器把语句var obj定义为了一个对象。而func这个方法中调用了this,自然指的就是obj这个对象。
其实,这里还有另外一个问题,就是说JS本身不是一个面向对象的语言,那么什么样的东西可以称之为一个对象呢?在很多技术资料里面也使用了对象这个名词,而且使用typeof关键字输出的话,也会输出object。那么是不是和java一样,有完整的类和对象的特性:封装、继承和多态。
这一块的东西,暂时了解到的是原型和原型链,这个坑还没怎么看懂,后续再填。

坑2 数据形式转换

canvas数据组织形式

在网上搜canvas绘制图像这个话题,一般的技术资料都是很简单的介绍:


ImageData对象结构
对象有三个成员:data,height,widht
我们点开data成员看一下:
【填坑日记4】JS this关键字 + Canvas图像格式转换_第3张图片
data是一个类型为Uint8ClampedArray的数组(这个数组在坑3部分来讲),长度为1843200。内容就是一些0-255的整数。
这个数组的内容就是图像的RGBA数据(不了解图像的同学可以自行搜索一下)。
我定义的*canvas大小为:960 480,那么整幅图像的像素点为460800,每个点用RGBA四个值来描述,那么需要460800 * 4 = 1843200个值。正好就是这个数组的长度。
也就是说,data[0]为第一个像素点的R值,data[1]为第一个像素点的G值,data[2]为第一个像素点的B值,data[3]为第一个像素点的A值,。后续类推,只是从一个二维结构成为了一个一维数组,是先描述完第一行的像素点,再描述第二行的像素点

我们也可以直接构造一个对象来调用putImageData函数进行绘制。这样,我们可以通过修改这个数组来达到绘制的目的。

FileReader读取文件的数据组织形式

接上面的逻辑,我们只需要把上传的文件转成RGBA这种方式的数组就可以了直接put到canvas里面进行绘制了。
这里我天真的认为FileReader读出来就是这样的一种格式,首先是发现数组长度对不上,后来仔细一琢磨,这里面的误会不是一般的大。
FileReader读出来的是文件对象的字节流,也就是文件编码本身。对于图像而言,一般就是JPG/BMP/PNG三种格式,其他的也是一个道理,而这三种格式的文件编码形式可以在网上搜一把:
随便找了几篇在这里引用一下:
JPG文件格式
BMP文件格式
PNG文件格式
如果想要构造一个RGBA的数据结构,那么就需要针对这几种文件格式进行反编码。那不是一般大的工作量了。
当然,在这里找到一份PNG的反编码JS代码:
PNG反编码
但是,这个远远不够,因为我之前做过一点图像处理方面的工作,知道opencv是图像处理领域的扛把子,在opencv包中的imread函数直接就可以把文件转成RGB格式,很方便的增加Alpha通道(C++版本)。而且还有opencv.js。
后续试一下看看。

ArrayBuffer

这里还涉及到一个数据结构:ArrayBuffer。
搜一下相关的内容,讲下自己的理解:

  1. ArrayBuffer指向一片内存区域,类似于C++中的指针(当然这里应该不是指针,应该是别的方式,暂且这么理解),内存操作和C++的内存操作类似,按照字节(byte)进行访问,因此判断ArrayBuffer长度的话不能使用 arraybuffer.length,而是要使用arraybuffer.byteLength成员。

  2. 和C++类似,arraybuffer里存的是什么,要看你怎么用(一般描述为窗口:view)。实际上就是一次取几个字节的问题,比如一个arraybuffer如下:

     a[0] = 255
     a[1] = 23
     a[2] = 11
     a[3] =45
    

    如果你按照Uint8Array,也就是一个字节做一个单位取出来的话,可以得到一个4个元素的数组

     int8_a[0] = 255
     int8_aa[1] = 23
     int8_aa[2] = 11
     int8_aa[3] =45
    

    如果你按照Uint16Array,也就是两个字节做一个单位取出来的话,可以得到一个2个元素的数组(数组后面的为高位)

     int16_a[0] = 23 *256 + 255
     int16_aa[1] = 45 *256 + 11
    

    如果你按照Uint32Array,也就是四个字节做一个单位取出来的话,可以得到一个1个元素的数组(数组后面的为高位)

     int32_a[0] = 45*256 * 256 * 256 + 11 * 256 * 256 + 23 * 256 *256 + 255
    
  3. 数组放在堆中,ArrayBuffer则把数据放在栈中(所以取数据时后者快)

  4. ArrayBuffer初始化后固定大小,数组则可以自由增减。

下一步

毫无疑问,下一步就是趟一趟opencv.js的坑了。

你可能感兴趣的:(【填坑日记4】JS this关键字 + Canvas图像格式转换)