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> <translate>{{ file.Orientation }}</translate>
</td> </td>
</tr> </tr>
<tr v-if="file.ColorProfile">
<td>
<translate>Color Profile</translate>
</td>
<td>{{ file.ColorProfile }}</td>
</tr>
<tr v-if="file.MainColor"> <tr v-if="file.MainColor">
<td> <td>
<translate>Main Color</translate> <translate>Main Color</translate>

View file

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

2
go.mod
View file

@ -35,6 +35,7 @@ require (
github.com/leonelquinteros/gotext v1.5.0 github.com/leonelquinteros/gotext v1.5.0
github.com/lib/pq v1.8.0 // indirect github.com/lib/pq v1.8.0 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 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/manifoldco/promptui v0.9.0
github.com/mattn/go-isatty v0.0.14 // indirect github.com/mattn/go-isatty v0.0.14 // indirect
github.com/mattn/go-sqlite3 v2.0.1+incompatible // 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/go-xmlfmt/xmlfmt v0.0.0-20191208150333-d5b6f63a941b // indirect
github.com/gosimple/unidecode v1.0.1 // indirect github.com/gosimple/unidecode v1.0.1 // indirect
github.com/leodido/go-urn v1.2.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/modern-go/reflect2 v1.0.2 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/russross/blackfriday/v2 v2.1.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 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= 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/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 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA=
github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg= github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg=
github.com/matryer/is v1.2.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlWXA= 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-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-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-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-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-20201208152932-35266b937fa6/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20210216034530-4410531fe030/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" "time"
"github.com/dustin/go-humanize/english" "github.com/dustin/go-humanize/english"
"github.com/gosimple/slug" "github.com/gosimple/slug"
"github.com/jinzhu/gorm" "github.com/jinzhu/gorm"
"github.com/ulule/deepcopier" "github.com/ulule/deepcopier"
"github.com/photoprism/photoprism/internal/face" "github.com/photoprism/photoprism/internal/face"
"github.com/photoprism/photoprism/pkg/colors"
"github.com/photoprism/photoprism/pkg/fs" "github.com/photoprism/photoprism/pkg/fs"
"github.com/photoprism/photoprism/pkg/rnd" "github.com/photoprism/photoprism/pkg/rnd"
"github.com/photoprism/photoprism/pkg/txt" "github.com/photoprism/photoprism/pkg/txt"
@ -57,6 +57,7 @@ type File struct {
FileOrientation int `json:"Orientation" yaml:"Orientation,omitempty"` FileOrientation int `json:"Orientation" yaml:"Orientation,omitempty"`
FileProjection string `gorm:"type:VARBINARY(32);" json:"Projection,omitempty" yaml:"Projection,omitempty"` FileProjection string `gorm:"type:VARBINARY(32);" json:"Projection,omitempty" yaml:"Projection,omitempty"`
FileAspectRatio float32 `gorm:"type:FLOAT;" json:"AspectRatio" yaml:"AspectRatio,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"` FileMainColor string `gorm:"type:VARBINARY(16);index;" json:"MainColor" yaml:"MainColor,omitempty"`
FileColors string `gorm:"type:VARBINARY(9);" json:"Colors" yaml:"Colors,omitempty"` FileColors string `gorm:"type:VARBINARY(9);" json:"Colors" yaml:"Colors,omitempty"`
FileLuminance string `gorm:"type:VARBINARY(9);" json:"Luminance" yaml:"Luminance,omitempty"` FileLuminance string `gorm:"type:VARBINARY(9);" json:"Luminance" yaml:"Luminance,omitempty"`
@ -467,14 +468,29 @@ func (m *File) Panorama() bool {
return m.Projection() != ProjDefault || (m.FileWidth/m.FileHeight) >= 2 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 { func (m *File) Projection() string {
return SanitizeTypeString(m.FileProjection) return SanitizeTypeString(m.FileProjection)
} }
// SetProjection sets the panorama projection type string. // SetProjection sets the panorama projection name.
func (m *File) SetProjection(projType string) { func (m *File) SetProjection(name string) {
m.FileProjection = SanitizeTypeString(projType) 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. // AddFaces adds face markers to the file.

View file

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

View file

@ -4,10 +4,11 @@ import (
"testing" "testing"
"time" "time"
"github.com/photoprism/photoprism/internal/face"
"github.com/photoprism/photoprism/pkg/fs"
"github.com/stretchr/testify/assert" "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) { 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) 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. // TypeString returns an entity type string for logging.
func TypeString(entityType string) string { func TypeString(entityType string) string {
if entityType == "" { if entityType == "" {

View file

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

View file

@ -129,7 +129,7 @@ func TestExif(t *testing.T) {
assert.Equal(t, 6, data.Orientation) assert.Equal(t, 6, data.Orientation)
assert.Equal(t, "Apple", data.LensMake) assert.Equal(t, "Apple", data.LensMake)
assert.Equal(t, "iPhone 7 back camera 3.99mm f/1.8", data.LensModel) 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) { 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, 6, data.FocalLength)
assert.Equal(t, 0, data.Orientation) assert.Equal(t, 0, data.Orientation)
assert.Equal(t, "", data.Projection) assert.Equal(t, "", data.Projection)
assert.Equal(t, "", data.ColorProfile)
}) })
t.Run("exif-example.tiff", func(t *testing.T) { 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, 0, data.FocalLength)
assert.Equal(t, 1, data.Orientation) assert.Equal(t, 1, data.Orientation)
assert.Equal(t, "", data.Projection) assert.Equal(t, "", data.Projection)
assert.Equal(t, "", data.ColorProfile)
}) })
t.Run("out-of-range-500.jpg", func(t *testing.T) { 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, 29, data.FocalLength)
assert.Equal(t, 3, data.Orientation) assert.Equal(t, 3, data.Orientation)
assert.Equal(t, "", data.Projection) assert.Equal(t, "", data.Projection)
assert.Equal(t, "", data.ColorProfile)
}) })
t.Run("digikam.jpg", func(t *testing.T) { 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, "", data.LensModel)
assert.Equal(t, 27, data.FocalLength) assert.Equal(t, 27, data.FocalLength)
assert.Equal(t, 0, int(data.Orientation)) assert.Equal(t, 0, int(data.Orientation))
assert.Equal(t, "", data.ColorProfile)
}) })
t.Run("notebook.jpg", func(t *testing.T) { 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, "EF70-200mm f/4L IS USM", data.LensModel)
assert.Equal(t, 1, data.Orientation) 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, "Canon EOS 6D", data.CameraModel)
assert.Equal(t, "", data.CameraOwner) assert.Equal(t, "", data.CameraOwner)
assert.Equal(t, "012324001432", data.CameraSerial) 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, 1, data.Orientation)
assert.Equal(t, "", data.Projection) assert.Equal(t, "", data.Projection)
}) })
@ -889,4 +889,100 @@ func TestJSON(t *testing.T) {
assert.Equal(t, 0, data.Altitude) assert.Equal(t, 0, data.Altitude)
assert.Equal(t, 1, data.Orientation) 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 Model string `xml:"Model"` // ELE-L29
ExifVersion string `xml:"ExifVersion"` // 0210 ExifVersion string `xml:"ExifVersion"` // 0210
FlashpixVersion string `xml:"FlashpixVersion"` // 0100 FlashpixVersion string `xml:"FlashpixVersion"` // 0100
ColorSpace string `xml:"ColorSpace"` // 1 ColorSpace string `xml:"ColorProfile"` // 1
ComponentsConfiguration struct { ComponentsConfiguration struct {
Text string `xml:",chardata" json:"text,omitempty"` Text string `xml:",chardata" json:"text,omitempty"`
Seq struct { 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) 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. // Fetch command output.
var out bytes.Buffer 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 { if metaData := m.MetaData(); metaData.Error == nil {
file.FileCodec = metaData.Codec file.FileCodec = metaData.Codec
file.SetProjection(metaData.Projection) file.SetProjection(metaData.Projection)
file.SetColorProfile(metaData.ColorProfile)
if metaData.HasInstanceID() { if metaData.HasInstanceID() {
log.Infof("index: %s has instance_id %s", logName, txt.Quote(metaData.InstanceID)) 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.FileAspectRatio = m.AspectRatio()
file.FilePortrait = m.Portrait() file.FilePortrait = m.Portrait()
file.SetProjection(metaData.Projection) file.SetProjection(metaData.Projection)
file.SetColorProfile(metaData.ColorProfile)
if res := m.Megapixels(); res > photo.PhotoResolution { if res := m.Megapixels(); res > photo.PhotoResolution {
photo.PhotoResolution = res photo.PhotoResolution = res
@ -437,6 +439,7 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) (
file.FilePortrait = m.Portrait() file.FilePortrait = m.Portrait()
file.FileDuration = metaData.Duration file.FileDuration = metaData.Duration
file.SetProjection(metaData.Projection) file.SetProjection(metaData.Projection)
file.SetColorProfile(metaData.ColorProfile)
if res := m.Megapixels(); res > photo.PhotoResolution { if res := m.Megapixels(); res > photo.PhotoResolution {
photo.PhotoResolution = res photo.PhotoResolution = res

View file

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

View file

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

View file

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