シェルでEXIF情報を読む

LinuxでJPEGのEXIF情報を読む時は`jhead`コマンドを使ったりするのですが、ネットで調べていたところJavaScriptで作成した[Exif-js](https://github.com/exif-js/exif-js)を発見しました。これに刺激を受けてTiny Core Linuxのashで動くものができないかとチャレンジしてみました。

## 符号なし32ビット整数の注意点
### Tiny Core Linuxのashでは符号なし32ビット整数が使えない？
EXIFフォーマットではunsigned longという符号なしの32ビット整数が使われるのですが、Tiny Core Linux標準のashの`$(())`は32ビット符号なし整数が扱えないという問題があります。※Tiny Core Linuxでもbashをインストールすれば扱えますしbashが標準なUbuntuなら意識する事なく扱えます。

```console:bashの場合
$ echo $((0xffff * 0x10000))
4294901760
$ echo $((65535 * 65536))
4294901760
```

```console:TinyCoreLinuxのashの場合
$ echo $((0xffff * 0x10000))
-65536
$ echo $((65535 * 65536))
-65536
```

### exprなら符号なし32ビット整数が扱える
そんなに大きな数は使わないだろうから無視しても構わないかなと思ったのですが、`expr`コマンドなら扱うことができることが分かりましたので今回は`$(())`ではなく`expr`コマンドを使って計算することとしました。
`$(())`と比較すると`expr`コマンドは速度面で不利なのですが、今回のようなプログラムの場合はさほど影響はないように思います。`$(())`よりもちょっとだけ括弧の数も減らせるので、ちょっとスッキリした感はあります。

```console:exprを使う
$ expr $((0xffff)) '*' $((0x10000))
4294901760
$ expr 65535 '*' 65536
4294901760
```

### 32ビット整数の16進数から10進数への変換はprintfで
`$((0xffff))`のように書く以外にも`printf '%d' 0xffff`のように書いても16進数から10進数に変換できます。Tiny Core Linuxのashの場合`$(())`は32ビット整数が扱えませんが`printf`コマンドは扱えるという違いがあります。※今回は結局のところ`hexdump`で10進数でダンプして`awk`で計算しましたのでこの方法は使いませんでしたが…。

```console:TinyCoreLinuxのashで10進数から16新数への変換
$ echo $((0xffff0000))
-65536
$ printf "%d" 0xffff0000
4294901760
```

```console:bashで10進数から16新数への変換
$ echo $((0xffff0000))
4294901760
$ printf "%d" 0xffff0000
4294901760
```

## バイナリーを扱うには
`od`コマンドや`hexdump`コマンドを使用することで任意の位置の値を取得することができます。Tiny Core Linuxはどちらのコマンドも標準で使えます。

```console:odを使って2バイト目から4バイト分取り出す
$ od -j2 -N4 -An -tx1 IMG_20170416_053005.jpg #16進数で取り出す
 ff e1 2e 60
$ od -j2 -N4 -An -tu1 IMG_20170416_053005.jpg #10進数で取り出す
 255 225  46  96
```

```console:hexdumpを使って2バイト目から4バイト分取り出す
$ hexdump -v -s2 -n4 -e'1/1 "%2.2x "' IMG_20170416_053005.jpg #16進数で取り出す
 ff e1 2e 60
$ hexdump -v -s2 -n4 -e'1/1 "%u "' IMG_20170416_053005.jpg #10進数で取り出す
255 225 46 96
```


## JPEGやEXIFの情報はどこで
JPEGファイルを解析しようという意気込みはあったもののEXIFの知識が全くなかったので、今回参考にしたのは以下のサイトです。

- [Description of Exif file format](https://www.media.mit.edu/pia/Research/deepview/exif.html) - mit
- [Metadata reference tables](http://www.exiv2.org/tags.html) - Exiv2.org

仕様と実際に動くソースコードを比較した方が分かりやすいので今回は以下のサイトのJavaScriptのソースコードを見ながら作成しました。

- [Exif.js](https://github.com/exif-js/exif-js) - GitHub

## ソースコード
以下がソースコードです。検証を目的としているので色んな部分を省いてしまっていますが、インストール直後のTiny Core LinuxのCoreでEXIF情報が読む事ができそうという事は分かりました。

```bash:exif.sh
#!/bin/sh

FILENAME=$1

getUint8() {
  #$1=byteOffset
  echo $(hexdump -v -s$1 -n1 -e'1/1 "%u"' "$FILENAME")
  return 0
}
getUint16() {
  #$1=byteOffset, $2=littleEndian
  [ $2 -eq 1 ] && echo $(hexdump -v -s$1 -n2 -e'1/1 "%u "' "$FILENAME" | awk '{print $1+$2*256}')
  [ $2 -ne 1 ] && echo $(hexdump -v -s$1 -n2 -e'1/1 "%u "' "$FILENAME" | awk '{print $1*256+$2}')
  return 0
}
getUint32() {
  #$1=byteOffset, $2=littleEndian
  local first=$(getUint16 $1 $2)
  local second=$(getUint16 $(expr $1 + 2) $2)
  [ $2 -eq 1 ] && expr $first + $second '*' 65536
  [ $2 -ne 1 ] && expr $first '*' 65536 + $second
  return 0
}
getString() {
  #$1=byteOffset, $2=length
  echo "$(hexdump -v -s$1 -n$2 -e'"%_p"' "$FILENAME")"
  return 0
}
getHex() {
  #$1=byteOffset, $2=length
  echo $(hexdump -v -s$1 -n$2 -e'1/1 "%2.2x" ' "$FILENAME")
  return 0
}
readTagValue() {
  local entryOffset=$1
  local tiffStart=$2
  local dirStart=$3
  local littleEnd=$4

  local type=$(getUint16 $(expr $entryOffset + 2) $littleEnd)
  local numValues=$(getUint32 $(expr $entryOffset + 4) $littleEnd)
  local valueOffset=$(expr $(getUint32 $(expr $entryOffset + 8) $littleEnd) + $tiffStart)

  #echo -n "type=$type,len=$numValues,data="
  if [ $type -eq 1 ]; then
    offset=$(expr $entryOffset + 8)
    [ $numValues -gt 4 ] && offset=$valueOffset
    local vals=''
    for n in $(seq 0 $(expr $numValues - 1)); do
      [ -n "$vals" ] && vals="$vals "
      vals="$vals$(getUint8 $(expr $offset + $n))"
    done
    echo "$vals"
  elif [ $type -eq 2 ]; then
    offset=$(expr $entryOffset + 8)
    [ $numValues -gt 4 ] && offset=$valueOffset
    echo "$(getString $offset $(expr $numValues - 1))"
  elif [ $type -eq 3 ]; then
    offset=$(expr $entryOffset + 8)
    [ $numValues -gt 2 ] && offset=$valueOffset
    local vals=''
    for n in $(seq 0 $(expr $numValues - 1)); do
      [ -n "$vals" ] && vals="$vals "
      vals="$vals$(getUint16 $(expr $offset + 2 '*' $n) $littleEnd)"
    done
    echo "$vals"
  elif [ $type -eq 4 ]; then
    offset=$(expr $entryOffset + 8)
    [ $numValues -gt 1 ] && offset=$valueOffset
    local vals=''
    for n in $(seq 0 $(expr $numValues - 1)); do
      [ -n "$vals" ] && vals="$vals "
      vals="$vals$(getUint32 $(expr $offset + 4 '*' $n) $littleEnd)"
    done
    echo "$vals"
  elif [ $type -eq 5 ]; then
    local vals=''
    for n in $(seq 0 $(expr $numValues - 1)); do
      local numerator=$(getUint32 $(expr $valueOffset + 8 '*' $n) $littleEnd)
      local denominator=$(getUint32 $(expr $valueOffset + 4 + 8 '*' $n) $littleEnd)
      [ -n "$vals" ] && vals="$vals "
      vals="$vals$numerator/$denominator"
    done
    echo "$vals"
  else
    offset=$(expr $entryOffset + 8)
    [ $numValues -gt 4 ] && offset=$valueOffset
    [ $numValues -gt 16 ] && vals="$(getHex $offset 16).."
    [ $numValues -le 16 ] && vals="$(getHex $offset $numValues)"
    vals=$(echo "$vals" | sed -r 's/(..)/\1 /g')
    echo "$vals"
  fi
  return 0
}
readTags() {
  local tiffStart=$1
  local dirStart=$2
  local littleEnd=$3
  local entries=$(getUint16 $dirStart $littleEnd)

  for i in $(seq 0 $(expr $entries - 1)); do
    local entryOffset=$(expr $dirStart + $i '*' 12 + 2)
    local tag=$(getUint16 $entryOffset $littleEnd)
    echo "$(printf '0x%4.4x' $tag) $(readTagValue $entryOffset $tiffStart $dirStart $littleEnd)"
  done
  return 0
}

readEXIFData() {
  local start=$1
  [ $(getString $start 4) != 'Exif' ] && return 1
  local tiffOffset=$(expr $start + 6)

  littleEnd=''
  [ $(getString $tiffOffset 2) = 'II' ] && littleEnd=1
  [ $(getString $tiffOffset 2) = 'MM' ] && littleEnd=0
  [ -z $littleEnd ] && return 1

  [ $(getUint16 $(expr $tiffOffset + 2) $littleEnd) -ne $((0x002A)) ] && return 1

  local firstIFDOffset=$(getUint32 $(expr $tiffOffset + 4) $littleEnd)

  [ $firstIFDOffset -lt 8 ] && return 1
  tags=$(readTags $tiffOffset $(expr $tiffOffset + $firstIFDOffset) $littleEnd)
  echo "$tags"
  if [ -n "$(echo "$tags" | egrep '^0x8825')" ]; then
    pointer=$(echo "$tags" | egrep '^0x8825' | egrep -o '[0-9]+$')
    printf "\nGPSInfoIFDPointer\n"
    readTags $tiffOffset $(expr $tiffOffset + $pointer) $littleEnd
  fi
  if [ -n "$(echo "$tags" | egrep '^0x8769')" ]; then
    pointer=$(echo "$tags" | egrep '^0x8769' | egrep -o '[0-9]+$')
    printf "\nExifIFDPointer\n"
    readTags $tiffOffset $(expr $tiffOffset + $pointer) $littleEnd
  fi
}

offset=2
length=$(wc -c "$FILENAME" | awk '{print $1}')

while [ $offset -lt $length ]; do
  [ "$(getHex $offset 1)" != 'ff' ] && exit 1
  if [ $(getHex $(expr $offset + 1) 1) = 'e1' ]; then
    echo "Found 0xFFE1 marker"
    readEXIFData $(expr $offset + 4)
    break
  fi
  offset=$(expr $offset + 2 + $(getUint16 $(expr $offset + 2)) 0 )
done
```