mirror of
https://github.com/Alexander-D-Karpov/photodock.git
synced 2026-03-16 22:06:35 +03:00
782 lines
19 KiB
Go
782 lines
19 KiB
Go
package services
|
|
|
|
import (
|
|
"encoding/binary"
|
|
"encoding/json"
|
|
"fmt"
|
|
"math"
|
|
"os"
|
|
"os/exec"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/Alexander-D-Karpov/photodock/internal/models"
|
|
"github.com/rwcarlsen/goexif/exif"
|
|
)
|
|
|
|
type ExifService struct {
|
|
hasExiftool bool
|
|
}
|
|
|
|
func NewExifService() *ExifService {
|
|
_, err := exec.LookPath("exiftool")
|
|
return &ExifService{
|
|
hasExiftool: err == nil,
|
|
}
|
|
}
|
|
|
|
func (s *ExifService) Extract(path string) (*models.ExifInfo, time.Time, error) {
|
|
if s.hasExiftool {
|
|
return s.extractWithExiftool(path)
|
|
}
|
|
return s.extractWithGoexif(path)
|
|
}
|
|
|
|
func (s *ExifService) extractWithExiftool(path string) (*models.ExifInfo, time.Time, error) {
|
|
cmd := exec.Command("exiftool", "-json", "-a", "-G1", "-n", path)
|
|
output, err := cmd.Output()
|
|
if err != nil {
|
|
return s.extractWithGoexif(path)
|
|
}
|
|
|
|
var results []map[string]interface{}
|
|
if err := json.Unmarshal(output, &results); err != nil || len(results) == 0 {
|
|
return s.extractWithGoexif(path)
|
|
}
|
|
|
|
data := results[0]
|
|
info := &models.ExifInfo{}
|
|
var takenAt time.Time
|
|
|
|
info.CameraMake = getString(data, "IFD0:Make")
|
|
info.CameraModel = getString(data, "IFD0:Model")
|
|
if info.CameraModel == "" {
|
|
info.CameraModel = getString(data, "IFD0:CameraModelName")
|
|
}
|
|
|
|
info.LensModel = getString(data, "ExifIFD:LensModel")
|
|
if info.LensModel == "" {
|
|
info.LensModel = getString(data, "Composite:Lens")
|
|
}
|
|
info.LensInfo = getString(data, "ExifIFD:LensInfo")
|
|
|
|
if fl := getFloat(data, "ExifIFD:FocalLength"); fl > 0 {
|
|
info.FocalLength = fmt.Sprintf("%.1f mm", fl)
|
|
}
|
|
if fl35 := getFloat(data, "ExifIFD:FocalLengthIn35mmFormat"); fl35 > 0 {
|
|
info.FocalLength35mm = fmt.Sprintf("%.0f mm", fl35)
|
|
} else if fl35 := getFloat(data, "Composite:FocalLength35efl"); fl35 > 0 {
|
|
info.FocalLength35mm = fmt.Sprintf("%.1f mm", fl35)
|
|
}
|
|
|
|
if minFL := getFloat(data, "Canon:MinFocalLength"); minFL > 0 {
|
|
maxFL := getFloat(data, "Canon:MaxFocalLength")
|
|
info.LensInfo = fmt.Sprintf("%.0f - %.0f mm", minFL, maxFL)
|
|
}
|
|
|
|
if fn := getFloat(data, "ExifIFD:FNumber"); fn > 0 {
|
|
info.Aperture = fmt.Sprintf("f/%.1f", fn)
|
|
}
|
|
if maxAp := getFloat(data, "ExifIFD:MaxApertureValue"); maxAp > 0 {
|
|
fnum := math.Pow(2, maxAp/2)
|
|
info.MaxApertureValue = fmt.Sprintf("f/%.1f", fnum)
|
|
}
|
|
|
|
if et := getFloat(data, "ExifIFD:ExposureTime"); et > 0 {
|
|
if et >= 1 {
|
|
info.ShutterSpeed = fmt.Sprintf("%.1f s", et)
|
|
} else {
|
|
info.ShutterSpeed = fmt.Sprintf("1/%.0f s", 1/et)
|
|
}
|
|
}
|
|
|
|
if iso := getInt(data, "ExifIFD:ISO"); iso > 0 {
|
|
info.ISO = iso
|
|
}
|
|
|
|
if ec := getFloat(data, "ExifIFD:ExposureCompensation"); ec != 0 {
|
|
if ec >= 0 {
|
|
info.ExposureComp = fmt.Sprintf("+%.1f EV", ec)
|
|
} else {
|
|
info.ExposureComp = fmt.Sprintf("%.1f EV", ec)
|
|
}
|
|
} else {
|
|
info.ExposureComp = "0 EV"
|
|
}
|
|
|
|
info.ExposureMode = decodeExposureModeValue(getInt(data, "ExifIFD:ExposureMode"))
|
|
info.ExposureProgram = decodeExposureProgram(getInt(data, "ExifIFD:ExposureProgram"))
|
|
info.MeteringMode = decodeMeteringMode(getInt(data, "ExifIFD:MeteringMode"))
|
|
|
|
if lv := getFloat(data, "Composite:LightValue"); lv != 0 {
|
|
info.LightValue = fmt.Sprintf("%.1f", lv)
|
|
}
|
|
|
|
flash := getInt(data, "ExifIFD:Flash")
|
|
info.Flash = decodeFlash(flash)
|
|
info.FlashMode = getString(data, "Canon:CanonFlashMode")
|
|
if fec := getFloat(data, "Canon:FlashExposureComp"); fec != 0 {
|
|
info.FlashExposureComp = fmt.Sprintf("%.1f EV", fec)
|
|
}
|
|
|
|
wb := getInt(data, "ExifIFD:WhiteBalance")
|
|
if wb == 0 {
|
|
info.WhiteBalance = "Auto"
|
|
} else {
|
|
info.WhiteBalance = "Manual"
|
|
}
|
|
if ct := getInt(data, "Canon:ColorTemperature"); ct > 0 {
|
|
info.ColorTemperature = ct
|
|
}
|
|
|
|
info.ColorSpace = decodeColorSpace(getInt(data, "ExifIFD:ColorSpace"))
|
|
|
|
info.FocusMode = getString(data, "Canon:FocusMode")
|
|
if info.FocusMode == "" {
|
|
info.FocusMode = getString(data, "ExifIFD:FocusMode")
|
|
}
|
|
|
|
if fdUpper := getFloat(data, "Canon:FocusDistanceUpper"); fdUpper > 0 {
|
|
fdLower := getFloat(data, "Canon:FocusDistanceLower")
|
|
if fdLower > 0 && fdLower != fdUpper {
|
|
info.FocusDistance = fmt.Sprintf("%.2f - %.2f m", fdLower, fdUpper)
|
|
} else {
|
|
info.FocusDistance = fmt.Sprintf("%.2f m", fdUpper)
|
|
}
|
|
}
|
|
|
|
if sd := getFloat(data, "ExifIFD:SubjectDistance"); sd > 0 {
|
|
if sd < 1 {
|
|
info.SubjectDistance = fmt.Sprintf("%.0f cm", sd*100)
|
|
} else {
|
|
info.SubjectDistance = fmt.Sprintf("%.2f m", sd)
|
|
}
|
|
}
|
|
|
|
info.DepthOfField = getString(data, "Composite:DOF")
|
|
info.HyperfocalDist = getString(data, "Composite:HyperfocalDistance")
|
|
|
|
info.Contrast = decodeCanonLevel(getString(data, "Canon:Contrast"))
|
|
info.Saturation = decodeCanonLevel(getString(data, "Canon:Saturation"))
|
|
info.Sharpness = getString(data, "Canon:Sharpness")
|
|
if info.Sharpness == "0" {
|
|
info.Sharpness = "Normal"
|
|
}
|
|
|
|
info.SceneCaptureType = decodeSceneCaptureType(getInt(data, "ExifIFD:SceneCaptureType"))
|
|
info.ShootingMode = getString(data, "Composite:ShootingMode")
|
|
if info.ShootingMode == "" {
|
|
info.ShootingMode = getString(data, "Canon:EasyMode")
|
|
}
|
|
info.DriveMode = getString(data, "Composite:DriveMode")
|
|
if info.DriveMode == "" {
|
|
info.DriveMode = getString(data, "Canon:ContinuousDrive")
|
|
}
|
|
info.MacroMode = getString(data, "Canon:MacroMode")
|
|
info.SelfTimer = getString(data, "Canon:SelfTimer")
|
|
info.ImageStabilization = getString(data, "Canon:ImageStabilization")
|
|
|
|
dzr := getFloat(data, "ExifIFD:DigitalZoomRatio")
|
|
if dzr <= 1 {
|
|
info.DigitalZoom = "None"
|
|
} else {
|
|
info.DigitalZoom = fmt.Sprintf("%.1fx", dzr)
|
|
}
|
|
|
|
info.Quality = getString(data, "Canon:Quality")
|
|
info.Orientation = getInt(data, "IFD0:Orientation")
|
|
|
|
info.SensingMethod = decodeSensingMethod(getInt(data, "ExifIFD:SensingMethod"))
|
|
info.FileSource = getString(data, "ExifIFD:FileSource")
|
|
if info.FileSource == "3" {
|
|
info.FileSource = "Digital Camera"
|
|
}
|
|
cr := getInt(data, "ExifIFD:CustomRendered")
|
|
if cr == 0 {
|
|
info.CustomRendered = "Normal"
|
|
} else if cr == 1 {
|
|
info.CustomRendered = "Custom"
|
|
}
|
|
|
|
info.FirmwareVersion = getString(data, "Canon:FirmwareVersion")
|
|
if info.FirmwareVersion == "" {
|
|
info.FirmwareVersion = getString(data, "Canon:CanonFirmwareVersion")
|
|
}
|
|
info.SerialNumber = getString(data, "Canon:SerialNumber")
|
|
if info.SerialNumber == "" {
|
|
info.SerialNumber = getString(data, "ExifIFD:SerialNumber")
|
|
}
|
|
info.CameraTemperature = getString(data, "Canon:CameraTemperature")
|
|
info.FileNumber = getString(data, "Canon:FileNumber")
|
|
info.OwnerName = getString(data, "Canon:OwnerName")
|
|
info.ImageUniqueID = getString(data, "Canon:ImageUniqueID")
|
|
if info.ImageUniqueID == "" {
|
|
info.ImageUniqueID = getString(data, "ExifIFD:ImageUniqueID")
|
|
}
|
|
|
|
if dto := getString(data, "ExifIFD:DateTimeOriginal"); dto != "" {
|
|
if t, err := time.Parse("2006:01:02 15:04:05", dto); err == nil {
|
|
takenAt = t
|
|
info.DateTimeOriginal = t.Format("2006-01-02 15:04:05")
|
|
}
|
|
}
|
|
info.CreateDate = getString(data, "ExifIFD:CreateDate")
|
|
info.ModifyDate = getString(data, "IFD0:ModifyDate")
|
|
|
|
info.Artist = getString(data, "IFD0:Artist")
|
|
info.Copyright = getString(data, "IFD0:Copyright")
|
|
info.Software = getString(data, "IFD0:Software")
|
|
info.ImageDescription = getString(data, "IFD0:ImageDescription")
|
|
|
|
info.ImageWidth = getInt(data, "File:ImageWidth")
|
|
info.ImageHeight = getInt(data, "File:ImageHeight")
|
|
|
|
return info, takenAt, nil
|
|
}
|
|
|
|
func getString(data map[string]interface{}, key string) string {
|
|
if v, ok := data[key]; ok {
|
|
switch val := v.(type) {
|
|
case string:
|
|
return strings.TrimSpace(val)
|
|
case float64:
|
|
if val == float64(int(val)) {
|
|
return strconv.Itoa(int(val))
|
|
}
|
|
return fmt.Sprintf("%.2f", val)
|
|
case int:
|
|
return strconv.Itoa(val)
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func getFloat(data map[string]interface{}, key string) float64 {
|
|
if v, ok := data[key]; ok {
|
|
switch val := v.(type) {
|
|
case float64:
|
|
return val
|
|
case int:
|
|
return float64(val)
|
|
case string:
|
|
if f, err := strconv.ParseFloat(val, 64); err == nil {
|
|
return f
|
|
}
|
|
}
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func getInt(data map[string]interface{}, key string) int {
|
|
if v, ok := data[key]; ok {
|
|
switch val := v.(type) {
|
|
case float64:
|
|
return int(val)
|
|
case int:
|
|
return val
|
|
case string:
|
|
if i, err := strconv.Atoi(val); err == nil {
|
|
return i
|
|
}
|
|
}
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func decodeCanonLevel(s string) string {
|
|
s = strings.TrimSpace(s)
|
|
switch strings.ToLower(s) {
|
|
case "0", "normal":
|
|
return "Normal"
|
|
case "1", "low":
|
|
return "Low"
|
|
case "2", "high":
|
|
return "High"
|
|
default:
|
|
if s != "" {
|
|
return s
|
|
}
|
|
return ""
|
|
}
|
|
}
|
|
|
|
func decodeExposureModeValue(val int) string {
|
|
modes := map[int]string{0: "Auto", 1: "Manual", 2: "Auto bracket"}
|
|
if m, ok := modes[val]; ok {
|
|
return m
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func decodeColorSpace(val int) string {
|
|
switch val {
|
|
case 1:
|
|
return "sRGB"
|
|
case 2:
|
|
return "Adobe RGB"
|
|
case 65535:
|
|
return "Uncalibrated"
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func (s *ExifService) extractWithGoexif(path string) (*models.ExifInfo, time.Time, error) {
|
|
f, err := os.Open(path)
|
|
if err != nil {
|
|
return nil, time.Time{}, err
|
|
}
|
|
defer func() { _ = f.Close() }()
|
|
|
|
x, err := exif.Decode(f)
|
|
if err != nil {
|
|
return &models.ExifInfo{}, time.Time{}, nil
|
|
}
|
|
|
|
info := &models.ExifInfo{}
|
|
var takenAt time.Time
|
|
|
|
info.CameraMake = s.getStringTag(x, exif.Make)
|
|
info.CameraModel = s.getStringTag(x, exif.Model)
|
|
info.LensModel = s.getStringTag(x, exif.LensModel)
|
|
info.Software = s.getStringTag(x, exif.Software)
|
|
info.Artist = s.getStringTag(x, exif.Artist)
|
|
info.Copyright = s.getStringTag(x, exif.Copyright)
|
|
info.ImageDescription = s.getStringTag(x, exif.ImageDescription)
|
|
|
|
if tag, err := x.Get(exif.FocalLength); err == nil {
|
|
if num, denom, err := tag.Rat2(0); err == nil && denom != 0 {
|
|
fl := float64(num) / float64(denom)
|
|
info.FocalLength = fmt.Sprintf("%.1f mm", fl)
|
|
}
|
|
}
|
|
|
|
if tag, err := x.Get(exif.FocalLengthIn35mmFilm); err == nil {
|
|
if val, err := tag.Int(0); err == nil {
|
|
info.FocalLength35mm = fmt.Sprintf("%d mm", val)
|
|
}
|
|
}
|
|
|
|
if tag, err := x.Get(exif.FNumber); err == nil {
|
|
if num, denom, err := tag.Rat2(0); err == nil && denom != 0 {
|
|
info.Aperture = fmt.Sprintf("f/%.1f", float64(num)/float64(denom))
|
|
}
|
|
}
|
|
|
|
if tag, err := x.Get(exif.MaxApertureValue); err == nil {
|
|
if num, denom, err := tag.Rat2(0); err == nil && denom != 0 {
|
|
apex := float64(num) / float64(denom)
|
|
fnum := math.Pow(2, apex/2)
|
|
info.MaxApertureValue = fmt.Sprintf("f/%.1f", fnum)
|
|
}
|
|
}
|
|
|
|
if tag, err := x.Get(exif.ExposureTime); err == nil {
|
|
if num, denom, err := tag.Rat2(0); err == nil && denom != 0 {
|
|
et := float64(num) / float64(denom)
|
|
if et >= 1 {
|
|
info.ShutterSpeed = fmt.Sprintf("%.1f s", et)
|
|
} else {
|
|
info.ShutterSpeed = fmt.Sprintf("1/%.0f s", 1/et)
|
|
}
|
|
}
|
|
}
|
|
|
|
if tag, err := x.Get(exif.ISOSpeedRatings); err == nil {
|
|
if val, err := tag.Int(0); err == nil {
|
|
info.ISO = val
|
|
}
|
|
}
|
|
|
|
if tag, err := x.Get(exif.ExposureBiasValue); err == nil {
|
|
if num, denom, err := tag.Rat2(0); err == nil && denom != 0 {
|
|
val := float64(num) / float64(denom)
|
|
if val >= 0 {
|
|
info.ExposureComp = fmt.Sprintf("+%.1f EV", val)
|
|
} else {
|
|
info.ExposureComp = fmt.Sprintf("%.1f EV", val)
|
|
}
|
|
}
|
|
}
|
|
|
|
if tag, err := x.Get(exif.Flash); err == nil {
|
|
if val, err := tag.Int(0); err == nil {
|
|
info.Flash = decodeFlash(val)
|
|
}
|
|
}
|
|
|
|
if tag, err := x.Get(exif.WhiteBalance); err == nil {
|
|
if val, err := tag.Int(0); err == nil {
|
|
if val == 0 {
|
|
info.WhiteBalance = "Auto"
|
|
} else {
|
|
info.WhiteBalance = "Manual"
|
|
}
|
|
}
|
|
}
|
|
|
|
if tag, err := x.Get(exif.MeteringMode); err == nil {
|
|
if val, err := tag.Int(0); err == nil {
|
|
info.MeteringMode = decodeMeteringMode(val)
|
|
}
|
|
}
|
|
|
|
if tag, err := x.Get(exif.ExposureMode); err == nil {
|
|
if val, err := tag.Int(0); err == nil {
|
|
info.ExposureMode = decodeExposureMode(val)
|
|
}
|
|
}
|
|
|
|
if tag, err := x.Get(exif.ExposureProgram); err == nil {
|
|
if val, err := tag.Int(0); err == nil {
|
|
info.ExposureProgram = decodeExposureProgram(val)
|
|
}
|
|
}
|
|
|
|
if tag, err := x.Get(exif.ColorSpace); err == nil {
|
|
if val, err := tag.Int(0); err == nil {
|
|
info.ColorSpace = decodeColorSpace(val)
|
|
}
|
|
}
|
|
|
|
if tag, err := x.Get(exif.Orientation); err == nil {
|
|
if val, err := tag.Int(0); err == nil {
|
|
info.Orientation = val
|
|
}
|
|
}
|
|
|
|
if tag, err := x.Get(exif.SceneCaptureType); err == nil {
|
|
if val, err := tag.Int(0); err == nil {
|
|
info.SceneCaptureType = decodeSceneCaptureType(val)
|
|
}
|
|
}
|
|
|
|
if tag, err := x.Get(exif.DigitalZoomRatio); err == nil {
|
|
if num, denom, err := tag.Rat2(0); err == nil && denom != 0 {
|
|
ratio := float64(num) / float64(denom)
|
|
if ratio <= 1 {
|
|
info.DigitalZoom = "None"
|
|
} else {
|
|
info.DigitalZoom = fmt.Sprintf("%.1fx", ratio)
|
|
}
|
|
}
|
|
}
|
|
|
|
if tag, err := x.Get(exif.SensingMethod); err == nil {
|
|
if val, err := tag.Int(0); err == nil {
|
|
info.SensingMethod = decodeSensingMethod(val)
|
|
}
|
|
}
|
|
|
|
if tag, err := x.Get(exif.CustomRendered); err == nil {
|
|
if val, err := tag.Int(0); err == nil {
|
|
if val == 0 {
|
|
info.CustomRendered = "Normal"
|
|
} else {
|
|
info.CustomRendered = "Custom"
|
|
}
|
|
}
|
|
}
|
|
|
|
if tag, err := x.Get(exif.Contrast); err == nil {
|
|
if val, err := tag.Int(0); err == nil {
|
|
info.Contrast = decodeLevelTag(val)
|
|
}
|
|
}
|
|
|
|
if tag, err := x.Get(exif.Saturation); err == nil {
|
|
if val, err := tag.Int(0); err == nil {
|
|
info.Saturation = decodeLevelTag(val)
|
|
}
|
|
}
|
|
|
|
if tag, err := x.Get(exif.Sharpness); err == nil {
|
|
if val, err := tag.Int(0); err == nil {
|
|
info.Sharpness = decodeLevelTag(val)
|
|
}
|
|
}
|
|
|
|
if tag, err := x.Get(exif.SubjectDistance); err == nil {
|
|
if num, denom, err := tag.Rat2(0); err == nil && denom != 0 {
|
|
dist := float64(num) / float64(denom)
|
|
if dist < 1 {
|
|
info.SubjectDistance = fmt.Sprintf("%.0f cm", dist*100)
|
|
} else {
|
|
info.SubjectDistance = fmt.Sprintf("%.2f m", dist)
|
|
}
|
|
}
|
|
}
|
|
|
|
if tag, err := x.Get(exif.SubjectDistanceRange); err == nil {
|
|
if val, err := tag.Int(0); err == nil {
|
|
info.SubjectDistRange = decodeSubjectDistRange(val)
|
|
}
|
|
}
|
|
|
|
if tag, err := x.Get(exif.GainControl); err == nil {
|
|
if val, err := tag.Int(0); err == nil {
|
|
info.GainControl = decodeGainControl(val)
|
|
}
|
|
}
|
|
|
|
if tag, err := x.Get(exif.PixelXDimension); err == nil {
|
|
if val, err := tag.Int(0); err == nil {
|
|
info.ImageWidth = val
|
|
}
|
|
}
|
|
|
|
if tag, err := x.Get(exif.PixelYDimension); err == nil {
|
|
if val, err := tag.Int(0); err == nil {
|
|
info.ImageHeight = val
|
|
}
|
|
}
|
|
|
|
if tag, err := x.Get(exif.ImageUniqueID); err == nil {
|
|
if v, err := tag.StringVal(); err == nil {
|
|
info.ImageUniqueID = cleanString(v)
|
|
}
|
|
}
|
|
|
|
if tm, err := x.DateTime(); err == nil {
|
|
takenAt = tm
|
|
info.DateTimeOriginal = tm.Format("2006-01-02 15:04:05")
|
|
}
|
|
|
|
return info, takenAt, nil
|
|
}
|
|
|
|
func (s *ExifService) getStringTag(x *exif.Exif, field exif.FieldName) string {
|
|
if tag, err := x.Get(field); err == nil {
|
|
if v, err := tag.StringVal(); err == nil {
|
|
return cleanString(v)
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func (s *ExifService) StripGPS(path string) error {
|
|
return stripGPSFromJPEG(path)
|
|
}
|
|
|
|
func stripGPSFromJPEG(path string) error {
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if len(data) < 2 || data[0] != 0xFF || data[1] != 0xD8 {
|
|
return nil
|
|
}
|
|
|
|
modified := false
|
|
result := []byte{0xFF, 0xD8}
|
|
pos := 2
|
|
|
|
for pos < len(data)-1 {
|
|
if data[pos] != 0xFF {
|
|
result = append(result, data[pos:]...)
|
|
break
|
|
}
|
|
|
|
marker := data[pos+1]
|
|
if marker == 0xD9 {
|
|
result = append(result, data[pos:]...)
|
|
break
|
|
}
|
|
if marker == 0xDA {
|
|
result = append(result, data[pos:]...)
|
|
break
|
|
}
|
|
|
|
if pos+4 > len(data) {
|
|
break
|
|
}
|
|
segLen := int(binary.BigEndian.Uint16(data[pos+2:pos+4])) + 2
|
|
|
|
if marker == 0xE1 && pos+10 < len(data) {
|
|
if string(data[pos+4:pos+10]) == "Exif\x00\x00" {
|
|
cleaned, changed := removeGPSFromExif(data[pos : pos+segLen])
|
|
if changed {
|
|
modified = true
|
|
}
|
|
result = append(result, cleaned...)
|
|
pos += segLen
|
|
continue
|
|
}
|
|
}
|
|
|
|
result = append(result, data[pos:pos+segLen]...)
|
|
pos += segLen
|
|
}
|
|
|
|
if modified {
|
|
return os.WriteFile(path, result, 0644)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func removeGPSFromExif(segment []byte) ([]byte, bool) {
|
|
if len(segment) < 12 {
|
|
return segment, false
|
|
}
|
|
|
|
exifData := segment[10:]
|
|
if len(exifData) < 8 {
|
|
return segment, false
|
|
}
|
|
|
|
var bo binary.ByteOrder
|
|
if string(exifData[0:2]) == "II" {
|
|
bo = binary.LittleEndian
|
|
} else if string(exifData[0:2]) == "MM" {
|
|
bo = binary.BigEndian
|
|
} else {
|
|
return segment, false
|
|
}
|
|
|
|
ifdOffset := bo.Uint32(exifData[4:8])
|
|
if int(ifdOffset) >= len(exifData) {
|
|
return segment, false
|
|
}
|
|
|
|
result := make([]byte, len(segment))
|
|
copy(result, segment)
|
|
exifResult := result[10:]
|
|
modified := false
|
|
|
|
offset := int(ifdOffset)
|
|
if offset+2 > len(exifResult) {
|
|
return segment, false
|
|
}
|
|
|
|
numEntries := int(bo.Uint16(exifResult[offset : offset+2]))
|
|
offset += 2
|
|
|
|
for i := 0; i < numEntries; i++ {
|
|
entryOffset := offset + i*12
|
|
if entryOffset+12 > len(exifResult) {
|
|
break
|
|
}
|
|
|
|
tag := bo.Uint16(exifResult[entryOffset : entryOffset+2])
|
|
if tag == 0x8825 {
|
|
for j := entryOffset; j < entryOffset+12; j++ {
|
|
exifResult[j] = 0
|
|
}
|
|
modified = true
|
|
}
|
|
}
|
|
|
|
return result, modified
|
|
}
|
|
|
|
func cleanString(s string) string {
|
|
return strings.TrimSpace(strings.Trim(s, "\x00"))
|
|
}
|
|
|
|
func decodeFlash(val int) string {
|
|
fired := val&1 == 1
|
|
mode := (val >> 3) & 3
|
|
|
|
var parts []string
|
|
|
|
if fired {
|
|
parts = append(parts, "Fired")
|
|
} else {
|
|
parts = append(parts, "Did not fire")
|
|
}
|
|
|
|
switch mode {
|
|
case 1:
|
|
parts = append(parts, "compulsory")
|
|
case 2:
|
|
parts = append(parts, "suppressed")
|
|
case 3:
|
|
parts = append(parts, "auto")
|
|
}
|
|
|
|
return strings.Join(parts, ", ")
|
|
}
|
|
|
|
func decodeMeteringMode(val int) string {
|
|
modes := map[int]string{
|
|
0: "Unknown", 1: "Average", 2: "Center-weighted average", 3: "Spot",
|
|
4: "Multi-spot", 5: "Multi-segment", 6: "Partial", 255: "Other",
|
|
}
|
|
if m, ok := modes[val]; ok {
|
|
return m
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func decodeExposureMode(val int) string {
|
|
modes := map[int]string{0: "Auto", 1: "Manual", 2: "Auto bracket"}
|
|
if m, ok := modes[val]; ok {
|
|
return m
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func decodeExposureProgram(val int) string {
|
|
programs := map[int]string{
|
|
0: "Not defined", 1: "Manual", 2: "Program AE", 3: "Aperture priority",
|
|
4: "Shutter priority", 5: "Creative (slow speed)", 6: "Action (high speed)",
|
|
7: "Portrait", 8: "Landscape", 9: "Bulb",
|
|
}
|
|
if p, ok := programs[val]; ok {
|
|
return p
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func decodeSceneCaptureType(val int) string {
|
|
types := map[int]string{
|
|
0: "Standard", 1: "Landscape", 2: "Portrait", 3: "Night scene",
|
|
}
|
|
if t, ok := types[val]; ok {
|
|
return t
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func decodeSubjectDistRange(val int) string {
|
|
ranges := map[int]string{
|
|
0: "Unknown", 1: "Macro", 2: "Close", 3: "Distant",
|
|
}
|
|
if r, ok := ranges[val]; ok {
|
|
return r
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func decodeSensingMethod(val int) string {
|
|
methods := map[int]string{
|
|
1: "Not defined", 2: "One-chip color area", 3: "Two-chip color area",
|
|
4: "Three-chip color area", 5: "Color sequential area",
|
|
7: "Trilinear", 8: "Color sequential linear",
|
|
}
|
|
if m, ok := methods[val]; ok {
|
|
return m
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func decodeGainControl(val int) string {
|
|
controls := map[int]string{
|
|
0: "None", 1: "Low gain up", 2: "High gain up",
|
|
3: "Low gain down", 4: "High gain down",
|
|
}
|
|
if c, ok := controls[val]; ok {
|
|
return c
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func decodeLevelTag(val int) string {
|
|
levels := map[int]string{0: "Normal", 1: "Low", 2: "High"}
|
|
if l, ok := levels[val]; ok {
|
|
return l
|
|
}
|
|
return ""
|
|
}
|