photoprism-client-go/vendor/github.com/dsoprea/go-heic-exif-extractor/media_parser.go

222 lines
5.0 KiB
Go
Raw Normal View History

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()
}