Initial QSfera import
This commit is contained in:
+2
@@ -0,0 +1,2 @@
|
||||
// Package jpegmeta provides support for working with embedded JPEG metadata.
|
||||
package jpegmeta
|
||||
+165
@@ -0,0 +1,165 @@
|
||||
package jpegmeta
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/kovidgoyal/go-parallel"
|
||||
"github.com/kovidgoyal/imaging/prism/meta"
|
||||
"github.com/kovidgoyal/imaging/streams"
|
||||
"github.com/kovidgoyal/imaging/types"
|
||||
)
|
||||
|
||||
const exifSignature = "Exif\x00\x00"
|
||||
|
||||
var iccProfileIdentifier = []byte("ICC_PROFILE\x00")
|
||||
|
||||
// Load loads the metadata for a JPEG image stream.
|
||||
//
|
||||
// Only as much of the stream is consumed as necessary to extract the metadata;
|
||||
// the returned stream contains a buffered copy of the consumed data such that
|
||||
// reading from it will produce the same results as fully reading the input
|
||||
// stream. This provides a convenient way to load the full image after loading
|
||||
// the metadata.
|
||||
func Load(r io.Reader) (md *meta.Data, imgStream io.Reader, err error) {
|
||||
imgStream, err = streams.CallbackWithSeekable(r, func(r io.Reader) (err error) {
|
||||
md, err = ExtractMetadata(r)
|
||||
return
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Same as Load() except that no new stream is provided
|
||||
func ExtractMetadata(r io.Reader) (md *meta.Data, err error) {
|
||||
metadataExtracted := false
|
||||
md = &meta.Data{Format: types.JPEG}
|
||||
segReader := NewSegmentReader(r)
|
||||
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
if !metadataExtracted {
|
||||
md = nil
|
||||
}
|
||||
err = parallel.Format_stacktrace_on_panic(r, 1)
|
||||
}
|
||||
}()
|
||||
|
||||
var iccProfileChunks [][]byte
|
||||
var iccProfileChunksExtracted int
|
||||
var exif []byte
|
||||
|
||||
allMetadataExtracted := func() bool {
|
||||
return metadataExtracted &&
|
||||
iccProfileChunks != nil &&
|
||||
iccProfileChunksExtracted == len(iccProfileChunks) &&
|
||||
exif != nil
|
||||
}
|
||||
|
||||
soiSegment, err := segReader.ReadSegment()
|
||||
if err != nil {
|
||||
if q := err.Error(); strings.Contains(q, "invalid marker identifier") || strings.Contains(q, "unrecognised marker type") {
|
||||
err = nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
if soiSegment.Marker.Type != markerTypeStartOfImage {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
parseSegments:
|
||||
for {
|
||||
segment, err := segReader.ReadSegment()
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
return nil, fmt.Errorf("unexpected EOF")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch segment.Marker.Type {
|
||||
|
||||
case markerTypeStartOfFrameBaseline,
|
||||
markerTypeStartOfFrameProgressive:
|
||||
md.BitsPerComponent = uint32(segment.Data[0])
|
||||
md.PixelHeight = uint32(segment.Data[1])<<8 | uint32(segment.Data[2])
|
||||
md.PixelWidth = uint32(segment.Data[3])<<8 | uint32(segment.Data[4])
|
||||
metadataExtracted = true
|
||||
|
||||
if allMetadataExtracted() {
|
||||
break parseSegments
|
||||
}
|
||||
|
||||
case markerTypeStartOfScan,
|
||||
markerTypeEndOfImage:
|
||||
break parseSegments
|
||||
|
||||
case markerTypeApp1:
|
||||
if bytes.HasPrefix(segment.Data, []byte(exifSignature)) {
|
||||
exif = segment.Data
|
||||
}
|
||||
case markerTypeApp2:
|
||||
if len(segment.Data) < len(iccProfileIdentifier)+2 {
|
||||
continue
|
||||
}
|
||||
|
||||
for i := range iccProfileIdentifier {
|
||||
if segment.Data[i] != iccProfileIdentifier[i] {
|
||||
continue parseSegments
|
||||
}
|
||||
}
|
||||
|
||||
iccData, iccErr := md.ICCProfileData()
|
||||
if iccData != nil || iccErr != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
chunkTotal := segment.Data[len(iccProfileIdentifier)+1]
|
||||
if iccProfileChunks == nil {
|
||||
iccProfileChunks = make([][]byte, chunkTotal)
|
||||
} else if int(chunkTotal) != len(iccProfileChunks) {
|
||||
md.SetICCProfileError(fmt.Errorf("inconsistent ICC profile chunk count"))
|
||||
continue
|
||||
}
|
||||
|
||||
chunkNum := segment.Data[len(iccProfileIdentifier)]
|
||||
if chunkNum == 0 || int(chunkNum) > len(iccProfileChunks) {
|
||||
md.SetICCProfileError(fmt.Errorf("invalid ICC profile chunk number"))
|
||||
continue
|
||||
}
|
||||
if iccProfileChunks[chunkNum-1] != nil {
|
||||
md.SetICCProfileError(fmt.Errorf("duplicated ICC profile chunk"))
|
||||
continue
|
||||
}
|
||||
iccProfileChunksExtracted++
|
||||
iccProfileChunks[chunkNum-1] = segment.Data[len(iccProfileIdentifier)+2:]
|
||||
|
||||
if allMetadataExtracted() {
|
||||
break parseSegments
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !metadataExtracted {
|
||||
return nil, fmt.Errorf("no metadata found")
|
||||
}
|
||||
md.SetExifData(exif)
|
||||
|
||||
// Incomplete or missing ICC profile
|
||||
if len(iccProfileChunks) != iccProfileChunksExtracted {
|
||||
_, iccErr := md.ICCProfileData()
|
||||
if iccErr == nil {
|
||||
md.SetICCProfileError(fmt.Errorf("incomplete ICC profile data"))
|
||||
}
|
||||
return md, nil
|
||||
}
|
||||
|
||||
iccProfileData := bytes.Buffer{}
|
||||
for i := range iccProfileChunks {
|
||||
iccProfileData.Write(iccProfileChunks[i])
|
||||
}
|
||||
md.SetICCProfileData(iccProfileData.Bytes())
|
||||
|
||||
return md, nil
|
||||
}
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
package jpegmeta
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/kovidgoyal/imaging/streams"
|
||||
)
|
||||
|
||||
var invalidMarker = marker{Type: markerTypeInvalid}
|
||||
|
||||
type marker struct {
|
||||
Type markerType
|
||||
DataLength int
|
||||
}
|
||||
|
||||
func makeMarker(mType byte, r io.Reader) (marker, error) {
|
||||
var length uint16
|
||||
switch mType {
|
||||
|
||||
case
|
||||
byte(markerTypeRestart0),
|
||||
byte(markerTypeRestart1),
|
||||
byte(markerTypeRestart2),
|
||||
byte(markerTypeRestart3),
|
||||
byte(markerTypeRestart4),
|
||||
byte(markerTypeRestart5),
|
||||
byte(markerTypeRestart6),
|
||||
byte(markerTypeRestart7),
|
||||
byte(markerTypeStartOfImage),
|
||||
byte(markerTypeEndOfImage):
|
||||
|
||||
length = 2
|
||||
|
||||
case byte(markerTypeStartOfFrameBaseline),
|
||||
byte(markerTypeStartOfFrameProgressive),
|
||||
byte(markerTypeDefineHuffmanTable),
|
||||
byte(markerTypeStartOfScan),
|
||||
byte(markerTypeDefineQuantisationTable),
|
||||
byte(markerTypeDefineRestartInterval),
|
||||
byte(markerTypeApp0),
|
||||
byte(markerTypeApp1),
|
||||
byte(markerTypeApp2),
|
||||
byte(markerTypeApp3),
|
||||
byte(markerTypeApp4),
|
||||
byte(markerTypeApp5),
|
||||
byte(markerTypeApp6),
|
||||
byte(markerTypeApp7),
|
||||
byte(markerTypeApp8),
|
||||
byte(markerTypeApp9),
|
||||
byte(markerTypeApp10),
|
||||
byte(markerTypeApp11),
|
||||
byte(markerTypeApp12),
|
||||
byte(markerTypeApp13),
|
||||
byte(markerTypeApp14),
|
||||
byte(markerTypeApp15),
|
||||
byte(markerTypeComment):
|
||||
|
||||
var err error
|
||||
if err = binary.Read(r, binary.BigEndian, &length); err != nil {
|
||||
return invalidMarker, err
|
||||
}
|
||||
|
||||
default:
|
||||
return invalidMarker, fmt.Errorf("unrecognised marker type %0x", mType)
|
||||
}
|
||||
|
||||
return marker{
|
||||
Type: markerType(mType),
|
||||
DataLength: int(length) - 2,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func readMarker(r io.Reader) (marker, error) {
|
||||
b, err := streams.ReadByte(r)
|
||||
if err != nil {
|
||||
return invalidMarker, err
|
||||
}
|
||||
|
||||
if b != 0xff {
|
||||
return invalidMarker, fmt.Errorf("invalid marker identifier %0x", b)
|
||||
}
|
||||
if b, err = streams.ReadByte(r); err != nil {
|
||||
return invalidMarker, err
|
||||
}
|
||||
|
||||
return makeMarker(b, r)
|
||||
}
|
||||
+115
@@ -0,0 +1,115 @@
|
||||
package jpegmeta
|
||||
|
||||
import "fmt"
|
||||
|
||||
type markerType int
|
||||
|
||||
const (
|
||||
markerTypeInvalid markerType = 0x00
|
||||
markerTypeStartOfFrameBaseline markerType = 0xc0
|
||||
markerTypeStartOfFrameProgressive markerType = 0xc2
|
||||
markerTypeDefineHuffmanTable markerType = 0xc4
|
||||
markerTypeRestart0 markerType = 0xd0
|
||||
markerTypeRestart1 markerType = 0xd1
|
||||
markerTypeRestart2 markerType = 0xd2
|
||||
markerTypeRestart3 markerType = 0xd3
|
||||
markerTypeRestart4 markerType = 0xd4
|
||||
markerTypeRestart5 markerType = 0xd5
|
||||
markerTypeRestart6 markerType = 0xd6
|
||||
markerTypeRestart7 markerType = 0xd7
|
||||
markerTypeStartOfImage markerType = 0xd8
|
||||
markerTypeEndOfImage markerType = 0xd9
|
||||
markerTypeStartOfScan markerType = 0xda
|
||||
markerTypeDefineQuantisationTable markerType = 0xdb
|
||||
markerTypeDefineRestartInterval markerType = 0xdd
|
||||
markerTypeApp0 markerType = 0xe0
|
||||
markerTypeApp1 markerType = 0xe1
|
||||
markerTypeApp2 markerType = 0xe2
|
||||
markerTypeApp3 markerType = 0xe3
|
||||
markerTypeApp4 markerType = 0xe4
|
||||
markerTypeApp5 markerType = 0xe5
|
||||
markerTypeApp6 markerType = 0xe6
|
||||
markerTypeApp7 markerType = 0xe7
|
||||
markerTypeApp8 markerType = 0xe8
|
||||
markerTypeApp9 markerType = 0xe9
|
||||
markerTypeApp10 markerType = 0xea
|
||||
markerTypeApp11 markerType = 0xeb
|
||||
markerTypeApp12 markerType = 0xec
|
||||
markerTypeApp13 markerType = 0xed
|
||||
markerTypeApp14 markerType = 0xee
|
||||
markerTypeApp15 markerType = 0xef
|
||||
markerTypeComment markerType = 0xfe
|
||||
)
|
||||
|
||||
func (mt markerType) String() string {
|
||||
switch mt {
|
||||
case markerTypeStartOfFrameBaseline:
|
||||
return "SOF0"
|
||||
case markerTypeStartOfFrameProgressive:
|
||||
return "SOF2"
|
||||
case markerTypeDefineHuffmanTable:
|
||||
return "DHT"
|
||||
case markerTypeRestart0:
|
||||
return "RST0"
|
||||
case markerTypeRestart1:
|
||||
return "RST1"
|
||||
case markerTypeRestart2:
|
||||
return "RST2"
|
||||
case markerTypeRestart3:
|
||||
return "RST3"
|
||||
case markerTypeRestart4:
|
||||
return "RST4"
|
||||
case markerTypeRestart5:
|
||||
return "RST5"
|
||||
case markerTypeRestart6:
|
||||
return "RST6"
|
||||
case markerTypeRestart7:
|
||||
return "RST7"
|
||||
case markerTypeStartOfImage:
|
||||
return "SOI"
|
||||
case markerTypeEndOfImage:
|
||||
return "EOI"
|
||||
case markerTypeStartOfScan:
|
||||
return "SOS"
|
||||
case markerTypeDefineQuantisationTable:
|
||||
return "DQT"
|
||||
case markerTypeDefineRestartInterval:
|
||||
return "DRI"
|
||||
case markerTypeApp0:
|
||||
return "APP0"
|
||||
case markerTypeApp1:
|
||||
return "APP1"
|
||||
case markerTypeApp2:
|
||||
return "APP2"
|
||||
case markerTypeApp3:
|
||||
return "APP3"
|
||||
case markerTypeApp4:
|
||||
return "APP4"
|
||||
case markerTypeApp5:
|
||||
return "APP5"
|
||||
case markerTypeApp6:
|
||||
return "APP6"
|
||||
case markerTypeApp7:
|
||||
return "APP7"
|
||||
case markerTypeApp8:
|
||||
return "APP8"
|
||||
case markerTypeApp9:
|
||||
return "APP9"
|
||||
case markerTypeApp10:
|
||||
return "APP10"
|
||||
case markerTypeApp11:
|
||||
return "APP11"
|
||||
case markerTypeApp12:
|
||||
return "APP12"
|
||||
case markerTypeApp13:
|
||||
return "APP13"
|
||||
case markerTypeApp14:
|
||||
return "APP14"
|
||||
case markerTypeApp15:
|
||||
return "APP15"
|
||||
case markerTypeComment:
|
||||
return "COM"
|
||||
default:
|
||||
return fmt.Sprintf("Unknown (%0x)", byte(mt))
|
||||
}
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
package jpegmeta
|
||||
|
||||
import (
|
||||
"io"
|
||||
)
|
||||
|
||||
var invalidSegment = segment{Marker: invalidMarker}
|
||||
|
||||
type segment struct {
|
||||
Marker marker
|
||||
Data []byte
|
||||
}
|
||||
|
||||
func makeSegment(markerType byte, r io.Reader) (segment, error) {
|
||||
m, err := makeMarker(markerType, r)
|
||||
return segment{Marker: m}, err
|
||||
}
|
||||
|
||||
func readSegment(r io.Reader) (segment, error) {
|
||||
m, err := readMarker(r)
|
||||
if err != nil {
|
||||
return invalidSegment, err
|
||||
}
|
||||
|
||||
seg := segment{
|
||||
Marker: m,
|
||||
}
|
||||
if m.DataLength > 0 {
|
||||
seg.Data = make([]byte, m.DataLength)
|
||||
|
||||
_, err := io.ReadFull(r, seg.Data)
|
||||
if err != nil {
|
||||
return invalidSegment, err
|
||||
}
|
||||
}
|
||||
|
||||
return seg, nil
|
||||
}
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
package jpegmeta
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"github.com/kovidgoyal/imaging/streams"
|
||||
)
|
||||
|
||||
type segmentReader struct {
|
||||
reader io.Reader
|
||||
inEntropyCodedData bool
|
||||
}
|
||||
|
||||
func (sr *segmentReader) ReadSegment() (segment, error) {
|
||||
if sr.inEntropyCodedData {
|
||||
for {
|
||||
b, err := streams.ReadByte(sr.reader)
|
||||
if err != nil {
|
||||
return segment{}, err
|
||||
}
|
||||
|
||||
if b == 0xFF {
|
||||
if b, err = streams.ReadByte(sr.reader); err != nil {
|
||||
return segment{}, err
|
||||
}
|
||||
|
||||
if b != 0x00 {
|
||||
seg, err := makeSegment(b, sr.reader)
|
||||
if err != nil {
|
||||
return segment{}, err
|
||||
}
|
||||
|
||||
sr.inEntropyCodedData = seg.Marker.Type == markerTypeStartOfScan ||
|
||||
(seg.Marker.Type >= markerTypeRestart0 && seg.Marker.Type <= markerTypeRestart7)
|
||||
|
||||
return seg, err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
seg, err := readSegment(sr.reader)
|
||||
if err != nil {
|
||||
return seg, err
|
||||
}
|
||||
|
||||
sr.inEntropyCodedData = seg.Marker.Type == markerTypeStartOfScan
|
||||
|
||||
return seg, nil
|
||||
}
|
||||
|
||||
func NewSegmentReader(r io.Reader) *segmentReader {
|
||||
return &segmentReader{
|
||||
reader: r,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user