src

Go monorepo.
git clone git://code.dwrz.net/src
Log | Files | Refs

exif.go (17308B)


      1 // Package exif implements decoding of EXIF data as defined in the EXIF 2.2
      2 // specification (http://www.exif.org/Exif2-2.PDF).
      3 package exif
      4 
      5 import (
      6 	"bufio"
      7 	"bytes"
      8 	"encoding/binary"
      9 	"encoding/json"
     10 	"errors"
     11 	"fmt"
     12 	"io"
     13 	"io/ioutil"
     14 	"math"
     15 	"strconv"
     16 	"strings"
     17 	"time"
     18 
     19 	"github.com/rwcarlsen/goexif/tiff"
     20 )
     21 
     22 const (
     23 	jpeg_APP1 = 0xE1
     24 
     25 	exifPointer    = 0x8769
     26 	gpsPointer     = 0x8825
     27 	interopPointer = 0xA005
     28 )
     29 
     30 // A decodeError is returned when the image cannot be decoded as a tiff image.
     31 type decodeError struct {
     32 	cause error
     33 }
     34 
     35 func (de decodeError) Error() string {
     36 	return fmt.Sprintf("exif: decode failed (%v) ", de.cause.Error())
     37 }
     38 
     39 // IsShortReadTagValueError identifies a ErrShortReadTagValue error.
     40 func IsShortReadTagValueError(err error) bool {
     41 	de, ok := err.(decodeError)
     42 	if ok {
     43 		return de.cause == tiff.ErrShortReadTagValue
     44 	}
     45 	return false
     46 }
     47 
     48 // A TagNotPresentError is returned when the requested field is not
     49 // present in the EXIF.
     50 type TagNotPresentError FieldName
     51 
     52 func (tag TagNotPresentError) Error() string {
     53 	return fmt.Sprintf("exif: tag %q is not present", string(tag))
     54 }
     55 
     56 func IsTagNotPresentError(err error) bool {
     57 	_, ok := err.(TagNotPresentError)
     58 	return ok
     59 }
     60 
     61 // Parser allows the registration of custom parsing and field loading
     62 // in the Decode function.
     63 type Parser interface {
     64 	// Parse should read data from x and insert parsed fields into x via
     65 	// LoadTags.
     66 	Parse(x *Exif) error
     67 }
     68 
     69 var parsers []Parser
     70 
     71 func init() {
     72 	RegisterParsers(&parser{})
     73 }
     74 
     75 // RegisterParsers registers one or more parsers to be automatically called
     76 // when decoding EXIF data via the Decode function.
     77 func RegisterParsers(ps ...Parser) {
     78 	parsers = append(parsers, ps...)
     79 }
     80 
     81 type parser struct{}
     82 
     83 type tiffErrors map[tiffError]string
     84 
     85 func (te tiffErrors) Error() string {
     86 	var allErrors []string
     87 	for k, v := range te {
     88 		allErrors = append(allErrors, fmt.Sprintf("%s: %v\n", stagePrefix[k], v))
     89 	}
     90 	return strings.Join(allErrors, "\n")
     91 }
     92 
     93 // IsCriticalError, given the error returned by Decode, reports whether the
     94 // returned *Exif may contain usable information.
     95 func IsCriticalError(err error) bool {
     96 	_, ok := err.(tiffErrors)
     97 	return !ok
     98 }
     99 
    100 // IsExifError reports whether the error happened while decoding the EXIF
    101 // sub-IFD.
    102 func IsExifError(err error) bool {
    103 	if te, ok := err.(tiffErrors); ok {
    104 		_, isExif := te[loadExif]
    105 		return isExif
    106 	}
    107 	return false
    108 }
    109 
    110 // IsGPSError reports whether the error happened while decoding the GPS sub-IFD.
    111 func IsGPSError(err error) bool {
    112 	if te, ok := err.(tiffErrors); ok {
    113 		_, isGPS := te[loadExif]
    114 		return isGPS
    115 	}
    116 	return false
    117 }
    118 
    119 // IsInteroperabilityError reports whether the error happened while decoding the
    120 // Interoperability sub-IFD.
    121 func IsInteroperabilityError(err error) bool {
    122 	if te, ok := err.(tiffErrors); ok {
    123 		_, isInterop := te[loadInteroperability]
    124 		return isInterop
    125 	}
    126 	return false
    127 }
    128 
    129 type tiffError int
    130 
    131 const (
    132 	loadExif tiffError = iota
    133 	loadGPS
    134 	loadInteroperability
    135 )
    136 
    137 var stagePrefix = map[tiffError]string{
    138 	loadExif:             "loading EXIF sub-IFD",
    139 	loadGPS:              "loading GPS sub-IFD",
    140 	loadInteroperability: "loading Interoperability sub-IFD",
    141 }
    142 
    143 // Parse reads data from the tiff data in x and populates the tags
    144 // in x. If parsing a sub-IFD fails, the error is recorded and
    145 // parsing continues with the remaining sub-IFDs.
    146 func (p *parser) Parse(x *Exif) error {
    147 	if len(x.Tiff.Dirs) == 0 {
    148 		return errors.New("Invalid exif data")
    149 	}
    150 	x.LoadTags(x.Tiff.Dirs[0], exifFields, false)
    151 
    152 	// thumbnails
    153 	if len(x.Tiff.Dirs) >= 2 {
    154 		x.LoadTags(x.Tiff.Dirs[1], thumbnailFields, false)
    155 	}
    156 
    157 	te := make(tiffErrors)
    158 
    159 	// recurse into exif, gps, and interop sub-IFDs
    160 	if err := loadSubDir(x, ExifIFDPointer, exifFields); err != nil {
    161 		te[loadExif] = err.Error()
    162 	}
    163 	if err := loadSubDir(x, GPSInfoIFDPointer, gpsFields); err != nil {
    164 		te[loadGPS] = err.Error()
    165 	}
    166 
    167 	if err := loadSubDir(x, InteroperabilityIFDPointer, interopFields); err != nil {
    168 		te[loadInteroperability] = err.Error()
    169 	}
    170 	if len(te) > 0 {
    171 		return te
    172 	}
    173 	return nil
    174 }
    175 
    176 func loadSubDir(x *Exif, ptr FieldName, fieldMap map[uint16]FieldName) error {
    177 	r := bytes.NewReader(x.Raw)
    178 
    179 	tag, err := x.Get(ptr)
    180 	if err != nil {
    181 		return nil
    182 	}
    183 	offset, err := tag.Int64(0)
    184 	if err != nil {
    185 		return nil
    186 	}
    187 
    188 	_, err = r.Seek(offset, 0)
    189 	if err != nil {
    190 		return fmt.Errorf("exif: seek to sub-IFD %s failed: %v", ptr, err)
    191 	}
    192 	subDir, _, err := tiff.DecodeDir(r, x.Tiff.Order)
    193 	if err != nil {
    194 		return fmt.Errorf("exif: sub-IFD %s decode failed: %v", ptr, err)
    195 	}
    196 	x.LoadTags(subDir, fieldMap, false)
    197 	return nil
    198 }
    199 
    200 // Exif provides access to decoded EXIF metadata fields and values.
    201 type Exif struct {
    202 	Tiff *tiff.Tiff
    203 	main map[FieldName]*tiff.Tag
    204 	Raw  []byte
    205 }
    206 
    207 // Decode parses EXIF data from r (a TIFF, JPEG, or raw EXIF block)
    208 // and returns a queryable Exif object. After the EXIF data section is
    209 // called and the TIFF structure is decoded, each registered parser is
    210 // called (in order of registration). If one parser returns an error,
    211 // decoding terminates and the remaining parsers are not called.
    212 //
    213 // The error can be inspected with functions such as IsCriticalError
    214 // to determine whether the returned object might still be usable.
    215 func Decode(r io.Reader) (*Exif, error) {
    216 
    217 	// EXIF data in JPEG is stored in the APP1 marker. EXIF data uses the TIFF
    218 	// format to store data.
    219 	// If we're parsing a TIFF image, we don't need to strip away any data.
    220 	// If we're parsing a JPEG image, we need to strip away the JPEG APP1
    221 	// marker and also the EXIF header.
    222 
    223 	header := make([]byte, 4)
    224 	n, err := io.ReadFull(r, header)
    225 	if err != nil {
    226 		return nil, fmt.Errorf("exif: error reading 4 byte header, got %d, %v", n, err)
    227 	}
    228 
    229 	var isTiff bool
    230 	var isRawExif bool
    231 	var assumeJPEG bool
    232 	switch string(header) {
    233 	case "II*\x00":
    234 		// TIFF - Little endian (Intel)
    235 		isTiff = true
    236 	case "MM\x00*":
    237 		// TIFF - Big endian (Motorola)
    238 		isTiff = true
    239 	case "Exif":
    240 		isRawExif = true
    241 	default:
    242 		// Not TIFF, assume JPEG
    243 		assumeJPEG = true
    244 	}
    245 
    246 	// Put the header bytes back into the reader.
    247 	r = io.MultiReader(bytes.NewReader(header), r)
    248 	var (
    249 		er  *bytes.Reader
    250 		tif *tiff.Tiff
    251 		sec *appSec
    252 	)
    253 
    254 	switch {
    255 	case isRawExif:
    256 		var header [6]byte
    257 		if _, err := io.ReadFull(r, header[:]); err != nil {
    258 			return nil, fmt.Errorf("exif: unexpected raw exif header read error")
    259 		}
    260 		if got, want := string(header[:]), "Exif\x00\x00"; got != want {
    261 			return nil, fmt.Errorf("exif: unexpected raw exif header; got %q, want %q", got, want)
    262 		}
    263 		fallthrough
    264 	case isTiff:
    265 		// Functions below need the IFDs from the TIFF data to be stored in a
    266 		// *bytes.Reader.  We use TeeReader to get a copy of the bytes as a
    267 		// side-effect of tiff.Decode() doing its work.
    268 		b := &bytes.Buffer{}
    269 		tr := io.TeeReader(r, b)
    270 		tif, err = tiff.Decode(tr)
    271 		er = bytes.NewReader(b.Bytes())
    272 	case assumeJPEG:
    273 		// Locate the JPEG APP1 header.
    274 		sec, err = newAppSec(jpeg_APP1, r)
    275 		if err != nil {
    276 			return nil, err
    277 		}
    278 		// Strip away EXIF header.
    279 		er, err = sec.exifReader()
    280 		if err != nil {
    281 			return nil, err
    282 		}
    283 		tif, err = tiff.Decode(er)
    284 	}
    285 
    286 	if err != nil {
    287 		return nil, decodeError{cause: err}
    288 	}
    289 
    290 	er.Seek(0, 0)
    291 	raw, err := ioutil.ReadAll(er)
    292 	if err != nil {
    293 		return nil, decodeError{cause: err}
    294 	}
    295 
    296 	// build an exif structure from the tiff
    297 	x := &Exif{
    298 		main: map[FieldName]*tiff.Tag{},
    299 		Tiff: tif,
    300 		Raw:  raw,
    301 	}
    302 
    303 	for i, p := range parsers {
    304 		if err := p.Parse(x); err != nil {
    305 			if _, ok := err.(tiffErrors); ok {
    306 				return x, err
    307 			}
    308 			// This should never happen, as Parse always returns a tiffError
    309 			// for now, but that could change.
    310 			return x, fmt.Errorf("exif: parser %v failed (%v)", i, err)
    311 		}
    312 	}
    313 
    314 	return x, nil
    315 }
    316 
    317 // LoadTags loads tags into the available fields from the tiff Directory
    318 // using the given tagid-fieldname mapping.  Used to load makernote and
    319 // other meta-data.  If showMissing is true, tags in d that are not in the
    320 // fieldMap will be loaded with the FieldName UnknownPrefix followed by the
    321 // tag ID (in hex format).
    322 func (x *Exif) LoadTags(d *tiff.Dir, fieldMap map[uint16]FieldName, showMissing bool) {
    323 	for _, tag := range d.Tags {
    324 		name := fieldMap[tag.Id]
    325 		if name == "" {
    326 			if !showMissing {
    327 				continue
    328 			}
    329 			name = FieldName(fmt.Sprintf("%v%x", UnknownPrefix, tag.Id))
    330 		}
    331 		x.main[name] = tag
    332 	}
    333 }
    334 
    335 // Get retrieves the EXIF tag for the given field name.
    336 //
    337 // If the tag is not known or not present, an error is returned. If the
    338 // tag name is known, the error will be a TagNotPresentError.
    339 func (x *Exif) Get(name FieldName) (*tiff.Tag, error) {
    340 	if tg, ok := x.main[name]; ok {
    341 		return tg, nil
    342 	}
    343 	return nil, TagNotPresentError(name)
    344 }
    345 
    346 // Walker is the interface used to traverse all fields of an Exif object.
    347 type Walker interface {
    348 	// Walk is called for each non-nil EXIF field. Returning a non-nil
    349 	// error aborts the walk/traversal.
    350 	Walk(name FieldName, tag *tiff.Tag) error
    351 }
    352 
    353 // Walk calls the Walk method of w with the name and tag for every non-nil
    354 // EXIF field.  If w aborts the walk with an error, that error is returned.
    355 func (x *Exif) Walk(w Walker) error {
    356 	for name, tag := range x.main {
    357 		if err := w.Walk(name, tag); err != nil {
    358 			return err
    359 		}
    360 	}
    361 	return nil
    362 }
    363 
    364 // DateTime returns the EXIF's "DateTimeOriginal" field, which
    365 // is the creation time of the photo. If not found, it tries
    366 // the "DateTime" (which is meant as the modtime) instead.
    367 // The error will be TagNotPresentErr if none of those tags
    368 // were found, or a generic error if the tag value was
    369 // not a string, or the error returned by time.Parse.
    370 //
    371 // If the EXIF lacks timezone information or GPS time, the returned
    372 // time's Location will be time.Local.
    373 func (x *Exif) DateTime() (time.Time, error) {
    374 	var dt time.Time
    375 	tag, err := x.Get(DateTimeOriginal)
    376 	if err != nil {
    377 		tag, err = x.Get(DateTime)
    378 		if err != nil {
    379 			return dt, err
    380 		}
    381 	}
    382 	if tag.Format() != tiff.StringVal {
    383 		return dt, errors.New("DateTime[Original] not in string format")
    384 	}
    385 	exifTimeLayout := "2006:01:02 15:04:05"
    386 	dateStr := strings.TrimRight(string(tag.Val), "\x00")
    387 	// TODO(bradfitz,mpl): look for timezone offset, GPS time, etc.
    388 	timeZone := time.Local
    389 	if tz, _ := x.TimeZone(); tz != nil {
    390 		timeZone = tz
    391 	}
    392 	return time.ParseInLocation(exifTimeLayout, dateStr, timeZone)
    393 }
    394 
    395 func (x *Exif) TimeZone() (*time.Location, error) {
    396 	// TODO: parse more timezone fields (e.g. Nikon WorldTime).
    397 	timeInfo, err := x.Get("Canon.TimeInfo")
    398 	if err != nil {
    399 		return nil, err
    400 	}
    401 	if timeInfo.Count < 2 {
    402 		return nil, errors.New("Canon.TimeInfo does not contain timezone")
    403 	}
    404 	offsetMinutes, err := timeInfo.Int(1)
    405 	if err != nil {
    406 		return nil, err
    407 	}
    408 	return time.FixedZone("", offsetMinutes*60), nil
    409 }
    410 
    411 func ratFloat(num, dem int64) float64 {
    412 	return float64(num) / float64(dem)
    413 }
    414 
    415 // Tries to parse a Geo degrees value from a string as it was found in some
    416 // EXIF data.
    417 // Supported formats so far:
    418 // - "52,00000,50,00000,34,01180" ==> 52 deg 50'34.0118"
    419 //   Probably due to locale the comma is used as decimal mark as well as the
    420 //   separator of three floats (degrees, minutes, seconds)
    421 //   http://en.wikipedia.org/wiki/Decimal_mark#Hindu.E2.80.93Arabic_numeral_system
    422 // - "52.0,50.0,34.01180" ==> 52deg50'34.0118"
    423 // - "52,50,34.01180"     ==> 52deg50'34.0118"
    424 func parseTagDegreesString(s string) (float64, error) {
    425 	const unparsableErrorFmt = "Unknown coordinate format: %s"
    426 	isSplitRune := func(c rune) bool {
    427 		return c == ',' || c == ';'
    428 	}
    429 	parts := strings.FieldsFunc(s, isSplitRune)
    430 	var degrees, minutes, seconds float64
    431 	var err error
    432 	switch len(parts) {
    433 	case 6:
    434 		degrees, err = strconv.ParseFloat(parts[0]+"."+parts[1], 64)
    435 		if err != nil {
    436 			return 0.0, fmt.Errorf(unparsableErrorFmt, s)
    437 		}
    438 		minutes, err = strconv.ParseFloat(parts[2]+"."+parts[3], 64)
    439 		if err != nil {
    440 			return 0.0, fmt.Errorf(unparsableErrorFmt, s)
    441 		}
    442 		minutes = math.Copysign(minutes, degrees)
    443 		seconds, err = strconv.ParseFloat(parts[4]+"."+parts[5], 64)
    444 		if err != nil {
    445 			return 0.0, fmt.Errorf(unparsableErrorFmt, s)
    446 		}
    447 		seconds = math.Copysign(seconds, degrees)
    448 	case 3:
    449 		degrees, err = strconv.ParseFloat(parts[0], 64)
    450 		if err != nil {
    451 			return 0.0, fmt.Errorf(unparsableErrorFmt, s)
    452 		}
    453 		minutes, err = strconv.ParseFloat(parts[1], 64)
    454 		if err != nil {
    455 			return 0.0, fmt.Errorf(unparsableErrorFmt, s)
    456 		}
    457 		minutes = math.Copysign(minutes, degrees)
    458 		seconds, err = strconv.ParseFloat(parts[2], 64)
    459 		if err != nil {
    460 			return 0.0, fmt.Errorf(unparsableErrorFmt, s)
    461 		}
    462 		seconds = math.Copysign(seconds, degrees)
    463 	default:
    464 		return 0.0, fmt.Errorf(unparsableErrorFmt, s)
    465 	}
    466 	return degrees + minutes/60.0 + seconds/3600.0, nil
    467 }
    468 
    469 func parse3Rat2(tag *tiff.Tag) ([3]float64, error) {
    470 	v := [3]float64{}
    471 	for i := range v {
    472 		num, den, err := tag.Rat2(i)
    473 		if err != nil {
    474 			return v, err
    475 		}
    476 		v[i] = ratFloat(num, den)
    477 		if tag.Count < uint32(i+2) {
    478 			break
    479 		}
    480 	}
    481 	return v, nil
    482 }
    483 
    484 func tagDegrees(tag *tiff.Tag) (float64, error) {
    485 	switch tag.Format() {
    486 	case tiff.RatVal:
    487 		// The usual case, according to the Exif spec
    488 		// (http://www.kodak.com/global/plugins/acrobat/en/service/digCam/exifStandard2.pdf,
    489 		// sec 4.6.6, p. 52 et seq.)
    490 		v, err := parse3Rat2(tag)
    491 		if err != nil {
    492 			return 0.0, err
    493 		}
    494 		return v[0] + v[1]/60 + v[2]/3600.0, nil
    495 	case tiff.StringVal:
    496 		// Encountered this weird case with a panorama picture taken with a HTC phone
    497 		s, err := tag.StringVal()
    498 		if err != nil {
    499 			return 0.0, err
    500 		}
    501 		return parseTagDegreesString(s)
    502 	default:
    503 		// don't know how to parse value, give up
    504 		return 0.0, fmt.Errorf("Malformed EXIF Tag Degrees")
    505 	}
    506 }
    507 
    508 // LatLong returns the latitude and longitude of the photo and
    509 // whether it was present.
    510 func (x *Exif) LatLong() (lat, long float64, err error) {
    511 	// All calls of x.Get might return an TagNotPresentError
    512 	longTag, err := x.Get(FieldName("GPSLongitude"))
    513 	if err != nil {
    514 		return
    515 	}
    516 	ewTag, err := x.Get(FieldName("GPSLongitudeRef"))
    517 	if err != nil {
    518 		return
    519 	}
    520 	latTag, err := x.Get(FieldName("GPSLatitude"))
    521 	if err != nil {
    522 		return
    523 	}
    524 	nsTag, err := x.Get(FieldName("GPSLatitudeRef"))
    525 	if err != nil {
    526 		return
    527 	}
    528 	if long, err = tagDegrees(longTag); err != nil {
    529 		return 0, 0, fmt.Errorf("Cannot parse longitude: %v", err)
    530 	}
    531 	if lat, err = tagDegrees(latTag); err != nil {
    532 		return 0, 0, fmt.Errorf("Cannot parse latitude: %v", err)
    533 	}
    534 	ew, err := ewTag.StringVal()
    535 	if err == nil && ew == "W" {
    536 		long *= -1.0
    537 	} else if err != nil {
    538 		return 0, 0, fmt.Errorf("Cannot parse longitude: %v", err)
    539 	}
    540 	ns, err := nsTag.StringVal()
    541 	if err == nil && ns == "S" {
    542 		lat *= -1.0
    543 	} else if err != nil {
    544 		return 0, 0, fmt.Errorf("Cannot parse longitude: %v", err)
    545 	}
    546 	return lat, long, nil
    547 }
    548 
    549 // String returns a pretty text representation of the decoded exif data.
    550 func (x *Exif) String() string {
    551 	var buf bytes.Buffer
    552 	for name, tag := range x.main {
    553 		fmt.Fprintf(&buf, "%s: %s\n", name, tag)
    554 	}
    555 	return buf.String()
    556 }
    557 
    558 // JpegThumbnail returns the jpeg thumbnail if it exists. If it doesn't exist,
    559 // TagNotPresentError will be returned
    560 func (x *Exif) JpegThumbnail() ([]byte, error) {
    561 	offset, err := x.Get(ThumbJPEGInterchangeFormat)
    562 	if err != nil {
    563 		return nil, err
    564 	}
    565 	start, err := offset.Int(0)
    566 	if err != nil {
    567 		return nil, err
    568 	}
    569 
    570 	length, err := x.Get(ThumbJPEGInterchangeFormatLength)
    571 	if err != nil {
    572 		return nil, err
    573 	}
    574 	l, err := length.Int(0)
    575 	if err != nil {
    576 		return nil, err
    577 	}
    578 
    579 	return x.Raw[start : start+l], nil
    580 }
    581 
    582 // MarshalJson implements the encoding/json.Marshaler interface providing output of
    583 // all EXIF fields present (names and values).
    584 func (x Exif) MarshalJSON() ([]byte, error) {
    585 	return json.Marshal(x.main)
    586 }
    587 
    588 type appSec struct {
    589 	marker byte
    590 	data   []byte
    591 }
    592 
    593 // newAppSec finds marker in r and returns the corresponding application data
    594 // section.
    595 func newAppSec(marker byte, r io.Reader) (*appSec, error) {
    596 	br := bufio.NewReader(r)
    597 	app := &appSec{marker: marker}
    598 	var dataLen int
    599 
    600 	// seek to marker
    601 	for dataLen == 0 {
    602 		if _, err := br.ReadBytes(0xFF); err != nil {
    603 			return nil, err
    604 		}
    605 		c, err := br.ReadByte()
    606 		if err != nil {
    607 			return nil, err
    608 		} else if c != marker {
    609 			continue
    610 		}
    611 
    612 		dataLenBytes := make([]byte, 2)
    613 		for k, _ := range dataLenBytes {
    614 			c, err := br.ReadByte()
    615 			if err != nil {
    616 				return nil, err
    617 			}
    618 			dataLenBytes[k] = c
    619 		}
    620 		dataLen = int(binary.BigEndian.Uint16(dataLenBytes)) - 2
    621 	}
    622 
    623 	// read section data
    624 	nread := 0
    625 	for nread < dataLen {
    626 		s := make([]byte, dataLen-nread)
    627 		n, err := br.Read(s)
    628 		nread += n
    629 		if err != nil && nread < dataLen {
    630 			return nil, err
    631 		}
    632 		app.data = append(app.data, s[:n]...)
    633 	}
    634 	return app, nil
    635 }
    636 
    637 // reader returns a reader on this appSec.
    638 func (app *appSec) reader() *bytes.Reader {
    639 	return bytes.NewReader(app.data)
    640 }
    641 
    642 // exifReader returns a reader on this appSec with the read cursor advanced to
    643 // the start of the exif's tiff encoded portion.
    644 func (app *appSec) exifReader() (*bytes.Reader, error) {
    645 	if len(app.data) < 6 {
    646 		return nil, errors.New("exif: failed to find exif intro marker")
    647 	}
    648 
    649 	// read/check for exif special mark
    650 	exif := app.data[:6]
    651 	if !bytes.Equal(exif, append([]byte("Exif"), 0x00, 0x00)) {
    652 		return nil, errors.New("exif: failed to find exif intro marker")
    653 	}
    654 	return bytes.NewReader(app.data[6:]), nil
    655 }