293 lines
7.3 KiB
Go
293 lines
7.3 KiB
Go
|
/*
|
||
|
Copyright 2018 The go4 Authors
|
||
|
|
||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||
|
you may not use this file except in compliance with the License.
|
||
|
You may obtain a copy of the License at
|
||
|
|
||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||
|
|
||
|
Unless required by applicable law or agreed to in writing, software
|
||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||
|
See the License for the specific language governing permissions and
|
||
|
limitations under the License.
|
||
|
*/
|
||
|
|
||
|
// Package heif reads HEIF containers, as found in Apple HEIC/HEVC images.
|
||
|
// This package does not decode images; it only reads the metadata.
|
||
|
//
|
||
|
// This package is a work in progress and makes no API compatibility
|
||
|
// promises.
|
||
|
package heif
|
||
|
|
||
|
import (
|
||
|
"errors"
|
||
|
"fmt"
|
||
|
"io"
|
||
|
"log"
|
||
|
|
||
|
"go4.org/media/heif/bmff"
|
||
|
)
|
||
|
|
||
|
// File represents a HEIF file.
|
||
|
//
|
||
|
// Methods on File should not be called concurrently.
|
||
|
type File struct {
|
||
|
ra io.ReaderAt
|
||
|
primary *Item
|
||
|
|
||
|
// Populated lazily, by getMeta:
|
||
|
metaErr error
|
||
|
meta *BoxMeta
|
||
|
}
|
||
|
|
||
|
// BoxMeta contains the low-level BMFF metadata boxes.
|
||
|
type BoxMeta struct {
|
||
|
FileType *bmff.FileTypeBox
|
||
|
Handler *bmff.HandlerBox
|
||
|
PrimaryItem *bmff.PrimaryItemBox
|
||
|
ItemInfo *bmff.ItemInfoBox
|
||
|
Properties *bmff.ItemPropertiesBox
|
||
|
ItemLocation *bmff.ItemLocationBox
|
||
|
}
|
||
|
|
||
|
// EXIFItemID returns the item ID of the EXIF part, or 0 if not found.
|
||
|
func (m *BoxMeta) EXIFItemID() uint32 {
|
||
|
if m.ItemInfo == nil {
|
||
|
return 0
|
||
|
}
|
||
|
for _, ife := range m.ItemInfo.ItemInfos {
|
||
|
if ife.ItemType == "Exif" {
|
||
|
return uint32(ife.ItemID)
|
||
|
}
|
||
|
}
|
||
|
return 0
|
||
|
}
|
||
|
|
||
|
// Item represents an item in a HEIF file.
|
||
|
type Item struct {
|
||
|
f *File
|
||
|
|
||
|
ID uint32
|
||
|
Info *bmff.ItemInfoEntry
|
||
|
Location *bmff.ItemLocationBoxEntry // location in file
|
||
|
Properties []bmff.Box
|
||
|
}
|
||
|
|
||
|
// SpatialExtents returns the item's spatial extents property values, if present,
|
||
|
// not correcting from any camera rotation metadata.
|
||
|
func (it *Item) SpatialExtents() (width, height int, ok bool) {
|
||
|
for _, p := range it.Properties {
|
||
|
if p, ok := p.(*bmff.ImageSpatialExtentsProperty); ok {
|
||
|
return int(p.ImageWidth), int(p.ImageHeight), true
|
||
|
}
|
||
|
}
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// Rotations returns the number of 90 degree rotations counter-clockwise that this
|
||
|
// image should be rendered at, in the range [0,3].
|
||
|
func (it *Item) Rotations() int {
|
||
|
for _, p := range it.Properties {
|
||
|
if p, ok := p.(*bmff.ImageRotation); ok {
|
||
|
return int(p.Angle)
|
||
|
}
|
||
|
}
|
||
|
return 0
|
||
|
}
|
||
|
|
||
|
// VisualDimensions returns the item's width and height after correcting
|
||
|
// for any rotations.
|
||
|
func (it *Item) VisualDimensions() (width, height int, ok bool) {
|
||
|
width, height, ok = it.SpatialExtents()
|
||
|
for i := 0; i < it.Rotations(); i++ {
|
||
|
width, height = height, width
|
||
|
}
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// TODO: add HEIF imir (mirroring) accessor, like Image.SpatialExtents.
|
||
|
|
||
|
// Open returns a handle to access a HEIF file.
|
||
|
func Open(f io.ReaderAt) *File {
|
||
|
return &File{ra: f}
|
||
|
}
|
||
|
|
||
|
// ErrNoEXIF is returned by File.EXIF when a file does not contain an EXIF item.
|
||
|
var ErrNoEXIF = errors.New("heif: no EXIF found")
|
||
|
|
||
|
// ErrUnknownItem is returned by File.ItemByID for unknown items.
|
||
|
var ErrUnknownItem = errors.New("heif: unknown item")
|
||
|
|
||
|
// EXIF returns the raw EXIF data from the file.
|
||
|
// The error is ErrNoEXIF if the file did not contain EXIF.
|
||
|
//
|
||
|
// The raw EXIF data can be parsed by the
|
||
|
// github.com/rwcarlsen/goexif/exif package's Decode function.
|
||
|
func (f *File) EXIF() ([]byte, error) {
|
||
|
meta, err := f.getMeta()
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
exifID := meta.EXIFItemID()
|
||
|
if exifID == 0 {
|
||
|
return nil, ErrNoEXIF
|
||
|
}
|
||
|
it, err := f.ItemByID(exifID)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
if it.Location == nil {
|
||
|
return nil, errors.New("heif: file said it contained EXIF, but didn't say where")
|
||
|
}
|
||
|
if n := len(it.Location.Extents); n != 1 {
|
||
|
return nil, fmt.Errorf("heif: expected 1 EXIF section, saw %d", n)
|
||
|
}
|
||
|
offLen := it.Location.Extents[0]
|
||
|
const maxSize = 20 << 10 // 20MB of EXIF seems excessive; cap it for sanity
|
||
|
if offLen.Length > maxSize {
|
||
|
return nil, fmt.Errorf("heif: declared EXIF size %d exceeds threshold of %d bytes", offLen.Length, maxSize)
|
||
|
}
|
||
|
buf := make([]byte, offLen.Length-4)
|
||
|
n, err := f.ra.ReadAt(buf, int64(offLen.Offset)+4) // TODO: why 4? did I miss something?
|
||
|
if err != nil {
|
||
|
log.Printf("Read %d bytes + %v: %q", n, err, buf)
|
||
|
return nil, err
|
||
|
}
|
||
|
return buf, nil
|
||
|
}
|
||
|
|
||
|
func (f *File) setMetaErr(err error) error {
|
||
|
if f.metaErr != nil {
|
||
|
f.metaErr = err
|
||
|
}
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
func (f *File) getMeta() (*BoxMeta, error) {
|
||
|
if f.metaErr != nil {
|
||
|
return nil, f.metaErr
|
||
|
}
|
||
|
if f.meta != nil {
|
||
|
return f.meta, nil
|
||
|
}
|
||
|
const assumedMaxSize = 5 << 40 // arbitrary
|
||
|
sr := io.NewSectionReader(f.ra, 0, assumedMaxSize)
|
||
|
bmr := bmff.NewReader(sr)
|
||
|
|
||
|
meta := &BoxMeta{}
|
||
|
|
||
|
pbox, err := bmr.ReadAndParseBox(bmff.TypeFtyp)
|
||
|
if err != nil {
|
||
|
return nil, f.setMetaErr(err)
|
||
|
}
|
||
|
meta.FileType = pbox.(*bmff.FileTypeBox)
|
||
|
|
||
|
pbox, err = bmr.ReadAndParseBox(bmff.TypeMeta)
|
||
|
if err != nil {
|
||
|
return nil, f.setMetaErr(err)
|
||
|
}
|
||
|
metabox := pbox.(*bmff.MetaBox)
|
||
|
|
||
|
for _, box := range metabox.Children {
|
||
|
boxp, err := box.Parse()
|
||
|
if err == bmff.ErrUnknownBox {
|
||
|
continue
|
||
|
}
|
||
|
if err != nil {
|
||
|
return nil, f.setMetaErr(err)
|
||
|
}
|
||
|
switch v := boxp.(type) {
|
||
|
case *bmff.HandlerBox:
|
||
|
meta.Handler = v
|
||
|
case *bmff.PrimaryItemBox:
|
||
|
meta.PrimaryItem = v
|
||
|
case *bmff.ItemInfoBox:
|
||
|
meta.ItemInfo = v
|
||
|
case *bmff.ItemPropertiesBox:
|
||
|
meta.Properties = v
|
||
|
case *bmff.ItemLocationBox:
|
||
|
meta.ItemLocation = v
|
||
|
}
|
||
|
}
|
||
|
|
||
|
f.meta = meta
|
||
|
return f.meta, nil
|
||
|
}
|
||
|
|
||
|
// PrimaryItem returns the HEIF file's primary item.
|
||
|
func (f *File) PrimaryItem() (*Item, error) {
|
||
|
meta, err := f.getMeta()
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
if meta.PrimaryItem == nil {
|
||
|
return nil, errors.New("heif: HEIF file lacks primary item box")
|
||
|
}
|
||
|
return f.ItemByID(uint32(meta.PrimaryItem.ItemID))
|
||
|
}
|
||
|
|
||
|
// ItemByID by returns the file's Item of a given ID.
|
||
|
// If the ID is known, the returned error is ErrUnknownItem.
|
||
|
func (f *File) ItemByID(id uint32) (*Item, error) {
|
||
|
meta, err := f.getMeta()
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
it := &Item{
|
||
|
f: f,
|
||
|
ID: id,
|
||
|
}
|
||
|
if meta.ItemLocation != nil {
|
||
|
for _, ilbe := range meta.ItemLocation.Items {
|
||
|
if uint32(ilbe.ItemID) == id {
|
||
|
shallowCopy := ilbe
|
||
|
it.Location = &shallowCopy
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
if meta.ItemInfo != nil {
|
||
|
for _, iie := range meta.ItemInfo.ItemInfos {
|
||
|
if uint32(iie.ItemID) == id {
|
||
|
it.Info = iie
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
if it.Info == nil {
|
||
|
return nil, ErrUnknownItem
|
||
|
}
|
||
|
if meta.Properties != nil {
|
||
|
allProps := meta.Properties.PropertyContainer.Properties
|
||
|
for _, ipa := range meta.Properties.Associations {
|
||
|
// TODO: I've never seen a file with more than
|
||
|
// top-level ItemPropertyAssociation box, but
|
||
|
// apparently they can exist with different
|
||
|
// versions/flags. For now we just merge them
|
||
|
// all together, but that's not really right.
|
||
|
// So for now, just bail once a previous loop
|
||
|
// found anything.
|
||
|
if len(it.Properties) > 0 {
|
||
|
break
|
||
|
}
|
||
|
|
||
|
for _, ipai := range ipa.Entries {
|
||
|
if ipai.ItemID != id {
|
||
|
continue
|
||
|
}
|
||
|
for _, ass := range ipai.Associations {
|
||
|
if ass.Index != 0 && int(ass.Index) <= len(allProps) {
|
||
|
box := allProps[ass.Index-1]
|
||
|
boxp, err := box.Parse()
|
||
|
if err == nil {
|
||
|
box = boxp
|
||
|
}
|
||
|
it.Properties = append(it.Properties, box)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
return it, nil
|
||
|
}
|