ctucx.git: nimjpg

parse jpg file header

1 
2 
3 
4 
5 
6 
7 
8 
9 
10 
11 
12 
13 
14 
15 
16 
17 
18 
19 
20 
21 
22 
23 
24 
25 
26 
27 
28 
29 
30 
31 
32 
33 
34 
35 
36 
37 
38 
39 
40 
41 
42 
43 
44 
45 
46 
47 
48 
49 
50 
51 
52 
53 
54 
55 
56 
57 
58 
59 
60 
61 
62 
63 
64 
65 
66 
67 
68 
69 
70 
71 
72 
73 
74 
75 
76 
77 
78 
79 
80 
81 
82 
83 
84 
85 
86 
87 
88 
89 
90 
91 
92 
93 
94 
95 
96 
97 
98 
99 
100 
101 
102 
103 
104 
105 
106 
107 
108 
109 
110 
111 
112 
113 
114 
115 
116 
117 
118 
119 
120 
121 
122 
123 
import streams
import strutils
import asyncfile
import asyncdispatch
import nimexif/libexif
import nimexif/helpers
import tables
import parseutils
import options

import utils

var buf {.threadvar.}: pointer

# MUST be called once per thread
proc init_jpg*() =
  init_exif()
  buf = alloc(65536)

proc deinit_jpg*() =
  deinit_exif()
  dealloc(buf)

const
  MARKER_START = parseHexStr("FF")

  # 0xCn SOFn

  SOS   = parseHexStr("DA") # Start Of Scan (begins compressed data)
  SOI   = parseHexStr("D8") # Start Of Image (beginning of datastream)
  EOI   = parseHexStr("D9") # End Of Image (end of datastream)

  EXIF  = parseHexStr("E1")
  JFIF  = parseHexStr("E0")

type SofData* = object
  components*: uint8
  precision*: uint8
  height*: uint16
  width*: uint16

type JpgInfo* = object
  exifData*: Option[Table[string, string]]
  sofData*: Option[SofData]

proc getSectionSize(file: Stream | AsyncFile): Future[uint16] {.multisync.} =
  # FIXME consider endianness
  var size: uint16
  let val = toHex(file.read(2).await)
  discard parseHex(val, size)
  return size - uint16(2)

proc skipSection(file: Stream | AsyncFile): Future[int] {.multisync.} =
  let size = int64(file.getSectionSize().await)
  debug("skipping ", size)
  file.setFilePos(file.getFilePos() + size)

proc expect(file: Stream | AsyncFile, expected: string): Future[int] {.multisync.} =
  let byte = file.read(1).await
  if byte != expected:
    error("expected ", toHex(expected),", got ", toHex(byte))

proc process_sof*(file: Stream | AsyncFile): Future[SofData] {.multisync.} =
  discard parseHex(toHex(file.read(1).await), result.precision)
  discard parseHex(toHex(file.read(2).await), result.height)
  discard parseHex(toHex(file.read(2).await), result.width)
  discard parseHex(toHex(file.read(1).await), result.components)

proc calcRes(sd: SofData): uint64 =
  return uint64(sd.height) * uint64(sd.width)

proc collect_jpg*(file: Stream | AsyncFile): Future[JpgInfo] {.multisync,gcsafe.} =
  var done = false
  var byte: string

  discard file.expect(MARKER_START).await
  discard file.expect(SOI).await

  while not done:
    discard file.expect(MARKER_START).await

    let marker = file.read(1).await

    case marker:
    of "":
      error("unexpected end of file")
    of SOS:
      # Beginning of compressed data
      done = true
    of EOI:
      # No compressed data? Okay...
      done = true
    of EXIF:
      debug "found EXIF"
      let size = int(file.getSectionSize().await)
      discard file.readBuffer(buf, size).await
      let ed = exif_data_new_from_data(cast[ptr[uint8]](buf), cuint(size))
      let ed_table = ed.collect_exif_data()
      if result.exifData.isNone:
        result.exifData = some(ed_table)
      debug ed_table
    of JFIF:
      debug "found JFIF"
      discard file.skipSection().await
    else:
      if toHex(marker).startsWith("C"):
        debug "found SOF"
        let section_start = file.getFilePos() + 2
        let section_end = section_start + int64(file.getSectionSize().await)
        let sd = file.process_sof().await
        if result.sofData.isNone or sd.calcRes() > result.sofData.get.calcRes():
          result.sofData = some(sd)
        debug sd
        file.setFilePos(section_end)
        continue

      debug("unknown section: ", toHex(marker))
      discard file.skipSection().await

proc collect_jpg*(file: File): JpgInfo =
  let stream = newFileStream(file)
  result = collect_jpg(stream)
  stream.close()