222 lines
5.0 KiB
Go
222 lines
5.0 KiB
Go
package heicexif
|
|
|
|
import (
|
|
"bytes"
|
|
"io"
|
|
"os"
|
|
"sort"
|
|
|
|
"github.com/dsoprea/go-exif/v2"
|
|
log "github.com/dsoprea/go-logging"
|
|
ridata "github.com/dsoprea/go-utility/data"
|
|
rifs "github.com/dsoprea/go-utility/filesystem"
|
|
riimage "github.com/dsoprea/go-utility/image"
|
|
"go4.org/media/heif"
|
|
"go4.org/media/heif/bmff"
|
|
)
|
|
|
|
const (
|
|
isoFtypBox = "ftyp"
|
|
|
|
// MinimumHeicStreamLengthForDetection describes the minimum amount of data
|
|
// required to detect HEIC content.
|
|
MinimumHeicStreamLengthForDetection = 1000
|
|
|
|
// HeicMimeType describes the theoretical, official HEIC mime-type
|
|
// (theoretical because it's not ubiquitously supported/known in tooling,
|
|
// yet).
|
|
HeicMimeType = "image/heif"
|
|
)
|
|
|
|
var (
|
|
// HeicBrands is the list of MP4 brands that are associated with HEIC
|
|
// streams.
|
|
HeicBrands = sort.StringSlice{
|
|
"mif1",
|
|
"msf1",
|
|
"heic",
|
|
}
|
|
)
|
|
|
|
// HeicExifContext is just a simple type-alias for a byte-array.
|
|
type HeicExifContext []byte
|
|
|
|
// Exif parses the raw EXIF data and returns the root IFD.
|
|
func (hec HeicExifContext) Exif() (rootIfd *exif.Ifd, data []byte, err error) {
|
|
defer func() {
|
|
if state := recover(); state != nil {
|
|
err = log.Wrap(state.(error))
|
|
}
|
|
}()
|
|
|
|
im := exif.NewIfdMappingWithStandard()
|
|
ti := exif.NewTagIndex()
|
|
|
|
_, index, err := exif.Collect(im, ti, hec)
|
|
log.PanicIf(err)
|
|
|
|
return index.RootIfd, hec, nil
|
|
}
|
|
|
|
// HeicExifMediaParser satisfies the `riimage.MediaParser` interface.
|
|
type HeicExifMediaParser struct {
|
|
}
|
|
|
|
// NewHeicExifMediaParser returns a new HeicExifMediaParser instance.
|
|
func NewHeicExifMediaParser() *HeicExifMediaParser {
|
|
return new(HeicExifMediaParser)
|
|
}
|
|
|
|
// Parse parses a stream using an `io.ReadSeeker`.
|
|
func (hemp *HeicExifMediaParser) Parse(rs io.ReadSeeker, size int) (mc riimage.MediaContext, err error) {
|
|
defer func() {
|
|
if state := recover(); state != nil {
|
|
err = log.Wrap(state.(error))
|
|
}
|
|
}()
|
|
|
|
ra := rifs.NewReadSeekerToReaderAt(rs)
|
|
hf := heif.Open(ra)
|
|
|
|
rawExif, err := hf.EXIF()
|
|
if err != nil {
|
|
if err == heif.ErrNoEXIF {
|
|
return nil, exif.ErrNoExif
|
|
}
|
|
|
|
log.Panic(err)
|
|
}
|
|
|
|
// NOTE(dustin): Kludge for broken third-party functionality. The EXIF data
|
|
// doesn't actually start on the first byte.
|
|
//
|
|
// https://github.com/go4org/go4/issues/51
|
|
rawExif, err = exif.SearchAndExtractExif(rawExif)
|
|
log.PanicIf(err)
|
|
|
|
return HeicExifContext(rawExif), nil
|
|
}
|
|
|
|
// ParseFile parses a stream using a file.
|
|
func (hemp *HeicExifMediaParser) ParseFile(filepath string) (mc riimage.MediaContext, err error) {
|
|
defer func() {
|
|
if state := recover(); state != nil {
|
|
err = log.Wrap(state.(error))
|
|
}
|
|
}()
|
|
|
|
f, err := os.Open(filepath)
|
|
log.PanicIf(err)
|
|
|
|
defer f.Close()
|
|
|
|
mc, err = hemp.Parse(f, 0)
|
|
if err != nil {
|
|
if err == exif.ErrNoExif {
|
|
return nil, exif.ErrNoExif
|
|
}
|
|
|
|
log.Panic(err)
|
|
}
|
|
|
|
return mc, nil
|
|
}
|
|
|
|
// ParseBytes parses a stream direct from bytes.
|
|
func (hemp *HeicExifMediaParser) ParseBytes(data []byte) (mc riimage.MediaContext, err error) {
|
|
defer func() {
|
|
if state := recover(); state != nil {
|
|
err = log.Wrap(state.(error))
|
|
}
|
|
}()
|
|
|
|
br := bytes.NewReader(data)
|
|
|
|
mc, err = hemp.Parse(br, 0)
|
|
if err != nil {
|
|
if err == exif.ErrNoExif {
|
|
return nil, exif.ErrNoExif
|
|
}
|
|
|
|
log.Panic(err)
|
|
}
|
|
|
|
return mc, nil
|
|
}
|
|
|
|
// LooksLikeFormat parses the data to determine if it's a compatible format.
|
|
func (hemp *HeicExifMediaParser) LooksLikeFormat(data []byte) bool {
|
|
// We need to assert that there's enough data to parse. Since the box that
|
|
// we care about is just about the first thing that is parsed, we shouldn't
|
|
// require very much, and this should be universally the case.
|
|
if len(data) < MinimumHeicStreamLengthForDetection {
|
|
return false
|
|
}
|
|
|
|
br := bytes.NewReader(data)
|
|
|
|
// Do a low-cost check for a mime-type, first. Currently, the mime-type for
|
|
// HEIC is still largely unsupported.
|
|
|
|
mimetype, err := ridata.GetMimetypeFromContent(br, MinimumHeicStreamLengthForDetection)
|
|
log.PanicIf(err)
|
|
|
|
if mimetype == HeicMimeType {
|
|
return true
|
|
}
|
|
|
|
// We were unable to detect HEIC based on the mime-type. Try to parse
|
|
// the first couple of boxes (we may not have enough data for more than
|
|
// that, but it's not necessary).
|
|
|
|
_, err = br.Seek(0, io.SeekStart)
|
|
log.PanicIf(err)
|
|
|
|
// We just need this to get us to the first box.
|
|
heif.Open(br)
|
|
|
|
r := bmff.NewReader(br)
|
|
|
|
boxHeader, err := r.ReadBox()
|
|
if err != nil {
|
|
return false
|
|
}
|
|
|
|
box, err := boxHeader.Parse()
|
|
if err != nil {
|
|
return false
|
|
}
|
|
|
|
// It's an ISO 14496-12 stream.
|
|
|
|
// Check if the first box is a "ftyp" type.
|
|
if boxHeader.Type().String() != isoFtypBox {
|
|
return false
|
|
}
|
|
|
|
ftyp, ok := box.(*bmff.FileTypeBox)
|
|
if ok != true {
|
|
// Box identifies as 'ftyp' but it parses-out to the wrong type.
|
|
return false
|
|
}
|
|
|
|
i := HeicBrands.Search(ftyp.MajorBrand)
|
|
if i < len(HeicBrands) && HeicBrands[i] == ftyp.MajorBrand {
|
|
return true
|
|
}
|
|
|
|
// If we get here, We got a stream that's eerily similar to an HEIC stream
|
|
// without being one.
|
|
|
|
return false
|
|
}
|
|
|
|
var (
|
|
// Enforce that `HeicExifMediaParser` looks like a `riimage.MediaParser`.
|
|
_ riimage.MediaParser = new(HeicExifMediaParser)
|
|
)
|
|
|
|
func init() {
|
|
HeicBrands.Sort()
|
|
}
|