mirror of https://github.com/perkeep/perkeep.git
vendor: add go4.org/media/heif
At rev 7b81d6948d11710f710d0c4ef52daac1dc7c936b Updates issue #969 Change-Id: I6f21de58c0865d3cbc8186b3a6834444b6d1206e
This commit is contained in:
parent
326a10c2cb
commit
139cd8bd01
|
@ -297,6 +297,7 @@
|
|||
revision = "9e1ac2306c701ca7477a169b2b49902b7b4c58bf"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "go4.org"
|
||||
packages = [
|
||||
"cloud/cloudlaunch",
|
||||
|
@ -308,6 +309,8 @@
|
|||
"jsonconfig",
|
||||
"legal",
|
||||
"lock",
|
||||
"media/heif",
|
||||
"media/heif/bmff",
|
||||
"net/throttle",
|
||||
"oauthutil",
|
||||
"readerutil",
|
||||
|
@ -319,7 +322,7 @@
|
|||
"wkfs/gcs",
|
||||
"writerutil"
|
||||
]
|
||||
revision = "c3a8ba339e20006b054736f8eb9fc5e1d5fa6eab"
|
||||
revision = "7b81d6948d11710f710d0c4ef52daac1dc7c936b"
|
||||
|
||||
[[projects]]
|
||||
name = "golang.org/x/crypto"
|
||||
|
@ -567,6 +570,6 @@
|
|||
[solve-meta]
|
||||
analyzer-name = "dep"
|
||||
analyzer-version = 1
|
||||
inputs-digest = "ea9200f608a8ca6a12f16d664cf7f251bac039dac1f45eae9ff0edd41c5d656b"
|
||||
inputs-digest = "aeffe8df9be98b70a039ac4878af79fd9ce3832218db0f1a62e349bfe4639c74"
|
||||
solver-name = "gps-cdcl"
|
||||
solver-version = 1
|
||||
|
|
|
@ -226,8 +226,7 @@ ignored = [
|
|||
|
||||
[[constraint]]
|
||||
name = "go4.org"
|
||||
revision = "c3a8ba339e20006b054736f8eb9fc5e1d5fa6eab"
|
||||
|
||||
branch = "master"
|
||||
|
||||
# golang/x
|
||||
|
||||
|
|
|
@ -38,6 +38,7 @@ import (
|
|||
"github.com/hjfreyer/taglib-go/taglib"
|
||||
"github.com/rwcarlsen/goexif/exif"
|
||||
"github.com/rwcarlsen/goexif/tiff"
|
||||
_ "go4.org/media/heif"
|
||||
"go4.org/readerutil"
|
||||
"go4.org/types"
|
||||
"perkeep.org/internal/images"
|
||||
|
|
|
@ -35,6 +35,7 @@ import (
|
|||
|
||||
"go4.org/cloud/google/gceutil"
|
||||
|
||||
"cloud.google.com/go/compute/metadata"
|
||||
"cloud.google.com/go/storage"
|
||||
"golang.org/x/net/context"
|
||||
"golang.org/x/oauth2"
|
||||
|
@ -194,10 +195,13 @@ func (c *Config) MaybeDeploy() {
|
|||
}
|
||||
|
||||
func (c *Config) restartLoop() {
|
||||
if !metadata.OnGCE() {
|
||||
return
|
||||
}
|
||||
if c.RestartPolicy == RestartNever {
|
||||
return
|
||||
}
|
||||
url := "https://storage.googleapis.com/" + c.BinaryBucket + "/" + c.binaryObject()
|
||||
url := c.binaryURL()
|
||||
var lastEtag string
|
||||
for {
|
||||
res, err := http.Head(url + "?" + fmt.Sprint(time.Now().Unix()))
|
||||
|
|
|
@ -0,0 +1,78 @@
|
|||
/*
|
||||
Copyright 2013 The Go 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 lock
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
func init() {
|
||||
lockFn = lockWindows
|
||||
}
|
||||
|
||||
type winUnlocker struct {
|
||||
h windows.Handle
|
||||
abs string
|
||||
// err holds the error returned by Close.
|
||||
err error
|
||||
// once guards the close method call.
|
||||
once sync.Once
|
||||
}
|
||||
|
||||
func (u *winUnlocker) Close() error {
|
||||
u.once.Do(u.close)
|
||||
return u.err
|
||||
}
|
||||
|
||||
func (u *winUnlocker) close() {
|
||||
lockmu.Lock()
|
||||
defer lockmu.Unlock()
|
||||
delete(locked, u.abs)
|
||||
|
||||
u.err = windows.CloseHandle(u.h)
|
||||
}
|
||||
|
||||
func lockWindows(name string) (io.Closer, error) {
|
||||
fi, err := os.Stat(name)
|
||||
if err == nil && fi.Size() > 0 {
|
||||
return nil, fmt.Errorf("can't lock file %q: %s", name, "has non-zero size")
|
||||
}
|
||||
|
||||
handle, err := winCreateEphemeral(name)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("creation of lock %s failed: %v", name, err)
|
||||
}
|
||||
|
||||
return &winUnlocker{h: handle, abs: name}, nil
|
||||
}
|
||||
|
||||
func winCreateEphemeral(name string) (windows.Handle, error) {
|
||||
const (
|
||||
FILE_ATTRIBUTE_TEMPORARY = 0x100
|
||||
FILE_FLAG_DELETE_ON_CLOSE = 0x04000000
|
||||
)
|
||||
handle, err := windows.CreateFile(windows.StringToUTF16Ptr(name), 0, 0, nil, windows.OPEN_ALWAYS, FILE_ATTRIBUTE_TEMPORARY|FILE_FLAG_DELETE_ON_CLOSE, 0)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return handle, nil
|
||||
}
|
|
@ -0,0 +1,818 @@
|
|||
/*
|
||||
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 bmff reads ISO BMFF boxes, as used by HEIF, etc.
|
||||
//
|
||||
// This is not so much as a generic BMFF reader as it is a BMFF reader
|
||||
// as needed by HEIF, though that may change in time. For now, only
|
||||
// boxes necessary for the go4.org/media/heif package have explicit
|
||||
// parsers.
|
||||
//
|
||||
// This package makes no API compatibility promises; it exists
|
||||
// primarily for use by the go4.org/media/heif package.
|
||||
package bmff
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func NewReader(r io.Reader) *Reader {
|
||||
br, ok := r.(*bufio.Reader)
|
||||
if !ok {
|
||||
br = bufio.NewReader(r)
|
||||
}
|
||||
return &Reader{br: bufReader{Reader: br}}
|
||||
}
|
||||
|
||||
type Reader struct {
|
||||
br bufReader
|
||||
lastBox Box // or nil
|
||||
noMoreBoxes bool // a box with size 0 (the final box) was seen
|
||||
}
|
||||
|
||||
type BoxType [4]byte
|
||||
|
||||
// Common box types.
|
||||
var (
|
||||
TypeFtyp = BoxType{'f', 't', 'y', 'p'}
|
||||
TypeMeta = BoxType{'m', 'e', 't', 'a'}
|
||||
)
|
||||
|
||||
func (t BoxType) String() string { return string(t[:]) }
|
||||
|
||||
func (t BoxType) EqualString(s string) bool {
|
||||
// Could be cleaner, but see ohttps://github.com/golang/go/issues/24765
|
||||
return len(s) == 4 && s[0] == t[0] && s[1] == t[1] && s[2] == t[2] && s[3] == t[3]
|
||||
}
|
||||
|
||||
type parseFunc func(b box, br *bufio.Reader) (Box, error)
|
||||
|
||||
// Box represents a BMFF box.
|
||||
type Box interface {
|
||||
Size() int64 // 0 means unknown (will read to end of file)
|
||||
Type() BoxType
|
||||
|
||||
// Parses parses the box, populating the fields
|
||||
// in the returned concrete type.
|
||||
//
|
||||
// If Parse has already been called, Parse returns nil.
|
||||
// If the box type is unknown, the returned error is ErrUnknownBox
|
||||
// and it's guaranteed that no bytes have been read from the box.
|
||||
Parse() (Box, error)
|
||||
|
||||
// Body returns the inner bytes of the box, ignoring the header.
|
||||
// The body may start with the 4 byte header of a "Full Box" if the
|
||||
// box's type derives from a full box. Most users will use Parse
|
||||
// instead.
|
||||
// Body will return a new reader at the beginning of the box if the
|
||||
// outer box has already been parsed.
|
||||
Body() io.Reader
|
||||
}
|
||||
|
||||
// ErrUnknownBox is returned by Box.Parse for unrecognized box types.
|
||||
var ErrUnknownBox = errors.New("heif: unknown box")
|
||||
|
||||
type parserFunc func(b *box, br *bufReader) (Box, error)
|
||||
|
||||
func boxType(s string) BoxType {
|
||||
if len(s) != 4 {
|
||||
panic("bogus boxType length")
|
||||
}
|
||||
return BoxType{s[0], s[1], s[2], s[3]}
|
||||
}
|
||||
|
||||
var parsers = map[BoxType]parserFunc{
|
||||
boxType("dinf"): parseDataInformationBox,
|
||||
boxType("dref"): parseDataReferenceBox,
|
||||
boxType("ftyp"): parseFileTypeBox,
|
||||
boxType("hdlr"): parseHandlerBox,
|
||||
boxType("iinf"): parseItemInfoBox,
|
||||
boxType("infe"): parseItemInfoEntry,
|
||||
boxType("iloc"): parseItemLocationBox,
|
||||
boxType("ipco"): parseItemPropertyContainerBox,
|
||||
boxType("ipma"): parseItemPropertyAssociation,
|
||||
boxType("iprp"): parseItemPropertiesBox,
|
||||
boxType("ispe"): parseImageSpatialExtentsProperty,
|
||||
boxType("meta"): parseMetaBox,
|
||||
boxType("pitm"): parsePrimaryItemBox,
|
||||
}
|
||||
|
||||
type box struct {
|
||||
size int64 // 0 means unknown, will read to end of file (box container)
|
||||
boxType BoxType
|
||||
body io.Reader
|
||||
parsed Box // if non-nil, the Parsed result
|
||||
slurp []byte // if non-nil, the contents slurped to memory
|
||||
}
|
||||
|
||||
func (b *box) Size() int64 { return b.size }
|
||||
func (b *box) Type() BoxType { return b.boxType }
|
||||
|
||||
func (b *box) Body() io.Reader {
|
||||
if b.slurp != nil {
|
||||
return bytes.NewReader(b.slurp)
|
||||
}
|
||||
return b.body
|
||||
}
|
||||
|
||||
func (b *box) Parse() (Box, error) {
|
||||
if b.parsed != nil {
|
||||
return b.parsed, nil
|
||||
}
|
||||
parser, ok := parsers[b.Type()]
|
||||
if !ok {
|
||||
return nil, ErrUnknownBox
|
||||
}
|
||||
v, err := parser(b, &bufReader{Reader: bufio.NewReader(b.Body())})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
b.parsed = v
|
||||
return v, nil
|
||||
}
|
||||
|
||||
type FullBox struct {
|
||||
*box
|
||||
Version uint8
|
||||
Flags uint32 // 24 bits
|
||||
}
|
||||
|
||||
// ReadBox reads the next box.
|
||||
//
|
||||
// If the previously read box was not read to completion, ReadBox consumes
|
||||
// the rest of its data.
|
||||
//
|
||||
// At the end, the error is io.EOF.
|
||||
func (r *Reader) ReadBox() (Box, error) {
|
||||
if r.noMoreBoxes {
|
||||
return nil, io.EOF
|
||||
}
|
||||
if r.lastBox != nil {
|
||||
if _, err := io.Copy(ioutil.Discard, r.lastBox.Body()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
var buf [8]byte
|
||||
|
||||
_, err := io.ReadFull(r.br, buf[:4])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
box := &box{
|
||||
size: int64(binary.BigEndian.Uint32(buf[:4])),
|
||||
}
|
||||
|
||||
_, err = io.ReadFull(r.br, box.boxType[:]) // 4 more bytes
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Special cases for size:
|
||||
var remain int64
|
||||
switch box.size {
|
||||
case 1:
|
||||
// 1 means it's actually a 64-bit size, after the type.
|
||||
_, err = io.ReadFull(r.br, buf[:8])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
box.size = int64(binary.BigEndian.Uint64(buf[:8]))
|
||||
if box.size < 0 {
|
||||
// Go uses int64 for sizes typically, but BMFF uses uint64.
|
||||
// We assume for now that nobody actually uses boxes larger
|
||||
// than int64.
|
||||
return nil, fmt.Errorf("unexpectedly large box %q", box.boxType)
|
||||
}
|
||||
remain = box.size - 2*4 - 8
|
||||
case 0:
|
||||
// 0 means unknown & to read to end of file. No more boxes.
|
||||
r.noMoreBoxes = true
|
||||
default:
|
||||
remain = box.size - 2*4
|
||||
}
|
||||
if remain < 0 {
|
||||
return nil, fmt.Errorf("Box header for %q has size %d, suggesting %d (negative) bytes remain", box.boxType, box.size, remain)
|
||||
}
|
||||
if box.size > 0 {
|
||||
box.body = io.LimitReader(r.br, remain)
|
||||
} else {
|
||||
box.body = r.br
|
||||
}
|
||||
r.lastBox = box
|
||||
return box, nil
|
||||
}
|
||||
|
||||
// ReadAndParseBox wraps the ReadBox method, ensuring that the read box is of type typ
|
||||
// and parses successfully. It returns the parsed box.
|
||||
func (r *Reader) ReadAndParseBox(typ BoxType) (Box, error) {
|
||||
box, err := r.ReadBox()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error reading %q box: %v", typ, err)
|
||||
}
|
||||
if box.Type() != typ {
|
||||
return nil, fmt.Errorf("error reading %q box: got box type %q instead", typ, box.Type())
|
||||
}
|
||||
pbox, err := box.Parse()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error parsing read %q box: %v", typ, err)
|
||||
}
|
||||
return pbox, nil
|
||||
}
|
||||
|
||||
func readFullBox(outer *box, br *bufReader) (fb FullBox, err error) {
|
||||
fb.box = outer
|
||||
// Parse FullBox header.
|
||||
buf, err := br.Peek(4)
|
||||
if err != nil {
|
||||
return FullBox{}, fmt.Errorf("failed to read 4 bytes of FullBox: %v", err)
|
||||
}
|
||||
fb.Version = buf[0]
|
||||
buf[0] = 0
|
||||
fb.Flags = binary.BigEndian.Uint32(buf[:4])
|
||||
br.Discard(4)
|
||||
return fb, nil
|
||||
}
|
||||
|
||||
type FileTypeBox struct {
|
||||
*box
|
||||
MajorBrand string // 4 bytes
|
||||
MinorVersion string // 4 bytes
|
||||
Compatible []string // all 4 bytes
|
||||
}
|
||||
|
||||
func parseFileTypeBox(outer *box, br *bufReader) (Box, error) {
|
||||
buf, err := br.Peek(8)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ft := &FileTypeBox{
|
||||
box: outer,
|
||||
MajorBrand: string(buf[:4]),
|
||||
MinorVersion: string(buf[4:8]),
|
||||
}
|
||||
br.Discard(8)
|
||||
for {
|
||||
buf, err := br.Peek(4)
|
||||
if err == io.EOF {
|
||||
return ft, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ft.Compatible = append(ft.Compatible, string(buf[:4]))
|
||||
br.Discard(4)
|
||||
}
|
||||
}
|
||||
|
||||
type MetaBox struct {
|
||||
FullBox
|
||||
Children []Box
|
||||
}
|
||||
|
||||
func parseMetaBox(outer *box, br *bufReader) (Box, error) {
|
||||
fb, err := readFullBox(outer, br)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
mb := &MetaBox{FullBox: fb}
|
||||
return mb, br.parseAppendBoxes(&mb.Children)
|
||||
}
|
||||
|
||||
func (br *bufReader) parseAppendBoxes(dst *[]Box) error {
|
||||
if br.err != nil {
|
||||
return br.err
|
||||
}
|
||||
boxr := NewReader(br.Reader)
|
||||
for {
|
||||
inner, err := boxr.ReadBox()
|
||||
if err == io.EOF {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
br.err = err
|
||||
return err
|
||||
}
|
||||
slurp, err := ioutil.ReadAll(inner.Body())
|
||||
if err != nil {
|
||||
br.err = err
|
||||
return err
|
||||
}
|
||||
inner.(*box).slurp = slurp
|
||||
*dst = append(*dst, inner)
|
||||
}
|
||||
}
|
||||
|
||||
// ItemInfoEntry represents an "infe" box.
|
||||
//
|
||||
// TODO: currently only parses Version 2 boxes.
|
||||
type ItemInfoEntry struct {
|
||||
FullBox
|
||||
|
||||
ItemID uint16
|
||||
ProtectionIndex uint16
|
||||
ItemType string // always 4 bytes
|
||||
|
||||
Name string
|
||||
|
||||
// If Type == "mime":
|
||||
ContentType string
|
||||
ContentEncoding string
|
||||
|
||||
// If Type == "uri ":
|
||||
ItemURIType string
|
||||
}
|
||||
|
||||
func parseItemInfoEntry(outer *box, br *bufReader) (Box, error) {
|
||||
fb, err := readFullBox(outer, br)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ie := &ItemInfoEntry{FullBox: fb}
|
||||
if fb.Version != 2 {
|
||||
return nil, fmt.Errorf("TODO: found version %d infe box. Only 2 is supported now.", fb.Version)
|
||||
}
|
||||
|
||||
ie.ItemID, _ = br.readUint16()
|
||||
ie.ProtectionIndex, _ = br.readUint16()
|
||||
if !br.ok() {
|
||||
return nil, br.err
|
||||
}
|
||||
buf, err := br.Peek(4)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ie.ItemType = string(buf[:4])
|
||||
ie.Name, _ = br.readString()
|
||||
|
||||
switch ie.ItemType {
|
||||
case "mime":
|
||||
ie.ContentType, _ = br.readString()
|
||||
if br.anyRemain() {
|
||||
ie.ContentEncoding, _ = br.readString()
|
||||
}
|
||||
case "uri ":
|
||||
ie.ItemURIType, _ = br.readString()
|
||||
}
|
||||
if !br.ok() {
|
||||
return nil, br.err
|
||||
}
|
||||
return ie, nil
|
||||
}
|
||||
|
||||
// ItemInfoBox represents an "iinf" box.
|
||||
type ItemInfoBox struct {
|
||||
FullBox
|
||||
Count uint16
|
||||
ItemInfos []*ItemInfoEntry
|
||||
}
|
||||
|
||||
func parseItemInfoBox(outer *box, br *bufReader) (Box, error) {
|
||||
fb, err := readFullBox(outer, br)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ib := &ItemInfoBox{FullBox: fb}
|
||||
|
||||
ib.Count, _ = br.readUint16()
|
||||
|
||||
var itemInfos []Box
|
||||
br.parseAppendBoxes(&itemInfos)
|
||||
if br.ok() {
|
||||
for _, box := range itemInfos {
|
||||
pb, err := box.Parse()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error parsing ItemInfoEntry in ItemInfoBox: %v", err)
|
||||
}
|
||||
if iie, ok := pb.(*ItemInfoEntry); ok {
|
||||
ib.ItemInfos = append(ib.ItemInfos, iie)
|
||||
}
|
||||
}
|
||||
}
|
||||
if !br.ok() {
|
||||
return FullBox{}, br.err
|
||||
}
|
||||
return ib, nil
|
||||
}
|
||||
|
||||
// bufReader adds some HEIF/BMFF-specific methods around a *bufio.Reader.
|
||||
type bufReader struct {
|
||||
*bufio.Reader
|
||||
err error // sticky error
|
||||
}
|
||||
|
||||
// ok reports whether all previous reads have been error-free.
|
||||
func (br *bufReader) ok() bool { return br.err == nil }
|
||||
|
||||
func (br *bufReader) anyRemain() bool {
|
||||
if br.err != nil {
|
||||
return false
|
||||
}
|
||||
_, err := br.Peek(1)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func (br *bufReader) readUintN(bits uint8) (uint64, error) {
|
||||
if br.err != nil {
|
||||
return 0, br.err
|
||||
}
|
||||
if bits == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
nbyte := bits / 8
|
||||
buf, err := br.Peek(int(nbyte))
|
||||
if err != nil {
|
||||
br.err = err
|
||||
return 0, err
|
||||
}
|
||||
defer br.Discard(int(nbyte))
|
||||
switch bits {
|
||||
case 8:
|
||||
return uint64(buf[0]), nil
|
||||
case 16:
|
||||
return uint64(binary.BigEndian.Uint16(buf[:2])), nil
|
||||
case 32:
|
||||
return uint64(binary.BigEndian.Uint32(buf[:4])), nil
|
||||
case 64:
|
||||
return binary.BigEndian.Uint64(buf[:8]), nil
|
||||
default:
|
||||
br.err = fmt.Errorf("invalid uintn read size")
|
||||
return 0, br.err
|
||||
}
|
||||
}
|
||||
|
||||
func (br *bufReader) readUint8() (uint8, error) {
|
||||
if br.err != nil {
|
||||
return 0, br.err
|
||||
}
|
||||
v, err := br.ReadByte()
|
||||
if err != nil {
|
||||
br.err = err
|
||||
return 0, err
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
func (br *bufReader) readUint16() (uint16, error) {
|
||||
if br.err != nil {
|
||||
return 0, br.err
|
||||
}
|
||||
buf, err := br.Peek(2)
|
||||
if err != nil {
|
||||
br.err = err
|
||||
return 0, err
|
||||
}
|
||||
v := binary.BigEndian.Uint16(buf[:2])
|
||||
br.Discard(2)
|
||||
return v, nil
|
||||
}
|
||||
|
||||
func (br *bufReader) readUint32() (uint32, error) {
|
||||
if br.err != nil {
|
||||
return 0, br.err
|
||||
}
|
||||
buf, err := br.Peek(4)
|
||||
if err != nil {
|
||||
br.err = err
|
||||
return 0, err
|
||||
}
|
||||
v := binary.BigEndian.Uint32(buf[:4])
|
||||
br.Discard(4)
|
||||
return v, nil
|
||||
}
|
||||
|
||||
func (br *bufReader) readString() (string, error) {
|
||||
if br.err != nil {
|
||||
return "", br.err
|
||||
}
|
||||
s0, err := br.ReadString(0)
|
||||
if err != nil {
|
||||
br.err = err
|
||||
return "", err
|
||||
}
|
||||
s := strings.TrimSuffix(s0, "\x00")
|
||||
if len(s) == len(s0) {
|
||||
err = fmt.Errorf("unexpected non-null terminated string")
|
||||
br.err = err
|
||||
return "", err
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// HEIF: ipco
|
||||
type ItemPropertyContainerBox struct {
|
||||
*box
|
||||
Properties []Box // of ItemProperty or ItemFullProperty
|
||||
}
|
||||
|
||||
func parseItemPropertyContainerBox(outer *box, br *bufReader) (Box, error) {
|
||||
ipc := &ItemPropertyContainerBox{box: outer}
|
||||
return ipc, br.parseAppendBoxes(&ipc.Properties)
|
||||
}
|
||||
|
||||
// HEIF: iprp
|
||||
type ItemPropertiesBox struct {
|
||||
*box
|
||||
PropertyContainer *ItemPropertyContainerBox
|
||||
Associations []*ItemPropertyAssociation // at least 1
|
||||
}
|
||||
|
||||
func parseItemPropertiesBox(outer *box, br *bufReader) (Box, error) {
|
||||
ip := &ItemPropertiesBox{
|
||||
box: outer,
|
||||
}
|
||||
|
||||
var boxes []Box
|
||||
err := br.parseAppendBoxes(&boxes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(boxes) < 2 {
|
||||
return nil, fmt.Errorf("expect at least 2 boxes in children; got 0")
|
||||
}
|
||||
|
||||
cb, err := boxes[0].Parse()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse first box, %q: %v", boxes[0].Type(), err)
|
||||
}
|
||||
|
||||
var ok bool
|
||||
ip.PropertyContainer, ok = cb.(*ItemPropertyContainerBox)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unexpected type %T for ItemPropertieBox.PropertyContainer", cb)
|
||||
}
|
||||
|
||||
// Association boxes
|
||||
ip.Associations = make([]*ItemPropertyAssociation, 0, len(boxes)-1)
|
||||
for _, box := range boxes[1:] {
|
||||
boxp, err := box.Parse()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse association box: %v", err)
|
||||
}
|
||||
ipa, ok := boxp.(*ItemPropertyAssociation)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unexpected box %q instead of ItemPropertyAssociation", boxp.Type())
|
||||
}
|
||||
ip.Associations = append(ip.Associations, ipa)
|
||||
}
|
||||
return ip, nil
|
||||
}
|
||||
|
||||
type ItemPropertyAssociation struct {
|
||||
FullBox
|
||||
EntryCount uint32
|
||||
Entries []ItemPropertyAssociationItem
|
||||
}
|
||||
|
||||
// not a box
|
||||
type ItemProperty struct {
|
||||
Essential bool
|
||||
Index uint16
|
||||
}
|
||||
|
||||
// not a box
|
||||
type ItemPropertyAssociationItem struct {
|
||||
ItemID uint32
|
||||
AssociationsCount int // as declared
|
||||
Associations []ItemProperty // as parsed
|
||||
}
|
||||
|
||||
func parseItemPropertyAssociation(outer *box, br *bufReader) (Box, error) {
|
||||
fb, err := readFullBox(outer, br)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ipa := &ItemPropertyAssociation{FullBox: fb}
|
||||
count, _ := br.readUint32()
|
||||
ipa.EntryCount = count
|
||||
|
||||
for i := uint64(0); i < uint64(count) && br.ok(); i++ {
|
||||
var itemID uint32
|
||||
if fb.Version < 1 {
|
||||
itemID16, _ := br.readUint16()
|
||||
itemID = uint32(itemID16)
|
||||
} else {
|
||||
itemID, _ = br.readUint32()
|
||||
}
|
||||
assocCount, _ := br.readUint8()
|
||||
ipai := ItemPropertyAssociationItem{
|
||||
ItemID: itemID,
|
||||
AssociationsCount: int(assocCount),
|
||||
}
|
||||
for j := 0; j < int(assocCount) && br.ok(); j++ {
|
||||
first, _ := br.readUint8()
|
||||
essential := first&(1<<7) != 0
|
||||
first &^= byte(1 << 7)
|
||||
|
||||
var index uint16
|
||||
if fb.Flags&1 != 0 {
|
||||
second, _ := br.readUint8()
|
||||
index = uint16(first)<<8 | uint16(second)
|
||||
} else {
|
||||
index = uint16(first)
|
||||
}
|
||||
ipai.Associations = append(ipai.Associations, ItemProperty{
|
||||
Essential: essential,
|
||||
Index: index,
|
||||
})
|
||||
}
|
||||
ipa.Entries = append(ipa.Entries, ipai)
|
||||
}
|
||||
if !br.ok() {
|
||||
return nil, br.err
|
||||
}
|
||||
return ipa, nil
|
||||
}
|
||||
|
||||
type ImageSpatialExtentsProperty struct {
|
||||
FullBox
|
||||
ImageWidth uint32
|
||||
ImageHeight uint32
|
||||
}
|
||||
|
||||
func parseImageSpatialExtentsProperty(outer *box, br *bufReader) (Box, error) {
|
||||
fb, err := readFullBox(outer, br)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
w, err := br.readUint32()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
h, err := br.readUint32()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &ImageSpatialExtentsProperty{
|
||||
FullBox: fb,
|
||||
ImageWidth: w,
|
||||
ImageHeight: h,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type OffsetLength struct {
|
||||
Offset, Length uint64
|
||||
}
|
||||
|
||||
// not a box
|
||||
type ItemLocationBoxEntry struct {
|
||||
ItemID uint16
|
||||
ConstructionMethod uint8 // actually uint4
|
||||
DataReferenceIndex uint16
|
||||
BaseOffset uint64 // uint32 or uint64, depending on encoding
|
||||
ExtentCount uint16
|
||||
Extents []OffsetLength
|
||||
}
|
||||
|
||||
// box "iloc"
|
||||
type ItemLocationBox struct {
|
||||
FullBox
|
||||
|
||||
offsetSize, lengthSize, baseOffsetSize, indexSize uint8 // actually uint4
|
||||
|
||||
ItemCount uint16
|
||||
Items []ItemLocationBoxEntry
|
||||
}
|
||||
|
||||
func parseItemLocationBox(outer *box, br *bufReader) (Box, error) {
|
||||
fb, err := readFullBox(outer, br)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ilb := &ItemLocationBox{
|
||||
FullBox: fb,
|
||||
}
|
||||
buf, err := br.Peek(4)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ilb.offsetSize = buf[0] >> 4
|
||||
ilb.lengthSize = buf[0] & 15
|
||||
ilb.baseOffsetSize = buf[1] >> 4
|
||||
if fb.Version > 0 { // version 1
|
||||
ilb.indexSize = buf[1] & 15
|
||||
}
|
||||
|
||||
ilb.ItemCount = binary.BigEndian.Uint16(buf[2:4])
|
||||
br.Discard(4)
|
||||
|
||||
for i := 0; br.ok() && i < int(ilb.ItemCount); i++ {
|
||||
var ent ItemLocationBoxEntry
|
||||
ent.ItemID, _ = br.readUint16()
|
||||
if fb.Version > 0 { // version 1
|
||||
cmeth, _ := br.readUint16()
|
||||
ent.ConstructionMethod = byte(cmeth & 15)
|
||||
}
|
||||
ent.DataReferenceIndex, _ = br.readUint16()
|
||||
if br.ok() && ilb.baseOffsetSize > 0 {
|
||||
br.Discard(int(ilb.baseOffsetSize) / 8)
|
||||
}
|
||||
ent.ExtentCount, _ = br.readUint16()
|
||||
for j := 0; br.ok() && j < int(ent.ExtentCount); j++ {
|
||||
var ol OffsetLength
|
||||
ol.Offset, _ = br.readUintN(ilb.offsetSize * 8)
|
||||
ol.Length, _ = br.readUintN(ilb.lengthSize * 8)
|
||||
if br.err != nil {
|
||||
return nil, br.err
|
||||
}
|
||||
ent.Extents = append(ent.Extents, ol)
|
||||
}
|
||||
ilb.Items = append(ilb.Items, ent)
|
||||
}
|
||||
if !br.ok() {
|
||||
return nil, br.err
|
||||
}
|
||||
return ilb, nil
|
||||
}
|
||||
|
||||
// a "hdlr" box.
|
||||
type HandlerBox struct {
|
||||
FullBox
|
||||
HandlerType string // always 4 bytes; usually "pict" for iOS Camera images
|
||||
Name string
|
||||
}
|
||||
|
||||
func parseHandlerBox(gen *box, br *bufReader) (Box, error) {
|
||||
fb, err := readFullBox(gen, br)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
hb := &HandlerBox{
|
||||
FullBox: fb,
|
||||
}
|
||||
buf, err := br.Peek(20)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
hb.HandlerType = string(buf[4:8])
|
||||
br.Discard(20)
|
||||
|
||||
hb.Name, _ = br.readString()
|
||||
return hb, br.err
|
||||
}
|
||||
|
||||
// a "dinf" box
|
||||
type DataInformationBox struct {
|
||||
*box
|
||||
Children []Box
|
||||
}
|
||||
|
||||
func parseDataInformationBox(gen *box, br *bufReader) (Box, error) {
|
||||
dib := &DataInformationBox{box: gen}
|
||||
return dib, br.parseAppendBoxes(&dib.Children)
|
||||
}
|
||||
|
||||
// a "dref" box.
|
||||
type DataReferenceBox struct {
|
||||
FullBox
|
||||
EntryCount uint32
|
||||
Children []Box
|
||||
}
|
||||
|
||||
func parseDataReferenceBox(gen *box, br *bufReader) (Box, error) {
|
||||
fb, err := readFullBox(gen, br)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
drb := &DataReferenceBox{FullBox: fb}
|
||||
drb.EntryCount, _ = br.readUint32()
|
||||
return drb, br.parseAppendBoxes(&drb.Children)
|
||||
}
|
||||
|
||||
// "pitm" box
|
||||
type PrimaryItemBox struct {
|
||||
FullBox
|
||||
ItemID uint16
|
||||
}
|
||||
|
||||
func parsePrimaryItemBox(gen *box, br *bufReader) (Box, error) {
|
||||
fb, err := readFullBox(gen, br)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pib := &PrimaryItemBox{FullBox: fb}
|
||||
pib.ItemID, _ = br.readUint16()
|
||||
if !br.ok() {
|
||||
return nil, br.err
|
||||
}
|
||||
return pib, nil
|
||||
}
|
|
@ -0,0 +1,272 @@
|
|||
/*
|
||||
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
|
||||
}
|
||||
|
||||
// TODO: add HEIF irot rotation accessor, like Image.SpatialExtents.
|
||||
// And imir (mirroring).
|
||||
|
||||
// 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
|
||||
}
|
Loading…
Reference in New Issue