diff --git a/assets/profiles/icc/NOTICE b/assets/profiles/icc/NOTICE new file mode 100644 index 000000000..661d66272 --- /dev/null +++ b/assets/profiles/icc/NOTICE @@ -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 + Marti Maria + Photogamut + Graeme Gill + ColorSolutions +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. \ No newline at end of file diff --git a/assets/profiles/icc/adobe_rgb_compat.icc b/assets/profiles/icc/adobe_rgb_compat.icc new file mode 100644 index 000000000..79699be80 Binary files /dev/null and b/assets/profiles/icc/adobe_rgb_compat.icc differ diff --git a/internal/thumb/icc.go b/internal/thumb/icc.go new file mode 100644 index 000000000..e567aa430 --- /dev/null +++ b/internal/thumb/icc.go @@ -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 +} diff --git a/internal/thumb/testdata/interop_index.jpg b/internal/thumb/testdata/interop_index.jpg new file mode 100644 index 000000000..8c121ca14 Binary files /dev/null and b/internal/thumb/testdata/interop_index.jpg differ diff --git a/internal/thumb/vips.go b/internal/thumb/vips.go index 115b1a1c2..4c562e80d 100644 --- a/internal/thumb/vips.go +++ b/internal/thumb/vips.go @@ -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 +} diff --git a/internal/thumb/vips_test.go b/internal/thumb/vips_test.go index 28dc33a9e..33c159a4d 100644 --- a/internal/thumb/vips_test.go +++ b/internal/thumb/vips_test.go @@ -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"