JPEG: Convert Apple "Display P3" colors to standard sRGB #1474

Other color profiles and file formats are not supported yet. Should
be easy to add though. Main difficulty will be profile name comparison:
For example "Adobe RGB (1998)" vs just "Adobe RGB".
This commit is contained in:
Michael Mayer 2021-12-09 07:00:39 +01:00
parent dd7caf83ea
commit 5be456a09f
28 changed files with 1065 additions and 65 deletions

View file

@ -154,6 +154,12 @@
<translate>{{ file.Orientation }}</translate>
</td>
</tr>
<tr v-if="file.ColorProfile">
<td>
<translate>Color Profile</translate>
</td>
<td>{{ file.ColorProfile }}</td>
</tr>
<tr v-if="file.MainColor">
<td>
<translate>Main Color</translate>

View file

@ -62,6 +62,7 @@ export class File extends RestModel {
Orientation: 0,
Projection: "",
AspectRatio: 1.0,
ColorProfile: "",
MainColor: "",
Colors: "",
Luminance: "",

2
go.mod
View file

@ -35,6 +35,7 @@ require (
github.com/leonelquinteros/gotext v1.5.0
github.com/lib/pq v1.8.0 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0
github.com/mandykoh/prism v0.34.0
github.com/manifoldco/promptui v0.9.0
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/mattn/go-sqlite3 v2.0.1+incompatible // indirect
@ -76,6 +77,7 @@ require (
github.com/go-xmlfmt/xmlfmt v0.0.0-20191208150333-d5b6f63a941b // indirect
github.com/gosimple/unidecode v1.0.1 // indirect
github.com/leodido/go-urn v1.2.1 // indirect
github.com/mandykoh/go-parallel v0.1.0 // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect

5
go.sum
View file

@ -213,6 +213,10 @@ github.com/lib/pq v1.8.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/machinebox/progress v0.2.0/go.mod h1:hl4FywxSjfmkmCrersGhmJH7KwuKl+Ueq9BXkOny+iE=
github.com/mandykoh/go-parallel v0.1.0 h1:7vJMNMC4dsbgZdkAb2A8tV5ENY1v7VxIO1wzQWZoT8k=
github.com/mandykoh/go-parallel v0.1.0/go.mod h1:lkYHqG1JNTaSS6lG+PgFCnyMd2VDy8pH9jN9pY899ig=
github.com/mandykoh/prism v0.34.0 h1:BWwxhdTVMZxhSur2sDmkFFLKzD8mFXOMy6bjJ4WuS4k=
github.com/mandykoh/prism v0.34.0/go.mod h1:mqzyMed6kgs8cImi+RoXBi5YtuzmSWShTakMRMD94VE=
github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA=
github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg=
github.com/matryer/is v1.2.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlWXA=
@ -329,6 +333,7 @@ golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+o
golang.org/x/image v0.0.0-20200119044424-58c23975cae1/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20200430140353-33d19683fad8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20200618115811-c13761719519/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20200801110659-972c09e46d76/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20200927104501-e162460cd6b5/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20201208152932-35266b937fa6/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20210216034530-4410531fe030/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=

View file

@ -8,12 +8,12 @@ import (
"time"
"github.com/dustin/go-humanize/english"
"github.com/gosimple/slug"
"github.com/jinzhu/gorm"
"github.com/ulule/deepcopier"
"github.com/photoprism/photoprism/internal/face"
"github.com/photoprism/photoprism/pkg/colors"
"github.com/photoprism/photoprism/pkg/fs"
"github.com/photoprism/photoprism/pkg/rnd"
"github.com/photoprism/photoprism/pkg/txt"
@ -32,46 +32,47 @@ type Files []File
// File represents an image or sidecar file that belongs to a photo.
type File struct {
ID uint `gorm:"primary_key" json:"-" yaml:"-"`
Photo *Photo `json:"-" yaml:"-"`
PhotoID uint `gorm:"index;" json:"-" yaml:"-"`
PhotoUID string `gorm:"type:VARBINARY(42);index;" json:"PhotoUID" yaml:"PhotoUID"`
InstanceID string `gorm:"type:VARBINARY(42);index;" json:"InstanceID,omitempty" yaml:"InstanceID,omitempty"`
FileUID string `gorm:"type:VARBINARY(42);unique_index;" json:"UID" yaml:"UID"`
FileName string `gorm:"type:VARBINARY(755);unique_index:idx_files_name_root;" json:"Name" yaml:"Name"`
FileRoot string `gorm:"type:VARBINARY(16);default:'/';unique_index:idx_files_name_root;" json:"Root" yaml:"Root,omitempty"`
OriginalName string `gorm:"type:VARBINARY(755);" json:"OriginalName" yaml:"OriginalName,omitempty"`
FileHash string `gorm:"type:VARBINARY(128);index" json:"Hash" yaml:"Hash,omitempty"`
FileSize int64 `json:"Size" yaml:"Size,omitempty"`
FileCodec string `gorm:"type:VARBINARY(32)" json:"Codec" yaml:"Codec,omitempty"`
FileType string `gorm:"type:VARBINARY(32)" json:"Type" yaml:"Type,omitempty"`
FileMime string `gorm:"type:VARBINARY(64)" json:"Mime" yaml:"Mime,omitempty"`
FilePrimary bool `json:"Primary" yaml:"Primary,omitempty"`
FileSidecar bool `json:"Sidecar" yaml:"Sidecar,omitempty"`
FileMissing bool `json:"Missing" yaml:"Missing,omitempty"`
FilePortrait bool `json:"Portrait" yaml:"Portrait,omitempty"`
FileVideo bool `json:"Video" yaml:"Video,omitempty"`
FileDuration time.Duration `json:"Duration" yaml:"Duration,omitempty"`
FileWidth int `json:"Width" yaml:"Width,omitempty"`
FileHeight int `json:"Height" yaml:"Height,omitempty"`
FileOrientation int `json:"Orientation" yaml:"Orientation,omitempty"`
FileProjection string `gorm:"type:VARBINARY(32);" json:"Projection,omitempty" yaml:"Projection,omitempty"`
FileAspectRatio float32 `gorm:"type:FLOAT;" json:"AspectRatio" yaml:"AspectRatio,omitempty"`
FileMainColor string `gorm:"type:VARBINARY(16);index;" json:"MainColor" yaml:"MainColor,omitempty"`
FileColors string `gorm:"type:VARBINARY(9);" json:"Colors" yaml:"Colors,omitempty"`
FileLuminance string `gorm:"type:VARBINARY(9);" json:"Luminance" yaml:"Luminance,omitempty"`
FileDiff uint32 `json:"Diff" yaml:"Diff,omitempty"`
FileChroma uint8 `json:"Chroma" yaml:"Chroma,omitempty"`
FileError string `gorm:"type:VARBINARY(512)" json:"Error" yaml:"Error,omitempty"`
ModTime int64 `json:"ModTime" yaml:"-"`
CreatedAt time.Time `json:"CreatedAt" yaml:"-"`
CreatedIn int64 `json:"CreatedIn" yaml:"-"`
UpdatedAt time.Time `json:"UpdatedAt" yaml:"-"`
UpdatedIn int64 `json:"UpdatedIn" yaml:"-"`
DeletedAt *time.Time `sql:"index" json:"DeletedAt,omitempty" yaml:"-"`
Share []FileShare `json:"-" yaml:"-"`
Sync []FileSync `json:"-" yaml:"-"`
markers *Markers
ID uint `gorm:"primary_key" json:"-" yaml:"-"`
Photo *Photo `json:"-" yaml:"-"`
PhotoID uint `gorm:"index;" json:"-" yaml:"-"`
PhotoUID string `gorm:"type:VARBINARY(42);index;" json:"PhotoUID" yaml:"PhotoUID"`
InstanceID string `gorm:"type:VARBINARY(42);index;" json:"InstanceID,omitempty" yaml:"InstanceID,omitempty"`
FileUID string `gorm:"type:VARBINARY(42);unique_index;" json:"UID" yaml:"UID"`
FileName string `gorm:"type:VARBINARY(755);unique_index:idx_files_name_root;" json:"Name" yaml:"Name"`
FileRoot string `gorm:"type:VARBINARY(16);default:'/';unique_index:idx_files_name_root;" json:"Root" yaml:"Root,omitempty"`
OriginalName string `gorm:"type:VARBINARY(755);" json:"OriginalName" yaml:"OriginalName,omitempty"`
FileHash string `gorm:"type:VARBINARY(128);index" json:"Hash" yaml:"Hash,omitempty"`
FileSize int64 `json:"Size" yaml:"Size,omitempty"`
FileCodec string `gorm:"type:VARBINARY(32)" json:"Codec" yaml:"Codec,omitempty"`
FileType string `gorm:"type:VARBINARY(32)" json:"Type" yaml:"Type,omitempty"`
FileMime string `gorm:"type:VARBINARY(64)" json:"Mime" yaml:"Mime,omitempty"`
FilePrimary bool `json:"Primary" yaml:"Primary,omitempty"`
FileSidecar bool `json:"Sidecar" yaml:"Sidecar,omitempty"`
FileMissing bool `json:"Missing" yaml:"Missing,omitempty"`
FilePortrait bool `json:"Portrait" yaml:"Portrait,omitempty"`
FileVideo bool `json:"Video" yaml:"Video,omitempty"`
FileDuration time.Duration `json:"Duration" yaml:"Duration,omitempty"`
FileWidth int `json:"Width" yaml:"Width,omitempty"`
FileHeight int `json:"Height" yaml:"Height,omitempty"`
FileOrientation int `json:"Orientation" yaml:"Orientation,omitempty"`
FileProjection string `gorm:"type:VARBINARY(32);" json:"Projection,omitempty" yaml:"Projection,omitempty"`
FileAspectRatio float32 `gorm:"type:FLOAT;" json:"AspectRatio" yaml:"AspectRatio,omitempty"`
FileColorProfile string `gorm:"type:VARBINARY(32);" json:"ColorProfile,omitempty" yaml:"ColorProfile,omitempty"`
FileMainColor string `gorm:"type:VARBINARY(16);index;" json:"MainColor" yaml:"MainColor,omitempty"`
FileColors string `gorm:"type:VARBINARY(9);" json:"Colors" yaml:"Colors,omitempty"`
FileLuminance string `gorm:"type:VARBINARY(9);" json:"Luminance" yaml:"Luminance,omitempty"`
FileDiff uint32 `json:"Diff" yaml:"Diff,omitempty"`
FileChroma uint8 `json:"Chroma" yaml:"Chroma,omitempty"`
FileError string `gorm:"type:VARBINARY(512)" json:"Error" yaml:"Error,omitempty"`
ModTime int64 `json:"ModTime" yaml:"-"`
CreatedAt time.Time `json:"CreatedAt" yaml:"-"`
CreatedIn int64 `json:"CreatedIn" yaml:"-"`
UpdatedAt time.Time `json:"UpdatedAt" yaml:"-"`
UpdatedIn int64 `json:"UpdatedIn" yaml:"-"`
DeletedAt *time.Time `sql:"index" json:"DeletedAt,omitempty" yaml:"-"`
Share []FileShare `json:"-" yaml:"-"`
Sync []FileSync `json:"-" yaml:"-"`
markers *Markers
}
// TableName returns the entity database table name.
@ -467,14 +468,29 @@ func (m *File) Panorama() bool {
return m.Projection() != ProjDefault || (m.FileWidth/m.FileHeight) >= 2
}
// Projection returns the panorama projection type string.
// Projection returns the panorama projection name if any.
func (m *File) Projection() string {
return SanitizeTypeString(m.FileProjection)
}
// SetProjection sets the panorama projection type string.
func (m *File) SetProjection(projType string) {
m.FileProjection = SanitizeTypeString(projType)
// SetProjection sets the panorama projection name.
func (m *File) SetProjection(name string) {
m.FileProjection = SanitizeTypeString(name)
}
// ColorProfile returns the ICC color profile name if any.
func (m *File) ColorProfile() string {
return SanitizeTypeCaseSensitive(m.FileColorProfile)
}
// HasColorProfile tests if the file has a matching color profile.
func (m *File) HasColorProfile(profile colors.Profile) bool {
return profile.Equal(m.FileColorProfile)
}
// SetColorProfile sets the ICC color profile name such as "Display P3".
func (m *File) SetColorProfile(name string) {
m.FileColorProfile = SanitizeTypeCaseSensitive(name)
}
// AddFaces adds face markers to the file.

View file

@ -30,6 +30,7 @@ func (m *File) MarshalJSON() ([]byte, error) {
Orientation int `json:",omitempty"`
Projection string `json:",omitempty"`
AspectRatio float32 `json:",omitempty"`
ColorProfile string `json:",omitempty"`
MainColor string `json:",omitempty"`
Colors string `json:",omitempty"`
Luminance string `json:",omitempty"`
@ -66,6 +67,7 @@ func (m *File) MarshalJSON() ([]byte, error) {
Orientation: m.FileOrientation,
Projection: m.FileProjection,
AspectRatio: m.FileAspectRatio,
ColorProfile: m.FileColorProfile,
MainColor: m.FileMainColor,
Colors: m.FileColors,
Luminance: m.FileLuminance,

View file

@ -4,10 +4,11 @@ import (
"testing"
"time"
"github.com/photoprism/photoprism/internal/face"
"github.com/photoprism/photoprism/pkg/fs"
"github.com/stretchr/testify/assert"
"github.com/photoprism/photoprism/internal/face"
"github.com/photoprism/photoprism/pkg/colors"
"github.com/photoprism/photoprism/pkg/fs"
)
func TestFirstFileByHash(t *testing.T) {
@ -625,3 +626,25 @@ func TestFile_ReplaceHash(t *testing.T) {
}
})
}
func TestFile_SetColorProfile(t *testing.T) {
t.Run("DisplayP3", func(t *testing.T) {
m := FileFixtures.Get("exampleFileName.jpg")
assert.Equal(t, "", m.ColorProfile())
assert.True(t, m.HasColorProfile(colors.Default))
assert.False(t, m.HasColorProfile(colors.ProfileDisplayP3))
m.SetColorProfile(string(colors.ProfileDisplayP3))
assert.Equal(t, "Display P3", m.ColorProfile())
assert.False(t, m.HasColorProfile(colors.Default))
assert.True(t, m.HasColorProfile(colors.ProfileDisplayP3))
m.SetColorProfile("")
assert.Equal(t, "", m.ColorProfile())
assert.True(t, m.HasColorProfile(colors.Default))
assert.False(t, m.HasColorProfile(colors.ProfileDisplayP3))
})
}

View file

@ -72,6 +72,11 @@ func SanitizeTypeString(s string) string {
return Trim(ToASCII(strings.ToLower(s)), TrimTypeString)
}
// SanitizeTypeCaseSensitive omits invalid runes, ensures a maximum length of 32 characters, and returns the result.
func SanitizeTypeCaseSensitive(s string) string {
return Trim(ToASCII(s), TrimTypeString)
}
// TypeString returns an entity type string for logging.
func TypeString(entityType string) string {
if entityType == "" {

View file

@ -25,6 +25,7 @@ type Data struct {
Description string `meta:"Description"`
Copyright string `meta:"Rights,Copyright"`
Projection string `meta:"ProjectionType"`
ColorProfile string `meta:"ICCProfileName,ProfileDescription"`
CameraMake string `meta:"CameraMake,Make"`
CameraModel string `meta:"CameraModel,Model"`
CameraOwner string `meta:"OwnerName"`
@ -32,7 +33,7 @@ type Data struct {
LensMake string `meta:"LensMake"`
LensModel string `meta:"Lens,LensModel"`
Flash bool `meta:"-"`
FocalLength int `meta:"-"`
FocalLength int `meta:"FocalLength"`
Exposure string `meta:"ExposureTime"`
Aperture float32 `meta:"ApertureValue"`
FNumber float32 `meta:"FNumber"`

View file

@ -129,7 +129,7 @@ func TestExif(t *testing.T) {
assert.Equal(t, 6, data.Orientation)
assert.Equal(t, "Apple", data.LensMake)
assert.Equal(t, "iPhone 7 back camera 3.99mm f/1.8", data.LensModel)
assert.Equal(t, "", data.ColorProfile)
})
t.Run("gps-2000.jpg", func(t *testing.T) {
@ -321,6 +321,7 @@ func TestExif(t *testing.T) {
assert.Equal(t, 6, data.FocalLength)
assert.Equal(t, 0, data.Orientation)
assert.Equal(t, "", data.Projection)
assert.Equal(t, "", data.ColorProfile)
})
t.Run("exif-example.tiff", func(t *testing.T) {
@ -352,6 +353,7 @@ func TestExif(t *testing.T) {
assert.Equal(t, 0, data.FocalLength)
assert.Equal(t, 1, data.Orientation)
assert.Equal(t, "", data.Projection)
assert.Equal(t, "", data.ColorProfile)
})
t.Run("out-of-range-500.jpg", func(t *testing.T) {
@ -383,6 +385,7 @@ func TestExif(t *testing.T) {
assert.Equal(t, 29, data.FocalLength)
assert.Equal(t, 3, data.Orientation)
assert.Equal(t, "", data.Projection)
assert.Equal(t, "", data.ColorProfile)
})
t.Run("digikam.jpg", func(t *testing.T) {
@ -417,6 +420,7 @@ func TestExif(t *testing.T) {
assert.Equal(t, "", data.LensModel)
assert.Equal(t, 27, data.FocalLength)
assert.Equal(t, 0, int(data.Orientation))
assert.Equal(t, "", data.ColorProfile)
})
t.Run("notebook.jpg", func(t *testing.T) {
@ -480,4 +484,68 @@ func TestExif(t *testing.T) {
assert.Equal(t, "EF70-200mm f/4L IS USM", data.LensModel)
assert.Equal(t, 1, data.Orientation)
})
t.Run("Iceland-P3.jpg", func(t *testing.T) {
data, err := Exif("testdata/Iceland-P3.jpg", fs.FormatJpeg)
if err != nil {
t.Fatal(err)
}
// t.Logf("all: %+v", data.All)
assert.Equal(t, "Nicolas Cornet", data.Artist)
assert.Equal(t, "2012-08-08T22:07:18Z", data.TakenAt.Format("2006-01-02T15:04:05Z"))
assert.Equal(t, "2012-08-08T22:07:18Z", data.TakenAtLocal.Format("2006-01-02T15:04:05Z"))
assert.Equal(t, "", data.Title)
assert.Equal(t, "", data.Keywords.String())
assert.Equal(t, "", data.Description)
assert.Equal(t, "Nicolas Cornet", data.Copyright)
assert.Equal(t, 400, data.Height)
assert.Equal(t, 600, data.Width)
assert.Equal(t, float32(65.05558), data.Lat)
assert.Equal(t, float32(-16.625702), data.Lng)
assert.Equal(t, 0, data.Altitude)
assert.Equal(t, "1/8", data.Exposure)
assert.Equal(t, "NIKON CORPORATION", data.CameraMake)
assert.Equal(t, "NIKON D800E", data.CameraModel)
assert.Equal(t, "", data.CameraOwner)
assert.Equal(t, "", data.CameraSerial)
assert.Equal(t, 16, data.FocalLength)
assert.Equal(t, 1, data.Orientation)
assert.Equal(t, "", data.Projection)
assert.Equal(t, "", data.ColorProfile)
})
t.Run("Iceland-sRGB.jpg", func(t *testing.T) {
data, err := Exif("testdata/Iceland-sRGB.jpg", fs.FormatJpeg)
if err != nil {
t.Fatal(err)
}
// t.Logf("all: %+v", data.All)
assert.Equal(t, "Nicolas Cornet", data.Artist)
assert.Equal(t, "2012-08-08T22:07:18Z", data.TakenAt.Format("2006-01-02T15:04:05Z"))
assert.Equal(t, "2012-08-08T22:07:18Z", data.TakenAtLocal.Format("2006-01-02T15:04:05Z"))
assert.Equal(t, "", data.Title)
assert.Equal(t, "", data.Keywords.String())
assert.Equal(t, "", data.Description)
assert.Equal(t, "Nicolas Cornet", data.Copyright)
assert.Equal(t, 400, data.Height)
assert.Equal(t, 600, data.Width)
assert.Equal(t, float32(65.05558), data.Lat)
assert.Equal(t, float32(-16.625702), data.Lng)
assert.Equal(t, 0, data.Altitude)
assert.Equal(t, "1/8", data.Exposure)
assert.Equal(t, "NIKON CORPORATION", data.CameraMake)
assert.Equal(t, "NIKON D800E", data.CameraModel)
assert.Equal(t, "", data.CameraOwner)
assert.Equal(t, "", data.CameraSerial)
assert.Equal(t, 16, data.FocalLength)
assert.Equal(t, 1, data.Orientation)
assert.Equal(t, "", data.Projection)
assert.Equal(t, "", data.ColorProfile)
})
}

View file

@ -867,7 +867,7 @@ func TestJSON(t *testing.T) {
assert.Equal(t, "Canon EOS 6D", data.CameraModel)
assert.Equal(t, "", data.CameraOwner)
assert.Equal(t, "012324001432", data.CameraSerial)
assert.Equal(t, 0, data.FocalLength)
assert.Equal(t, 35, data.FocalLength)
assert.Equal(t, 1, data.Orientation)
assert.Equal(t, "", data.Projection)
})
@ -889,4 +889,100 @@ func TestJSON(t *testing.T) {
assert.Equal(t, 0, data.Altitude)
assert.Equal(t, 1, data.Orientation)
})
t.Run("Iceland-P3.jpg", func(t *testing.T) {
data, err := JSON("testdata/Iceland-P3.json", "")
if err != nil {
t.Fatal(err)
}
// t.Logf("all: %+v", data.All)
assert.Equal(t, "Nicolas Cornet", data.Artist)
assert.Equal(t, "2012-08-08T22:07:18Z", data.TakenAt.Format("2006-01-02T15:04:05Z"))
assert.Equal(t, "2012-08-08T22:07:18Z", data.TakenAtLocal.Format("2006-01-02T15:04:05Z"))
assert.Equal(t, "", data.Title)
assert.Equal(t, "", data.Keywords.String())
assert.Equal(t, "", data.Description)
assert.Equal(t, "Nicolas Cornet", data.Copyright)
assert.Equal(t, 400, data.Height)
assert.Equal(t, 600, data.Width)
assert.Equal(t, float32(65.05558), data.Lat)
assert.Equal(t, float32(-16.625702), data.Lng)
assert.Equal(t, 30, data.Altitude)
assert.Equal(t, "1/8", data.Exposure)
assert.Equal(t, "NIKON CORPORATION", data.CameraMake)
assert.Equal(t, "NIKON D800E", data.CameraModel)
assert.Equal(t, "", data.CameraOwner)
assert.Equal(t, "6001440", data.CameraSerial)
assert.Equal(t, 0, data.FocalLength)
assert.Equal(t, 1, data.Orientation)
assert.Equal(t, "", data.Projection)
assert.Equal(t, "Display P3", data.ColorProfile)
})
t.Run("Iceland-P3-n.jpg", func(t *testing.T) {
data, err := JSON("testdata/Iceland-P3-n.json", "")
if err != nil {
t.Fatal(err)
}
// t.Logf("all: %+v", data.All)
assert.Equal(t, "Nicolas Cornet", data.Artist)
assert.Equal(t, "2012-08-08T22:07:18Z", data.TakenAt.Format("2006-01-02T15:04:05Z"))
assert.Equal(t, "2012-08-08T22:07:18Z", data.TakenAtLocal.Format("2006-01-02T15:04:05Z"))
assert.Equal(t, "", data.Title)
assert.Equal(t, "", data.Keywords.String())
assert.Equal(t, "", data.Description)
assert.Equal(t, "Nicolas Cornet", data.Copyright)
assert.Equal(t, 400, data.Height)
assert.Equal(t, 600, data.Width)
assert.Equal(t, float32(65.05558), data.Lat)
assert.Equal(t, float32(-16.625702), data.Lng)
assert.Equal(t, 30, data.Altitude)
assert.Equal(t, "0.125", data.Exposure)
assert.Equal(t, "NIKON CORPORATION", data.CameraMake)
assert.Equal(t, "NIKON D800E", data.CameraModel)
assert.Equal(t, "", data.CameraOwner)
assert.Equal(t, "6001440", data.CameraSerial)
assert.Equal(t, 16, data.FocalLength)
assert.Equal(t, 1, data.Orientation)
assert.Equal(t, "", data.Projection)
assert.Equal(t, "Display P3", data.ColorProfile)
})
t.Run("Iceland-sRGB.jpg", func(t *testing.T) {
data, err := JSON("testdata/Iceland-sRGB.json", "")
if err != nil {
t.Fatal(err)
}
// t.Logf("all: %+v", data.All)
assert.Equal(t, "Nicolas Cornet", data.Artist)
assert.Equal(t, "2012-08-08T22:07:18Z", data.TakenAt.Format("2006-01-02T15:04:05Z"))
assert.Equal(t, "2012-08-08T22:07:18Z", data.TakenAtLocal.Format("2006-01-02T15:04:05Z"))
assert.Equal(t, "", data.Title)
assert.Equal(t, "", data.Keywords.String())
assert.Equal(t, "", data.Description)
assert.Equal(t, "Nicolas Cornet", data.Copyright)
assert.Equal(t, 400, data.Height)
assert.Equal(t, 600, data.Width)
assert.Equal(t, float32(65.05558), data.Lat)
assert.Equal(t, float32(-16.625702), data.Lng)
assert.Equal(t, 30, data.Altitude)
assert.Equal(t, "1/8", data.Exposure)
assert.Equal(t, "NIKON CORPORATION", data.CameraMake)
assert.Equal(t, "NIKON D800E", data.CameraModel)
assert.Equal(t, "", data.CameraOwner)
assert.Equal(t, "6001440", data.CameraSerial)
assert.Equal(t, 0, data.FocalLength)
assert.Equal(t, 1, data.Orientation)
assert.Equal(t, "", data.Projection)
assert.Equal(t, "Display P3", data.ColorProfile)
})
}

179
internal/meta/testdata/Iceland-P3-n.json vendored Normal file
View file

@ -0,0 +1,179 @@
[{
"SourceFile": "Iceland-P3.jpg",
"ExifToolVersion": 12.16,
"FileName": "Iceland-P3.jpg",
"Directory": ".",
"FileSize": 102415,
"FileModifyDate": "2021:12:09 02:13:29+00:00",
"FileAccessDate": "2021:12:09 02:38:24+00:00",
"FileInodeChangeDate": "2021:12:09 02:38:22+00:00",
"FilePermissions": 664,
"FileType": "JPEG",
"FileTypeExtension": "JPG",
"MIMEType": "image/jpeg",
"ExifByteOrder": "MM",
"PhotometricInterpretation": 2,
"Make": "NIKON CORPORATION",
"Model": "NIKON D800E",
"Orientation": 1,
"SamplesPerPixel": 3,
"XResolution": 72,
"YResolution": 72,
"ResolutionUnit": 2,
"Software": "Adobe Photoshop CC 2015.5 (Macintosh)",
"ModifyDate": "2016:06:28 09:47:33",
"Artist": "Nicolas Cornet",
"Copyright": "Nicolas Cornet",
"ExposureTime": 0.125,
"FNumber": 8,
"ExposureProgram": 1,
"ISO": 200,
"ExifVersion": "0230",
"DateTimeOriginal": "2012:08:08 22:07:18",
"CreateDate": "2012:08:08 22:07:18",
"ComponentsConfiguration": "1 2 3 0",
"ShutterSpeedValue": 0.125,
"ApertureValue": 8,
"ExposureCompensation": 0,
"MaxApertureValue": 4,
"MeteringMode": 5,
"LightSource": 0,
"Flash": 16,
"FocalLength": 16,
"SubSecTimeOriginal": 1,
"SubSecTimeDigitized": 1,
"FlashpixVersion": "0100",
"ColorSpace": 65535,
"ExifImageWidth": 600,
"ExifImageHeight": 400,
"FocalPlaneXResolution": 204.8402062,
"FocalPlaneYResolution": 204.8402062,
"FocalPlaneResolutionUnit": 4,
"SensingMethod": 2,
"FileSource": 3,
"CustomRendered": 0,
"ExposureMode": 1,
"WhiteBalance": 0,
"DigitalZoomRatio": 1,
"FocalLengthIn35mmFormat": 16,
"SceneCaptureType": 0,
"GainControl": 0,
"Contrast": 0,
"Saturation": 0,
"Sharpness": 0,
"SubjectDistanceRange": 0,
"GPSVersionID": "2 3 0 0",
"GPSLatitudeRef": "N",
"GPSLongitudeRef": "W",
"GPSAltitude": 904.1,
"GPSSpeedRef": "K",
"GPSSpeed": 1.4,
"GPSImgDirectionRef": "T",
"GPSImgDirection": 235.3,
"GPSMapDatum": "WGS-84",
"Compression": 6,
"ThumbnailOffset": 1270,
"ThumbnailLength": 3926,
"CurrentIPTCDigest": "ffc6b44b107e8aa9f5305f77eb834c2f",
"CodedCharacterSet": "\u001B%G",
"ApplicationRecordVersion": 2,
"By-line": "Nicolas Cornet",
"TimeCreated": "22:07:18+00:00",
"DigitalCreationDate": "2012:08:08",
"CopyrightNotice": "Nicolas Cornet",
"IPTCDigest": "ffc6b44b107e8aa9f5305f77eb834c2f",
"DisplayedUnitsX": 1,
"DisplayedUnitsY": 1,
"PrintStyle": 0,
"PrintPosition": "0 0",
"PrintScale": 1,
"GlobalAngle": 30,
"GlobalAltitude": 30,
"URL_List": [],
"SlicesGroupName": "Iceland-sRGB",
"NumSlices": 1,
"PixelAspectRatio": 1,
"PhotoshopThumbnail": "(Binary data 3926 bytes, use -b option to extract)",
"HasRealMergedData": 1,
"WriterName": "Adobe Photoshop",
"ReaderName": "Adobe Photoshop CC 2015.5",
"PhotoshopQuality": 6,
"PhotoshopFormat": 0,
"ProgressiveScans": 1,
"XMPToolkit": "Adobe XMP Core 5.6-c132 79.159284, 2016/04/19-13:13:40 ",
"LensInfo": "16 35 4 4",
"ImageNumber": 9327,
"Lens": "16.0-35.0 mm f/4.0",
"SerialNumber": 6001440,
"CreatorTool": "Aperture 3.4.5",
"MetadataDate": "2016:06:28 09:47:33+10:00",
"DateCreated": "2012:08:08 22:07:18.001",
"LegacyIPTCDigest": "701D986FEA688D5F52057CC7EDA3E4EA",
"ColorMode": 3,
"ICCProfileName": "Display P3",
"Format": "image/jpeg",
"DocumentID": "adobe:docid:photoshop:eb7a4192-7d4f-1179-93d3-8c7542d122ed",
"InstanceID": "xmp.iid:6ccb51ee-5b2d-4171-a14a-b80873652a9c",
"OriginalDocumentID": "15D34E05D6803BC4CD8AAE1D1B1B2BEE",
"Creator": "Nicolas Cornet",
"Rights": "Nicolas Cornet",
"HistoryAction": ["saved","saved"],
"HistoryInstanceID": ["xmp.iid:385160c4-d06d-4708-a378-30d3efc40e82","xmp.iid:6ccb51ee-5b2d-4171-a14a-b80873652a9c"],
"HistoryWhen": ["2016:06:28 09:45:49+10:00","2016:06:28 09:47:33+10:00"],
"HistorySoftwareAgent": ["Adobe Photoshop CC 2015.5 (Macintosh)","Adobe Photoshop CC 2015.5 (Macintosh)"],
"HistoryChanged": ["/","/"],
"ProfileCMMType": "appl",
"ProfileVersion": 1024,
"ProfileClass": "mntr",
"ColorSpaceData": "RGB ",
"ProfileConnectionSpace": "XYZ ",
"ProfileDateTime": "2015:10:14 13:08:57",
"ProfileFileSignature": "acsp",
"PrimaryPlatform": "APPL",
"CMMFlags": 0,
"DeviceManufacturer": "APPL",
"DeviceModel": "",
"DeviceAttributes": "0 0",
"RenderingIntent": 0,
"ConnectionSpaceIlluminant": "0.9642 1 0.82491",
"ProfileCreator": "appl",
"ProfileID": "229 187 14 152 103 189 70 205 75 190 68 110 189 27 117 152",
"ProfileDescription": "Display P3",
"ProfileCopyright": "Copyright Apple Inc., 2015",
"MediaWhitePoint": "0.95045 1 1.08905",
"RedMatrixColumn": "0.51512 0.2412 -0.00105",
"GreenMatrixColumn": "0.29198 0.69225 0.04189",
"BlueMatrixColumn": "0.1571 0.06657 0.78407",
"RedTRC": "(Binary data 32 bytes, use -b option to extract)",
"ChromaticAdaptation": "1.04788 0.02292 -0.0502 0.02959 0.99048 -0.01706 -0.00923 0.01508 0.75168",
"BlueTRC": "(Binary data 32 bytes, use -b option to extract)",
"GreenTRC": "(Binary data 32 bytes, use -b option to extract)",
"DCTEncodeVersion": 100,
"APP14Flags0": 16384,
"APP14Flags1": 0,
"ColorTransform": 1,
"ImageWidth": 600,
"ImageHeight": 400,
"EncodingProcess": 0,
"BitsPerSample": 8,
"ColorComponents": 3,
"YCbCrSubSampling": "1 1",
"Aperture": 8,
"ImageSize": "600 400",
"Megapixels": 0.24,
"ScaleFactor35efl": 1,
"ShutterSpeed": 0.125,
"SubSecCreateDate": "2012:08:08 22:07:18.1",
"SubSecDateTimeOriginal": "2012:08:08 22:07:18.1",
"ThumbnailImage": "(Binary data 3926 bytes, use -b option to extract)",
"GPSLatitude": 65.0555777777778,
"GPSLongitude": -16.6257027777778,
"DateTimeCreated": "2012:08:08 22:07:18+00:00",
"LensID": 163,
"CircleOfConfusion": 0.0300462606288666,
"FOV": 96.7330030337318,
"FocalLength35efl": 16,
"GPSPosition": "65.0555777777778 -16.6257027777778",
"HyperfocalDistance": 1.06502437675244,
"LightValue": 8
}]

BIN
internal/meta/testdata/Iceland-P3.jpg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

179
internal/meta/testdata/Iceland-P3.json vendored Normal file
View file

@ -0,0 +1,179 @@
[{
"SourceFile": "Iceland-P3.jpg",
"ExifToolVersion": 12.16,
"FileName": "Iceland-P3.jpg",
"Directory": ".",
"FileSize": "100 KiB",
"FileModifyDate": "2021:12:09 02:13:29+00:00",
"FileAccessDate": "2021:12:09 02:38:24+00:00",
"FileInodeChangeDate": "2021:12:09 02:38:22+00:00",
"FilePermissions": "rw-rw-r--",
"FileType": "JPEG",
"FileTypeExtension": "jpg",
"MIMEType": "image/jpeg",
"ExifByteOrder": "Big-endian (Motorola, MM)",
"PhotometricInterpretation": "RGB",
"Make": "NIKON CORPORATION",
"Model": "NIKON D800E",
"Orientation": "Horizontal (normal)",
"SamplesPerPixel": 3,
"XResolution": 72,
"YResolution": 72,
"ResolutionUnit": "inches",
"Software": "Adobe Photoshop CC 2015.5 (Macintosh)",
"ModifyDate": "2016:06:28 09:47:33",
"Artist": "Nicolas Cornet",
"Copyright": "Nicolas Cornet",
"ExposureTime": "1/8",
"FNumber": 8.0,
"ExposureProgram": "Manual",
"ISO": 200,
"ExifVersion": "0230",
"DateTimeOriginal": "2012:08:08 22:07:18",
"CreateDate": "2012:08:08 22:07:18",
"ComponentsConfiguration": "Y, Cb, Cr, -",
"ShutterSpeedValue": "1/8",
"ApertureValue": 8.0,
"ExposureCompensation": 0,
"MaxApertureValue": 4.0,
"MeteringMode": "Multi-segment",
"LightSource": "Unknown",
"Flash": "Off, Did not fire",
"FocalLength": "16.0 mm",
"SubSecTimeOriginal": 1,
"SubSecTimeDigitized": 1,
"FlashpixVersion": "0100",
"ColorSpace": "Uncalibrated",
"ExifImageWidth": 600,
"ExifImageHeight": 400,
"FocalPlaneXResolution": 204.8402062,
"FocalPlaneYResolution": 204.8402062,
"FocalPlaneResolutionUnit": "mm",
"SensingMethod": "One-chip color area",
"FileSource": "Digital Camera",
"CustomRendered": "Normal",
"ExposureMode": "Manual",
"WhiteBalance": "Auto",
"DigitalZoomRatio": 1,
"FocalLengthIn35mmFormat": "16 mm",
"SceneCaptureType": "Standard",
"GainControl": "None",
"Contrast": "Normal",
"Saturation": "Normal",
"Sharpness": "Normal",
"SubjectDistanceRange": "Unknown",
"GPSVersionID": "2.3.0.0",
"GPSLatitudeRef": "North",
"GPSLongitudeRef": "West",
"GPSAltitude": "904.1 m",
"GPSSpeedRef": "km/h",
"GPSSpeed": 1.4,
"GPSImgDirectionRef": "True North",
"GPSImgDirection": 235.3,
"GPSMapDatum": "WGS-84",
"Compression": "JPEG (old-style)",
"ThumbnailOffset": 1270,
"ThumbnailLength": 3926,
"CurrentIPTCDigest": "ffc6b44b107e8aa9f5305f77eb834c2f",
"CodedCharacterSet": "UTF8",
"ApplicationRecordVersion": 2,
"By-line": "Nicolas Cornet",
"TimeCreated": "22:07:18+00:00",
"DigitalCreationDate": "2012:08:08",
"CopyrightNotice": "Nicolas Cornet",
"IPTCDigest": "ffc6b44b107e8aa9f5305f77eb834c2f",
"DisplayedUnitsX": "inches",
"DisplayedUnitsY": "inches",
"PrintStyle": "Centered",
"PrintPosition": "0 0",
"PrintScale": 1,
"GlobalAngle": 30,
"GlobalAltitude": 30,
"URL_List": [],
"SlicesGroupName": "Iceland-sRGB",
"NumSlices": 1,
"PixelAspectRatio": 1,
"PhotoshopThumbnail": "(Binary data 3926 bytes, use -b option to extract)",
"HasRealMergedData": "Yes",
"WriterName": "Adobe Photoshop",
"ReaderName": "Adobe Photoshop CC 2015.5",
"PhotoshopQuality": 10,
"PhotoshopFormat": "Standard",
"ProgressiveScans": "3 Scans",
"XMPToolkit": "Adobe XMP Core 5.6-c132 79.159284, 2016/04/19-13:13:40 ",
"LensInfo": "16-35mm f/4",
"ImageNumber": 9327,
"Lens": "16.0-35.0 mm f/4.0",
"SerialNumber": 6001440,
"CreatorTool": "Aperture 3.4.5",
"MetadataDate": "2016:06:28 09:47:33+10:00",
"DateCreated": "2012:08:08 22:07:18.001",
"LegacyIPTCDigest": "701D986FEA688D5F52057CC7EDA3E4EA",
"ColorMode": "RGB",
"ICCProfileName": "Display P3",
"Format": "image/jpeg",
"DocumentID": "adobe:docid:photoshop:eb7a4192-7d4f-1179-93d3-8c7542d122ed",
"InstanceID": "xmp.iid:6ccb51ee-5b2d-4171-a14a-b80873652a9c",
"OriginalDocumentID": "15D34E05D6803BC4CD8AAE1D1B1B2BEE",
"Creator": "Nicolas Cornet",
"Rights": "Nicolas Cornet",
"HistoryAction": ["saved","saved"],
"HistoryInstanceID": ["xmp.iid:385160c4-d06d-4708-a378-30d3efc40e82","xmp.iid:6ccb51ee-5b2d-4171-a14a-b80873652a9c"],
"HistoryWhen": ["2016:06:28 09:45:49+10:00","2016:06:28 09:47:33+10:00"],
"HistorySoftwareAgent": ["Adobe Photoshop CC 2015.5 (Macintosh)","Adobe Photoshop CC 2015.5 (Macintosh)"],
"HistoryChanged": ["/","/"],
"ProfileCMMType": "Apple Computer Inc.",
"ProfileVersion": "4.0.0",
"ProfileClass": "Display Device Profile",
"ColorSpaceData": "RGB ",
"ProfileConnectionSpace": "XYZ ",
"ProfileDateTime": "2015:10:14 13:08:57",
"ProfileFileSignature": "acsp",
"PrimaryPlatform": "Apple Computer Inc.",
"CMMFlags": "Not Embedded, Independent",
"DeviceManufacturer": "Apple Computer Inc.",
"DeviceModel": "",
"DeviceAttributes": "Reflective, Glossy, Positive, Color",
"RenderingIntent": "Perceptual",
"ConnectionSpaceIlluminant": "0.9642 1 0.82491",
"ProfileCreator": "Apple Computer Inc.",
"ProfileID": "e5bb0e9867bd46cd4bbe446ebd1b7598",
"ProfileDescription": "Display P3",
"ProfileCopyright": "Copyright Apple Inc., 2015",
"MediaWhitePoint": "0.95045 1 1.08905",
"RedMatrixColumn": "0.51512 0.2412 -0.00105",
"GreenMatrixColumn": "0.29198 0.69225 0.04189",
"BlueMatrixColumn": "0.1571 0.06657 0.78407",
"RedTRC": "(Binary data 32 bytes, use -b option to extract)",
"ChromaticAdaptation": "1.04788 0.02292 -0.0502 0.02959 0.99048 -0.01706 -0.00923 0.01508 0.75168",
"BlueTRC": "(Binary data 32 bytes, use -b option to extract)",
"GreenTRC": "(Binary data 32 bytes, use -b option to extract)",
"DCTEncodeVersion": 100,
"APP14Flags0": "[14]",
"APP14Flags1": "(none)",
"ColorTransform": "YCbCr",
"ImageWidth": 600,
"ImageHeight": 400,
"EncodingProcess": "Baseline DCT, Huffman coding",
"BitsPerSample": 8,
"ColorComponents": 3,
"YCbCrSubSampling": "YCbCr4:4:4 (1 1)",
"Aperture": 8.0,
"ImageSize": "600x400",
"Megapixels": 0.240,
"ScaleFactor35efl": 1.0,
"ShutterSpeed": "1/8",
"SubSecCreateDate": "2012:08:08 22:07:18.1",
"SubSecDateTimeOriginal": "2012:08:08 22:07:18.1",
"ThumbnailImage": "(Binary data 3926 bytes, use -b option to extract)",
"GPSLatitude": "65 deg 3' 20.08\" N",
"GPSLongitude": "16 deg 37' 32.53\" W",
"DateTimeCreated": "2012:08:08 22:07:18+00:00",
"LensID": "AF-S Nikkor 16-35mm f/4G ED VR",
"CircleOfConfusion": "0.030 mm",
"FOV": "96.7 deg",
"FocalLength35efl": "16.0 mm (35 mm equivalent: 16.0 mm)",
"GPSPosition": "65 deg 3' 20.08\" N, 16 deg 37' 32.53\" W",
"HyperfocalDistance": "1.07 m",
"LightValue": 8.0
}]

BIN
internal/meta/testdata/Iceland-sRGB.jpg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

153
internal/meta/testdata/Iceland-sRGB.json vendored Normal file
View file

@ -0,0 +1,153 @@
[{
"SourceFile": "Iceland-sRGB.jpg",
"ExifToolVersion": 12.16,
"FileName": "Iceland-sRGB.jpg",
"Directory": ".",
"FileSize": "99 KiB",
"FileModifyDate": "2021:12:09 02:12:56+00:00",
"FileAccessDate": "2021:12:09 02:38:24+00:00",
"FileInodeChangeDate": "2021:12:09 02:38:22+00:00",
"FilePermissions": "rw-rw-r--",
"FileType": "JPEG",
"FileTypeExtension": "jpg",
"MIMEType": "image/jpeg",
"ExifByteOrder": "Big-endian (Motorola, MM)",
"PhotometricInterpretation": "RGB",
"Make": "NIKON CORPORATION",
"Model": "NIKON D800E",
"Orientation": "Horizontal (normal)",
"SamplesPerPixel": 3,
"XResolution": 72,
"YResolution": 72,
"ResolutionUnit": "inches",
"Software": "Adobe Photoshop CC 2015.5 (Macintosh)",
"ModifyDate": "2016:06:28 09:47:23",
"Artist": "Nicolas Cornet",
"Copyright": "Nicolas Cornet",
"ExposureTime": "1/8",
"FNumber": 8.0,
"ExposureProgram": "Manual",
"ISO": 200,
"ExifVersion": "0230",
"DateTimeOriginal": "2012:08:08 22:07:18",
"CreateDate": "2012:08:08 22:07:18",
"ComponentsConfiguration": "Y, Cb, Cr, -",
"ShutterSpeedValue": "1/8",
"ApertureValue": 8.0,
"ExposureCompensation": 0,
"MaxApertureValue": 4.0,
"MeteringMode": "Multi-segment",
"LightSource": "Unknown",
"Flash": "Off, Did not fire",
"FocalLength": "16.0 mm",
"SubSecTimeOriginal": 1,
"SubSecTimeDigitized": 1,
"FlashpixVersion": "0100",
"ColorSpace": "Uncalibrated",
"ExifImageWidth": 600,
"ExifImageHeight": 400,
"FocalPlaneXResolution": 204.8402062,
"FocalPlaneYResolution": 204.8402062,
"FocalPlaneResolutionUnit": "mm",
"SensingMethod": "One-chip color area",
"FileSource": "Digital Camera",
"CustomRendered": "Normal",
"ExposureMode": "Manual",
"WhiteBalance": "Auto",
"DigitalZoomRatio": 1,
"FocalLengthIn35mmFormat": "16 mm",
"SceneCaptureType": "Standard",
"GainControl": "None",
"Contrast": "Normal",
"Saturation": "Normal",
"Sharpness": "Normal",
"SubjectDistanceRange": "Unknown",
"GPSVersionID": "2.3.0.0",
"GPSLatitudeRef": "North",
"GPSLongitudeRef": "West",
"GPSAltitude": "904.1 m",
"GPSSpeedRef": "km/h",
"GPSSpeed": 1.4,
"GPSImgDirectionRef": "True North",
"GPSImgDirection": 235.3,
"GPSMapDatum": "WGS-84",
"Compression": "JPEG (old-style)",
"ThumbnailOffset": 1270,
"ThumbnailLength": 3926,
"CurrentIPTCDigest": "ffc6b44b107e8aa9f5305f77eb834c2f",
"CodedCharacterSet": "UTF8",
"ApplicationRecordVersion": 2,
"By-line": "Nicolas Cornet",
"TimeCreated": "22:07:18+00:00",
"DigitalCreationDate": "2012:08:08",
"CopyrightNotice": "Nicolas Cornet",
"IPTCDigest": "ffc6b44b107e8aa9f5305f77eb834c2f",
"DisplayedUnitsX": "inches",
"DisplayedUnitsY": "inches",
"PrintStyle": "Centered",
"PrintPosition": "0 0",
"PrintScale": 1,
"GlobalAngle": 30,
"GlobalAltitude": 30,
"URL_List": [],
"SlicesGroupName": "Iceland-sRGB",
"NumSlices": 1,
"PixelAspectRatio": 1,
"PhotoshopThumbnail": "(Binary data 3926 bytes, use -b option to extract)",
"HasRealMergedData": "Yes",
"WriterName": "Adobe Photoshop",
"ReaderName": "Adobe Photoshop CC 2015.5",
"PhotoshopQuality": 10,
"PhotoshopFormat": "Standard",
"ProgressiveScans": "3 Scans",
"XMPToolkit": "Adobe XMP Core 5.6-c132 79.159284, 2016/04/19-13:13:40 ",
"LensInfo": "16-35mm f/4",
"ImageNumber": 9327,
"Lens": "16.0-35.0 mm f/4.0",
"SerialNumber": 6001440,
"CreatorTool": "Aperture 3.4.5",
"MetadataDate": "2016:06:28 09:47:23+10:00",
"DateCreated": "2012:08:08 22:07:18.001",
"LegacyIPTCDigest": "701D986FEA688D5F52057CC7EDA3E4EA",
"ColorMode": "RGB",
"ICCProfileName": "Display P3",
"Format": "image/jpeg",
"DocumentID": "adobe:docid:photoshop:b989e253-7d4f-1179-93d3-8c7542d122ed",
"InstanceID": "xmp.iid:0dc1cf45-beab-4655-8008-5d47a919d5ee",
"OriginalDocumentID": "15D34E05D6803BC4CD8AAE1D1B1B2BEE",
"Creator": "Nicolas Cornet",
"Rights": "Nicolas Cornet",
"HistoryAction": ["saved","saved"],
"HistoryInstanceID": ["xmp.iid:385160c4-d06d-4708-a378-30d3efc40e82","xmp.iid:0dc1cf45-beab-4655-8008-5d47a919d5ee"],
"HistoryWhen": ["2016:06:28 09:45:49+10:00","2016:06:28 09:47:23+10:00"],
"HistorySoftwareAgent": ["Adobe Photoshop CC 2015.5 (Macintosh)","Adobe Photoshop CC 2015.5 (Macintosh)"],
"HistoryChanged": ["/","/"],
"DCTEncodeVersion": 100,
"APP14Flags0": "[14]",
"APP14Flags1": "(none)",
"ColorTransform": "YCbCr",
"ImageWidth": 600,
"ImageHeight": 400,
"EncodingProcess": "Baseline DCT, Huffman coding",
"BitsPerSample": 8,
"ColorComponents": 3,
"YCbCrSubSampling": "YCbCr4:4:4 (1 1)",
"Aperture": 8.0,
"ImageSize": "600x400",
"Megapixels": 0.240,
"ScaleFactor35efl": 1.0,
"ShutterSpeed": "1/8",
"SubSecCreateDate": "2012:08:08 22:07:18.1",
"SubSecDateTimeOriginal": "2012:08:08 22:07:18.1",
"ThumbnailImage": "(Binary data 3926 bytes, use -b option to extract)",
"GPSLatitude": "65 deg 3' 20.08\" N",
"GPSLongitude": "16 deg 37' 32.53\" W",
"DateTimeCreated": "2012:08:08 22:07:18+00:00",
"LensID": "AF-S Nikkor 16-35mm f/4G ED VR",
"CircleOfConfusion": "0.030 mm",
"FOV": "96.7 deg",
"FocalLength35efl": "16.0 mm (35 mm equivalent: 16.0 mm)",
"GPSPosition": "65 deg 3' 20.08\" N, 16 deg 37' 32.53\" W",
"HyperfocalDistance": "1.07 m",
"LightValue": 8.0
}]

View file

@ -112,7 +112,7 @@ type XmpDocument struct {
Model string `xml:"Model"` // ELE-L29
ExifVersion string `xml:"ExifVersion"` // 0210
FlashpixVersion string `xml:"FlashpixVersion"` // 0100
ColorSpace string `xml:"ColorSpace"` // 1
ColorSpace string `xml:"ColorProfile"` // 1
ComponentsConfiguration struct {
Text string `xml:",chardata" json:"text,omitempty"`
Seq struct {

View file

@ -153,7 +153,7 @@ func (c *Convert) ToJson(f *MediaFile) (jsonName string, err error) {
log.Debugf("exiftool: extracting metadata from %s", relName)
cmd := exec.Command(c.conf.ExifToolBin(), "-m", "-api", "LargeFileSupport", "-j", f.FileName())
cmd := exec.Command(c.conf.ExifToolBin(), "-n", "-m", "-api", "LargeFileSupport", "-j", f.FileName())
// Fetch command output.
var out bytes.Buffer

View file

@ -329,6 +329,7 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) (
if metaData := m.MetaData(); metaData.Error == nil {
file.FileCodec = metaData.Codec
file.SetProjection(metaData.Projection)
file.SetColorProfile(metaData.ColorProfile)
if metaData.HasInstanceID() {
log.Infof("index: %s has instance_id %s", logName, txt.Quote(metaData.InstanceID))
@ -387,6 +388,7 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) (
file.FileAspectRatio = m.AspectRatio()
file.FilePortrait = m.Portrait()
file.SetProjection(metaData.Projection)
file.SetColorProfile(metaData.ColorProfile)
if res := m.Megapixels(); res > photo.PhotoResolution {
photo.PhotoResolution = res
@ -437,6 +439,7 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) (
file.FilePortrait = m.Portrait()
file.FileDuration = metaData.Duration
file.SetProjection(metaData.Projection)
file.SetColorProfile(metaData.ColorProfile)
if res := m.Megapixels(); res > photo.PhotoResolution {
photo.PhotoResolution = res

View file

@ -958,15 +958,13 @@ func (m *MediaFile) ResampleDefault(thumbPath string, force bool) (err error) {
}
if originalImg == nil {
img, err := imaging.Open(m.FileName())
img, err := thumb.Open(m.FileName(), m.Orientation())
if err != nil {
log.Debugf("media: %s in %s", err.Error(), txt.Quote(m.BaseName()))
return err
}
img = thumb.Rotate(img, m.Orientation())
originalImg = img
}

View file

@ -9,10 +9,10 @@ import (
"path"
"path/filepath"
"github.com/disintegration/imaging"
"github.com/photoprism/photoprism/pkg/fs"
"github.com/photoprism/photoprism/pkg/txt"
"github.com/disintegration/imaging"
)
// Suffix returns the thumb cache file suffix.
@ -86,6 +86,7 @@ func FromFile(imageFilename, hash, thumbPath string, width, height, orientation
return "", err
}
// Generate thumb cache filename.
fileName, err = FileName(hash, thumbPath, width, height, opts...)
if err != nil {
@ -93,17 +94,15 @@ func FromFile(imageFilename, hash, thumbPath string, width, height, orientation
return "", err
}
img, err := imaging.Open(imageFilename)
// Load image from storage.
img, err := Open(imageFilename, orientation)
if err != nil {
log.Debugf("resample: %s in %s", err, txt.Quote(filepath.Base(imageFilename)))
log.Error(err)
return "", err
}
if orientation > 1 {
img = Rotate(img, orientation)
}
// Create thumb from image.
if _, err := Create(img, fileName, width, height, opts...); err != nil {
return "", err
}

View file

@ -5,8 +5,9 @@ import (
"testing"
"github.com/disintegration/imaging"
"github.com/photoprism/photoprism/pkg/fs"
"github.com/stretchr/testify/assert"
"github.com/photoprism/photoprism/pkg/fs"
)
func TestResampleOptions(t *testing.T) {

97
internal/thumb/open.go Normal file
View file

@ -0,0 +1,97 @@
package thumb
import (
"fmt"
"image"
_ "image/gif"
_ "image/jpeg"
_ "image/png"
"os"
"path/filepath"
"github.com/disintegration/imaging"
"github.com/mandykoh/prism/meta/autometa"
"github.com/photoprism/photoprism/pkg/colors"
"github.com/photoprism/photoprism/pkg/fs"
"github.com/photoprism/photoprism/pkg/txt"
)
// Open loads an image from disk, rotates it, and converts the color profile if necessary.
func Open(fileName string, orientation int) (result image.Image, err error) {
if fileName == "" {
return result, fmt.Errorf("filename missing")
}
// Open JPEG?
if fs.GetFileFormat(fileName) == fs.FormatJpeg {
return OpenJpeg(fileName, orientation)
}
// Open file with imaging function.
img, err := imaging.Open(fileName)
if err != nil {
return result, err
}
// Rotate?
if orientation > 1 {
img = Rotate(img, orientation)
}
return img, nil
}
// OpenJpeg loads a JPEG image from disk, rotates it, and converts the color profile if necessary.
func OpenJpeg(fileName string, orientation int) (result image.Image, err error) {
if fileName == "" {
return result, fmt.Errorf("filename missing")
}
logName := txt.Quote(filepath.Base(fileName))
// Open file.
fileReader, err := os.Open(fileName)
if err != nil {
return result, err
}
defer fileReader.Close()
// Read color metadata.
md, imgStream, err := autometa.Load(fileReader)
var img image.Image
if err != nil {
log.Warnf("resample: %s in %s (read color metadata)", err, logName)
img, err = imaging.Decode(fileReader)
} else {
img, err = imaging.Decode(imgStream)
}
if err != nil {
return result, err
}
// Read ICC profile and convert colors if possible.
if iccProfile, err := md.ICCProfile(); err != nil || iccProfile == nil {
// Do nothing.
log.Tracef("resample: detected no color profile in %s", logName)
} else if profile, err := iccProfile.Description(); err == nil && profile != "" {
log.Debugf("resample: detected color profile %s in %s", txt.Quote(profile), logName)
switch {
case colors.ProfileDisplayP3.Equal(profile):
img = colors.ToSRGB(img, colors.ProfileDisplayP3)
}
}
// Rotate?
if orientation > 1 {
img = Rotate(img, orientation)
}
return img, nil
}

View file

@ -0,0 +1,58 @@
package thumb
import (
"testing"
)
func TestOpen(t *testing.T) {
t.Run("JPEG", func(t *testing.T) {
img, err := Open("testdata/example.jpg", 0)
if err != nil {
t.Fatal(err)
}
if img == nil {
t.Error("img must not be nil")
}
})
t.Run("BMP", func(t *testing.T) {
img, err := Open("testdata/example.bmp", 0)
if err != nil {
t.Fatal(err)
}
if img == nil {
t.Error("img must not be nil")
}
})
t.Run("GIF", func(t *testing.T) {
img, err := Open("testdata/example.gif", 0)
if err != nil {
t.Fatal(err)
}
if img == nil {
t.Error("img must not be nil")
}
})
t.Run("PNG", func(t *testing.T) {
img, err := Open("testdata/example.png", 0)
if err != nil {
t.Fatal(err)
}
if img == nil {
t.Error("img must not be nil")
}
})
t.Run("TIFF", func(t *testing.T) {
img, err := Open("testdata/example.tif", 0)
if err != nil {
t.Fatal(err)
}
if img == nil {
t.Error("img must not be nil")
}
})
}

16
pkg/colors/profiles.go Normal file
View file

@ -0,0 +1,16 @@
package colors
import "strings"
type Profile string
// Supported color profiles.
const (
Default Profile = ""
ProfileDisplayP3 Profile = "Display P3"
)
// Equal compares the color profile name case-insensitively.
func (p Profile) Equal(s string) bool {
return strings.EqualFold(string(p), s)
}

31
pkg/colors/srgb.go Normal file
View file

@ -0,0 +1,31 @@
package colors
import (
"image"
_ "image/jpeg"
"runtime"
"github.com/mandykoh/prism"
"github.com/mandykoh/prism/displayp3"
"github.com/mandykoh/prism/srgb"
)
// ToSRGB converts an image to sRGB colors.
func ToSRGB(img image.Image, profile Profile) image.Image {
switch profile {
case ProfileDisplayP3:
in := prism.ConvertImageToNRGBA(img, runtime.NumCPU())
out := image.NewNRGBA(in.Rect)
for i := in.Rect.Min.Y; i < in.Rect.Max.Y; i++ {
for j := in.Rect.Min.X; j < in.Rect.Max.X; j++ {
inCol, alpha := displayp3.ColorFromNRGBA(in.NRGBAAt(j, i))
outCol := srgb.ColorFromXYZ(inCol.ToXYZ())
out.SetNRGBA(j, i, outCol.ToNRGBA(alpha))
}
}
return out
default:
return img
}
}

61
pkg/colors/srgb_test.go Normal file
View file

@ -0,0 +1,61 @@
package colors
import (
"image"
"image/jpeg"
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
)
func writeImage(path string, img image.Image) error {
imgFile, err := os.Create(path)
if err != nil {
return err
}
defer imgFile.Close()
opt := jpeg.Options{
Quality: 95,
}
return jpeg.Encode(imgFile, img, &opt)
}
func TestToSRGB(t *testing.T) {
t.Run("DisplayP3", func(t *testing.T) {
testFile, _ := filepath.Abs("./testdata/DisplayP3.jpg")
t.Logf("testfile: %s", testFile)
imgFile, err := os.Open(testFile)
if err != nil {
t.Fatal(err)
}
defer imgFile.Close()
img, _, err := image.Decode(imgFile)
if err != nil {
t.Fatal(err)
}
imgSRGB := ToSRGB(img, ProfileDisplayP3)
srgbFile := "./testdata/SRGB.jpg"
if err := writeImage(srgbFile, imgSRGB); err != nil {
t.Error(err)
}
assert.FileExists(t, srgbFile)
_ = os.Remove(srgbFile)
})
}

BIN
pkg/colors/testdata/DisplayP3.jpg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB