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 }