JPEG: Embed Adobe RGB ICC profile with an InteropIndex tag #5178

Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
Michael Mayer
2025-11-23 10:07:30 +01:00
6 changed files with 145 additions and 0 deletions

View File

@@ -0,0 +1,54 @@
Files: compatibleWithAdobeRGB1998.icc
Source: Debian icc-profiles-free
https://salsa.debian.org/debian/icc-profiles-free/-/tree/a7a3c11b8a6d3bc2937447183b87dc89de9d2388/icc-profiles-openicc/default_profiles/base
Copyright: Kai-Uwe Behrmann <www.behrmann.name>
Marti Maria <www.littlecms.com>
Photogamut <www.photogamut.org>
Graeme Gill <www.argyllcms.com>
ColorSolutions <www.basICColor.com>
License: Zlib
License: Zlib
The zlib/libpng License
.
This software is provided 'as-is', without any express or implied
warranty. In no event will the authors be held liable for any damages
arising from the use of this software.
.
Permission is granted to anyone to use this software for any purpose,
including commercial applications, and to alter it and redistribute it
freely, subject to the following restrictions:
.
1. The origin of this software must not be misrepresented; you must not
claim that you wrote the original software. If you use this software
in a product, an acknowledgment in the product documentation would be
appreciated but is not required.
.
2. Altered source versions must be plainly marked as such, and must not be
misrepresented as being the original software.
.
3. This notice may not be removed or altered from any source
distribution.
.
NO WARRANTY
.
BECAUSE THE DATA IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
FOR THE DATA, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
PROVIDE THE DATA "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
TO THE QUALITY AND PERFORMANCE OF THE DATA IS WITH YOU. SHOULD THE
DATA PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
REPAIR OR CORRECTION.
.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
REDISTRIBUTE THE DATA AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
OUT OF THE USE OR INABILITY TO USE THE DATA (INCLUDING BUT NOT LIMITED
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
YOU OR THIRD PARTIES OR A FAILURE OF THE DATA TO OPERATE WITH ANY OTHER
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGES.

Binary file not shown.

21
internal/thumb/icc.go Normal file
View File

@@ -0,0 +1,21 @@
package thumb
import (
"os"
"path"
)
/*
Possible TODO: move this into a shared pkg/ so non-thumb
consumers can also use it. However, it looks fiddly to hook that
up to `assets`, so I'm punting on that for now.
*/
func MustGetAdobeRGB1998Path() string {
p := path.Join(IccProfilesPath, "adobe_rgb_compat.icc")
_, err := os.Stat(p)
if err != nil {
panic(err)
}
return p
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 528 KiB

View File

@@ -77,6 +77,10 @@ func Vips(imageName string, imageBuffer []byte, hash, thumbPath string, width, h
size = vips.SizeBoth
}
if err = vipsSetIccProfileForInteropIndex(img, clean.Log(filepath.Base(imageName))); err != nil {
log.Debugf("vips: %s in %s (set icc profile for interop index tag)", err, clean.Log(filepath.Base(imageName)))
}
// Create thumbnail image.
if err = img.ThumbnailWithSize(width, height, crop, size); err != nil {
log.Debugf("vips: %s in %s (create thumbnail)", err, clean.Log(filepath.Base(imageName)))
@@ -157,3 +161,47 @@ func VipsJpegExportParams(width, height int) *vips.JpegExportParams {
return params
}
func vipsSetIccProfileForInteropIndex(img *vips.ImageRef, logName string) error {
// Many cameras will define a JPEG's colour space by setting the InteroperabilityIndex
// tag instead of embedding an inline ICC profile.
// We detect this and embed explicit icc profiles for thumbs of such images, for the benefit of Vips
// and web browsers, none of which pay any attention to the InteropIndex tag.
iiFull := img.GetString("exif-ifd4-InteroperabilityIndex")
if iiFull == "" {
return nil
}
// according to my reading, I think [:4] should be e.g. "R98\x00".
// However, vips always returns [:4] = "R98 ", e.g. space instead of null.
// I'm pulling [:3] instead to paper over this - the exif spec says "4 bytes
// incl null terminator" so I think this is safe.
ii := iiFull[:3]
log.Tracef("interopindex: %s read exif and got interopindex %s, %s", logName, ii, iiFull)
if img.HasICCProfile() {
log.Debugf("interopindex: %s has both an interop index tag and an embedded ICC profile. ignoring.", logName)
return nil
}
fallbackProfile := ""
switch ii {
case "R03":
// adobe rgb
fallbackProfile = MustGetAdobeRGB1998Path()
case "R98":
// srgb
// we could logically embed an srgb profile in the image here, but
// there's no value in doing so; everything assumes srgb anyway.
case "THM":
// a thumbnail file. I can't find a ref on what colour space
// this is, so I'm assuming without evidence that they are also srgb.
default:
log.Debugf("interopindex: %s has unknown interop index %s", logName, ii)
}
if fallbackProfile == "" {
return nil
}
return img.TransformICCProfileWithFallback(fallbackProfile, fallbackProfile) // icc profile gets embedded here
}

View File

@@ -25,6 +25,28 @@ func TestVips(t *testing.T) {
assert.True(t, strings.HasSuffix(fileName, dst))
assert.FileExists(t, dst)
})
t.Run("InteropIndexColors", func(t *testing.T) {
thumb := Sizes[Tile500]
src := "testdata/interop_index.jpg"
dst := "testdata/vips/1/3/3/133456789098765432_500x500_center.jpg"
assert.FileExists(t, src)
fileName, _, err := Vips(src, nil, "133456789098765432", "testdata/vips", thumb.Width, thumb.Height, thumb.Options...)
if err != nil {
t.Fatal(err)
}
assert.True(t, strings.HasSuffix(fileName, dst))
assert.Equal(t, fileName, dst)
assert.FileExists(t, dst)
dstimg, err := vips.LoadImageFromFile(dst, vips.NewImportParams())
assert.NoError(t, err)
assert.True(t, dstimg.HasICCProfile())
assert.True(t, dstimg.IsColorSpaceSupported())
})
t.Run("Left224", func(t *testing.T) {
thumb := SizeLeft224
src := "testdata/fixed.jpg"