Skip to content

Commit

Permalink
Handle IPTC slice values etc.
Browse files Browse the repository at this point in the history
  • Loading branch information
bep committed Aug 14, 2023
1 parent 00df729 commit e5eaa5c
Show file tree
Hide file tree
Showing 10 changed files with 161 additions and 54 deletions.
6 changes: 3 additions & 3 deletions imagedecoder_jpg.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ type imageDecoderJPEG struct {
func (e *imageDecoderJPEG) decode() (err error) {
// JPEG SOI marker.
var soi uint16
if err = e.readFullE(&soi); err != nil {
if soi, err = e.read2E(); err != nil {
return nil
}
if soi != markerSOI {
Expand All @@ -21,10 +21,10 @@ func (e *imageDecoderJPEG) decode() (err error) {
findMarker := func(markerToFind uint16) int {
for {
var marker, length uint16
if err = e.readFullE(&marker); err != nil {
if marker, err = e.read2E(); err != nil {
return -1
}
if err = e.readFullE(&length); err != nil {
if length, err = e.read2E(); err != nil {
return -1
}

Expand Down
9 changes: 5 additions & 4 deletions imagedecoder_webp.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ var (
fccXMP = fourCC{'X', 'M', 'P', ' '}
)

var errInvalidFormat = fmt.Errorf("imagemeta: invalid format")
// ErrInvalidFormat is returned when the format is not recognized.
var ErrInvalidFormat = fmt.Errorf("imagemeta: invalid format")

type baseStreamingDecoder struct {
*streamReader
Expand Down Expand Up @@ -48,15 +49,15 @@ func (e *decoderWebP) decode() (err error) {
// Read the RIFF header.
e.readBytes(chunkID[:])
if chunkID != fccRIFF {
return errInvalidFormat
return ErrInvalidFormat
}

// File size.
e.skip(4)

e.readBytes(chunkID[:])
if chunkID != fccWEBP {
return errInvalidFormat
return ErrInvalidFormat
}

for {
Expand All @@ -72,7 +73,7 @@ func (e *decoderWebP) decode() (err error) {

case fccVP8X:
if chunkLen != 10 {
return errInvalidFormat
return ErrInvalidFormat
}

const (
Expand Down
79 changes: 74 additions & 5 deletions imagemeta_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,46 @@ func TestDecodeBasic(t *testing.T) {
}
}

func TestDecodeIPTCReference(t *testing.T) {
c := qt.New(t)
const filename = "IPTC-PhotometadataRef-Std2021.1.jpg"

img, err := os.Open(filepath.Join("testdata", filename))
c.Assert(err, qt.IsNil)

c.Cleanup(func() {
c.Assert(img.Close(), qt.IsNil)
})

tags := make(map[string]imagemeta.TagInfo)
handleTag := func(ti imagemeta.TagInfo) error {
if _, seen := tags[ti.Tag]; seen {
c.Fatalf("duplicate tag: %s", ti.Tag)
}
c.Assert(ti.Tag, qt.Not(qt.Contains), "Unknown")
tags[ti.Tag] = ti
return nil
}

err = imagemeta.Decode(
imagemeta.Options{
R: img,
ImageFormat: imagemeta.ImageFormatJPEG,
HandleTag: handleTag,
Sources: imagemeta.TagSourceIPTC,
},
)
c.Assert(err, qt.IsNil)

c.Assert(len(tags), qt.Equals, 22)
c.Assert(tags["Byline"].Value, qt.Equals, "Creator1 (ref2021.1)")
c.Assert(tags["BylineTitle"].Value, qt.Equals, "Creator's Job Title (ref2021.1)")
c.Assert(tags["RecordVersion"].Value, qt.Equals, uint16(4))
c.Assert(tags["DateCreated"].Value, qt.Equals, "20211020")
c.Assert(tags["Keywords"].Value, qt.DeepEquals, []string{"Keyword1ref2021.1", "Keyword2ref2021.1", "Keyword3ref2021.1"})

}

func TestDecodeOrientationOnly(t *testing.T) {
c := qt.New(t)

Expand Down Expand Up @@ -85,12 +125,8 @@ func TestSmoke(t *testing.T) {

for _, file := range files {
img, err := os.Open(file)
format := imagemeta.ImageFormatJPEG
if filepath.Ext(file) == ".webp" {
format = imagemeta.ImageFormatWebP
}

c.Assert(err, qt.IsNil)
format := extToFormat(filepath.Ext(file))
tags := make(map[string]imagemeta.TagInfo)
handleTag := func(ti imagemeta.TagInfo) error {
tags[ti.Tag] = ti
Expand All @@ -104,6 +140,39 @@ func TestSmoke(t *testing.T) {

}

func TestCorrupt(t *testing.T) {
c := qt.New(t)

files, err := filepath.Glob(filepath.Join("testdata", "corrupt", "*.*"))
c.Assert(err, qt.IsNil)

for _, file := range files {
img, err := os.Open(file)
c.Assert(err, qt.IsNil)
format := extToFormat(filepath.Ext(file))
handleTag := func(ti imagemeta.TagInfo) error {
return nil
}
err = imagemeta.Decode(imagemeta.Options{R: img, ImageFormat: format, HandleTag: handleTag})
c.Assert(err, qt.Equals, imagemeta.ErrInvalidFormat)
img.Close()
}

}

func extToFormat(ext string) imagemeta.ImageFormat {
switch ext {
case ".jpg":
return imagemeta.ImageFormatJPEG
case ".webp":
return imagemeta.ImageFormatWebP
case ".png":
return imagemeta.ImageFormatPNG
default:
panic("unknown image format")
}
}

func getSunrise(c *qt.C, imageFormat imagemeta.ImageFormat) (imagemeta.Reader, func()) {
ext := ""
switch imageFormat {
Expand Down
39 changes: 30 additions & 9 deletions io.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package imagemeta
import (
"bytes"
"encoding/binary"
"errors"
"io"
"sync"
)
Expand Down Expand Up @@ -60,12 +61,17 @@ type streamReader struct {
readerOffset int
}

var errShortRead = errors.New("short read")

func (e *streamReader) bufferedReader(length int) (readerCloser, error) {
buff := getBuffer()
n, err := io.CopyN(buff, e.r, int64(length))
if err != nil || n != int64(length) {
if err != nil {
return nil, err
}
if n != int64(length) {
return nil, errShortRead
}
r := bytes.NewReader(buff.Bytes())

var closer closerFunc = func() error {
Expand Down Expand Up @@ -113,6 +119,14 @@ func (e *streamReader) read2r(r io.Reader) uint16 {
return e.byteOrder.Uint16(e.buf[:n])
}

func (e *streamReader) read2E() (uint16, error) {
const n = 2
if err := e.readNIntoBufE(n); err != nil {
return 0, err
}
return e.byteOrder.Uint16(e.buf[:n]), nil
}

func (e *streamReader) read4() uint32 {
const n = 4
e.readNIntoBuf(n)
Expand Down Expand Up @@ -145,21 +159,28 @@ func (e *streamReader) readBytesVolatile(n int) []byte {
return e.buf[:n]
}

func (e *streamReader) readFullE(v any) error {
return e.readFullrE(v, e.r)
}

func (e *streamReader) readFullrE(v any, r io.Reader) error {
return binary.Read(r, e.byteOrder, v)
func (e *streamReader) readNFromRIntoBufE(n int, r io.Reader) error {
e.allocateBuf(n)
n2, err := io.ReadFull(r, e.buf[:n])
if err != nil {
return err
}
if n != n2 {
return errShortRead
}
return nil
}

func (e *streamReader) readNFromRIntoBuf(n int, r io.Reader) {
e.allocateBuf(n)
if _, err := io.ReadFull(r, e.buf[:n]); err != nil {
if err := e.readNFromRIntoBufE(n, r); err != nil {
e.stop(err)
}
}

func (e *streamReader) readNIntoBufE(n int) error {
return e.readNFromRIntoBufE(n, e.r)
}

func (e *streamReader) readNIntoBuf(n int) {
e.readNFromRIntoBuf(n, e.r)
}
Expand Down
2 changes: 1 addition & 1 deletion metadecoder_exif.go
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@ func (e *metaDecoderEXIF) decodeTag() error {

size, ok := typeSize[typ]
if !ok {
return fmt.Errorf("unknown type for tag %s %d", tagName, typ)
return ErrInvalidFormat
}
valLen := size * count

Expand Down
80 changes: 48 additions & 32 deletions metadecoder_iptc.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ func (e *metaDecoderIPTC) decode() (err error) {

const iptcMetaDataBlockID = 0x0404

stringSlices := make(map[uint8][]string)

decodeBlock := func() error {
blockType := e.readBytesVolatile(4)
if string(blockType) != "8BIM" {
Expand Down Expand Up @@ -71,47 +73,44 @@ func (e *metaDecoderIPTC) decode() (err error) {
return errStop
}

var recordType, datasetNumber uint8
var recordSize uint16
if err := binary.Read(r, e.byteOrder, &recordType); err != nil {
return err
}
if err := binary.Read(r, e.byteOrder, &datasetNumber); err != nil {
return err
}
if err := binary.Read(r, e.byteOrder, &recordSize); err != nil {
return err
}
e.skip(1) // recordType
datasetNumber := e.read1()
recordSize := e.read2()

recordBytes := make([]byte, recordSize)
if err := binary.Read(r, e.byteOrder, recordBytes); err != nil {
return err
}

// TODO1 get an up to date field map.
// TODO1 handle unkonwn dataset numbers.
recordDef, ok := iptcFieldMap[datasetNumber]
if !ok {
fmt.Println("unknown datasetNumber", datasetNumber)
continue
// Assume a non repeatable string.
recordDef = iptcField{
name: fmt.Sprintf("%s%d", UnknownPrefix, datasetNumber),
format: "string",
repeatable: false,
}
}

var v any
switch recordDef.format {
case "string":
v = string(recordBytes)
case "B": // TODO1 check these
v = recordBytes
b := e.readBytesVolatile(int(recordSize))
v = string(b)
case "short":
v = e.read2()
case "byte":
v = e.read1()
default:
panic(fmt.Sprintf("unhandled format %q", recordDef.format))
}

if err := e.handleTag(TagInfo{
Source: TagSourceIPTC,
Tag: recordDef.name,
Value: v,
}); err != nil {
return err
if recordDef.repeatable {
stringSlices[datasetNumber] = append(stringSlices[datasetNumber], v.(string))
} else {
if err := e.handleTag(TagInfo{
Source: TagSourceIPTC,
Tag: recordDef.name,
Value: v,
}); err != nil {
return err
}
}

}
}

Expand All @@ -124,6 +123,19 @@ func (e *metaDecoderIPTC) decode() (err error) {
}
}

if len(stringSlices) > 0 {
for datasetNumber, values := range stringSlices {
if err := e.handleTag(TagInfo{
Source: TagSourceIPTC,
Tag: iptcFieldMap[datasetNumber].name,
Value: values,
}); err != nil {
return err
}
}

}

return nil

}
Expand All @@ -135,10 +147,12 @@ type iptcField struct {
}

var iptcFieldMap = map[uint8]iptcField{
0: {"RecordVersion", false, "B"},
0: {"RecordVersion", false, "short"},
4: {"ObjectTypeReference", false, "string"},
5: {"ObjectName", false, "string"},
7: {"EditStatus", false, "string"},
10: {"Urgency", false, "B"},
10: {"Urgency", false, "byte"},
12: {"SubjectReference", true, "string"},
15: {"Category", true, "string"},
20: {"SupplementalCategory", true, "string"},
22: {"FixtureIdentifier", false, "string"},
Expand Down Expand Up @@ -174,4 +188,6 @@ var iptcFieldMap = map[uint8]iptcField{
115: {"Source", false, "string"},
116: {"Copyright", false, "string"},
118: {"Contact", false, "string"},
120: {"Caption", false, "string"},
122: {"LocalCaption", false, "string"},
}
Binary file added testdata/IPTC-PhotometadataRef-Std2021.1.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added testdata/corrupt/huge_tag_exif.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added testdata/corrupt/infinite_loop_exif.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added testdata/corrupt/max_uint32_exif.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit e5eaa5c

Please sign in to comment.