图片处理

2021-05-01

图片处理

读取图片exif中的旋转角度

背景

移动端选取手机相册中的图片写入canvas时偶尔会遇到写入的图片莫名自动旋转。

原因

手机或摄像机拍摄的图片如果是旋转设备拍摄时,图片会有一个旋转属性Orientation,该属性会写在图片的exif内。在使用drawImage方法绘制图片时需要手动旋转。

什么是exif

Exif 信息就是由数码相机在拍摄过程中采集一系列的信息,然后把信息放置在我们熟知的 JPEG/TIFF 文件的头部,也就是说 Exif信息是镶嵌在 JPEG/TIFF 图像文件格式内的一组拍摄参数,它就好像是傻瓜相机的日期打印功能一样,只不过 Exif信息所记录的资讯更为详尽和完备。Exif 所记录的元数据信息非常丰富,主要包含了以下几类信息:

  • 拍摄日期
  • 摄器材(机身、镜头、闪光灯等)
  • 拍摄参数(快门速度、光圈F值、ISO速度、焦距、测光模式等)
  • 图像处理参数(锐化、对比度、饱和度、白平衡等)
  • 图像描述及版权信息
  • GPS定位数据
  • 缩略图

JPEG格式和标记

每个JPEG文件均从二进制值“ 0xFFD8”开始,以二进制值“ 0xFFD9”结束。JPEG数据中有几个二进制0xFFXX数据,它们被称为“标记”,它表示JPEG信息数据的周期。0xFFD8表示SOI(图像开始),0xFFD9表示EOI(图像结束)。这两个特殊标记后面没有数据,其他标记旁边有数据。标记的基本格式如下。

exif协议规范

  • 标记0xFFE0〜0xFFEF被称为“应用标记”,对于解码JPEG图像不是必需的。它们由用户应用程序使用。例如,较早的olympus / canon / casio / agfa数码相机使用JFIF(JPEG文件交换格式)存储图像。JFIF使用APP0(0xFFE0)标记插入数码相机配置数据和缩略图。

  • 此外,Exif使用应用程序标记插入数据,但Exif使用APP1(0xFFE1)标记以避免与JFIF格式冲突。每个Exif文件格式都从这种格式开始;

  • Exif数据(APP1)的大致结构如下所示。这是“ Intel”字节对齐的情况,它包含JPEG格式的缩略图。如上所述,Exif数据从ASCII字符“ Exif”和2个字节的0x00开始,然后是Exif数据。Exif使用TIFF格式存储数据。

image-20210501143802096

image-20210501144416378

TIFF标头的结构

TIFF格式的前8个字节是TIFF标头。前2个字节定义TIFF数据的字节对齐。如果它是0x4949 =“ I I”,则表示“ Intel”类型的字节对齐。如果它是0x4d4d =“ MM”,则表示“ Motorola”类型的字节对齐。例如,第十六个系统将值“ 305,419,896”标记为0x12345678。在Motrola对齐时,将其存储为0x12,0x34,0x56,0x78。如果是Intel align,则将其存储为0x78,0x56,0x34,0x12。似乎大多数数字货币使用Intel align。理光使用Motorola align。索尼使用D700以外的Intel Align。柯达DC200 / 210/240使用Motorola align,但DC220 / 260使用Intel align,尽管它们使用的是PowerPC!因此,当我们需要Exif数据的值时,我们必须每次都检查字节对齐。尽管JPEG数据仅使用Motorola对齐方式,但Exif允许两种对齐方式

接下来的2个字节始终是2个字节的长度值0x002A。如果数据使用Intel align,则接下来的2个字节为“ 0x2a00”。如果使用摩托罗拉,则为“ 0x002a”。TIFF标头的最后4个字节是第一个IFD(图像文件目录,在下一章中介绍)的偏移量。包括此偏移量,TIFF格式中使用的所有偏移量值都将从TIFF标头(“ I I”或“ MM”)的第一个字节开始偏移字节。通常,第一个IFD在紧靠TIFF头之后开始,因此此偏移量的值为’0x00000008’。

IFD结构

TIFF标头旁边是第一个IFD:Image File Directory。它包含图像信息数据。在下面的图表中,前2个字节(’00 0d’)表示此IFD中包含的目录条目数(13)。然后是目录条目(每个条目12字节)。在最后一个目录条目之后,有4个字节的数据(在图表上为“ LLLLLLLL”),这意味着到下一个IFD的偏移量。如果其值为“ 0x00000000”,则表示这是最后一个IFD,并且没有链接的IFD。

IFD条目的结构

前0-1字节为标签号,表示一种数据。2-3字节表示数据格式,4-7字节是组件数。8-11字节(4字节)包含一个数据值或数据值的偏移量(如果最后4个字节不够表示该条目的值的话,则这4个字节表示为值地址的偏移量)

数据格式

image-20210501145426755

标签号对照表

image-20210501145525060

这里0x0112标签号就是对应方向,我们取出该值即可

例子图片

例子图片

实现代码

async function loadImg (url: string) {
  try {
    const res = await fetch(url)
    return res.arrayBuffer()
  } catch (error) {
    return Promise.reject(error)
  }
}
async function getJpegOrientation (url: string):Promise<number> {
  try {
    let app1Start = 0
    let offset = 0
    let littleEndian = false
    let ifdStart = 0
    let orientation = 1
    const arryBuffer = await loadImg(url)
    const dataView = new DataView(arryBuffer)
    const length = dataView.byteLength
    if (dataView.getUint8(0) === 0xff && dataView.getUint8(1) === 0xd8) {
      offset = 2
      while (offset < length) {
        if (dataView.getUint8(offset) === 0xFF && dataView.getUint8(offset + 1) === 0xE1) {
          app1Start = offset
          break
        }
        offset++
      }
    }
    if (app1Start) {
      const exifIDCode = app1Start + 4
      const tiffOffset = app1Start + 10
      if (dataView.getUint32(exifIDCode) === 0x45786966) {
        const endianness = dataView.getUint16(tiffOffset)
        littleEndian = endianness === 0x4949

        if (littleEndian || endianness === 0x4D4D /* bigEndian */) {
          if (dataView.getUint16(tiffOffset + 2, littleEndian) === 0x002A) {
            const firstIFDOffset = dataView.getUint32(tiffOffset + 4, littleEndian)

            if (firstIFDOffset >= 0x00000008) {
              ifdStart = tiffOffset + firstIFDOffset
            }
          }
        }
      }
    }
    if (ifdStart) {
      const ifdLength = dataView.getUint16(ifdStart, littleEndian)
      const ifdContentStart = ifdStart + 2
      let idx = 0
      while (idx < ifdLength) {
        offset = ifdContentStart + 12 * idx
        if (dataView.getUint16(offset, littleEndian) === 0x0112) {
          offset += 8
          orientation = dataView.getUint16(offset, littleEndian)
          break
        }
        idx++
      }
    }
    return orientation
  } catch (error) {
    return Promise.reject(error)
  }
}

评论(0条):