Full Mattermost server source with integrated Community Enterprise features. Includes vendor directory for offline/air-gapped builds. Structure: - enterprise-impl/: Enterprise feature implementations - enterprise-community/: Init files that register implementations - enterprise/: Bridge imports (community_imports.go) - vendor/: All dependencies for offline builds Build (online): go build ./cmd/mattermost Build (offline/air-gapped): go build -mod=vendor ./cmd/mattermost 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
513 lines
12 KiB
Go
513 lines
12 KiB
Go
// Copyright 2024 Bjørn Erik Pedersen
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package imagemeta
|
|
|
|
import (
|
|
"encoding/binary"
|
|
"fmt"
|
|
"io"
|
|
"maps"
|
|
"math"
|
|
"path"
|
|
"strings"
|
|
)
|
|
|
|
var markerXMP = []byte("http://ns.adobe.com/xap/1.0/\x00")
|
|
|
|
const (
|
|
markerSOI = 0xffd8
|
|
markerApp1EXIF = 0xffe1
|
|
markerrApp1XMP = 0xffe1
|
|
markerApp13 = 0xffed
|
|
markerSOS = 0xffda
|
|
exifHeader = 0x45786966
|
|
byteOrderBigEndian = 0x4d4d
|
|
byteOrderLittleEndian = 0x4949
|
|
|
|
tagNameThumbnailOffset = "ThumbnailOffset"
|
|
)
|
|
|
|
const (
|
|
xmpMarker = 0x02bc // EXIF ApplicationNotes
|
|
iptcMarker = 0x83bb // EXIF IPTC-NAA
|
|
)
|
|
|
|
//go:generate stringer -type=exifType
|
|
|
|
const (
|
|
exifTypeUnsignedByte1 exifType = 1
|
|
exifTypeASCIIString1 exifType = 2
|
|
exifTypeUnsignedShort2 exifType = 3
|
|
exifTypeUnsignedLong4 exifType = 4
|
|
exifTypeUnsignedRat8 exifType = 5
|
|
exifTypeSignedByte1 exifType = 6
|
|
exifTypeUndef1 exifType = 7
|
|
exifTypeSignedShort2 exifType = 8
|
|
exifTypeSignedLong4 exifType = 9
|
|
exifTypeSignedRat8 exifType = 10
|
|
exifTypeSignedFloat4 exifType = 11
|
|
exifTypeSignedDouble8 exifType = 12
|
|
)
|
|
|
|
// Used for +inf/-inf/nan. This is in line with Exiftool.
|
|
const undef = "undef"
|
|
|
|
// Size in bytes of each type.
|
|
var exifTypeSize = map[exifType]uint32{
|
|
exifTypeUnsignedByte1: 1,
|
|
exifTypeASCIIString1: 1,
|
|
exifTypeUnsignedShort2: 2,
|
|
exifTypeUnsignedLong4: 4,
|
|
exifTypeUnsignedRat8: 8,
|
|
exifTypeSignedByte1: 1,
|
|
exifTypeUndef1: 1,
|
|
exifTypeSignedShort2: 2,
|
|
exifTypeSignedLong4: 4,
|
|
exifTypeSignedRat8: 8,
|
|
exifTypeSignedFloat4: 4,
|
|
exifTypeSignedDouble8: 8,
|
|
}
|
|
|
|
var (
|
|
exifFieldsAll = map[uint16]string{}
|
|
exifIFDPointers = map[uint16]string{
|
|
0x8769: "ExifIFDP",
|
|
0x8825: "GPSInfoIFD",
|
|
0xa005: "InteroperabilityIFD",
|
|
}
|
|
)
|
|
|
|
var (
|
|
exifConverters = &vc{}
|
|
exifValueConverterMap = map[string]valueConverter{
|
|
"ApertureValue": exifConverters.convertAPEXToFNumber,
|
|
"MaxApertureValue": exifConverters.convertAPEXToFNumber,
|
|
"ShutterSpeedValue": exifConverters.convertAPEXToSeconds,
|
|
"GPSLatitude": exifConverters.convertDegreesToDecimal,
|
|
"GPSLongitude": exifConverters.convertDegreesToDecimal,
|
|
"GPSMeasureMode": exifConverters.convertStringToInt,
|
|
"SubSecTimeDigitized": exifConverters.convertStringToInt,
|
|
"SubSecTimeOriginal": exifConverters.convertStringToInt,
|
|
"SubSecTime": exifConverters.convertStringToInt,
|
|
"GPSSatellites": exifConverters.convertStringToInt,
|
|
"GPSTimeStamp": exifConverters.convertToTimestampString,
|
|
"GPSVersionID": exifConverters.convertBytesToStringSpaceDelim,
|
|
"SubjectArea": exifConverters.convertNumbersToSpaceLimited,
|
|
"BitsPerSample": exifConverters.convertNumbersToSpaceLimited,
|
|
"PageNumber": exifConverters.convertNumbersToSpaceLimited,
|
|
"StripByteCounts": exifConverters.convertNumbersToSpaceLimited,
|
|
"StripOffsets": exifConverters.convertNumbersToSpaceLimited,
|
|
"PrimaryChromaticities": exifConverters.convertRatsToSpaceLimited,
|
|
"WhitePoint": exifConverters.convertRatsToSpaceLimited,
|
|
"ReferenceBlackWhite": exifConverters.convertRatsToSpaceLimited,
|
|
"YCbCrCoefficients": exifConverters.convertRatsToSpaceLimited,
|
|
"ComponentsConfiguration": exifConverters.convertBytesToStringSpaceDelim,
|
|
"LensInfo": exifConverters.convertRatsToSpaceLimited,
|
|
"Padding": exifConverters.convertBinaryData,
|
|
"UserComment": exifConverters.convertUserComment,
|
|
"CFAPattern": func(ctx valueConverterContext, v any) any {
|
|
b := v.([]byte)
|
|
horizontalRepeat := ctx.s.byteOrder.Uint16(b[:2])
|
|
verticalRepeat := ctx.s.byteOrder.Uint16(b[2:])
|
|
repeatLen := int(horizontalRepeat) * int(verticalRepeat)
|
|
hi := 4 + repeatLen
|
|
if hi > len(b) {
|
|
// See issue 34.
|
|
// There are cameras that writes CFAPattern with a byte order that's not the one specified in the EXIF header.
|
|
order := ctx.s.otherByteOrder()
|
|
horizontalRepeat = order.Uint16(b[:2])
|
|
verticalRepeat = order.Uint16(b[2:])
|
|
repeatLen := int(horizontalRepeat) * int(verticalRepeat)
|
|
hi = 4 + repeatLen
|
|
if hi > len(b) {
|
|
// Just return the raw bytes.
|
|
return trimBytesNulls(b)
|
|
}
|
|
}
|
|
|
|
val := b[4:hi]
|
|
return fmt.Sprintf("%d %d %s", horizontalRepeat, verticalRepeat, exifConverters.convertBytesToStringSpaceDelim(ctx, val))
|
|
},
|
|
}
|
|
)
|
|
|
|
func newMetaDecoderEXIF(r io.Reader, byteOrder binary.ByteOrder, thumbnailOffset int64, opts Options) *metaDecoderEXIF {
|
|
s := newStreamReader(r, byteOrder)
|
|
return newMetaDecoderEXIFFromStreamReader(s, thumbnailOffset, opts)
|
|
}
|
|
|
|
func newMetaDecoderEXIFFromStreamReader(s *streamReader, thumbnailOffset int64, opts Options) *metaDecoderEXIF {
|
|
return &metaDecoderEXIF{
|
|
thumbnailOffset: thumbnailOffset,
|
|
seenIFDs: map[string]struct{}{},
|
|
streamReader: s,
|
|
opts: opts,
|
|
valueConverterCtx: valueConverterContext{
|
|
s: s,
|
|
warnfFunc: opts.Warnf,
|
|
},
|
|
}
|
|
}
|
|
|
|
// exifType represents the basic tiff tag data types.
|
|
type exifType uint16
|
|
|
|
type metaDecoderEXIF struct {
|
|
*streamReader
|
|
thumbnailOffset int64
|
|
seenIFDs map[string]struct{}
|
|
valueConverterCtx valueConverterContext
|
|
opts Options
|
|
}
|
|
|
|
func (e *metaDecoderEXIF) convertValue(typ exifType, r io.Reader) any {
|
|
v := e.doConvertValue(typ, r)
|
|
|
|
switch v := v.(type) {
|
|
case float64:
|
|
if isUndefined(v) {
|
|
return undef
|
|
}
|
|
case float32:
|
|
if isUndefined(float64(v)) {
|
|
return undef
|
|
}
|
|
|
|
}
|
|
|
|
return v
|
|
}
|
|
|
|
func (e *metaDecoderEXIF) doConvertValue(typ exifType, r io.Reader) any {
|
|
switch typ {
|
|
case exifTypeUnsignedByte1, exifTypeUndef1, exifTypeASCIIString1, exifTypeSignedByte1:
|
|
return e.read1r(r)
|
|
case exifTypeUnsignedShort2, exifTypeSignedShort2:
|
|
return e.read2r(r)
|
|
case exifTypeUnsignedLong4:
|
|
return e.read4r(r)
|
|
case exifTypeUnsignedRat8:
|
|
n, d := e.read4r(r), e.read4r(r)
|
|
if d == 0 {
|
|
return undef
|
|
}
|
|
r, err := NewRat[uint32](n, d)
|
|
if err != nil {
|
|
e.opts.Warnf("failed to convert rational: %v", err)
|
|
return 0
|
|
}
|
|
return r
|
|
case exifTypeSignedLong4:
|
|
return e.read4sr(r)
|
|
case exifTypeSignedRat8:
|
|
n, d := e.read4sr(r), e.read4sr(r)
|
|
r, err := NewRat[int32](n, d)
|
|
if err != nil {
|
|
e.opts.Warnf("failed to convert signed rational: %v", err)
|
|
return 0
|
|
}
|
|
return r
|
|
case exifTypeSignedFloat4:
|
|
return math.Float32frombits(e.read4r(r))
|
|
case exifTypeSignedDouble8:
|
|
return math.Float64frombits(e.read8r(r))
|
|
default:
|
|
return newInvalidFormatError(fmt.Errorf("unknown EXIF type %d", typ))
|
|
}
|
|
}
|
|
|
|
func (e *metaDecoderEXIF) convertValues(typ exifType, count, len int, r io.Reader) any {
|
|
if count == 0 {
|
|
return nil
|
|
}
|
|
|
|
if typ == exifTypeASCIIString1 {
|
|
b := e.readBytesFromRVolatile(len, r)
|
|
return string(trimBytesNulls(b[:count]))
|
|
}
|
|
|
|
if count == 1 {
|
|
return e.convertValue(typ, r)
|
|
}
|
|
|
|
values := make([]any, count)
|
|
allBytes := true
|
|
for i := range count {
|
|
v := e.convertValue(typ, r)
|
|
values[i] = v
|
|
if allBytes {
|
|
_, ok := v.(byte)
|
|
if !ok {
|
|
allBytes = false
|
|
}
|
|
}
|
|
}
|
|
|
|
if allBytes {
|
|
bs := make([]byte, count)
|
|
for i, v := range values {
|
|
b := v.(byte)
|
|
bs[i] = b
|
|
}
|
|
return bs
|
|
|
|
}
|
|
return values
|
|
}
|
|
|
|
func (e *metaDecoderEXIF) decode() (err error) {
|
|
e.readerOffset = e.pos()
|
|
byteOrderTag := e.read2()
|
|
|
|
switch byteOrderTag {
|
|
case byteOrderBigEndian:
|
|
e.byteOrder = binary.BigEndian
|
|
case byteOrderLittleEndian:
|
|
e.byteOrder = binary.LittleEndian
|
|
default:
|
|
return nil
|
|
}
|
|
|
|
e.skip(2)
|
|
|
|
// Main image.
|
|
ifd0Offset := e.read4()
|
|
|
|
if ifd0Offset < 8 {
|
|
return nil
|
|
}
|
|
|
|
e.skip(int64(ifd0Offset - 8))
|
|
|
|
if err := e.decodeTags("IFD0"); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Thumbnail IFD.
|
|
ifd1Offset := e.read4()
|
|
if ifd1Offset == 0 {
|
|
// No more.
|
|
return nil
|
|
}
|
|
e.seek(int64(ifd1Offset) + e.readerOffset)
|
|
|
|
if err := e.decodeTags("IFD1"); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// A tag is represented in 12 bytes:
|
|
// - 2 bytes for the tag ID
|
|
// - 2 bytes for the data type
|
|
// - 4 bytes for the number of data values of the specified type
|
|
// - 4 bytes for the value itself, if it fits, otherwise for a pointer to another location where the data may be found;
|
|
// this could be a pointer to the beginning of another IFD.
|
|
func (e *metaDecoderEXIF) decodeTag(namespace string) error {
|
|
tagID := e.read2()
|
|
dataType := e.read2()
|
|
count := e.read4()
|
|
if count > 0x10000 {
|
|
e.skip(4)
|
|
return nil
|
|
}
|
|
|
|
tagName := exifFieldsAll[tagID]
|
|
if tagName == "" {
|
|
tagName = fmt.Sprintf("%s0x%x", UnknownPrefix, tagID)
|
|
}
|
|
|
|
if strings.Contains(tagName, " ") {
|
|
// Space separated, pick first.
|
|
parts := strings.Split(tagName, " ")
|
|
tagName = parts[0]
|
|
|
|
}
|
|
|
|
ifd, isIFDPointer := exifIFDPointers[tagID]
|
|
if isIFDPointer {
|
|
if _, ok := e.seenIFDs[ifd]; ok {
|
|
return nil
|
|
}
|
|
e.seenIFDs[ifd] = struct{}{}
|
|
}
|
|
|
|
typ := exifType(dataType)
|
|
|
|
size, ok := exifTypeSize[typ]
|
|
if !ok {
|
|
return newInvalidFormatErrorf("unknown EXIF type %d", typ)
|
|
}
|
|
valLen := size * count
|
|
|
|
if tagID == xmpMarker {
|
|
if !e.opts.Sources.Has(XMP) {
|
|
e.skip(4)
|
|
return nil
|
|
}
|
|
|
|
valueOffset := e.read4()
|
|
return e.preservePos(func() error {
|
|
offset := valueOffset + uint32(e.readerOffset)
|
|
e.seek(int64(offset))
|
|
r, err := e.bufferedReader(int64(valLen))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer r.Close()
|
|
return decodeXMP(r, e.opts)
|
|
})
|
|
|
|
}
|
|
|
|
if tagID == iptcMarker {
|
|
if !e.opts.Sources.Has(IPTC) {
|
|
e.skip(4)
|
|
return nil
|
|
}
|
|
valueOffset := e.read4()
|
|
return e.preservePos(func() error {
|
|
offset := valueOffset + uint32(e.readerOffset)
|
|
e.seek(int64(offset))
|
|
r, err := e.bufferedReader(int64(valLen))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer r.Close()
|
|
iptcDec := newMetaDecoderIPTC(r, e.opts)
|
|
return iptcDec.decodeRecords()
|
|
})
|
|
|
|
}
|
|
|
|
// Below is EXIF
|
|
if !e.opts.Sources.Has(EXIF) || valLen > uint32(e.opts.LimitTagSize) {
|
|
e.skip(4)
|
|
return nil
|
|
}
|
|
|
|
tagInfo := TagInfo{
|
|
Source: EXIF,
|
|
Tag: tagName,
|
|
Namespace: namespace,
|
|
}
|
|
|
|
if !isIFDPointer && !e.opts.ShouldHandleTag(tagInfo) {
|
|
e.skip(4)
|
|
return nil
|
|
}
|
|
|
|
var val any
|
|
|
|
if err := func() error {
|
|
var r io.Reader = e.r
|
|
if valLen > 4 {
|
|
valueOffset := e.read4()
|
|
offset := valueOffset + uint32(e.readerOffset)
|
|
oldPos := e.pos()
|
|
defer e.seek(oldPos)
|
|
e.seek(int64(offset))
|
|
rc, err := e.bufferedReader(int64(valLen))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
r = rc
|
|
defer rc.Close()
|
|
}
|
|
|
|
val = e.convertValues(typ, int(count), int(valLen), r)
|
|
|
|
if valLen <= 4 {
|
|
padding := 4 - valLen
|
|
if padding > 0 {
|
|
e.skip(int64(padding))
|
|
}
|
|
}
|
|
return nil
|
|
}(); err != nil {
|
|
return err
|
|
}
|
|
|
|
if isIFDPointer {
|
|
offset, ok := val.(uint32)
|
|
if !ok {
|
|
return newInvalidFormatErrorf("invalid IFD pointer value")
|
|
}
|
|
namespace := path.Join(namespace, ifd)
|
|
return e.decodeTagsAt(namespace, int64(offset))
|
|
}
|
|
|
|
if convert, found := exifValueConverterMap[tagName]; found {
|
|
e.valueConverterCtx.tagName = tagName
|
|
val = convert(e.valueConverterCtx, val)
|
|
if f, ok := val.(float64); ok && isUndefined(f) {
|
|
val = undef
|
|
}
|
|
} else {
|
|
val = toPrintableValue(val)
|
|
}
|
|
|
|
if val == nil {
|
|
val = ""
|
|
}
|
|
|
|
if tagName == tagNameThumbnailOffset {
|
|
// When set, thumbnailOffset is set to the offset of the EXIF data in the original file.
|
|
val = val.(uint32) + uint32(e.readerOffset+e.thumbnailOffset)
|
|
}
|
|
|
|
tagInfo.Value = val
|
|
|
|
if err := e.opts.HandleTag(tagInfo); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (e *metaDecoderEXIF) decodeTags(namespace string) error {
|
|
numTags := e.read2()
|
|
|
|
for range int(numTags) {
|
|
if err := e.decodeTag(namespace); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (e *metaDecoderEXIF) decodeTagsAt(namespace string, offset int64) error {
|
|
return e.preservePos(
|
|
func() error {
|
|
e.seek(offset + e.readerOffset)
|
|
return e.decodeTags(namespace)
|
|
})
|
|
}
|
|
|
|
type valueConverterContext struct {
|
|
tagName string
|
|
s *streamReader
|
|
warnfFunc func(string, ...any)
|
|
}
|
|
|
|
func (ctx valueConverterContext) warnf(format string, args ...any) {
|
|
format = ctx.tagName + ": " + format
|
|
ctx.warnfFunc(format, args...)
|
|
}
|
|
|
|
type valueConverter func(valueConverterContext, any) any
|
|
|
|
func init() {
|
|
maps.Copy(exifFieldsAll, exifFields)
|
|
maps.Copy(exifFieldsAll, exifFieldsGPS)
|
|
|
|
for k := range exifFieldsAll {
|
|
if k > maxEXIFField {
|
|
maxEXIFField = k
|
|
}
|
|
}
|
|
}
|