mattermost-community-enterp.../vendor/github.com/bep/imagemeta/metadecoder_iptc.go
Claude ec1f89217a Merge: Complete Mattermost Server with Community Enterprise
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>
2025-12-17 23:59:07 +09:00

418 lines
8.8 KiB
Go

// Copyright 2024 Bjørn Erik Pedersen
// SPDX-License-Identifier: MIT
package imagemeta
import (
_ "embed" // needed for the embedded IPTC fields JSON
"encoding/binary"
"encoding/json"
"fmt"
"io"
"strconv"
"strings"
"golang.org/x/text/encoding"
"golang.org/x/text/encoding/charmap"
)
// Source: https://exiftool.org/TagNames/IPTC.html
//
//go:embed metadecoder_iptc_fields.json
var ipctTagsJSON []byte
var (
iptcRecordFields = map[uint8]map[uint8]iptcField{}
iptcRerordNames = map[uint8]string{
1: "IPTCEnvelope",
2: "IPTCApplication",
3: "IPTCNewsPhoto",
7: "IPTCPreObjectData",
8: "IPTCObjectData",
9: "IPTCPostObjectData",
240: "IPTCFotoStation",
}
)
const (
ipcCodedCharacterSet = 90
iptcMetaDataBlockID = 0x0404
)
type vcIPTC struct {
//*vc
}
func (c *vcIPTC) convertDateString(ctx valueConverterContext, v any) any {
s := toString(v)
// 20211020 => 2021:10:20
if len(s) == 8 {
return fmt.Sprintf("%s:%s:%s", s[:4], s[4:6], s[6:])
}
// 2015-01-22 => 2015:01:22
if len(s) == 10 {
return fmt.Sprintf("%s:%s:%s", s[:4], s[5:7], s[8:])
}
return s
}
func (c *vcIPTC) convertTime(ctx valueConverterContext, v any) any {
s := toString(v)
// 111116 => 11:11:16
if len(s) == 6 {
return fmt.Sprintf("%s:%s:%s", s[:2], s[2:4], s[4:])
}
// 130444+1000 => 13:04:44+10:00
if len(s) == 11 {
return fmt.Sprintf("%s:%s:%s%s:%s", s[:2], s[2:4], s[4:6], s[6:9], s[9:])
}
return s
}
var (
iptcConverters = &vcIPTC{}
iptcValueConverterMap = map[string]valueConverter{
"DateCreated": iptcConverters.convertDateString,
"DateSent": iptcConverters.convertDateString,
"DigitalCreationDate": iptcConverters.convertDateString,
"DigitalCreationTime": iptcConverters.convertTime,
"TimeSent": iptcConverters.convertTime,
"TimeCreated": func(ctx valueConverterContext, v any) any {
s := toString(v)
if len(s) == 11 {
// 210101+0000 => 21:01:01+00:00
return fmt.Sprintf("%s:%s:%s%s:%s", s[:2], s[2:4], s[4:7], s[7:9], s[9:])
}
if len(s) == 6 {
// 124633 => 12:46:33
return fmt.Sprintf("%s:%s:%s", s[:2], s[2:4], s[4:])
}
return s
},
"ProgramVersion": func(ctx valueConverterContext, v any) any {
s := toString(v)
s = strings.TrimSuffix(s, ".0")
return s
},
"CodedCharacterSet": func(ctx valueConverterContext, v any) any {
b := v.([]byte)
s := resolveCodedCharacterSet(b)
if s == "" {
return characterSetUTF8
}
return s
},
}
)
func newMetaDecoderIPTC(r io.Reader, opts Options) *metaDecoderIPTC {
s := newStreamReader(r, binary.BigEndian)
return &metaDecoderIPTC{
streamReader: s,
iso88591CharsetDecoder: charmap.ISO8859_1.NewDecoder(),
valueConverterContext: valueConverterContext{
s: s,
warnfFunc: opts.Warnf,
},
opts: opts,
}
}
type iptcField struct {
Record uint8 `json:"record"`
RecordName string `json:"record_name"`
ID uint8 `json:"id"`
Name string `json:"name"`
Format string `json:"format"`
Repeatable bool `json:"repeatable"`
Notes string `json:"notes"`
}
type metaDecoderIPTC struct {
*streamReader
charset string
iso88591CharsetDecoder *encoding.Decoder
valueConverterContext valueConverterContext
opts Options
}
// Decode decodes the IPTC records delimited by 0x1C.
func (e *metaDecoderIPTC) decodeRecords() (err error) {
stringSlices := make(map[TagInfo][]string)
for {
var marker uint8
if err := binary.Read(e.r, e.byteOrder, &marker); err != nil {
if err == io.EOF {
break
}
return err
}
if marker != 0x1C {
break
}
if err := e.decodeRecord(stringSlices); err != nil {
return err
}
}
if err := e.handlestringSlices(stringSlices); err != nil {
return err
}
return nil
}
func (e *metaDecoderIPTC) handlestringSlices(m map[TagInfo][]string) error {
if len(m) == 0 {
return nil
}
for ti, values := range m {
if len(values) == 0 || len(values) > 1 {
ti.Value = values
} else {
ti.Value = values[0]
}
if err := e.opts.HandleTag(ti); err != nil {
return err
}
}
return nil
}
// decodeBlocks decodes the IPTC data from segments separated by 8BIM.
// This assumes a reader that starts out at 8BIM (no headers)
func (e *metaDecoderIPTC) decodeBlocks() (err error) {
stringSlices := make(map[TagInfo][]string)
decodeBlock := func() error {
blockType := e.readBytesVolatile(4)
if string(blockType) != "8BIM" {
return errStop
}
identifier := e.read2()
isNotMeta := identifier != iptcMetaDataBlockID
nameLength := e.read1()
if nameLength == 0 {
nameLength = 2
} else if nameLength%2 == 1 {
nameLength++
}
e.skip(int64(nameLength - 1))
dataSize := e.read4()
if isNotMeta {
e.skip(int64(dataSize))
return nil
}
if dataSize%2 != 0 {
defer func() {
// Skip padding byte.
e.skip(1)
}()
}
for {
marker := e.read1()
if e.isEOF || marker != 0x1C {
return errStop
}
if err := e.decodeRecord(stringSlices); err != nil {
return err
}
}
}
for {
if err := decodeBlock(); err != nil {
if err == errStop {
break
}
return err
}
}
if err := e.handlestringSlices(stringSlices); err != nil {
return err
}
return nil
}
func (e *metaDecoderIPTC) decodeRecord(stringSlices map[TagInfo][]string) error {
recordType := e.read1()
datasetNumber := e.read1()
recordSize := e.read2()
recordDef, ok := getIptcRecordFieldDef(recordType, datasetNumber)
if !ok {
// Assume a non repeatable string.
recordDef = iptcField{
Name: fmt.Sprintf("%s%d", UnknownPrefix, datasetNumber),
RecordName: "IPTCUnknownRecord",
Format: "string",
Repeatable: false,
}
}
ti := TagInfo{
Source: IPTC,
Tag: recordDef.Name,
Namespace: recordDef.RecordName,
}
if recordSize > uint16(e.opts.LimitTagSize) || !e.opts.ShouldHandleTag(ti) {
e.skip(int64(recordSize))
return nil
}
var v any
switch recordDef.Format {
case "string":
v = e.readBytesVolatile(int(recordSize))
if e.charset == "" || e.charset == characterSetISO88591 {
v, _ = e.iso88591CharsetDecoder.Bytes(v.([]byte))
}
case "uint32":
v = e.read4()
case "short":
v = e.read2()
case "byte":
v = e.read1()
default:
panic(fmt.Errorf("unsupported format %q", recordDef.Format))
}
if convert, found := iptcValueConverterMap[recordDef.Name]; found {
e.valueConverterContext.tagName = recordDef.Name
v = convert(e.valueConverterContext, v)
}
if recordType == 1 && datasetNumber == ipcCodedCharacterSet {
e.charset = v.(string)
}
if b, ok := v.([]byte); ok {
v = strings.TrimSpace(string(trimBytesNulls(b)))
}
if recordDef.Repeatable {
stringSlices[ti] = append(stringSlices[ti], toString(v))
} else {
ti.Value = v
if err := e.opts.HandleTag(ti); err != nil {
return err
}
}
return nil
}
func getIptcRecordFieldDef(record, id uint8) (iptcField, bool) {
recordFields, ok := iptcRecordFields[record]
if !ok {
return iptcField{}, false
}
field, ok := recordFields[id]
return field, ok
}
func getIptcRecordName(record uint8) string {
name, ok := iptcRerordNames[record]
if !ok {
return fmt.Sprintf("IPTCUnknownRecord%d", record)
}
return name
}
func init() {
var fields []map[string]any
if err := json.Unmarshal(ipctTagsJSON, &fields); err != nil {
panic(err)
}
toUint8 := func(v any) uint8 {
s := v.(string)
i, err := strconv.Atoi(s)
if err != nil {
return 0
}
return uint8(i)
}
toString := func(v any) string {
if v == nil {
return ""
}
return v.(string)
}
for _, fieldv := range fields {
id := toUint8(fieldv["id"])
record := toUint8(fieldv["record"])
recordFields, ok := iptcRecordFields[record]
if !ok {
recordFields = map[uint8]iptcField{}
iptcRecordFields[record] = recordFields
}
recordFields[id] = iptcField{
Record: record,
RecordName: getIptcRecordName(record),
ID: id,
Name: toString(fieldv["name"]),
Format: toString(fieldv["format"]),
Notes: toString(fieldv["notes"]),
Repeatable: fieldv["repeatable"] == "true",
}
}
}
const (
characterSetUTF8 = "UTF-8"
characterSetISO88591 = "ISO-8859-1"
)
// resolveCodedCharacterSet resolves the coded character set from the IPTC data
// to be either UTF-8 or ISO-8859-1 or an empty string if it cannot be resolved.
func resolveCodedCharacterSet(b []byte) string {
const (
esc = 0x1B
percent = 0x25
latinCapitalG = 0x47
dot = 0x2E
latinCapitalA = 0x41
minus = 0x2D
)
if len(b) > 2 && b[0] == esc && b[1] == percent && b[2] == latinCapitalG {
return characterSetUTF8
}
if len(b) > 2 && b[0] == esc && b[1] == dot && b[2] == latinCapitalA {
return characterSetISO88591
}
if len(b) > 3 && b[0] == esc && (b[1] == dot || b[2] == dot || b[3] == dot) && b[4] == latinCapitalA {
return characterSetISO88591
}
if len(b) > 2 && b[0] == esc && b[1] == minus && b[2] == latinCapitalA {
return characterSetISO88591
}
return ""
}