go4.org@v0.0.0-20230225012048-214862532bf5/media/heif/heif.go (about)

     1  /*
     2  Copyright 2018 The go4 Authors
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8       http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  // Package heif reads HEIF containers, as found in Apple HEIC/HEVC images.
    18  // This package does not decode images; it only reads the metadata.
    19  //
    20  // This package is a work in progress and makes no API compatibility
    21  // promises.
    22  package heif
    23  
    24  import (
    25  	"errors"
    26  	"fmt"
    27  	"io"
    28  	"log"
    29  
    30  	"go4.org/media/heif/bmff"
    31  )
    32  
    33  // File represents a HEIF file.
    34  //
    35  // Methods on File should not be called concurrently.
    36  type File struct {
    37  	ra      io.ReaderAt
    38  	primary *Item
    39  
    40  	// Populated lazily, by getMeta:
    41  	metaErr error
    42  	meta    *BoxMeta
    43  }
    44  
    45  // BoxMeta contains the low-level BMFF metadata boxes.
    46  type BoxMeta struct {
    47  	FileType     *bmff.FileTypeBox
    48  	Handler      *bmff.HandlerBox
    49  	PrimaryItem  *bmff.PrimaryItemBox
    50  	ItemInfo     *bmff.ItemInfoBox
    51  	Properties   *bmff.ItemPropertiesBox
    52  	ItemLocation *bmff.ItemLocationBox
    53  }
    54  
    55  // EXIFItemID returns the item ID of the EXIF part, or 0 if not found.
    56  func (m *BoxMeta) EXIFItemID() uint32 {
    57  	if m.ItemInfo == nil {
    58  		return 0
    59  	}
    60  	for _, ife := range m.ItemInfo.ItemInfos {
    61  		if ife.ItemType == "Exif" {
    62  			return uint32(ife.ItemID)
    63  		}
    64  	}
    65  	return 0
    66  }
    67  
    68  // Item represents an item in a HEIF file.
    69  type Item struct {
    70  	f *File
    71  
    72  	ID         uint32
    73  	Info       *bmff.ItemInfoEntry
    74  	Location   *bmff.ItemLocationBoxEntry // location in file
    75  	Properties []bmff.Box
    76  }
    77  
    78  // SpatialExtents returns the item's spatial extents property values, if present,
    79  // not correcting from any camera rotation metadata.
    80  func (it *Item) SpatialExtents() (width, height int, ok bool) {
    81  	for _, p := range it.Properties {
    82  		if p, ok := p.(*bmff.ImageSpatialExtentsProperty); ok {
    83  			return int(p.ImageWidth), int(p.ImageHeight), true
    84  		}
    85  	}
    86  	return
    87  }
    88  
    89  // Rotations returns the number of 90 degree rotations counter-clockwise that this
    90  // image should be rendered at, in the range [0,3].
    91  func (it *Item) Rotations() int {
    92  	for _, p := range it.Properties {
    93  		if p, ok := p.(*bmff.ImageRotation); ok {
    94  			return int(p.Angle)
    95  		}
    96  	}
    97  	return 0
    98  }
    99  
   100  // VisualDimensions returns the item's width and height after correcting
   101  // for any rotations.
   102  func (it *Item) VisualDimensions() (width, height int, ok bool) {
   103  	width, height, ok = it.SpatialExtents()
   104  	for i := 0; i < it.Rotations(); i++ {
   105  		width, height = height, width
   106  	}
   107  	return
   108  }
   109  
   110  // TODO: add HEIF imir (mirroring) accessor, like Image.SpatialExtents.
   111  
   112  // Open returns a handle to access a HEIF file.
   113  func Open(f io.ReaderAt) *File {
   114  	return &File{ra: f}
   115  }
   116  
   117  // ErrNoEXIF is returned by File.EXIF when a file does not contain an EXIF item.
   118  var ErrNoEXIF = errors.New("heif: no EXIF found")
   119  
   120  // ErrUnknownItem is returned by File.ItemByID for unknown items.
   121  var ErrUnknownItem = errors.New("heif: unknown item")
   122  
   123  // EXIF returns the raw EXIF data from the file.
   124  // The error is ErrNoEXIF if the file did not contain EXIF.
   125  //
   126  // The raw EXIF data can be parsed by the
   127  // github.com/rwcarlsen/goexif/exif package's Decode function.
   128  func (f *File) EXIF() ([]byte, error) {
   129  	meta, err := f.getMeta()
   130  	if err != nil {
   131  		return nil, err
   132  	}
   133  	exifID := meta.EXIFItemID()
   134  	if exifID == 0 {
   135  		return nil, ErrNoEXIF
   136  	}
   137  	it, err := f.ItemByID(exifID)
   138  	if err != nil {
   139  		return nil, err
   140  	}
   141  	if it.Location == nil {
   142  		return nil, errors.New("heif: file said it contained EXIF, but didn't say where")
   143  	}
   144  	if n := len(it.Location.Extents); n != 1 {
   145  		return nil, fmt.Errorf("heif: expected 1 EXIF section, saw %d", n)
   146  	}
   147  	offLen := it.Location.Extents[0]
   148  	const maxSize = 20 << 10 // 20MB of EXIF seems excessive; cap it for sanity
   149  	if offLen.Length > maxSize {
   150  		return nil, fmt.Errorf("heif: declared EXIF size %d exceeds threshold of %d bytes", offLen.Length, maxSize)
   151  	}
   152  	buf := make([]byte, offLen.Length-4)
   153  	n, err := f.ra.ReadAt(buf, int64(offLen.Offset)+4) // TODO: why 4? did I miss something?
   154  	if err != nil {
   155  		log.Printf("Read %d bytes + %v: %q", n, err, buf)
   156  		return nil, err
   157  	}
   158  	return buf, nil
   159  }
   160  
   161  func (f *File) setMetaErr(err error) error {
   162  	if f.metaErr != nil {
   163  		f.metaErr = err
   164  	}
   165  	return err
   166  }
   167  
   168  func (f *File) getMeta() (*BoxMeta, error) {
   169  	if f.metaErr != nil {
   170  		return nil, f.metaErr
   171  	}
   172  	if f.meta != nil {
   173  		return f.meta, nil
   174  	}
   175  	const assumedMaxSize = 5 << 40 // arbitrary
   176  	sr := io.NewSectionReader(f.ra, 0, assumedMaxSize)
   177  	bmr := bmff.NewReader(sr)
   178  
   179  	meta := &BoxMeta{}
   180  
   181  	pbox, err := bmr.ReadAndParseBox(bmff.TypeFtyp)
   182  	if err != nil {
   183  		return nil, f.setMetaErr(err)
   184  	}
   185  	meta.FileType = pbox.(*bmff.FileTypeBox)
   186  
   187  	pbox, err = bmr.ReadAndParseBox(bmff.TypeMeta)
   188  	if err != nil {
   189  		return nil, f.setMetaErr(err)
   190  	}
   191  	metabox := pbox.(*bmff.MetaBox)
   192  
   193  	for _, box := range metabox.Children {
   194  		boxp, err := box.Parse()
   195  		if err == bmff.ErrUnknownBox {
   196  			continue
   197  		}
   198  		if err != nil {
   199  			return nil, f.setMetaErr(err)
   200  		}
   201  		switch v := boxp.(type) {
   202  		case *bmff.HandlerBox:
   203  			meta.Handler = v
   204  		case *bmff.PrimaryItemBox:
   205  			meta.PrimaryItem = v
   206  		case *bmff.ItemInfoBox:
   207  			meta.ItemInfo = v
   208  		case *bmff.ItemPropertiesBox:
   209  			meta.Properties = v
   210  		case *bmff.ItemLocationBox:
   211  			meta.ItemLocation = v
   212  		}
   213  	}
   214  
   215  	f.meta = meta
   216  	return f.meta, nil
   217  }
   218  
   219  // PrimaryItem returns the HEIF file's primary item.
   220  func (f *File) PrimaryItem() (*Item, error) {
   221  	meta, err := f.getMeta()
   222  	if err != nil {
   223  		return nil, err
   224  	}
   225  	if meta.PrimaryItem == nil {
   226  		return nil, errors.New("heif: HEIF file lacks primary item box")
   227  	}
   228  	return f.ItemByID(uint32(meta.PrimaryItem.ItemID))
   229  }
   230  
   231  // ItemByID by returns the file's Item of a given ID.
   232  // If the ID is known, the returned error is ErrUnknownItem.
   233  func (f *File) ItemByID(id uint32) (*Item, error) {
   234  	meta, err := f.getMeta()
   235  	if err != nil {
   236  		return nil, err
   237  	}
   238  	it := &Item{
   239  		f:  f,
   240  		ID: id,
   241  	}
   242  	if meta.ItemLocation != nil {
   243  		for _, ilbe := range meta.ItemLocation.Items {
   244  			if uint32(ilbe.ItemID) == id {
   245  				shallowCopy := ilbe
   246  				it.Location = &shallowCopy
   247  			}
   248  		}
   249  	}
   250  	if meta.ItemInfo != nil {
   251  		for _, iie := range meta.ItemInfo.ItemInfos {
   252  			if uint32(iie.ItemID) == id {
   253  				it.Info = iie
   254  			}
   255  		}
   256  	}
   257  	if it.Info == nil {
   258  		return nil, ErrUnknownItem
   259  	}
   260  	if meta.Properties != nil {
   261  		allProps := meta.Properties.PropertyContainer.Properties
   262  		for _, ipa := range meta.Properties.Associations {
   263  			// TODO: I've never seen a file with more than
   264  			// top-level ItemPropertyAssociation box, but
   265  			// apparently they can exist with different
   266  			// versions/flags. For now we just merge them
   267  			// all together, but that's not really right.
   268  			// So for now, just bail once a previous loop
   269  			// found anything.
   270  			if len(it.Properties) > 0 {
   271  				break
   272  			}
   273  
   274  			for _, ipai := range ipa.Entries {
   275  				if ipai.ItemID != id {
   276  					continue
   277  				}
   278  				for _, ass := range ipai.Associations {
   279  					if ass.Index != 0 && int(ass.Index) <= len(allProps) {
   280  						box := allProps[ass.Index-1]
   281  						boxp, err := box.Parse()
   282  						if err == nil {
   283  							box = boxp
   284  						}
   285  						it.Properties = append(it.Properties, box)
   286  					}
   287  				}
   288  			}
   289  		}
   290  	}
   291  	return it, nil
   292  }