Initial QSfera import

This commit is contained in:
Курнат Андрей
2026-06-07 10:20:04 +03:00
commit 2315f25754
16485 changed files with 4826827 additions and 0 deletions
@@ -0,0 +1,104 @@
package icc
import (
"fmt"
)
var _ = fmt.Print
func (p *Profile) IsMatrixShaper() bool {
h := p.TagTable.Has
switch p.Header.DataColorSpace {
case ColorSpaceGray:
return h(GrayTRCTagSignature)
case ColorSpaceRGB:
return h(RedColorantTagSignature) && h(RedTRCTagSignature) && h(GreenColorantTagSignature) && h(GreenTRCTagSignature) && h(BlueColorantTagSignature) && h(BlueTRCTagSignature)
default:
return false
}
}
func (p *Profile) BlackPoint(intent RenderingIntent, debug General_debug_callback) (ans XYZType) {
if q := p.blackpoints[intent]; q != nil {
return *q
}
defer func() {
p.blackpoints[intent] = &ans
}()
if p.Header.DeviceClass == DeviceClassLink || p.Header.DeviceClass == DeviceClassAbstract || p.Header.DeviceClass == DeviceClassNamedColor {
return
}
if !(intent == PerceptualRenderingIntent || intent == SaturationRenderingIntent || intent == RelativeColorimetricRenderingIntent) {
return
}
if p.Header.Version.Major >= 4 && (intent == PerceptualRenderingIntent || intent == SaturationRenderingIntent) {
if p.IsMatrixShaper() {
return p.black_point_as_darker_colorant(RelativeColorimetricRenderingIntent, debug)
}
return XYZType{0.00336, 0.0034731, 0.00287}
}
if intent == RelativeColorimetricRenderingIntent && p.Header.DeviceClass == DeviceClassOutput && p.Header.DataColorSpace == ColorSpaceCMYK {
return p.black_point_using_perceptual_black(debug)
}
return p.black_point_as_darker_colorant(intent, debug)
}
func (p *Profile) black_point_as_darker_colorant(intent RenderingIntent, debug General_debug_callback) XYZType {
bp := p.Header.DataColorSpace.BlackPoint()
if bp == nil || (len(bp) != 3 && len(bp) != 4) {
return XYZType{}
}
tr, err := p.CreateTransformerToPCS(intent, len(bp), debug == nil)
if err != nil {
return XYZType{}
}
if p.Header.ProfileConnectionSpace == ColorSpaceXYZ {
tr.Append(NewXYZtoLAB(p.PCSIlluminant))
}
var l, a, b unit_float
if debug == nil {
if len(bp) == 3 {
l, a, b = tr.Transform(bp[0], bp[1], bp[2])
} else {
var x [4]unit_float
tr.TransformGeneral(x[:], bp)
l, a, b = x[0], x[1], x[2]
}
} else {
var x [4]unit_float
tr.TransformGeneralDebug(x[:], bp, debug)
l, a, b = x[0], x[1], x[2]
}
a, b = 0, 0
if l < 0 || l > 50 {
l = 0
}
x, y, z := NewLABtoXYZ(p.PCSIlluminant).Transform(l, a, b)
return XYZType{x, y, z}
}
func (p *Profile) black_point_using_perceptual_black(debug General_debug_callback) XYZType {
dev, err := p.CreateTransformerToDevice(PerceptualRenderingIntent, false, debug == nil)
if err != nil {
return XYZType{}
}
tr, err := p.CreateTransformerToPCS(RelativeColorimetricRenderingIntent, 4, debug == nil)
if err != nil {
return XYZType{}
}
dev = dev.Weld(tr, debug == nil)
if !dev.IsSuitableFor(3, 3) {
return XYZType{}
}
lab := [4]unit_float{}
if debug == nil {
dev.TransformGeneral(lab[:], []unit_float{0, 0, 0, 0})
} else {
dev.TransformGeneralDebug(lab[:], []unit_float{0, 0, 0, 0}, debug)
}
l, a, b := lab[0], lab[1], lab[2]
l = min(l, 50)
a, b = 0, 0
x, y, z := NewLABtoXYZ(p.PCSIlluminant).Transform(l, a, b)
return XYZType{x, y, z}
}
@@ -0,0 +1,104 @@
package icc
import "fmt"
type ColorSpace uint32
const (
ColorSpaceXYZ ColorSpace = 0x58595A20 // 'XYZ '
ColorSpaceLab ColorSpace = 0x4C616220 // 'Lab '
ColorSpaceLuv ColorSpace = 0x4C757620 // 'Luv '
ColorSpaceYCbCr ColorSpace = 0x59436272 // 'YCbr'
ColorSpaceYxy ColorSpace = 0x59787920 // 'Yxy '
ColorSpaceRGB ColorSpace = 0x52474220 // 'RGB '
ColorSpaceGray ColorSpace = 0x47524159 // 'Gray'
ColorSpaceHSV ColorSpace = 0x48535620 // 'HSV '
ColorSpaceHLS ColorSpace = 0x484C5320 // 'HLS '
ColorSpaceCMYK ColorSpace = 0x434D594B // 'CMYK'
ColorSpaceCMY ColorSpace = 0x434D5920 // 'CMY '
ColorSpace2Color ColorSpace = 0x32434C52 // '2CLR'
ColorSpace3Color ColorSpace = 0x33434C52 // '3CLR'
ColorSpace4Color ColorSpace = 0x34434C52 // '4CLR'
ColorSpace5Color ColorSpace = 0x35434C52 // '5CLR'
ColorSpace6Color ColorSpace = 0x36434C52 // '6CLR'
ColorSpace7Color ColorSpace = 0x37434C52 // '7CLR'
ColorSpace8Color ColorSpace = 0x38434C52 // '8CLR'
ColorSpace9Color ColorSpace = 0x39434C52 // '9CLR'
ColorSpace10Color ColorSpace = 0x41434C52 // 'ACLR'
ColorSpace11Color ColorSpace = 0x42434C52 // 'BCLR'
ColorSpace12Color ColorSpace = 0x43434C52 // 'CCLR'
ColorSpace13Color ColorSpace = 0x44434C52 // 'DCLR'
ColorSpace14Color ColorSpace = 0x45434C52 // 'ECLR'
ColorSpace15Color ColorSpace = 0x46434C52 // 'FCLR'
)
func (cs ColorSpace) BlackPoint() []unit_float {
switch cs {
case ColorSpaceLab, ColorSpaceRGB, ColorSpaceXYZ:
return []unit_float{0, 0, 0}
case ColorSpaceGray:
return []unit_float{0}
case ColorSpaceCMYK:
return []unit_float{1, 1, 1, 1}
case ColorSpaceCMY:
return []unit_float{1, 1, 1}
}
return nil
}
func (cs ColorSpace) String() string {
switch cs {
case ColorSpaceXYZ:
return "XYZ"
case ColorSpaceLab:
return "Lab"
case ColorSpaceLuv:
return "Luv"
case ColorSpaceYCbCr:
return "YCbCr"
case ColorSpaceYxy:
return "Yxy"
case ColorSpaceRGB:
return "RGB"
case ColorSpaceGray:
return "Gray"
case ColorSpaceHSV:
return "HSV"
case ColorSpaceHLS:
return "HLS"
case ColorSpaceCMYK:
return "CMYK"
case ColorSpaceCMY:
return "CMY"
case ColorSpace2Color:
return "2 color"
case ColorSpace3Color:
return "3 color"
case ColorSpace4Color:
return "4 color"
case ColorSpace5Color:
return "5 color"
case ColorSpace6Color:
return "6 color"
case ColorSpace7Color:
return "7 color"
case ColorSpace8Color:
return "8 color"
case ColorSpace9Color:
return "9 color"
case ColorSpace10Color:
return "10 color"
case ColorSpace11Color:
return "11 color"
case ColorSpace12Color:
return "12 color"
case ColorSpace13Color:
return "13 color"
case ColorSpace14Color:
return "14 color"
case ColorSpace15Color:
return "15 color"
default:
return fmt.Sprintf("Unknown (%s)", Signature(cs))
}
}
@@ -0,0 +1,36 @@
package icc
import "fmt"
type DeviceClass uint32
const (
DeviceClassInput DeviceClass = 0x73636E72 // 'scnr'
DeviceClassDisplay DeviceClass = 0x6D6E7472 // 'mntr'
DeviceClassOutput DeviceClass = 0x70727472 // 'prtr'
DeviceClassLink DeviceClass = 0x6C696E6B // 'link'
DeviceClassColorSpace DeviceClass = 0x73706163 // 'spac'
DeviceClassAbstract DeviceClass = 0x61627374 // 'abst'
DeviceClassNamedColor DeviceClass = 0x6E6D636C // 'nmcl'
)
func (dc DeviceClass) String() string {
switch dc {
case DeviceClassInput:
return "Input"
case DeviceClassDisplay:
return "Display"
case DeviceClassOutput:
return "Output"
case DeviceClassLink:
return "Device link"
case DeviceClassColorSpace:
return "Color space"
case DeviceClassAbstract:
return "Abstract"
case DeviceClassNamedColor:
return "Named color"
default:
return fmt.Sprintf("Unknown (%s)", Signature(dc))
}
}
+2
View File
@@ -0,0 +1,2 @@
// Package icc provides support for working with ICC colour profile data.
package icc
+57
View File
@@ -0,0 +1,57 @@
package icc
import (
"fmt"
"math"
"time"
)
type unit_float = float64
// We consider two floats equal if they result in the same uint16 representation
const FLOAT_EQUALITY_THRESHOLD = 1. / math.MaxUint16
func pow(a, b unit_float) unit_float { return unit_float(math.Pow(float64(a), float64(b))) }
func abs(a unit_float) unit_float { return unit_float(math.Abs(float64(a))) }
type Header struct {
ProfileSize uint32
PreferredCMM Signature
Version Version
DeviceClass DeviceClass
DataColorSpace ColorSpace
ProfileConnectionSpace ColorSpace
CreatedAtRaw [6]uint16
FileSignature Signature
PrimaryPlatform PrimaryPlatform
Flags uint32
DeviceManufacturer Signature
DeviceModel Signature
DeviceAttributes uint64
RenderingIntent RenderingIntent
PCSIlluminant [12]uint8
ProfileCreator Signature
ProfileID [16]byte
Reserved [28]byte
}
func (h Header) CreatedAt() time.Time {
b := h.CreatedAtRaw
return time.Date(int(b[0]), time.Month(b[1]), int(b[2]), int(b[3]), int(b[4]), int(b[5]), 0, time.UTC)
}
func (h Header) Embedded() bool {
return (h.Flags >> 31) != 0
}
func (h Header) DependsOnEmbeddedData() bool {
return (h.Flags>>30)&1 != 0
}
func (h Header) ParsedPCSIlluminant() XYZType {
return xyz_type(h.PCSIlluminant[:])
}
func (h Header) String() string {
return fmt.Sprintf("Header{PreferredCMM: %s, Version: %s, DeviceManufacturer: %s, DeviceModel: %s, ProfileCreator: %s, RenderingIntent: %s, CreatedAt: %v PCSIlluminant: %v}", h.PreferredCMM, h.Version, h.DeviceManufacturer, h.DeviceModel, h.ProfileCreator, h.RenderingIntent, h.CreatedAt(), h.ParsedPCSIlluminant())
}
@@ -0,0 +1,193 @@
package icc
import (
"fmt"
"math"
)
var _ = fmt.Print
type interpolation_data struct {
num_inputs, num_outputs int
samples []unit_float
grid_points []int
max_grid_points []int
tetrahedral_index_lookup []int
}
func make_interpolation_data(num_inputs, num_outputs int, grid_points []int, samples []unit_float) *interpolation_data {
var tetrahedral_index_lookup [4]int
max_grid_points := make([]int, len(grid_points))
for i, g := range grid_points {
max_grid_points[i] = g - 1
}
if num_inputs >= 3 {
tetrahedral_index_lookup[0] = num_outputs
for i := 1; i < num_inputs; i++ {
tetrahedral_index_lookup[i] = tetrahedral_index_lookup[i-1] * grid_points[num_inputs-1]
}
}
return &interpolation_data{
num_inputs: num_inputs, num_outputs: num_outputs, grid_points: grid_points, max_grid_points: max_grid_points,
tetrahedral_index_lookup: tetrahedral_index_lookup[:], samples: samples,
}
}
func (c *interpolation_data) tetrahedral_interpolation(r, g, b unit_float, output []unit_float) {
r, g, b = clamp01(r), clamp01(g), clamp01(b)
px := r * unit_float(c.max_grid_points[0])
py := g * unit_float(c.max_grid_points[1])
pz := b * unit_float(c.max_grid_points[2])
x0, y0, z0 := int(px), int(py), int(pz)
rx, ry, rz := px-unit_float(x0), py-unit_float(y0), pz-unit_float(z0)
X0 := c.tetrahedral_index_lookup[2] * x0
X1 := X0
if r < 1 {
X1 += c.tetrahedral_index_lookup[2]
}
Y0 := c.tetrahedral_index_lookup[1] * y0
Y1 := Y0
if g < 1 {
Y1 += c.tetrahedral_index_lookup[1]
}
Z0 := c.tetrahedral_index_lookup[0] * z0
Z1 := Z0
if b < 1 {
Z1 += c.tetrahedral_index_lookup[0]
}
type w struct{ a, b int }
var c1, c2, c3 w
c0 := X0 + Y0 + Z0
// The six tetrahedra
switch {
case rx >= ry && ry >= rz:
c1 = w{X1 + Y0 + Z0, c0}
c2 = w{X1 + Y1 + Z0, X1 + Y0 + Z0}
c3 = w{X1 + Y1 + Z1, X1 + Y1 + Z0}
case rx >= rz && rz >= ry:
c1 = w{X1 + Y0 + Z0, c0}
c2 = w{X1 + Y1 + Z1, X1 + Y0 + Z1}
c3 = w{X1 + Y0 + Z1, X1 + Y0 + Z0}
case rz >= rx && rx >= ry:
c1 = w{X1 + Y0 + Z1, X0 + Y0 + Z1}
c2 = w{X1 + Y1 + Z1, X1 + Y0 + Z1}
c3 = w{X0 + Y0 + Z1, c0}
case ry >= rx && rx >= rz:
c1 = w{X1 + Y1 + Z0, X0 + Y1 + Z0}
c2 = w{X0 + Y1 + Z0, c0}
c3 = w{X1 + Y1 + Z1, X1 + Y1 + Z0}
case ry >= rz && rz >= rx:
c1 = w{X1 + Y1 + Z1, X0 + Y1 + Z1}
c2 = w{X0 + Y1 + Z0, c0}
c3 = w{X0 + Y1 + Z1, X0 + Y1 + Z0}
case rz >= ry && ry >= rx:
c1 = w{X1 + Y1 + Z1, X0 + Y1 + Z1}
c2 = w{X0 + Y1 + Z1, X0 + Y0 + Z1}
c3 = w{X0 + Y0 + Z1, c0}
}
for o := range c.num_outputs {
s := c.samples[o:]
output[o] = s[c0] + (s[c1.a]-s[c1.b])*rx + (s[c2.a]-s[c2.b])*ry + (s[c3.a]-s[c3.b])*rz
}
}
// For more that 3 inputs (i.e., CMYK)
// evaluate two 3-dimensional interpolations and then linearly interpolate between them.
func (d *interpolation_data) tetrahedral_interpolation4(c, m, y, k unit_float, output []unit_float) {
var tmp1, tmp2 [4]float64
pk := clamp01(c) * unit_float(d.max_grid_points[0])
k0 := int(math.Trunc(pk))
rest := pk - unit_float(k0)
K0 := d.tetrahedral_index_lookup[3] * k0
K1 := K0 + IfElse(c >= 1, 0, d.tetrahedral_index_lookup[3])
half := *d
half.grid_points = half.grid_points[1:]
half.max_grid_points = half.max_grid_points[1:]
half.samples = d.samples[K0:]
half.tetrahedral_interpolation(m, y, k, tmp1[:len(output)])
half.samples = d.samples[K1:]
half.tetrahedral_interpolation(m, y, k, tmp2[:len(output)])
for i := range output {
y0, y1 := tmp1[i], tmp2[i]
output[i] = y0 + (y1-y0)*rest
}
}
func sampled_value(samples []unit_float, max_idx unit_float, x unit_float) unit_float {
idx := clamp01(x) * max_idx
lof := unit_float(math.Trunc(float64(idx)))
lo := int(lof)
if lof == idx {
return samples[lo]
}
p := idx - unit_float(lo)
vhi := unit_float(samples[lo+1])
vlo := unit_float(samples[lo])
return vlo + p*(vhi-vlo)
}
// Performs an n-linear interpolation on the CLUT values for the given input color using an iterative method.
// Input values should be normalized between 0.0 and 1.0. Output MUST be zero initialized.
func (c *interpolation_data) trilinear_interpolate(input, output []unit_float) {
// Pre-allocate slices for indices and weights
var buf [4]int
var wbuf [4]unit_float
indices := buf[:c.num_inputs]
weights := wbuf[:c.num_inputs]
input = input[:c.num_inputs]
output = output[:c.num_outputs]
// Calculate the base indices and interpolation weights for each dimension.
for i, val := range input {
val = clamp01(val)
// Scale the value to the grid dimensions
pos := val * unit_float(c.max_grid_points[i])
// The base index is the floor of the position.
idx := int(pos)
// The weight is the fractional part of the position.
weight := pos - unit_float(idx)
// Clamp index to be at most the second to last grid point.
if idx >= c.max_grid_points[i] {
idx = c.max_grid_points[i] - 1
weight = 1 // set weight to 1 for border index
}
indices[i] = idx
weights[i] = weight
}
// Iterate through all 2^InputChannels corners of the n-dimensional hypercube
for i := range 1 << c.num_inputs {
// Calculate the combined weight for this corner
cornerWeight := unit_float(1)
// Calculate the N-dimensional index to look up in the table
tableIndex := 0
multiplier := unit_float(1)
// As per section 10.12.3 of ICC.1-2022-5.pdf spec the first input channel
// varies least rapidly and the last varies most rapidly
for j := c.num_inputs - 1; j >= 0; j-- {
// Check the j-th bit of i to decide if we are at the lower or upper bound for this dimension
if (i>>j)&1 == 1 {
// Upper bound for this dimension
cornerWeight *= weights[j]
tableIndex += int(unit_float(indices[j]+1) * multiplier)
} else {
// Lower bound for this dimension
cornerWeight *= (1.0 - weights[j])
tableIndex += int(unit_float(indices[j]) * multiplier)
}
multiplier *= unit_float(c.grid_points[j])
}
// Get the color value from the table for the current corner
offset := tableIndex * c.num_outputs
// Add the weighted corner color to the output
for k, v := range c.samples[offset : offset+c.num_outputs] {
output[k] += v * cornerWeight
}
}
}
+252
View File
@@ -0,0 +1,252 @@
package icc
import (
"fmt"
"github.com/kovidgoyal/imaging/colorconv"
)
var _ = fmt.Println
const MAX_ENCODEABLE_XYZ = 1.0 + 32767.0/32768.0
const MAX_ENCODEABLE_XYZ_INVERSE = 1.0 / (MAX_ENCODEABLE_XYZ)
const LAB_MFT2_ENCODING_CORRECTION = 65535.0 / 65280.0
const LAB_MFT2_ENCODING_CORRECTION_INVERSE = 65280.0 / 65535.0
func tg33(t func(r, g, b unit_float) (x, y, z unit_float), o, i []unit_float) {
o[0], o[1], o[2] = t(i[0], i[1], i[2])
}
type Scaling struct {
name string
s unit_float
}
func (n *Scaling) String() string { return fmt.Sprintf("%s{%.6v}", n.name, n.s) }
func (n *Scaling) IOSig() (int, int) { return 3, 3 }
func (n *Scaling) Iter(f func(ChannelTransformer) bool) { f(n) }
func (m *Scaling) Transform(x, y, z unit_float) (unit_float, unit_float, unit_float) {
return x * m.s, y * m.s, z * m.s
}
func (m *Scaling) AsMatrix3() *Matrix3 { return NewScalingMatrix3(m.s) }
func (m *Scaling) TransformGeneral(o, i []unit_float) { tg33(m.Transform, o, i) }
func NewScaling(name string, s unit_float) *Scaling { return &Scaling{name, s} }
type Scaling4 struct {
name string
s unit_float
}
func (n *Scaling4) String() string { return fmt.Sprintf("%s{%.6v}", n.name, n.s) }
func (n *Scaling4) IOSig() (int, int) { return 4, 4 }
func (n *Scaling4) Iter(f func(ChannelTransformer) bool) { f(n) }
func (m *Scaling4) Transform(x, y, z unit_float) (unit_float, unit_float, unit_float) {
return x * m.s, y * m.s, z * m.s
}
func (m *Scaling4) TransformGeneral(o, i []unit_float) {
for x := range 4 {
o[x] = m.s * i[x]
}
}
// A transformer to convert normalized [0,1] values to the [0,1.99997]
// (u1Fixed15Number) values used by ICC XYZ PCS space
func NewNormalizedToXYZ() *Scaling { return &Scaling{"NormalizedToXYZ", MAX_ENCODEABLE_XYZ} }
func NewXYZToNormalized() *Scaling { return &Scaling{"XYZToNormalized", MAX_ENCODEABLE_XYZ_INVERSE} }
// A transformer that converts from the legacy LAB encoding used in the obsolete lut16type (mft2) tags
func NewLABFromMFT2() *Scaling { return &Scaling{"LABFromMFT2", LAB_MFT2_ENCODING_CORRECTION} }
func NewLABToMFT2() *Scaling { return &Scaling{"LABToMFT2", LAB_MFT2_ENCODING_CORRECTION_INVERSE} }
// A transformer to convert normalized [0,1] to the LAB co-ordinate system
// used by ICC PCS LAB profiles [0-100], [-128, 127]
type NormalizedToLAB int
func (n NormalizedToLAB) String() string { return "NormalizedToLAB" }
func (n NormalizedToLAB) IOSig() (int, int) { return 3, 3 }
func (n *NormalizedToLAB) Iter(f func(ChannelTransformer) bool) { f(n) }
func (m *NormalizedToLAB) Transform(x, y, z unit_float) (unit_float, unit_float, unit_float) {
// See PackLabDoubleFromFloat in lcms source code
return x * 100, (y*255 - 128), (z*255 - 128)
}
func (m *NormalizedToLAB) TransformGeneral(o, i []unit_float) { tg33(m.Transform, o, i) }
func NewNormalizedToLAB() *NormalizedToLAB {
x := NormalizedToLAB(0)
return &x
}
type LABToNormalized int
func (n LABToNormalized) String() string { return "LABToNormalized" }
func (n LABToNormalized) IOSig() (int, int) { return 3, 3 }
func (n *LABToNormalized) Iter(f func(ChannelTransformer) bool) { f(n) }
func (m *LABToNormalized) Transform(x, y, z unit_float) (unit_float, unit_float, unit_float) {
// See PackLabDoubleFromFloat in lcms source code
return x * (1. / 100), (y*(1./255) + 128./255), (z*(1./255) + 128./255)
}
func (m *LABToNormalized) TransformGeneral(o, i []unit_float) { tg33(m.Transform, o, i) }
func NewLABToNormalized() *LABToNormalized {
x := LABToNormalized(0)
return &x
}
type BlackPointCorrection struct {
scale, offset XYZType
}
func (n BlackPointCorrection) IOSig() (int, int) { return 3, 3 }
func (n *BlackPointCorrection) Iter(f func(ChannelTransformer) bool) {
m := &Matrix3{{n.scale.X, 0, 0}, {0, n.scale.Y, 0}, {0, 0, n.scale.Z}}
if !is_identity_matrix(m) {
if !f(m) {
return
}
}
t := &Translation{n.offset.X, n.offset.Y, n.offset.Z}
if !t.Empty() {
f(t)
}
}
func NewBlackPointCorrection(in_whitepoint, in_blackpoint, out_blackpoint XYZType) *BlackPointCorrection {
tx := in_blackpoint.X - in_whitepoint.X
ty := in_blackpoint.Y - in_whitepoint.Y
tz := in_blackpoint.Z - in_whitepoint.Z
ans := BlackPointCorrection{}
ans.scale.X = (out_blackpoint.X - in_whitepoint.X) / tx
ans.scale.Y = (out_blackpoint.Y - in_whitepoint.Y) / ty
ans.scale.Z = (out_blackpoint.Z - in_whitepoint.Z) / tz
ans.offset.X = -in_whitepoint.X * (out_blackpoint.X - in_blackpoint.X) / tx
ans.offset.Y = -in_whitepoint.Y * (out_blackpoint.Y - in_blackpoint.Y) / ty
ans.offset.Z = -in_whitepoint.Z * (out_blackpoint.Z - in_blackpoint.Z) / tz
ans.offset.X *= MAX_ENCODEABLE_XYZ_INVERSE
ans.offset.Y *= MAX_ENCODEABLE_XYZ_INVERSE
ans.offset.Z *= MAX_ENCODEABLE_XYZ_INVERSE
return &ans
}
func (c *BlackPointCorrection) String() string {
return fmt.Sprintf("BlackPointCorrection{scale: %v offset: %v}", c.scale, c.offset)
}
func (c *BlackPointCorrection) Transform(r, g, b unit_float) (unit_float, unit_float, unit_float) {
return c.scale.X*r + c.offset.X, c.scale.Y*g + c.offset.Y, c.scale.Z*b + c.offset.Z
}
func (m *BlackPointCorrection) TransformGeneral(o, i []unit_float) { tg33(m.Transform, o, i) }
type LABtosRGB struct {
c *colorconv.ConvertColor
t func(l, a, b unit_float) (x, y, z unit_float)
}
func NewLABtosRGB(whitepoint XYZType, clamp, map_gamut bool) *LABtosRGB {
c := colorconv.NewConvertColor(whitepoint.X, whitepoint.Y, whitepoint.Z, 1)
if clamp {
if map_gamut {
return &LABtosRGB{c, c.LabToSRGB}
}
return &LABtosRGB{c, c.LabToSRGBClamp}
}
return &LABtosRGB{c, c.LabToSRGBNoGamutMap}
}
func (c LABtosRGB) Transform(l, a, b unit_float) (unit_float, unit_float, unit_float) {
return c.t(l, a, b)
}
func (m LABtosRGB) TransformGeneral(o, i []unit_float) { tg33(m.Transform, o, i) }
func (n LABtosRGB) IOSig() (int, int) { return 3, 3 }
func (n LABtosRGB) String() string { return fmt.Sprintf("%T%s", n, n.c.String()) }
func (n LABtosRGB) Iter(f func(ChannelTransformer) bool) { f(n) }
type UniformFunctionTransformer struct {
name string
f func(unit_float) unit_float
}
func (n UniformFunctionTransformer) IOSig() (int, int) { return 3, 3 }
func (n UniformFunctionTransformer) String() string { return n.name }
func (n *UniformFunctionTransformer) Iter(f func(ChannelTransformer) bool) { f(n) }
func (c *UniformFunctionTransformer) Transform(x, y, z unit_float) (unit_float, unit_float, unit_float) {
return c.f(x), c.f(y), c.f(z)
}
func (c *UniformFunctionTransformer) TransformGeneral(o, i []unit_float) {
for k, x := range i {
o[k] = c.f(x)
}
}
func NewUniformFunctionTransformer(name string, f func(unit_float) unit_float) *UniformFunctionTransformer {
return &UniformFunctionTransformer{name, f}
}
type XYZtosRGB struct {
c *colorconv.ConvertColor
t func(l, a, b unit_float) (x, y, z unit_float)
}
func NewXYZtosRGB(whitepoint XYZType, clamp, map_gamut bool) *XYZtosRGB {
c := colorconv.NewConvertColor(whitepoint.X, whitepoint.Y, whitepoint.Z, 1)
if clamp {
if map_gamut {
return &XYZtosRGB{c, c.XYZToSRGB}
}
return &XYZtosRGB{c, c.XYZToSRGBNoGamutMap}
}
return &XYZtosRGB{c, c.XYZToSRGBNoClamp}
}
func (n *XYZtosRGB) AddPreviousMatrix(m Matrix3) {
n.c.AddPreviousMatrix(m[0], m[1], m[2])
}
func (c *XYZtosRGB) Transform(l, a, b unit_float) (unit_float, unit_float, unit_float) {
return c.t(l, a, b)
}
func (m *XYZtosRGB) TransformGeneral(o, i []unit_float) { tg33(m.Transform, o, i) }
func (n *XYZtosRGB) IOSig() (int, int) { return 3, 3 }
func (n *XYZtosRGB) String() string { return fmt.Sprintf("%T%s", n, n.c.String()) }
func (n *XYZtosRGB) Iter(f func(ChannelTransformer) bool) { f(n) }
type LABtoXYZ struct {
c *colorconv.ConvertColor
t func(l, a, b unit_float) (x, y, z unit_float)
}
func NewLABtoXYZ(whitepoint XYZType) *LABtoXYZ {
c := colorconv.NewConvertColor(whitepoint.X, whitepoint.Y, whitepoint.Z, 1)
return &LABtoXYZ{c, c.LabToXYZ}
}
func (c *LABtoXYZ) Transform(l, a, b unit_float) (unit_float, unit_float, unit_float) {
return c.t(l, a, b)
}
func (m *LABtoXYZ) TransformGeneral(o, i []unit_float) { tg33(m.Transform, o, i) }
func (n *LABtoXYZ) IOSig() (int, int) { return 3, 3 }
func (n *LABtoXYZ) String() string { return fmt.Sprintf("%T%s", n, n.c.String()) }
func (n *LABtoXYZ) Iter(f func(ChannelTransformer) bool) { f(n) }
type XYZtoLAB struct {
c *colorconv.ConvertColor
t func(l, a, b unit_float) (x, y, z unit_float)
}
func NewXYZtoLAB(whitepoint XYZType) *XYZtoLAB {
c := colorconv.NewConvertColor(whitepoint.X, whitepoint.Y, whitepoint.Z, 1)
return &XYZtoLAB{c, c.XYZToLab}
}
func (c *XYZtoLAB) Transform(l, a, b unit_float) (unit_float, unit_float, unit_float) {
return c.t(l, a, b)
}
func (m *XYZtoLAB) TransformGeneral(o, i []unit_float) { tg33(m.Transform, o, i) }
func (n *XYZtoLAB) IOSig() (int, int) { return 3, 3 }
func (n *XYZtoLAB) String() string { return fmt.Sprintf("%T%s", n, n.c.String()) }
func (n *XYZtoLAB) Iter(f func(ChannelTransformer) bool) { f(n) }
+253
View File
@@ -0,0 +1,253 @@
package icc
import (
"fmt"
"reflect"
"slices"
"strings"
)
var _ = fmt.Print
type Pipeline struct {
transformers []ChannelTransformer
tfuncs []func(r, g, b unit_float) (unit_float, unit_float, unit_float)
has_lut16type_tag bool
finalized bool
}
type AsMatrix3 interface {
AsMatrix3() *Matrix3
}
// check for interface being nil or the dynamic value it points to being nil
func is_nil(i any) bool {
if i == nil {
return true // interface itself is nil
}
v := reflect.ValueOf(i)
switch v.Kind() {
case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Pointer, reflect.Slice:
return v.IsNil()
default:
return false
}
}
func (p *Pipeline) finalize(optimize bool) {
if p.finalized {
panic("pipeline already finalized")
}
p.finalized = true
if optimize && len(p.transformers) > 1 {
// Combine all neighboring Matrix3 transformers into a single transformer by multiplying the matrices
var pm AsMatrix3
nt := make([]ChannelTransformer, 0, len(p.transformers))
for i := 0; i < len(p.transformers); {
t := p.transformers[i]
if tm, ok := t.(AsMatrix3); ok {
for i+1 < len(p.transformers) {
if pm, ok = p.transformers[i+1].(AsMatrix3); !ok {
break
}
a, b := tm.AsMatrix3(), pm.AsMatrix3()
combined := b.Multiply(*a)
tm = &combined
t = &combined
i++
}
}
nt = append(nt, t)
i++
}
p.transformers = nt
// Check if the last transform can absorb previous matrices
if len(p.transformers) > 1 {
last := p.transformers[len(p.transformers)-1]
if apm, ok := last.(*XYZtosRGB); ok {
p.transformers = p.transformers[:len(p.transformers)-1]
for {
m := p.remove_last_matrix3()
if m == nil {
break
}
apm.AddPreviousMatrix(*m)
}
p.transformers = append(p.transformers, last)
}
}
}
p.tfuncs = make([]func(r unit_float, g unit_float, b unit_float) (unit_float, unit_float, unit_float), len(p.transformers))
for i, t := range p.transformers {
p.tfuncs[i] = t.Transform
}
}
func (p *Pipeline) Finalize(optimize bool) { p.finalize(optimize) }
func (p *Pipeline) insert(idx int, c ChannelTransformer) {
if is_nil(c) {
return
}
switch c.(type) {
case *IdentityMatrix:
return
}
if len(p.transformers) == 0 {
p.transformers = append(p.transformers, c)
return
}
if idx >= len(p.transformers) {
panic(fmt.Sprintf("cannot insert at idx: %d in pipeline of length: %d", idx, len(p.transformers)))
}
prepend := idx > -1
if prepend {
p.transformers = slices.Insert(p.transformers, idx, c)
} else {
p.transformers = append(p.transformers, c)
}
}
func (p *Pipeline) Insert(idx int, c ChannelTransformer) {
s := slices.Collect(c.Iter)
if idx > -1 {
slices.Reverse(s)
}
for _, x := range s {
p.insert(idx, x)
}
if mft, ok := c.(*MFT); ok && !mft.is8bit {
p.has_lut16type_tag = true
}
}
func (p *Pipeline) Append(c ...ChannelTransformer) {
for _, x := range c {
p.Insert(-1, x)
}
}
func (p *Pipeline) remove_last_matrix3() *Matrix3 {
if len(p.transformers) > 0 {
if q, ok := p.transformers[len(p.transformers)-1].(AsMatrix3); ok {
p.transformers = p.transformers[:len(p.transformers)-1]
return q.AsMatrix3()
}
}
return nil
}
func (p *Pipeline) Transform(r, g, b unit_float) (unit_float, unit_float, unit_float) {
for _, t := range p.tfuncs {
r, g, b = t(r, g, b)
}
return r, g, b
}
func (p *Pipeline) TransformDebug(r, g, b unit_float, f Debug_callback) (unit_float, unit_float, unit_float) {
for _, t := range p.transformers {
x, y, z := t.Transform(r, g, b)
f(r, g, b, x, y, z, t)
r, g, b = x, y, z
}
return r, g, b
}
func (p *Pipeline) TransformGeneral(out, in []unit_float) {
for _, t := range p.transformers {
t.TransformGeneral(out, in)
copy(in, out)
}
}
type General_debug_callback = func(in, out []unit_float, t ChannelTransformer)
func (p *Pipeline) TransformGeneralDebug(out, in []unit_float, f General_debug_callback) {
for _, t := range p.transformers {
t.TransformGeneral(out, in)
nin, nout := t.IOSig()
f(in[:nin], out[:nout], t)
copy(in, out)
}
}
func (p *Pipeline) Len() int { return len(p.transformers) }
func (p *Pipeline) Weld(other *Pipeline, optimize bool) (ans *Pipeline) {
ans = &Pipeline{}
ans.transformers = append(ans.transformers, p.transformers...)
ans.transformers = append(ans.transformers, other.transformers...)
ans.finalize(true)
ans.has_lut16type_tag = p.has_lut16type_tag || other.has_lut16type_tag
return ans
}
func transformers_as_string(t ...ChannelTransformer) string {
items := make([]string, len(t))
for i, t := range t {
items[i] = t.String()
}
return strings.Join(items, " → ")
}
func (p *Pipeline) String() string {
return transformers_as_string(p.transformers...)
}
func (p *Pipeline) IOSig() (i int, o int) {
if len(p.transformers) == 0 {
return -1, -1
}
i, _ = p.transformers[0].IOSig()
_, o = p.transformers[len(p.transformers)-1].IOSig()
return
}
func (p *Pipeline) IsSuitableFor(i, o int) bool {
for _, t := range p.transformers {
qi, qo := t.IOSig()
if qi != i {
return false
}
i = qo
}
return i == o
}
func (p *Pipeline) UseTrilinearInsteadOfTetrahedral() {
for i, q := range p.transformers {
if x, ok := q.(*TetrahedralInterpolate); ok {
p.transformers[i] = &TrilinearInterpolate{x.d, x.legacy}
}
}
}
func (p *Pipeline) IsXYZSRGB() bool {
if p.Len() == 2 {
if c, ok := p.transformers[0].(Curves); ok {
is_srgb := true
for _, cc := range c.Curves() {
if q, ok := cc.(IsSRGB); ok {
is_srgb = q.IsSRGB()
} else {
is_srgb = false
}
if !is_srgb {
break
}
}
if is_srgb {
if c, ok := p.transformers[1].(AsMatrix3); ok {
q := c.AsMatrix3()
var expected_matrix = Matrix3{{0.218036, 0.192576, 0.0715343}, {0.111246, 0.358442, 0.0303044}, {0.00695811, 0.0485389, 0.357053}}
// unfortunately there exist profiles in the wild that
// deviate from the expected matrix by more than FLOAT_EQUALITY_THRESHOLD
if q.Equals(&expected_matrix, 8.5*FLOAT_EQUALITY_THRESHOLD) {
return true
}
}
}
}
}
return false
}
@@ -0,0 +1,30 @@
package icc
import "fmt"
type PrimaryPlatform uint32
const (
PrimaryPlatformNone PrimaryPlatform = 0x00000000
PrimaryPlatformApple PrimaryPlatform = 0x4150504C // 'AAPL'
PrimaryPlatformMicrosoft PrimaryPlatform = 0x4D534654 // 'MSFT'
PrimaryPlatformSGI PrimaryPlatform = 0x53474920 // 'SGI '
PrimaryPlatformSun PrimaryPlatform = 0x53554E57 // 'SUNW'
)
func (pp PrimaryPlatform) String() string {
switch pp {
case PrimaryPlatformNone:
return "None"
case PrimaryPlatformApple:
return "Apple Computer, Inc."
case PrimaryPlatformMicrosoft:
return "Microsoft Corporation"
case PrimaryPlatformSGI:
return "Silicon Graphics, Inc."
case PrimaryPlatformSun:
return "Sun Microsystems, Inc."
default:
return fmt.Sprintf("Unknown (%d)", Signature(pp))
}
}
+413
View File
@@ -0,0 +1,413 @@
package icc
import (
"bytes"
_ "embed"
"fmt"
"io"
"os"
"sync"
)
var _ = fmt.Println
type WellKnownProfile int
//go:embed test-profiles/sRGB-v4.icc
var Srgb_xyz_profile_data []byte
const (
UnknownProfile WellKnownProfile = iota
SRGBProfile
AdobeRGBProfile
PhotoProProfile
DisplayP3Profile
)
type Profile struct {
Header Header
TagTable TagTable
PCSIlluminant XYZType
blackpoints map[RenderingIntent]*XYZType
}
func (p *Profile) Description() (string, error) {
return p.TagTable.getProfileDescription()
}
func (p *Profile) DeviceManufacturerDescription() (string, error) {
return p.TagTable.getDeviceManufacturerDescription()
}
func (p *Profile) DeviceModelDescription() (string, error) {
return p.TagTable.getDeviceModelDescription()
}
func (p *Profile) get_effective_chromatic_adaption(forward bool, intent RenderingIntent) (ans *Matrix3, err error) {
if intent != AbsoluteColorimetricRenderingIntent { // ComputeConversion() in lcms
return nil, nil
}
pcs_whitepoint := p.Header.ParsedPCSIlluminant()
x, err := p.TagTable.get_parsed(MediaWhitePointTagSignature, p.Header.DataColorSpace, p.Header.ProfileConnectionSpace)
if err != nil {
return nil, err
}
wtpt, ok := x.(*XYZType)
if !ok {
return nil, fmt.Errorf("wtpt tag is not of XYZType")
}
if pcs_whitepoint == *wtpt {
return nil, nil
}
defer func() {
if err == nil && ans != nil && !forward {
m, ierr := ans.Inverted()
if ierr == nil {
ans = &m
} else {
ans, err = nil, ierr
}
}
}()
return p.TagTable.get_chromatic_adaption()
}
func (p *Profile) create_matrix_trc_transformer(forward bool, chromatic_adaptation *Matrix3, pipeline *Pipeline) (err error) {
if p.Header.ProfileConnectionSpace != ColorSpaceXYZ {
return fmt.Errorf("matrix/TRC based profile using non XYZ PCS color space: %v", p.Header.ProfileConnectionSpace)
}
// See section F.3 of ICC.1-2202-5.pdf for how these transforms are composed
var rc, gc, bc Curve1D
if rc, err = p.TagTable.load_curve_tag(RedTRCTagSignature); err != nil {
return err
}
if gc, err = p.TagTable.load_curve_tag(GreenTRCTagSignature); err != nil {
return err
}
if bc, err = p.TagTable.load_curve_tag(BlueTRCTagSignature); err != nil {
return err
}
m, err := p.TagTable.load_rgb_matrix(forward)
if err != nil {
return err
}
var c Curves
if forward {
c = NewCurveTransformer("TRC", rc, gc, bc)
} else {
c = NewInverseCurveTransformer("TRC", rc, gc, bc)
}
if forward {
pipeline.Append(c, m, chromatic_adaptation)
} else {
pipeline.Append(chromatic_adaptation, m, NewInverseCurveTransformer("TRC", rc, gc, bc))
}
return nil
}
// See section 8.10.2 of ICC.1-2202-05.pdf for tag selection algorithm
func (p *Profile) find_conversion_tag(forward bool, rendering_intent RenderingIntent) (ans ChannelTransformer, err error) {
var ans_sig Signature = UnknownSignature
found_tag := false
if forward {
switch rendering_intent {
case PerceptualRenderingIntent:
ans_sig = AToB0TagSignature
case RelativeColorimetricRenderingIntent:
ans_sig = AToB1TagSignature
case SaturationRenderingIntent:
ans_sig = AToB2TagSignature
case AbsoluteColorimetricRenderingIntent:
ans_sig = AToB3TagSignature
default:
return nil, fmt.Errorf("unknown rendering intent: %v", rendering_intent)
}
found_tag = p.TagTable.Has(ans_sig)
const fallback = AToB0TagSignature
if !found_tag && p.TagTable.Has(fallback) {
ans_sig = fallback
found_tag = true
}
} else {
switch rendering_intent {
case PerceptualRenderingIntent:
ans_sig = BToA0TagSignature
case RelativeColorimetricRenderingIntent:
ans_sig = BToA1TagSignature
case SaturationRenderingIntent:
ans_sig = BToA2TagSignature
case AbsoluteColorimetricRenderingIntent:
ans_sig = BToA3TagSignature
default:
return nil, fmt.Errorf("unknown rendering intent: %v", rendering_intent)
}
found_tag = p.TagTable.Has(ans_sig)
const fallback = BToA0TagSignature
if !found_tag && p.TagTable.Has(fallback) {
ans_sig = fallback
found_tag = true
}
}
if !found_tag {
return nil, nil
}
// We rely on profile reader to error out if the PCS color space is not XYZ
// or LAB and the device colorspace is not RGB or CMYK
input_colorspace, output_colorspace := p.Header.DataColorSpace, p.Header.ProfileConnectionSpace
if !forward {
input_colorspace, output_colorspace = output_colorspace, input_colorspace
}
c, err := p.TagTable.get_parsed(ans_sig, input_colorspace, output_colorspace)
if err != nil {
return nil, err
}
ans, ok := c.(ChannelTransformer)
if !ok {
return nil, fmt.Errorf("%s tag is not a ChannelTransformer: %T", ans_sig, c)
}
return ans, nil
}
func (p *Profile) effective_bpc(intent RenderingIntent, user_requested_bpc bool) bool {
// See _cmsLinkProfiles() in cmscnvrt.c
if intent == AbsoluteColorimetricRenderingIntent {
return false
}
if (intent == PerceptualRenderingIntent || intent == SaturationRenderingIntent) && p.Header.Version.Major >= 4 {
return true
}
return user_requested_bpc
}
func (p *Profile) CreateTransformerToDevice(rendering_intent RenderingIntent, use_blackpoint_compensation, optimize bool) (ans *Pipeline, err error) {
num_output_channels := len(p.Header.DataColorSpace.BlackPoint())
if num_output_channels == 0 {
return nil, fmt.Errorf("unsupported device color space: %s", p.Header.DataColorSpace)
}
defer func() {
if err == nil && !ans.IsSuitableFor(3, num_output_channels) {
err = fmt.Errorf("transformer to PCS %s not suitable for 3 output channels", ans.String())
}
if err == nil {
ans.finalize(optimize)
}
}()
ans = &Pipeline{}
if p.effective_bpc(rendering_intent, use_blackpoint_compensation) {
var PCS_blackpoint XYZType // 0, 0, 0
output_blackpoint := p.BlackPoint(rendering_intent, nil)
if PCS_blackpoint != output_blackpoint {
is_lab := p.Header.ProfileConnectionSpace == ColorSpaceLab
if is_lab {
ans.Append(NewLABtoXYZ(p.PCSIlluminant))
ans.Append(NewXYZToNormalized())
}
ans.Append(NewBlackPointCorrection(p.PCSIlluminant, PCS_blackpoint, output_blackpoint))
if is_lab {
ans.Append(NewNormalizedToXYZ())
ans.Append(NewXYZtoLAB(p.PCSIlluminant))
}
}
}
ans.Append(transform_for_pcs_colorspace(p.Header.ProfileConnectionSpace, false))
const forward = false
b2a, err := p.find_conversion_tag(forward, rendering_intent)
if err != nil {
return nil, err
}
chromatic_adaptation, err := p.get_effective_chromatic_adaption(forward, rendering_intent)
if err != nil {
return nil, err
}
if b2a != nil {
ans.Append(b2a)
ans.Append(chromatic_adaptation)
if p.Header.ProfileConnectionSpace == ColorSpaceLab {
// For some reason, lcms prefers trilinear over tetrahedral in this
// case, see _cmsReadOutputLUT() in cmsio1.c
ans.UseTrilinearInsteadOfTetrahedral()
}
} else {
err = p.create_matrix_trc_transformer(forward, chromatic_adaptation, ans)
}
return
}
func (p *Profile) createTransformerToPCS(rendering_intent RenderingIntent) (ans *Pipeline, err error) {
const forward = true
ans = &Pipeline{}
a2b, err := p.find_conversion_tag(forward, rendering_intent)
if err != nil {
return nil, err
}
chromatic_adaptation, err := p.get_effective_chromatic_adaption(forward, rendering_intent)
if err != nil {
return nil, err
}
if a2b != nil {
ans.Append(a2b)
ans.Append(chromatic_adaptation)
if ans.has_lut16type_tag && p.Header.ProfileConnectionSpace == ColorSpaceLab {
// Need to scale the lut16type data for legacy LAB encoding in ICC profiles
if p.Header.DataColorSpace == ColorSpaceLab {
ans.Insert(0, NewLABToMFT2())
}
ans.Append(NewLABFromMFT2())
}
} else {
err = p.create_matrix_trc_transformer(forward, chromatic_adaptation, ans)
}
return
}
func (p *Profile) IsSRGB() bool {
if p.Header.ProfileConnectionSpace == ColorSpaceXYZ {
tr, err := p.createTransformerToPCS(p.Header.RenderingIntent)
if err != nil {
return false
}
tr.finalize(true)
return tr.IsXYZSRGB()
}
return false
}
func transform_for_pcs_colorspace(cs ColorSpace, forward bool) ChannelTransformer {
switch cs {
case ColorSpaceXYZ:
if forward {
return NewNormalizedToXYZ()
}
return NewXYZToNormalized()
case ColorSpaceLab:
if forward {
return NewNormalizedToLAB()
}
return NewLABToNormalized()
default:
panic(fmt.Sprintf("unsupported PCS colorspace in profile: %s", cs))
}
}
func (p *Profile) CreateTransformerToPCS(rendering_intent RenderingIntent, input_channels int, optimize bool) (ans *Pipeline, err error) {
ans, err = p.createTransformerToPCS(rendering_intent)
if err == nil && !ans.IsSuitableFor(input_channels, 3) {
err = fmt.Errorf("transformer to PCS %s not suitable for %d input channels", ans.String(), input_channels)
}
if err == nil {
ans.Append(transform_for_pcs_colorspace(p.Header.ProfileConnectionSpace, true))
ans.finalize(optimize)
}
return
}
func (p *Profile) CreateTransformerToSRGB(rendering_intent RenderingIntent, use_blackpoint_compensation bool, input_channels int, clamp, map_gamut, optimize bool) (ans *Pipeline, err error) {
if ans, err = p.createTransformerToPCS(rendering_intent); err != nil {
return
}
if !ans.IsSuitableFor(input_channels, 3) {
return nil, fmt.Errorf("transformer to PCS %s not suitable for %d input channels", ans.String(), input_channels)
}
input_colorspace := p.Header.ProfileConnectionSpace
if p.effective_bpc(rendering_intent, use_blackpoint_compensation) {
var sRGB_blackpoint XYZType // 0, 0, 0
input_blackpoint := p.BlackPoint(rendering_intent, nil)
if input_blackpoint != sRGB_blackpoint {
if input_colorspace == ColorSpaceLab {
ans.Append(transform_for_pcs_colorspace(input_colorspace, true))
ans.Append(NewLABtoXYZ(p.PCSIlluminant))
ans.Append(NewXYZToNormalized())
input_colorspace = ColorSpaceXYZ
}
ans.Append(NewBlackPointCorrection(p.PCSIlluminant, input_blackpoint, sRGB_blackpoint))
}
}
ans.Append(transform_for_pcs_colorspace(input_colorspace, true))
switch input_colorspace {
case ColorSpaceXYZ:
t := NewXYZtosRGB(p.PCSIlluminant, clamp, map_gamut)
ans.Append(t)
case ColorSpaceLab:
ans.Append(NewLABtosRGB(p.PCSIlluminant, clamp, map_gamut))
default:
return nil, fmt.Errorf("unknown PCS colorspace: %s", input_colorspace)
}
ans.finalize(optimize)
return
}
func (p *Profile) CreateDefaultTransformerToDevice() (*Pipeline, error) {
return p.CreateTransformerToDevice(p.Header.RenderingIntent, false, true)
}
func (p *Profile) CreateDefaultTransformerToPCS(input_channels int) (*Pipeline, error) {
return p.CreateTransformerToPCS(p.Header.RenderingIntent, input_channels, true)
}
func newProfile() *Profile {
return &Profile{
TagTable: emptyTagTable(),
blackpoints: make(map[RenderingIntent]*XYZType),
}
}
// Recursively generates all points in an m-dimensional hypercube.
// currentPoint stores the coordinates of the current point being built.
// dimension is the current dimension being processed (from 0 to m-1).
// m is the total number of dimensions.
// n is the number of points per dimension (0 to n-1).
func iterate_hypercube(currentPoint []int, dimension, m, n int, callback func([]int)) {
// Base case: If all dimensions have been assigned, print the point.
if dimension == m {
callback(currentPoint)
return
}
// Recursive step: Iterate through all possible values for the current dimension.
for i := range n {
currentPoint[dimension] = i // Assign value to the current dimension
// Recursively call for the next dimension
iterate_hypercube(currentPoint, dimension+1, m, n, callback)
}
}
func points_for_transformer_comparison(input_channels, num_points_per_input_channel int) []unit_float {
m, n := input_channels, num_points_per_input_channel
sz := input_channels // n ** m * m
for range m {
sz *= n
}
ans := make([]unit_float, 0, sz)
current_point := make([]int, input_channels)
factor := 1 / unit_float(num_points_per_input_channel-1)
iterate_hypercube(current_point, 0, m, n, func(p []int) {
for _, x := range current_point {
ans = append(ans, unit_float(x)*factor)
}
})
if len(ans) != sz {
panic(fmt.Sprintf("insufficient points: wanted %d, got %d", sz, len(ans)))
}
return ans
}
var Points_for_transformer_comparison3 = sync.OnceValue(func() []unit_float {
return points_for_transformer_comparison(3, 16)
})
var Points_for_transformer_comparison4 = sync.OnceValue(func() []unit_float {
return points_for_transformer_comparison(4, 16)
})
func DecodeProfile(r io.Reader) (ans *Profile, err error) {
return NewProfileReader(r).ReadProfile()
}
func ReadProfile(path string) (ans *Profile, err error) {
data, err := os.ReadFile(path)
if err == nil {
ans, err = NewProfileReader(bytes.NewReader(data)).ReadProfile()
}
return
}
@@ -0,0 +1,104 @@
package icc
import (
"encoding/binary"
"fmt"
"io"
"os"
"github.com/kovidgoyal/go-parallel"
)
var _ = fmt.Println
var _ = os.Stderr
type ProfileReader struct {
reader io.Reader
}
func (pr *ProfileReader) ReadProfile() (p *Profile, err error) {
defer func() {
if r := recover(); r != nil {
p = nil
err = parallel.Format_stacktrace_on_panic(r, 1)
}
}()
profile := newProfile()
err = pr.readHeader(&profile.Header)
if err != nil {
return nil, fmt.Errorf("failed to reader header from ICC profile: %w", err)
}
profile.PCSIlluminant = profile.Header.ParsedPCSIlluminant()
err = pr.readTagTable(&profile.TagTable)
if err != nil {
return nil, fmt.Errorf("failed to read tag table from ICC profile: %w", err)
}
return profile, nil
}
func (pr *ProfileReader) readHeader(header *Header) (err error) {
var data [128]byte
if _, err = io.ReadFull(pr.reader, data[:]); err == nil {
var n int
n, err = binary.Decode(data[:], binary.BigEndian, header)
if err == nil {
if header.FileSignature != ProfileFileSignature {
return fmt.Errorf("ICC header has invalid signature: %s", header.FileSignature)
}
if n != len(data) {
return fmt.Errorf("decoding header consumed %d instead of %d bytes", n, len(data))
}
if header.ProfileConnectionSpace != ColorSpaceXYZ && header.ProfileConnectionSpace != ColorSpaceLab {
return fmt.Errorf("unsupported profile connection space colorspace: %s", header.ProfileConnectionSpace)
}
if header.DataColorSpace != ColorSpaceRGB && header.DataColorSpace != ColorSpaceCMYK {
return fmt.Errorf("unsupported device colorspace: %s", header.DataColorSpace)
}
}
}
return
}
func (pr *ProfileReader) readTagTable(tagTable *TagTable) (err error) {
var tagCount uint32
if err = binary.Read(pr.reader, binary.BigEndian, &tagCount); err != nil {
return
}
type tagIndexEntry struct {
Sig Signature
Offset uint32
Size uint32
}
endOfTagData := uint32(0)
tag_indices := make([]tagIndexEntry, tagCount)
if err = binary.Read(pr.reader, binary.BigEndian, tag_indices); err != nil {
return fmt.Errorf("failed to read tag indices from ICC profile: %w", err)
}
for _, t := range tag_indices {
endOfTagData = max(endOfTagData, t.Offset+t.Size)
}
tagDataOffset := 132 + tagCount*12
if endOfTagData > tagDataOffset {
tagData := make([]byte, endOfTagData-tagDataOffset)
if _, err = io.ReadFull(pr.reader, tagData); err != nil {
return fmt.Errorf("failed to read tag data from ICC profile: %w", err)
}
for _, t := range tag_indices {
startOffset := t.Offset - tagDataOffset
endOffset := startOffset + t.Size
tagTable.add(t.Sig, int(startOffset), tagData[startOffset:endOffset])
}
}
return nil
}
func NewProfileReader(r io.Reader) *ProfileReader {
return &ProfileReader{
reader: r,
}
}
@@ -0,0 +1,13 @@
package icc
import "fmt"
type Version struct {
Major byte
MinorAndRev byte
Reserved1, Reserved2 byte
}
func (pv Version) String() string {
return fmt.Sprintf("%d.%d.%d", pv.Major, pv.MinorAndRev>>4, pv.MinorAndRev&3)
}
@@ -0,0 +1,27 @@
package icc
import "fmt"
const (
PerceptualRenderingIntent RenderingIntent = 0
RelativeColorimetricRenderingIntent RenderingIntent = 1
SaturationRenderingIntent RenderingIntent = 2
AbsoluteColorimetricRenderingIntent RenderingIntent = 3
)
type RenderingIntent uint32
func (ri RenderingIntent) String() string {
switch ri {
case PerceptualRenderingIntent:
return "Perceptual"
case RelativeColorimetricRenderingIntent:
return "Relative"
case SaturationRenderingIntent:
return "Saturation"
case AbsoluteColorimetricRenderingIntent:
return "Absolute"
default:
return fmt.Sprintf("Unknown (%d)", ri)
}
}
+212
View File
@@ -0,0 +1,212 @@
package icc
type Signature uint32
const (
UnknownSignature Signature = 0
ProfileFileSignature Signature = 0x61637370 // 'acsp'
TextTagSignature Signature = 0x74657874 // 'text'
SignateTagSignature Signature = 0x73696720 // 'sig '
DescSignature Signature = 0x64657363 // 'desc'
MultiLocalisedUnicodeSignature Signature = 0x6D6C7563 // 'mluc'
DeviceManufacturerDescriptionSignature Signature = 0x646d6e64 // 'dmnd'
DeviceModelDescriptionSignature Signature = 0x646d6464 // 'dmdd'
AdobeManufacturerSignature Signature = 0x41444245 // 'ADBE'
AppleManufacturerSignature Signature = 0x6170706c // 'appl'
AppleUpperManufacturerSignature Signature = 0x4150504c // 'APPL'
IECManufacturerSignature Signature = 0x49454320 // 'IEC '
AdobeRGBModelSignature Signature = 0x52474220 // 'RGB '
SRGBModelSignature Signature = 0x73524742 // 'sRGB'
PhotoProModelSignature Signature = 0x50525452 // 'PTPR'
DisplayP3ModelSignature Signature = 0x70332020 // 'p3 '
ChromaticityTypeSignature Signature = 0x6368726D /* 'chrm' */
ColorantOrderTypeSignature Signature = 0x636C726F /* 'clro' */
ColorantTableTypeSignature Signature = 0x636C7274 /* 'clrt' */
CrdInfoTypeSignature Signature = 0x63726469 /* 'crdi' Removed in V4 */
CurveTypeSignature Signature = 0x63757276 /* 'curv' */
DataTypeSignature Signature = 0x64617461 /* 'data' */
DictTypeSignature Signature = 0x64696374 /* 'dict' */
DateTimeTypeSignature Signature = 0x6474696D /* 'dtim' */
DeviceSettingsTypeSignature Signature = 0x64657673 /* 'devs' Removed in V4 */
Lut16TypeSignature Signature = 0x6d667432 /* 'mft2' */
Lut8TypeSignature Signature = 0x6d667431 /* 'mft1' */
LutAtoBTypeSignature Signature = 0x6d414220 /* 'mAB ' */
LutBtoATypeSignature Signature = 0x6d424120 /* 'mBA ' */
MeasurementTypeSignature Signature = 0x6D656173 /* 'meas' */
MultiLocalizedUnicodeTypeSignature Signature = 0x6D6C7563 /* 'mluc' */
MultiProcessElementTypeSignature Signature = 0x6D706574 /* 'mpet' */
NamedColorTypeSignature Signature = 0x6E636f6C /* 'ncol' OBSOLETE use ncl2 */
NamedColor2TypeSignature Signature = 0x6E636C32 /* 'ncl2' */
ParametricCurveTypeSignature Signature = 0x70617261 /* 'para' */
ProfileSequenceDescTypeSignature Signature = 0x70736571 /* 'pseq' */
ProfileSequceIdTypeSignature Signature = 0x70736964 /* 'psid' */
ResponseCurveSet16TypeSignature Signature = 0x72637332 /* 'rcs2' */
S15Fixed16ArrayTypeSignature Signature = 0x73663332 /* 'sf32' */
ScreeningTypeSignature Signature = 0x7363726E /* 'scrn' Removed in V4 */
SignatureTypeSignature Signature = 0x73696720 /* 'sig ' */
TextTypeSignature Signature = 0x74657874 /* 'text' */
TextDescriptionTypeSignature Signature = 0x64657363 /* 'desc' Removed in V4 */
U16Fixed16ArrayTypeSignature Signature = 0x75663332 /* 'uf32' */
UcrBgTypeSignature Signature = 0x62666420 /* 'bfd ' Removed in V4 */
UInt16ArrayTypeSignature Signature = 0x75693136 /* 'ui16' */
UInt32ArrayTypeSignature Signature = 0x75693332 /* 'ui32' */
UInt64ArrayTypeSignature Signature = 0x75693634 /* 'ui64' */
UInt8ArrayTypeSignature Signature = 0x75693038 /* 'ui08' */
ViewingConditionsTypeSignature Signature = 0x76696577 /* 'view' */
XYZTypeSignature Signature = 0x58595A20 /* 'XYZ ' */
XYZArrayTypeSignature Signature = 0x58595A20 /* 'XYZ ' */
XYZSignature Signature = 0x58595A20 /* 'XYZ ' */
LabSignature Signature = 0x4C616220 /* 'Lab ' */
LUVSignature Signature = 0x4C757620 /* 'Luv ' */
YCbrSignature Signature = 0x59436272 /* 'YCbr' */
YxySignature Signature = 0x59787920 /* 'Yxy ' */
RGBSignature Signature = 0x52474220 /* 'RGB ' */
GraySignature Signature = 0x47524159 /* 'GRAY' */
HSVSignature Signature = 0x48535620 /* 'HSV ' */
HLSSignature Signature = 0x484C5320 /* 'HLS ' */
CMYKSignature Signature = 0x434D594B /* 'CMYK' */
CMYSignature Signature = 0x434D5920 /* 'CMY ' */
MCH2Signature Signature = 0x32434C52 /* '2CLR' */
MCH3Signature Signature = 0x33434C52 /* '3CLR' */
MCH4Signature Signature = 0x34434C52 /* '4CLR' */
MCH5Signature Signature = 0x35434C52 /* '5CLR' */
MCH6Signature Signature = 0x36434C52 /* '6CLR' */
MCH7Signature Signature = 0x37434C52 /* '7CLR' */
MCH8Signature Signature = 0x38434C52 /* '8CLR' */
MCH9Signature Signature = 0x39434C52 /* '9CLR' */
MCHASignature Signature = 0x41434C52 /* 'ACLR' */
MCHBSignature Signature = 0x42434C52 /* 'BCLR' */
MCHCSignature Signature = 0x43434C52 /* 'CCLR' */
MCHDSignature Signature = 0x44434C52 /* 'DCLR' */
MCHESignature Signature = 0x45434C52 /* 'ECLR' */
MCHFSignature Signature = 0x46434C52 /* 'FCLR' */
NamedSignature Signature = 0x6e6d636c /* 'nmcl' */
Color2Signature Signature = 0x32434C52 /* '2CLR' */
Color3Signature Signature = 0x33434C52 /* '3CLR' */
Color4Signature Signature = 0x34434C52 /* '4CLR' */
Color5Signature Signature = 0x35434C52 /* '5CLR' */
Color6Signature Signature = 0x36434C52 /* '6CLR' */
Color7Signature Signature = 0x37434C52 /* '7CLR' */
Color8Signature Signature = 0x38434C52 /* '8CLR' */
Color9Signature Signature = 0x39434C52 /* '9CLR' */
Color10Signature Signature = 0x41434C52 /* 'ACLR' */
Color11Signature Signature = 0x42434C52 /* 'BCLR' */
Color12Signature Signature = 0x43434C52 /* 'CCLR' */
Color13Signature Signature = 0x44434C52 /* 'DCLR' */
Color14Signature Signature = 0x45434C52 /* 'ECLR' */
Color15Signature Signature = 0x46434C52 /* 'FCLR' */
AToB0TagSignature Signature = 0x41324230 /* 'A2B0' */
AToB1TagSignature Signature = 0x41324231 /* 'A2B1' */
AToB2TagSignature Signature = 0x41324232 /* 'A2B2' */
AToB3TagSignature Signature = 0x41324233 /* 'A2B3' */
BlueColorantTagSignature Signature = 0x6258595A /* 'bXYZ' */
BlueMatrixColumnTagSignature Signature = 0x6258595A /* 'bXYZ' */
BlueTRCTagSignature Signature = 0x62545243 /* 'bTRC' */
BToA0TagSignature Signature = 0x42324130 /* 'B2A0' */
BToA1TagSignature Signature = 0x42324131 /* 'B2A1' */
BToA2TagSignature Signature = 0x42324132 /* 'B2A2' */
BToA3TagSignature Signature = 0x42324133 /* 'B2A3' */
CalibrationDateTimeTagSignature Signature = 0x63616C74 /* 'calt' */
CharTargetTagSignature Signature = 0x74617267 /* 'targ' */
ChromaticAdaptationTagSignature Signature = 0x63686164 /* 'chad' */
ChromaticityTagSignature Signature = 0x6368726D /* 'chrm' */
ColorantOrderTagSignature Signature = 0x636C726F /* 'clro' */
ColorantTableTagSignature Signature = 0x636C7274 /* 'clrt' */
ColorantTableOutTagSignature Signature = 0x636C6F74 /* 'clot' */
ColorimetricIntentImageStateTagSignature Signature = 0x63696973 /* 'ciis' */
CopyrightTagSignature Signature = 0x63707274 /* 'cprt' */
CrdInfoTagSignature Signature = 0x63726469 /* 'crdi' Removed in V4 */
DataTagSignature Signature = 0x64617461 /* 'data' Removed in V4 */
DateTimeTagSignature Signature = 0x6474696D /* 'dtim' Removed in V4 */
DeviceMfgDescTagSignature Signature = 0x646D6E64 /* 'dmnd' */
DeviceModelDescTagSignature Signature = 0x646D6464 /* 'dmdd' */
DeviceSettingsTagSignature Signature = 0x64657673 /* 'devs' Removed in V4 */
DToB0TagSignature Signature = 0x44324230 /* 'D2B0' */
DToB1TagSignature Signature = 0x44324231 /* 'D2B1' */
DToB2TagSignature Signature = 0x44324232 /* 'D2B2' */
DToB3TagSignature Signature = 0x44324233 /* 'D2B3' */
BToD0TagSignature Signature = 0x42324430 /* 'B2D0' */
BToD1TagSignature Signature = 0x42324431 /* 'B2D1' */
BToD2TagSignature Signature = 0x42324432 /* 'B2D2' */
BToD3TagSignature Signature = 0x42324433 /* 'B2D3' */
GamutTagSignature Signature = 0x67616D74 /* 'gamt' */
GrayTRCTagSignature Signature = 0x6b545243 /* 'kTRC' */
GreenColorantTagSignature Signature = 0x6758595A /* 'gXYZ' */
GreenMatrixColumnTagSignature Signature = 0x6758595A /* 'gXYZ' */
GreenTRCTagSignature Signature = 0x67545243 /* 'gTRC' */
LuminanceTagSignature Signature = 0x6C756d69 /* 'lumi' */
MeasurementTagSignature Signature = 0x6D656173 /* 'meas' */
MediaBlackPointTagSignature Signature = 0x626B7074 /* 'bkpt' */
MediaWhitePointTagSignature Signature = 0x77747074 /* 'wtpt' */
MetaDataTagSignature Signature = 0x6D657461 /* 'meta' */
NamedColorTagSignature Signature = 0x6E636f6C /* 'ncol' OBSOLETE use ncl2 */
NamedColor2TagSignature Signature = 0x6E636C32 /* 'ncl2' */
OutputResponseTagSignature Signature = 0x72657370 /* 'resp' */
PerceptualRenderingIntentGamutTagSignature Signature = 0x72696730 /* 'rig0' */
Preview0TagSignature Signature = 0x70726530 /* 'pre0' */
Preview1TagSignature Signature = 0x70726531 /* 'pre1' */
Preview2TagSignature Signature = 0x70726532 /* 'pre2' */
PrintConditionTagSignature Signature = 0x7074636e /* 'ptcn' */
ProfileDescriptionTagSignature Signature = 0x64657363 /* 'desc' */
ProfileSequenceDescTagSignature Signature = 0x70736571 /* 'pseq' */
ProfileSequceIdTagSignature Signature = 0x70736964 /* 'psid' */
Ps2CRD0TagSignature Signature = 0x70736430 /* 'psd0' Removed in V4 */
Ps2CRD1TagSignature Signature = 0x70736431 /* 'psd1' Removed in V4 */
Ps2CRD2TagSignature Signature = 0x70736432 /* 'psd2' Removed in V4 */
Ps2CRD3TagSignature Signature = 0x70736433 /* 'psd3' Removed in V4 */
Ps2CSATagSignature Signature = 0x70733273 /* 'ps2s' Removed in V4 */
Ps2RenderingIntentTagSignature Signature = 0x70733269 /* 'ps2i' Removed in V4 */
RedColorantTagSignature Signature = 0x7258595A /* 'rXYZ' */
RedMatrixColumnTagSignature Signature = 0x7258595A /* 'rXYZ' */
RedTRCTagSignature Signature = 0x72545243 /* 'rTRC' */
SaturationRenderingIntentGamutTagSignature Signature = 0x72696732 /* 'rig2' */
ScreeningDescTagSignature Signature = 0x73637264 /* 'scrd' Removed in V4 */
ScreeningTagSignature Signature = 0x7363726E /* 'scrn' Removed in V4 */
TechnologyTagSignature Signature = 0x74656368 /* 'tech' */
UcrBgTagSignature Signature = 0x62666420 /* 'bfd ' Removed in V4 */
ViewingCondDescTagSignature Signature = 0x76756564 /* 'vued' */
ViewingConditionsTagSignature Signature = 0x76696577 /* 'view' */
CurveSetElemTypeSignature Signature = 0x63767374 /* 'cvst' */
MatrixElemTypeSignature Signature = 0x6D617466 /* 'matf' */
CLutElemTypeSignature Signature = 0x636C7574 /* 'clut' */
BAcsElemTypeSignature Signature = 0x62414353 /* 'bACS' */
EAcsElemTypeSignature Signature = 0x65414353 /* 'eACS' */
)
func maskNull(b byte) byte {
switch b {
case 0:
return ' '
default:
return b
}
}
func signature(b []byte) Signature {
return Signature(uint32(b[0])<<24 | uint32(b[1])<<16 | uint32(b[2])<<8 | uint32(b[3]))
}
func SignatureFromString(sig string) Signature {
var b []byte = []byte{0x20, 0x20, 0x20, 0x20}
copy(b, sig)
return signature(b)
}
func (s Signature) String() string {
v := []byte{
(maskNull(byte((s >> 24) & 0xff))),
(maskNull(byte((s >> 16) & 0xff))),
(maskNull(byte((s >> 8) & 0xff))),
(maskNull(byte(s & 0xff))),
}
return "'" + string(v) + "'"
}
@@ -0,0 +1,183 @@
package icc
import (
"bytes"
"encoding/binary"
"errors"
"fmt"
"unicode/utf16"
)
var _ = fmt.Print
func parse_text_tag(data []byte) (any, error) {
var tag_type Signature
_, err := binary.Decode(data, binary.BigEndian, &tag_type)
if err != nil {
return nil, err
}
switch tag_type {
case TextTagSignature:
return textDecoder(data)
case DescSignature:
return descDecoder(data)
default:
return mlucDecoder(data)
}
}
type TextTag interface {
BestGuessValue() string
}
type DescriptionTag struct {
ASCII string
Unicode string
Script string
}
func (d DescriptionTag) BestGuessValue() string {
if d.ASCII != "" {
return d.ASCII
}
return d.Unicode
}
var _ TextTag = (*DescriptionTag)(nil)
func descDecoder(raw []byte) (any, error) {
if len(raw) < 12 {
return nil, errors.New("desc tag too short")
}
asciiLen := int(binary.BigEndian.Uint32(raw[8:12]))
if asciiLen < 1 || 12+asciiLen > len(raw) {
return nil, errors.New("invalid ASCII length in desc tag")
}
ascii := raw[12 : 12+asciiLen]
if i := bytes.IndexByte(ascii, 0); i >= 0 {
ascii = ascii[:i]
}
offset := 12 + asciiLen
if len(raw) < offset+4 {
return &DescriptionTag{ASCII: string(ascii)}, nil // ASCII-only, no Unicode
}
unicodeCount := int(binary.BigEndian.Uint32(raw[offset : offset+4]))
offset += 4
if len(raw) < offset+(unicodeCount*2) {
return nil, errors.New("desc tag truncated: missing UTF-16 data")
}
unicodeData := raw[offset : offset+(unicodeCount*2)]
offset += unicodeCount * 2
unicode := decodeUTF16BE(unicodeData)
if len(raw) <= offset {
return &DescriptionTag{
ASCII: string(ascii),
Unicode: unicode,
}, nil
}
scriptCount := int(raw[offset])
offset++
if len(raw) < offset+scriptCount {
return nil, errors.New("desc tag truncated: missing ScriptCode data")
}
script := string(raw[offset : offset+scriptCount])
return &DescriptionTag{
ASCII: string(ascii),
Unicode: unicode,
Script: script,
}, nil
}
type PlainText struct {
val string
}
var _ TextTag = (*PlainText)(nil)
func (p PlainText) BestGuessValue() string { return p.val }
func textDecoder(raw []byte) (any, error) {
if len(raw) < 8 {
return nil, errors.New("text tag too short")
}
text := raw[8:]
text = bytes.TrimRight(text, "\x00")
return &PlainText{string(text)}, nil
}
type MultiLocalizedTag struct {
Strings []LocalizedString
}
func (p MultiLocalizedTag) BestGuessValue() string {
for _, t := range p.Strings {
if t.Value != "" && (t.Language == "en" || t.Language == "eng") {
return t.Value
}
}
for _, t := range p.Strings {
if t.Value != "" {
return t.Value
}
}
return ""
}
type LocalizedString struct {
Language string // e.g. "en"
Country string // e.g. "US"
Value string
}
func mlucDecoder(raw []byte) (any, error) {
if len(raw) < 16 {
return nil, errors.New("mluc tag too short")
}
count := int(binary.BigEndian.Uint32(raw[8:12]))
recordSize := int(binary.BigEndian.Uint32(raw[12:16]))
if recordSize != 12 {
return nil, fmt.Errorf("unexpected mluc record size: %d", recordSize)
}
if len(raw) < 16+(count*recordSize) {
return nil, fmt.Errorf("mluc tag too small for %d records", count)
}
tag := &MultiLocalizedTag{Strings: make([]LocalizedString, 0, count)}
for i := range count {
base := 16 + i*recordSize
langCode := string(raw[base : base+2])
countryCode := string(raw[base+2 : base+4])
strLen := int(binary.BigEndian.Uint32(raw[base+4 : base+8]))
strOffset := int(binary.BigEndian.Uint32(raw[base+8 : base+12]))
if strOffset+strLen > len(raw) || strLen%2 != 0 {
return nil, fmt.Errorf("invalid string offset/length in mluc record %d", i)
}
strData := raw[strOffset : strOffset+strLen]
decoded := decodeUTF16BE(strData)
tag.Strings = append(tag.Strings, LocalizedString{
Language: langCode,
Country: countryCode,
Value: decoded,
})
}
return tag, nil
}
func decodeUTF16BE(data []byte) string {
codeUnits := make([]uint16, len(data)/2)
_, _ = binary.Decode(data, binary.BigEndian, codeUnits)
return string(utf16.Decode(codeUnits))
}
func sigDecoder(raw []byte) (any, error) {
if len(raw) < 12 {
return nil, errors.New("sig tag too short")
}
return signature(raw[8:12]), nil
}
+155
View File
@@ -0,0 +1,155 @@
package icc
import (
"encoding/binary"
"errors"
"fmt"
"math"
)
// TrilinearInterpolate represents a color lookup table tag (TagColorLookupTable)
type TrilinearInterpolate struct {
d *interpolation_data
legacy bool
}
type TetrahedralInterpolate struct {
d *interpolation_data
legacy bool
}
type CLUT interface {
ChannelTransformer
Samples() []unit_float
}
func (c *TrilinearInterpolate) Samples() []unit_float { return c.d.samples }
func (c *TetrahedralInterpolate) Samples() []unit_float { return c.d.samples }
func (c TetrahedralInterpolate) String() string {
return fmt.Sprintf("TetrahedralInterpolate{ inp:%v outp:%v grid:%v values[:9]:%v }", c.d.num_inputs, c.d.num_outputs, c.d.grid_points, c.d.samples[:min(9, len(c.d.samples))])
}
func (c TrilinearInterpolate) String() string {
return fmt.Sprintf("TrilinearInterpolate{ inp:%v outp:%v grid:%v values[:9]:%v }", c.d.num_inputs, c.d.num_outputs, c.d.grid_points, c.d.samples[:min(9, len(c.d.samples))])
}
var _ CLUT = (*TrilinearInterpolate)(nil)
var _ CLUT = (*TetrahedralInterpolate)(nil)
func decode_clut_table8(raw []byte, ans []unit_float) {
for i, x := range raw {
ans[i] = unit_float(x) / math.MaxUint8
}
}
func decode_clut_table16(raw []byte, ans []unit_float) {
raw = raw[:2*len(ans)]
const inv = 1. / math.MaxUint16
for i := range ans {
val := binary.BigEndian.Uint16(raw)
ans[i] = unit_float(val) * inv
raw = raw[2:]
}
}
func decode_clut_table(raw []byte, bytes_per_channel, OutputChannels int, grid_points []int, output_colorspace ColorSpace) (ans []unit_float, consumed int, err error) {
expected_num_of_output_channels := 3
switch output_colorspace {
case ColorSpaceCMYK:
expected_num_of_output_channels = 4
}
if expected_num_of_output_channels != OutputChannels {
return nil, 0, fmt.Errorf("CLUT table number of output channels %d inappropriate for output_colorspace: %s", OutputChannels, output_colorspace)
}
expected_num_of_values := expectedValues(grid_points, OutputChannels)
consumed = bytes_per_channel * expected_num_of_values
if len(raw) < consumed {
return nil, 0, fmt.Errorf("CLUT table too short %d < %d", len(raw), bytes_per_channel*expected_num_of_values)
}
ans = make([]unit_float, expected_num_of_values)
if bytes_per_channel == 1 {
decode_clut_table8(raw[:consumed], ans)
} else {
decode_clut_table16(raw[:consumed], ans)
}
return
}
func make_clut(grid_points []int, num_inputs, num_outputs int, samples []unit_float, legacy, prefer_trilinear bool) CLUT {
if num_inputs >= 3 && !prefer_trilinear {
return &TetrahedralInterpolate{make_interpolation_data(num_inputs, num_outputs, grid_points, samples), legacy}
}
return &TrilinearInterpolate{make_interpolation_data(num_inputs, num_outputs, grid_points, samples), legacy}
}
// section 10.12.3 (CLUT) in ICC.1-2202-05.pdf
func embeddedClutDecoder(raw []byte, InputChannels, OutputChannels int, output_colorspace ColorSpace, prefer_trilinear bool) (any, error) {
if len(raw) < 20 {
return nil, errors.New("clut tag too short")
}
if InputChannels > 4 {
return nil, fmt.Errorf("clut supports at most 4 input channels not: %d", InputChannels)
}
gridPoints := make([]int, InputChannels)
for i, b := range raw[:InputChannels] {
gridPoints[i] = int(b)
}
for i, nPoints := range gridPoints {
if nPoints < 2 {
return nil, fmt.Errorf("CLUT input channel %d has invalid grid points: %d", i, nPoints)
}
}
bytes_per_channel := raw[16]
raw = raw[20:]
values, _, err := decode_clut_table(raw, int(bytes_per_channel), OutputChannels, gridPoints, output_colorspace)
if err != nil {
return nil, err
}
return make_clut(gridPoints, InputChannels, OutputChannels, values, false, prefer_trilinear), nil
}
func expectedValues(gridPoints []int, outputChannels int) int {
expectedPoints := 1
for _, g := range gridPoints {
expectedPoints *= int(g)
}
return expectedPoints * outputChannels
}
func (c *TrilinearInterpolate) IOSig() (int, int) { return c.d.num_inputs, c.d.num_outputs }
func (c *TetrahedralInterpolate) IOSig() (int, int) { return c.d.num_inputs, c.d.num_outputs }
func (c *TrilinearInterpolate) Iter(f func(ChannelTransformer) bool) { f(c) }
func (c *TetrahedralInterpolate) Iter(f func(ChannelTransformer) bool) { f(c) }
func (c *TrilinearInterpolate) Transform(r, g, b unit_float) (unit_float, unit_float, unit_float) {
var obuf [3]unit_float
var ibuf = [3]unit_float{r, g, b}
c.d.trilinear_interpolate(ibuf[:], obuf[:])
return obuf[0], obuf[1], obuf[2]
}
func (m *TrilinearInterpolate) TransformGeneral(o, i []unit_float) {
o = o[0:m.d.num_outputs:m.d.num_outputs]
for i := range o {
o[i] = 0
}
m.d.trilinear_interpolate(i[0:m.d.num_inputs:m.d.num_inputs], o)
}
func (c *TetrahedralInterpolate) Tetrahedral_interpolate(r, g, b unit_float) (unit_float, unit_float, unit_float) {
var obuf [3]unit_float
c.d.tetrahedral_interpolation(r, g, b, obuf[:])
return obuf[0], obuf[1], obuf[2]
}
func (c *TetrahedralInterpolate) Transform(r, g, b unit_float) (unit_float, unit_float, unit_float) {
return c.Tetrahedral_interpolate(r, g, b)
}
func (m *TetrahedralInterpolate) TransformGeneral(o, i []unit_float) {
m.d.tetrahedral_interpolation4(i[0], i[1], i[2], i[3], o[:m.d.num_outputs:m.d.num_outputs])
}
func clamp01(v unit_float) unit_float {
return max(0, min(v, 1))
}
@@ -0,0 +1,668 @@
package icc
import (
"encoding/binary"
"errors"
"fmt"
"math"
"strings"
"sync"
)
type IdentityCurve int
type GammaCurve struct {
gamma, inv_gamma unit_float
is_one bool
}
type PointsCurve struct {
points, reverse_lookup []unit_float
max_idx, reverse_max_idx unit_float
}
type ConditionalZeroCurve struct{ g, a, b, threshold, inv_gamma, inv_a unit_float }
type ConditionalCCurve struct{ g, a, b, c, threshold, inv_gamma, inv_a unit_float }
type SplitCurve struct{ g, a, b, c, d, inv_g, inv_a, inv_c, threshold unit_float }
type ComplexCurve struct{ g, a, b, c, d, e, f, inv_g, inv_a, inv_c, threshold unit_float }
type Curve1D interface {
Transform(x unit_float) unit_float
InverseTransform(x unit_float) unit_float
Prepare() error
String() string
}
var _ Curve1D = (*IdentityCurve)(nil)
var _ Curve1D = (*GammaCurve)(nil)
var _ Curve1D = (*PointsCurve)(nil)
var _ Curve1D = (*ConditionalZeroCurve)(nil)
var _ Curve1D = (*ConditionalCCurve)(nil)
var _ Curve1D = (*SplitCurve)(nil)
var _ Curve1D = (*ComplexCurve)(nil)
type CurveTransformer struct {
curves []Curve1D
name string
}
type InverseCurveTransformer struct {
curves []Curve1D
name string
}
func (c CurveTransformer) IOSig() (int, int) {
return len(c.curves), len(c.curves)
}
func (c CurveTransformer) Transform(r, g, b unit_float) (unit_float, unit_float, unit_float) {
return c.curves[0].Transform(r), c.curves[1].Transform(g), c.curves[2].Transform(b)
}
func (c CurveTransformer) TransformGeneral(o, i []unit_float) {
for n, c := range c.curves {
o[n] = c.Transform(i[n])
}
}
func (c InverseCurveTransformer) IOSig() (int, int) {
return len(c.curves), len(c.curves)
}
func (c InverseCurveTransformer) Transform(r, g, b unit_float) (unit_float, unit_float, unit_float) {
// we need to clamp as per spec section F.3 of ICC.1-2202-05.pdf
return c.curves[0].InverseTransform(clamp01(r)), c.curves[1].InverseTransform(clamp01(g)), c.curves[2].InverseTransform(clamp01(b))
}
func (c InverseCurveTransformer) TransformGeneral(o, i []unit_float) {
for n, c := range c.curves {
o[n] = c.InverseTransform(i[n])
}
}
type CurveTransformer3 struct {
r, g, b Curve1D
name string
}
func (c CurveTransformer3) IOSig() (int, int) { return 3, 3 }
func (c CurveTransformer3) Transform(r, g, b unit_float) (unit_float, unit_float, unit_float) {
return c.r.Transform(r), c.g.Transform(g), c.b.Transform(b)
}
func (m CurveTransformer3) TransformGeneral(o, i []unit_float) { tg33(m.Transform, o, i) }
type InverseCurveTransformer3 struct {
r, g, b Curve1D
name string
}
func (c *CurveTransformer) Iter(f func(ChannelTransformer) bool) { f(c) }
func (c *CurveTransformer3) Iter(f func(ChannelTransformer) bool) { f(c) }
func (c *InverseCurveTransformer) Iter(f func(ChannelTransformer) bool) { f(c) }
func (c *InverseCurveTransformer3) Iter(f func(ChannelTransformer) bool) { f(c) }
func (c *CurveTransformer) Curves() []Curve1D { return c.curves }
func (c *InverseCurveTransformer) Curves() []Curve1D { return c.curves }
func (c *CurveTransformer3) Curves() []Curve1D { return []Curve1D{c.r, c.g, c.b} }
func (c *InverseCurveTransformer3) Curves() []Curve1D { return []Curve1D{c.r, c.g, c.b} }
func curve_string(name string, is_inverse bool, curves ...Curve1D) string {
var b strings.Builder
if is_inverse {
name += "Inverted"
}
b.WriteString(name + "{")
for i, c := range curves {
b.WriteString(fmt.Sprintf("[%d]%s ", i, c.String()))
}
b.WriteString("}")
return b.String()
}
func (c CurveTransformer3) String() string { return curve_string(c.name, false, c.r, c.g, c.b) }
func (c CurveTransformer) String() string { return curve_string(c.name, false, c.curves...) }
func (c InverseCurveTransformer3) String() string { return curve_string(c.name, true, c.r, c.g, c.b) }
func (c InverseCurveTransformer) String() string { return curve_string(c.name, true, c.curves...) }
func (c InverseCurveTransformer3) IOSig() (int, int) { return 3, 3 }
func (c InverseCurveTransformer3) Transform(r, g, b unit_float) (unit_float, unit_float, unit_float) {
// we need to clamp as per spec section F.3 of ICC.1-2202-05.pdf
return c.r.InverseTransform(clamp01(r)), c.g.InverseTransform(clamp01(g)), c.b.InverseTransform(clamp01(b))
}
func (m InverseCurveTransformer3) TransformGeneral(o, i []unit_float) { tg33(m.Transform, o, i) }
type Curves interface {
ChannelTransformer
Curves() []Curve1D
}
func NewCurveTransformer(name string, curves ...Curve1D) Curves {
all_identity := true
for _, c := range curves {
if c == nil {
ident := IdentityCurve(0)
c = &ident
}
if _, is_ident := c.(*IdentityCurve); !is_ident {
all_identity = false
}
}
if all_identity {
return nil
}
switch len(curves) {
case 3:
return &CurveTransformer3{curves[0], curves[1], curves[2], name}
default:
return &CurveTransformer{curves, name}
}
}
func NewInverseCurveTransformer(name string, curves ...Curve1D) Curves {
all_identity := true
for _, c := range curves {
if c == nil {
ident := IdentityCurve(0)
c = &ident
}
if _, is_ident := c.(*IdentityCurve); !is_ident {
all_identity = false
}
}
if all_identity {
return nil
}
switch len(curves) {
case 3:
return &InverseCurveTransformer3{curves[0], curves[1], curves[2], name}
default:
return &InverseCurveTransformer{curves, name}
}
}
type ParametricCurveFunction uint16
const (
SimpleGammaFunction ParametricCurveFunction = 0 // Y = X^g
ConditionalZeroFunction ParametricCurveFunction = 1 // Y = (aX+b)^g for X >= d, else 0
ConditionalCFunction ParametricCurveFunction = 2 // Y = (aX+b)^g for X >= d, else c
SplitFunction ParametricCurveFunction = 3 // Two different functions split at d
ComplexFunction ParametricCurveFunction = 4 // More complex piecewise function
)
func align_to_4(x int) int {
if extra := x % 4; extra > 0 {
x += 4 - extra
}
return x
}
func fixed88ToFloat(raw []byte) unit_float {
return unit_float(uint16(raw[0])<<8|uint16(raw[1])) / 256
}
func samples_to_analytic(points []unit_float) Curve1D {
threshold := 1e-3
switch {
case len(points) < 2:
return nil
case len(points) > 400:
threshold = FLOAT_EQUALITY_THRESHOLD
case len(points) > 100:
threshold = 2 * FLOAT_EQUALITY_THRESHOLD
case len(points) > 40:
threshold = 16 * FLOAT_EQUALITY_THRESHOLD
}
if len(points) < 2 {
return nil
}
n := 1 / unit_float(len(points)-1)
srgb := SRGBCurve().Transform
is_srgb, is_identity := true, true
for i, y := range points {
x := unit_float(i) * n
if is_srgb {
is_srgb = math.Abs(float64(y-srgb(x))) <= threshold
}
if is_identity {
is_identity = math.Abs(float64(y-x)) <= threshold
}
if !is_identity && !is_srgb {
break
}
}
if is_identity {
ans := IdentityCurve(0)
return &ans
}
if is_srgb {
return SRGBCurve()
}
return nil
}
func load_points_curve(fp []unit_float) (Curve1D, error) {
analytic := samples_to_analytic(fp)
if analytic != nil {
return analytic, nil
}
c := &PointsCurve{points: fp}
if err := c.Prepare(); err != nil {
return nil, err
}
return c, nil
}
func embeddedCurveDecoder(raw []byte) (any, int, error) {
if len(raw) < 12 {
return nil, 0, errors.New("curv tag too short")
}
count := int(binary.BigEndian.Uint32(raw[8:12]))
consumed := align_to_4(12 + count*2)
switch count {
case 0:
c := IdentityCurve(0)
return &c, consumed, nil
case 1:
if len(raw) < 14 {
return nil, 0, errors.New("curv tag missing gamma value")
}
g := &GammaCurve{gamma: fixed88ToFloat(raw[12:14])}
if err := g.Prepare(); err != nil {
return nil, 0, err
}
var c Curve1D = g
if g.is_one {
ic := IdentityCurve(0)
c = &ic
}
return c, consumed, nil
default:
points := make([]uint16, count)
_, err := binary.Decode(raw[12:], binary.BigEndian, points)
if err != nil {
return nil, 0, errors.New("curv tag truncated")
}
fp := make([]unit_float, len(points))
for i, p := range points {
fp[i] = unit_float(p) / math.MaxUint16
}
c, err := load_points_curve(fp)
return c, consumed, err
}
}
func curveDecoder(raw []byte) (any, error) {
ans, _, err := embeddedCurveDecoder(raw)
return ans, err
}
func readS15Fixed16BE(raw []byte) unit_float {
msb := int16(raw[0])<<8 | int16(raw[1])
lsb := uint16(raw[2])<<8 | uint16(raw[3])
return unit_float(msb) + unit_float(lsb)/(1<<16)
}
func embeddedParametricCurveDecoder(raw []byte) (ans any, consumed int, err error) {
block_len := len(raw)
if block_len < 16 {
return nil, 0, errors.New("para tag too short")
}
funcType := ParametricCurveFunction(binary.BigEndian.Uint16(raw[8:10]))
const header_len = 12
raw = raw[header_len:]
p := func() unit_float {
ans := readS15Fixed16BE(raw[:4])
raw = raw[4:]
return ans
}
defer func() { consumed = align_to_4(consumed) }()
var c Curve1D
switch funcType {
case SimpleGammaFunction:
if consumed = header_len + 4; block_len < consumed {
return nil, 0, errors.New("para tag too short")
}
g := &GammaCurve{gamma: p()}
if abs(g.gamma-1) < FLOAT_EQUALITY_THRESHOLD {
ic := IdentityCurve(0)
c = &ic
} else {
c = g
}
case ConditionalZeroFunction:
if consumed = header_len + 3*4; block_len < consumed {
return nil, 0, errors.New("para tag too short")
}
c = &ConditionalZeroCurve{g: p(), a: p(), b: p()}
case ConditionalCFunction:
if consumed = header_len + 4*4; block_len < consumed {
return nil, 0, errors.New("para tag too short")
}
c = &ConditionalCCurve{g: p(), a: p(), b: p(), c: p()}
case SplitFunction:
if consumed = header_len + 5*4; block_len < consumed {
return nil, 0, errors.New("para tag too short")
}
c = &SplitCurve{g: p(), a: p(), b: p(), c: p(), d: p()}
case ComplexFunction:
if consumed = header_len + 7*4; block_len < consumed {
return nil, 0, errors.New("para tag too short")
}
c = &ComplexCurve{g: p(), a: p(), b: p(), c: p(), d: p(), e: p(), f: p()}
default:
return nil, 0, fmt.Errorf("unknown parametric function type: %d", funcType)
}
if err = c.Prepare(); err != nil {
return nil, 0, err
}
return c, consumed, nil
}
func parametricCurveDecoder(raw []byte) (any, error) {
ans, _, err := embeddedParametricCurveDecoder(raw)
return ans, err
}
func (c IdentityCurve) Transform(x unit_float) unit_float {
return x
}
func (c IdentityCurve) InverseTransform(x unit_float) unit_float {
return x
}
func (c IdentityCurve) Prepare() error { return nil }
func (c IdentityCurve) String() string { return "IdentityCurve" }
func (c GammaCurve) Transform(x unit_float) unit_float {
if x < 0 {
if c.is_one {
return x
}
return 0
}
return pow(x, c.gamma)
}
func (c GammaCurve) InverseTransform(x unit_float) unit_float {
if x < 0 {
if c.is_one {
return x
}
return 0
}
return pow(x, c.inv_gamma)
}
func (c *GammaCurve) Prepare() error {
if c.gamma == 0 {
return fmt.Errorf("gamma curve has zero gamma value")
}
c.inv_gamma = 1 / c.gamma
c.is_one = abs(c.gamma-1) < FLOAT_EQUALITY_THRESHOLD
return nil
}
func (c GammaCurve) String() string { return fmt.Sprintf("GammaCurve{%f}", c.gamma) }
func calculate_reverse_for_well_behaved_sampled_curve(points []unit_float) []unit_float {
n := len(points) - 1
if n < 1 {
return nil
}
var prev, maxy unit_float
var miny unit_float = math.MaxFloat32
for _, y := range points {
if y < prev || y < 0 || y > 1 {
return nil // not monotonic or range not in [0, 1]
}
prev = y
miny = min(y, miny)
maxy = max(y, maxy)
}
y_to_x := make([]unit_float, n+1)
points_y_idx := 0
n_inv := 1.0 / unit_float(n)
for i := range y_to_x {
if points_y_idx > n {
// we are between maxy and 1
y_to_x[i] = 1
continue
}
if int(points[points_y_idx]*unit_float(n)) == i {
y_to_x[i] = unit_float(points_y_idx) * n_inv
for {
points_y_idx++
if points_y_idx > n || int(points[points_y_idx]*unit_float(n)) != i {
break
}
}
} else {
if points_y_idx == 0 {
// we are between 0 and miny
y_to_x[i] = 0
continue
}
// we are between points_y_idx-1 and points_y_idx
y1, y2 := points[points_y_idx-1], points[points_y_idx]
if y1 == 1 {
y_to_x[i] = 1
continue
}
for y1 == y2 {
points_y_idx++
y2 = 1
if points_y_idx <= n {
y2 = points[points_y_idx]
}
}
y := unit_float(i) * n_inv
frac := (y - y1) / (y2 - y1)
x1 := unit_float(points_y_idx-1) * n_inv
// x = x1 + frac * (x2 - x1)
y_to_x[i] = x1 + frac*n_inv
}
}
return y_to_x
}
func (c *PointsCurve) Prepare() error {
c.max_idx = unit_float(len(c.points) - 1)
reverse_lookup := calculate_reverse_for_well_behaved_sampled_curve(c.points)
if reverse_lookup == nil {
reverse_lookup = make([]unit_float, len(c.points))
for i := range len(reverse_lookup) {
y := unit_float(i) / unit_float(len(reverse_lookup)-1)
idx := get_interval(c.points, y)
if idx < 0 {
reverse_lookup[i] = 0
} else {
y1, y2 := c.points[idx], c.points[idx+1]
if y2 < y1 {
y1, y2 = y2, y1
}
x1, x2 := unit_float(idx)/c.max_idx, unit_float(idx+1)/c.max_idx
frac := (y - y1) / (y2 - y1)
reverse_lookup[i] = x1 + frac*(x2-x1)
}
}
}
c.reverse_lookup = reverse_lookup
c.reverse_max_idx = unit_float(len(reverse_lookup) - 1)
return nil
}
func (c PointsCurve) Transform(v unit_float) unit_float {
return sampled_value(c.points, c.max_idx, v)
}
func (c PointsCurve) InverseTransform(v unit_float) unit_float {
return sampled_value(c.reverse_lookup, c.reverse_max_idx, v)
}
func (c PointsCurve) String() string { return fmt.Sprintf("PointsCurve{%d}", len(c.points)) }
func get_interval(lookup []unit_float, y unit_float) int {
if len(lookup) < 2 {
return -1
}
for i := range len(lookup) - 1 {
y0, y1 := lookup[i], lookup[i+1]
if y1 < y0 {
y0, y1 = y1, y0
}
if y0 <= y && y <= y1 {
return i
}
}
return -1
}
func safe_inverse(x, fallback unit_float) unit_float {
if x == 0 {
return fallback
}
return 1 / x
}
func (c *ConditionalZeroCurve) Prepare() error {
c.inv_a = safe_inverse(c.a, 1)
c.threshold, c.inv_gamma = -c.b*c.inv_a, safe_inverse(c.g, 0)
return nil
}
func (c *ConditionalZeroCurve) String() string {
return fmt.Sprintf("ConditionalZeroCurve{a: %v b: %v g: %v}", c.a, c.b, c.g)
}
func (c *ConditionalZeroCurve) Transform(x unit_float) unit_float {
// Y = (aX+b)^g if X ≥ -b/a else 0
if x >= c.threshold {
if e := c.a*x + c.b; e > 0 {
return pow(e, c.g)
}
}
return 0
}
func (c *ConditionalZeroCurve) InverseTransform(y unit_float) unit_float {
// X = (Y^(1/g) - b) / a if Y >= 0 else X = -b/a
// the below doesnt match the actual spec but matches lcms2 implementation
return max(0, (pow(y, c.inv_gamma)-c.b)*c.inv_a)
}
func (c *ConditionalCCurve) Prepare() error {
c.inv_a = safe_inverse(c.a, 1)
c.threshold, c.inv_gamma = -c.b*c.inv_a, safe_inverse(c.g, 0)
return nil
}
func (c *ConditionalCCurve) String() string {
return fmt.Sprintf("ConditionalCCurve{a: %v b: %v c: %v g: %v}", c.a, c.b, c.c, c.g)
}
func (c *ConditionalCCurve) Transform(x unit_float) unit_float {
// Y = (aX+b)^g + c if X ≥ -b/a else c
if x >= c.threshold {
if e := c.a*x + c.b; e > 0 {
return pow(e, c.g) + c.c
}
return 0
}
return c.c
}
func (c *ConditionalCCurve) InverseTransform(y unit_float) unit_float {
// X = ((Y-c)^(1/g) - b) / a if Y >= c else X = -b/a
if e := y - c.c; e >= 0 {
if e == 0 {
return 0
}
return (pow(e, c.inv_gamma) - c.b) * c.inv_a
}
return c.threshold
}
func (c *SplitCurve) Prepare() error {
c.threshold, c.inv_g, c.inv_a, c.inv_c = pow(c.a*c.d+c.b, c.g), safe_inverse(c.g, 0), safe_inverse(c.a, 1), safe_inverse(c.c, 1)
return nil
}
func eq(a, b unit_float) bool { return abs(a-b) <= FLOAT_EQUALITY_THRESHOLD }
func (c *SplitCurve) IsSRGB() bool {
s := SRGBCurve()
return eq(s.a, c.a) && eq(s.b, c.b) && eq(s.c, c.c) && eq(s.d, c.d)
}
func (c *SplitCurve) String() string {
if c.IsSRGB() {
return "SRGBCurve"
}
return fmt.Sprintf("SplitCurve{a: %v b: %v c: %v d: %v g: %v}", c.a, c.b, c.c, c.d, c.g)
}
func (c *SplitCurve) Transform(x unit_float) unit_float {
// Y = (aX+b)^g if X ≥ d else cX
if x >= c.d {
if e := c.a*x + c.b; e > 0 {
return pow(e, c.g)
}
return 0
}
return c.c * x
}
func (c *SplitCurve) InverseTransform(y unit_float) unit_float {
// X=((Y^1/g-b)/a) | Y >= (ad+b)^g
// X=Y/c | Y< (ad+b)^g
if y < c.threshold {
return y * c.inv_c
}
return (pow(y, c.inv_g) - c.b) * c.inv_a
}
func (c *ComplexCurve) IsSRGB() bool {
s := SRGBCurve()
return eq(s.a, c.a) && eq(s.b, c.b) && eq(s.c, c.c) && eq(s.d, c.d) && eq(c.e, 0) && eq(c.f, 0)
}
type IsSRGB interface {
IsSRGB() bool
}
func (c *ComplexCurve) Prepare() error {
c.threshold, c.inv_g, c.inv_a, c.inv_c = pow(c.a*c.d+c.b, c.g)+c.e, safe_inverse(c.g, 0), safe_inverse(c.a, 1), safe_inverse(c.c, 1)
return nil
}
func (c *ComplexCurve) String() string {
return fmt.Sprintf("ComplexCurve{a: %v b: %v c: %v d: %v e: %v f: %v g: %v}", c.a, c.b, c.c, c.d, c.e, c.f, c.g)
}
func (c *ComplexCurve) Transform(x unit_float) unit_float {
// Y = (aX+b)^g + e if X ≥ d else cX+f
if x >= c.d {
if e := c.a*x + c.b; e > 0 {
return pow(e, c.g) + c.e
}
return c.e
}
return c.c*x + c.f
}
func (c *ComplexCurve) InverseTransform(y unit_float) unit_float {
// X=((Y-e)1/g-b)/a | Y >=(ad+b)^g+e), cd+f
// X=(Y-f)/c | else
if y < c.threshold {
return (y - c.f) * c.inv_c
}
if e := y - c.e; e > 0 {
return (pow(e, c.inv_g) - c.b) * c.inv_a
}
return 0
}
var SRGBCurve = sync.OnceValue(func() *SplitCurve {
ans := &SplitCurve{g: 2.4, a: 1 / 1.055, b: 0.055 / 1.055, c: 1 / 12.92, d: 0.0031308 * 12.92}
ans.Prepare()
return ans
})
var SRGBCurveTransformer = sync.OnceValue(func() Curves {
return NewCurveTransformer("sRGB curve", SRGBCurve(), SRGBCurve(), SRGBCurve())
})
var SRGBCurveInverseTransformer = sync.OnceValue(func() Curves {
return NewInverseCurveTransformer("TRC", SRGBCurve(), SRGBCurve(), SRGBCurve())
})
@@ -0,0 +1,209 @@
package icc
import (
"errors"
"fmt"
"math"
)
type Translation [3]unit_float
type Matrix3 [3][3]unit_float
type IdentityMatrix int
type MatrixWithOffset struct {
m ChannelTransformer
offset1, offset2, offset3 unit_float
}
func (m MatrixWithOffset) String() string {
return fmt.Sprintf("MatrixWithOffset{ %.6v %.6v }", m.m, []unit_float{m.offset1, m.offset2, m.offset3})
}
func is_identity_matrix(m *Matrix3) bool {
if m == nil {
return true
}
for r := range 3 {
for c := range 3 {
q := IfElse(r == c, unit_float(1), unit_float(0))
if math.Abs(float64(m[r][c]-q)) > FLOAT_EQUALITY_THRESHOLD {
return false
}
}
}
return true
}
func (c Translation) String() string { return fmt.Sprintf("Translation{%.6v}", [3]unit_float(c)) }
func (c *Translation) IOSig() (int, int) { return 3, 3 }
func (c *Translation) Empty() bool { return c[0] == 0 && c[1] == 0 && c[2] == 0 }
func (c *Translation) Iter(f func(ChannelTransformer) bool) { f(c) }
func (c *IdentityMatrix) String() string { return "IdentityMatrix" }
func (c *IdentityMatrix) IOSig() (int, int) { return 3, 3 }
func (c *MatrixWithOffset) IOSig() (int, int) { return 3, 3 }
func (c *Matrix3) IOSig() (int, int) { return 3, 3 }
func (c *IdentityMatrix) Iter(f func(ChannelTransformer) bool) { f(c) }
func (c *MatrixWithOffset) Iter(f func(ChannelTransformer) bool) { f(c) }
func (c *Matrix3) Iter(f func(ChannelTransformer) bool) { f(c) }
var _ ChannelTransformer = (*MatrixWithOffset)(nil)
func embeddedMatrixDecoder(body []byte) (any, error) {
result := Matrix3{}
if len(body) < 36 {
return nil, fmt.Errorf("embedded matrix tag too short: %d < 36", len(body))
}
var m ChannelTransformer = &result
for i := range 9 {
result[i/3][i%3] = readS15Fixed16BE(body[:4])
body = body[4:]
}
if is_identity_matrix(&result) {
t := IdentityMatrix(0)
m = &t
}
if len(body) < 3*4 {
return m, nil
}
r2 := &MatrixWithOffset{m: m}
r2.offset1 = readS15Fixed16BE(body[:4])
r2.offset2 = readS15Fixed16BE(body[4:8])
r2.offset3 = readS15Fixed16BE(body[8:12])
if r2.offset1 == 0 && r2.offset2 == 0 && r2.offset3 == 0 {
return m, nil
}
return r2, nil
}
func matrixDecoder(raw []byte) (any, error) {
if len(raw) < 8+36 {
return nil, errors.New("mtx tag too short")
}
return embeddedMatrixDecoder(raw[8:])
}
func (m *Matrix3) Transform(r, g, b unit_float) (unit_float, unit_float, unit_float) {
return m[0][0]*r + m[0][1]*g + m[0][2]*b, m[1][0]*r + m[1][1]*g + m[1][2]*b, m[2][0]*r + m[2][1]*g + m[2][2]*b
}
func (m *Matrix3) TransformGeneral(o, i []unit_float) { tg33(m.Transform, o, i) }
func (m *Matrix3) Transpose() Matrix3 {
return Matrix3{
{m[0][0], m[1][0], m[2][0]},
{m[0][1], m[1][1], m[2][1]},
{m[0][2], m[1][2], m[2][2]},
}
}
func (m *Matrix3) Scale(s unit_float) {
m[0][0] *= s
m[0][1] *= s
m[0][2] *= s
m[1][0] *= s
m[1][1] *= s
m[1][2] *= s
m[2][0] *= s
m[2][1] *= s
m[2][2] *= s
}
func Dot(v1, v2 [3]unit_float) unit_float {
return v1[0]*v2[0] + v1[1]*v2[1] + v1[2]*v2[2]
}
func (m *Matrix3) Equals(o *Matrix3, threshold unit_float) bool {
for r := range 3 {
ar, br := m[r], o[r]
for c := range 3 {
if abs(ar[c]-br[c]) > threshold {
return false
}
}
}
return true
}
func (m *Matrix3) String() string {
return fmt.Sprintf("Matrix3{ %.6v, %.6v, %.6v }", m[0], m[1], m[2])
}
func (m *Matrix3) AsMatrix3() *Matrix3 { return m }
func NewScalingMatrix3(scale unit_float) *Matrix3 {
return &Matrix3{{scale, 0, 0}, {0, scale, 0}, {0, 0, scale}}
}
func (m *IdentityMatrix) AsMatrix3() *Matrix3 { return NewScalingMatrix3(1) }
// Return m * o
func (m *Matrix3) Multiply(o Matrix3) Matrix3 {
t := o.Transpose()
return Matrix3{
{Dot(t[0], m[0]), Dot(t[1], m[0]), Dot(t[2], m[0])},
{Dot(t[0], m[1]), Dot(t[1], m[1]), Dot(t[2], m[1])},
{Dot(t[0], m[2]), Dot(t[1], m[2]), Dot(t[2], m[2])},
}
}
func (m *Matrix3) Inverted() (ans Matrix3, err error) {
o := Matrix3{
{
m[1][1]*m[2][2] - m[2][1]*m[1][2],
-(m[0][1]*m[2][2] - m[2][1]*m[0][2]),
m[0][1]*m[1][2] - m[1][1]*m[0][2],
},
{
-(m[1][0]*m[2][2] - m[2][0]*m[1][2]),
m[0][0]*m[2][2] - m[2][0]*m[0][2],
-(m[0][0]*m[1][2] - m[1][0]*m[0][2]),
},
{
m[1][0]*m[2][1] - m[2][0]*m[1][1],
-(m[0][0]*m[2][1] - m[2][0]*m[0][1]),
m[0][0]*m[1][1] - m[1][0]*m[0][1],
},
}
det := m[0][0]*o[0][0] + m[1][0]*o[0][1] + m[2][0]*o[0][2]
if abs(det) < FLOAT_EQUALITY_THRESHOLD {
return ans, fmt.Errorf("matrix is singular and cannot be inverted, det=%v", det)
}
det = 1 / det
o[0][0] *= det
o[0][1] *= det
o[0][2] *= det
o[1][0] *= det
o[1][1] *= det
o[1][2] *= det
o[2][0] *= det
o[2][1] *= det
o[2][2] *= det
return o, nil
}
func (m *Translation) Transform(r, g, b unit_float) (unit_float, unit_float, unit_float) {
return r + m[0], g + m[1], b + m[2]
}
func (m *Translation) TransformGeneral(o, i []unit_float) { tg33(m.Transform, o, i) }
func (m IdentityMatrix) Transform(r, g, b unit_float) (unit_float, unit_float, unit_float) {
return r, g, b
}
func (m IdentityMatrix) TransformGeneral(o, i []unit_float) { tg33(m.Transform, o, i) }
func (m *MatrixWithOffset) Translation() *Translation {
if m.offset1 == 0 && m.offset2 == 0 && m.offset3 == 0 {
return nil
}
return &Translation{m.offset1, m.offset2, m.offset3}
}
func (m *MatrixWithOffset) Transform(r, g, b unit_float) (unit_float, unit_float, unit_float) {
r, g, b = m.m.Transform(r, g, b)
r += m.offset1
g += m.offset2
b += m.offset3
return r, g, b
}
func (m *MatrixWithOffset) TransformGeneral(o, i []unit_float) { tg33(m.Transform, o, i) }
+171
View File
@@ -0,0 +1,171 @@
package icc
import (
"encoding/binary"
"errors"
"fmt"
)
var _ = fmt.Print
type MFT struct {
in_channels, out_channels int
grid_points []int
input_curve, output_curve Curves
clut CLUT
matrix ChannelTransformer
is8bit bool
}
func (c MFT) String() string {
return fmt.Sprintf("MFT{grid_points:%v, matrix:%v input:%v, clut:%v, output:%v }", c.grid_points, c.matrix, c.input_curve, c.clut, c.output_curve)
}
func (c *MFT) IOSig() (int, int) { return c.in_channels, c.out_channels }
func (c *MFT) Iter(f func(ChannelTransformer) bool) {
if mo, ok := c.matrix.(*MatrixWithOffset); ok {
if _, ok := mo.m.(*IdentityMatrix); !ok {
if !f(mo.m) {
return
}
}
if tt := mo.Translation(); tt != nil {
if !f(tt) {
return
}
}
} else if !f(c.matrix) {
return
}
if !f(c.input_curve) {
return
}
if !f(c.clut) {
return
}
if !f(c.output_curve) {
return
}
}
var _ ChannelTransformer = (*MFT)(nil)
func (mft *MFT) Transform(r, g, b unit_float) (unit_float, unit_float, unit_float) {
// Apply matrix
r, g, b = mft.matrix.Transform(r, g, b)
// Apply input curves with linear interpolation
r, g, b = mft.input_curve.Transform(r, g, b)
// Apply CLUT
r, g, b = mft.clut.Transform(r, g, b)
// Apply output curves with interpolation
r, g, b = mft.output_curve.Transform(r, g, b)
return r, g, b
}
func (mft *MFT) TransformGeneral(o, i []unit_float) {
mft.matrix.TransformGeneral(o, i)
// Apply input curves with linear interpolation
mft.input_curve.TransformGeneral(o, i)
// Apply CLUT
mft.clut.TransformGeneral(o, i)
// Apply output curves with interpolation
mft.output_curve.TransformGeneral(o, i)
}
func load_8bit_table(raw []byte, n int) (output []unit_float, leftover []byte, err error) {
if len(raw) < n {
return nil, raw, fmt.Errorf("mft2 tag too short")
}
output = make([]unit_float, n)
for i := range n {
output[i] = unit_float(raw[0]) / 255
raw = raw[1:]
}
return output, raw, nil
}
func load_16bit_table(raw []byte, n int) (output []unit_float, leftover []byte, err error) {
if len(raw) < 2*n {
return nil, raw, fmt.Errorf("mft2 tag too short")
}
output = make([]unit_float, n)
for i := range n {
output[i] = unit_float(binary.BigEndian.Uint16(raw[:2])) / 65535
raw = raw[2:]
}
return output, raw, nil
}
func load_mft_header(raw []byte) (ans *MFT, leftover []byte, err error) {
if len(raw) < 48 {
return nil, raw, errors.New("mft tag too short")
}
a := MFT{}
var grid_points int
a.in_channels, a.out_channels, grid_points = int(raw[8]), int(raw[9]), int(raw[10])
if grid_points < 2 {
return nil, raw, fmt.Errorf("mft tag has invalid number of CLUT grid points: %d", a.grid_points)
}
a.grid_points = make([]int, a.in_channels)
for i := range a.in_channels {
a.grid_points[i] = grid_points
}
ma, err := embeddedMatrixDecoder(raw[12:48])
if err != nil {
return nil, nil, err
}
a.matrix = ma.(ChannelTransformer)
return &a, raw[48:], nil
}
func load_mft_body(a *MFT, raw []byte, load_table func([]byte, int) ([]unit_float, []byte, error), input_table_entries, output_table_entries int, input_colorspace, output_colorspace ColorSpace, bytes_per_channel int) (err error) {
input_curves := make([]Curve1D, a.in_channels)
output_curves := make([]Curve1D, a.out_channels)
var fp []unit_float
for i := range a.in_channels {
if fp, raw, err = load_table(raw, input_table_entries); err != nil {
return err
}
if input_curves[i], err = load_points_curve(fp); err != nil {
return err
}
}
a.input_curve = NewCurveTransformer("Input", input_curves...)
fp, consumed, err := decode_clut_table(raw, int(bytes_per_channel), a.out_channels, a.grid_points, output_colorspace)
if err != nil {
return err
}
raw = raw[consumed:]
a.clut = make_clut(a.grid_points, a.in_channels, a.out_channels, fp, true, false)
for i := range a.out_channels {
if fp, raw, err = load_table(raw, output_table_entries); err != nil {
return err
}
if output_curves[i], err = load_points_curve(fp); err != nil {
return err
}
}
a.output_curve = NewCurveTransformer("Output", output_curves...)
return nil
}
func decode_mft8(raw []byte, input_colorspace, output_colorspace ColorSpace) (ans any, err error) {
var a *MFT
if a, raw, err = load_mft_header(raw); err != nil {
return nil, err
}
a.is8bit = true
err = load_mft_body(a, raw, load_8bit_table, 256, 256, input_colorspace, output_colorspace, 1)
return a, err
}
func decode_mft16(raw []byte, input_colorspace, output_colorspace ColorSpace) (ans any, err error) {
var a *MFT
if a, raw, err = load_mft_header(raw); err != nil {
return nil, err
}
input_table_entries, output_table_entries := binary.BigEndian.Uint16(raw[:2]), binary.BigEndian.Uint16(raw[2:4])
err = load_mft_body(a, raw[4:], load_16bit_table, int(input_table_entries), int(output_table_entries), input_colorspace, output_colorspace, 2)
return a, err
}
@@ -0,0 +1,180 @@
package icc
import (
"encoding/binary"
"errors"
"fmt"
"slices"
)
// ModularTag represents a modular tag section 10.12 and 10.13 of ICC.1-2202-05.pdf
type ModularTag struct {
num_input_channels, num_output_channels int
a_curves, m_curves, b_curves []Curve1D
clut, matrix ChannelTransformer
transform_objects []ChannelTransformer
is_a_to_b bool
}
func (m ModularTag) String() string {
return fmt.Sprintf("%s{ %s }", IfElse(m.is_a_to_b, "mAB", "mBA"), transformers_as_string(m.transform_objects...))
}
var _ ChannelTransformer = (*ModularTag)(nil)
func (m *ModularTag) Iter(f func(ChannelTransformer) bool) {
for _, c := range m.transform_objects {
if !f(c) {
break
}
}
}
func (m *ModularTag) IOSig() (i int, o int) {
i, _ = m.transform_objects[0].IOSig()
_, o = m.transform_objects[len(m.transform_objects)-1].IOSig()
return
}
func (m *ModularTag) IsSuitableFor(num_input_channels, num_output_channels int) bool {
return m.num_input_channels == num_input_channels && m.num_output_channels == num_output_channels
}
func (m *ModularTag) Transform(r, g, b unit_float) (unit_float, unit_float, unit_float) {
for _, t := range m.transform_objects {
r, g, b = t.Transform(r, g, b)
}
return r, g, b
}
func (m *ModularTag) TransformGeneral(o, i []unit_float) {
for _, t := range m.transform_objects {
t.TransformGeneral(o, i)
}
}
func IfElse[T any](condition bool, if_val T, else_val T) T {
if condition {
return if_val
}
return else_val
}
func modularDecoder(raw []byte, _, output_colorspace ColorSpace) (ans any, err error) {
if len(raw) < 40 {
return nil, errors.New("modular (mAB/mBA) tag too short")
}
var s Signature
_, _ = binary.Decode(raw[:4], binary.BigEndian, &s)
is_a_to_b := false
switch s {
case LutAtoBTypeSignature:
is_a_to_b = true
case LutBtoATypeSignature:
is_a_to_b = false
default:
return nil, fmt.Errorf("modular tag has unknown signature: %s", s)
}
inputCh, outputCh := int(raw[8]), int(raw[9])
var offsets [5]uint32
if _, err := binary.Decode(raw[12:], binary.BigEndian, offsets[:]); err != nil {
return nil, err
}
b, matrix, m, clut, a := offsets[0], offsets[1], offsets[2], offsets[3], offsets[4]
mt := &ModularTag{num_input_channels: inputCh, num_output_channels: outputCh, is_a_to_b: is_a_to_b}
read_curves := func(offset uint32, num_curves_reqd int) (ans []Curve1D, err error) {
if offset == 0 {
return nil, nil
}
if int(offset)+8 > len(raw) {
return nil, errors.New("modular (mAB/mBA) tag too short")
}
block := raw[offset:]
var c any
var consumed int
for range inputCh {
if len(block) < 4 {
return nil, errors.New("modular (mAB/mBA) tag too short")
}
sig := Signature(binary.BigEndian.Uint32(block[:4]))
switch sig {
case CurveTypeSignature:
c, consumed, err = embeddedCurveDecoder(block)
case ParametricCurveTypeSignature:
c, consumed, err = embeddedParametricCurveDecoder(block)
default:
return nil, fmt.Errorf("unknown curve type: %s in modularDecoder", sig)
}
if err != nil {
return nil, err
}
block = block[consumed:]
ans = append(ans, c.(Curve1D))
}
if len(ans) != num_curves_reqd {
return nil, fmt.Errorf("number of curves in modular tag: %d does not match the number of channels: %d", len(ans), num_curves_reqd)
}
return
}
if mt.b_curves, err = read_curves(b, IfElse(is_a_to_b, outputCh, inputCh)); err != nil {
return nil, err
}
if mt.a_curves, err = read_curves(a, IfElse(is_a_to_b, inputCh, outputCh)); err != nil {
return nil, err
}
if mt.m_curves, err = read_curves(m, outputCh); err != nil {
return nil, err
}
var temp any
if clut > 0 {
if temp, err = embeddedClutDecoder(raw[clut:], inputCh, outputCh, output_colorspace, false); err != nil {
return nil, err
}
mt.clut = temp.(ChannelTransformer)
}
if matrix > 0 {
if temp, err = embeddedMatrixDecoder(raw[matrix:]); err != nil {
return nil, err
}
if _, is_identity_matrix := temp.(*IdentityMatrix); !is_identity_matrix {
mt.matrix = temp.(ChannelTransformer)
}
}
ans = mt
add_curves := func(name string, c []Curve1D) {
if len(c) > 0 {
has_non_identity := false
for _, x := range c {
if _, ok := x.(*IdentityCurve); !ok {
has_non_identity = true
break
}
}
if has_non_identity {
nc := NewCurveTransformer(name, c...)
mt.transform_objects = append(mt.transform_objects, nc)
}
}
}
add_curves("A", mt.a_curves)
if mt.clut != nil {
mt.transform_objects = append(mt.transform_objects, mt.clut)
}
add_curves("M", mt.m_curves)
if mt.matrix != nil {
if mo, ok := mt.matrix.(*MatrixWithOffset); ok {
if _, ok := mo.m.(*IdentityMatrix); !ok {
mt.transform_objects = append(mt.transform_objects, mo.m)
}
if tt := mo.Translation(); tt != nil {
mt.transform_objects = append(mt.transform_objects, tt)
}
} else {
mt.transform_objects = append(mt.transform_objects, mt.matrix)
}
}
add_curves("B", mt.b_curves)
if !is_a_to_b {
slices.Reverse(mt.transform_objects)
}
return
}
+297
View File
@@ -0,0 +1,297 @@
package icc
import (
"errors"
"fmt"
"sync"
)
type not_found struct {
sig Signature
}
func (e *not_found) Error() string {
return fmt.Sprintf("no tag for signature: %s found in this ICC profile", e.sig)
}
type unsupported struct {
sig Signature
}
func (e *unsupported) Error() string {
return fmt.Sprintf("the tag: %s (0x%x) is not supported", e.sig, uint32(e.sig))
}
type XYZType struct{ X, Y, Z unit_float }
func xyz_type(data []byte) XYZType {
return XYZType{readS15Fixed16BE(data[:4]), readS15Fixed16BE(data[4:8]), readS15Fixed16BE(data[8:12])}
}
func f(t unit_float) unit_float {
const Limit = (24.0 / 116.0) * (24.0 / 116.0) * (24.0 / 116.0)
if t <= Limit {
return (841.0/108.0)*t + (16.0 / 116.0)
}
return pow(t, 1.0/3.0)
}
func f_1(t unit_float) unit_float {
const Limit = (24.0 / 116.0)
if t <= Limit {
return (108.0 / 841.0) * (t - (16.0 / 116.0))
}
return t * t * t
}
func (wt *XYZType) Lab_to_XYZ(l, a, b unit_float) (x, y, z unit_float) {
y = (l + 16.0) / 116.0
x = y + 0.002*a
z = y - 0.005*b
x = f_1(x) * wt.X
y = f_1(y) * wt.Y
z = f_1(z) * wt.Z
return
}
func (wt *XYZType) XYZ_to_Lab(x, y, z unit_float) (l, a, b unit_float) {
fx := f(x / wt.X)
fy := f(y / wt.Y)
fz := f(z / wt.Z)
l = 116.0*fy - 16.0
a = 500.0 * (fx - fy)
b = 200.0 * (fy - fz)
return
}
func decode_xyz(data []byte) (ans any, err error) {
if len(data) < 20 {
return nil, fmt.Errorf("xyz tag too short")
}
a := xyz_type(data[8:])
return &a, nil
}
func decode_array(data []byte) (ans any, err error) {
data = data[8:]
a := make([]unit_float, len(data)/4)
for i := range a {
a[i] = readS15Fixed16BE(data[:4:4])
data = data[4:]
}
return a, nil
}
func parse_tag(sig Signature, data []byte, input_colorspace, output_colorspace ColorSpace) (result any, err error) {
if len(data) == 0 {
return nil, &not_found{sig}
}
if len(data) < 4 {
return nil, &unsupported{sig}
}
s := signature(data)
switch s {
default:
return nil, &unsupported{s}
case DescSignature, DeviceManufacturerDescriptionSignature, DeviceModelDescriptionSignature, MultiLocalisedUnicodeSignature, TextTagSignature:
return parse_text_tag(data)
case SignateTagSignature:
return sigDecoder(data)
case MatrixElemTypeSignature:
return matrixDecoder(data)
case LutAtoBTypeSignature, LutBtoATypeSignature:
return modularDecoder(data, input_colorspace, output_colorspace)
case Lut16TypeSignature:
return decode_mft16(data, input_colorspace, output_colorspace)
case Lut8TypeSignature:
return decode_mft8(data, input_colorspace, output_colorspace)
case XYZTypeSignature:
return decode_xyz(data)
case S15Fixed16ArrayTypeSignature:
return decode_array(data)
case CurveTypeSignature:
return curveDecoder(data)
case ParametricCurveTypeSignature:
return parametricCurveDecoder(data)
}
}
type parsed_tag struct {
tag any
err error
}
type raw_tag_entry struct {
offset int
data []byte
}
type parse_cache_key struct {
offset, size int
}
type TagTable struct {
entries map[Signature]raw_tag_entry
lock sync.Mutex
parsed map[Signature]parsed_tag
parse_cache map[parse_cache_key]parsed_tag
}
func (t *TagTable) Has(sig Signature) bool {
return t.entries[sig].data != nil
}
func (t *TagTable) add(sig Signature, offset int, data []byte) {
t.entries[sig] = raw_tag_entry{offset, data}
}
func (t *TagTable) get_parsed(sig Signature, input_colorspace, output_colorspace ColorSpace) (ans any, err error) {
t.lock.Lock()
defer t.lock.Unlock()
if t.parsed == nil {
t.parsed = make(map[Signature]parsed_tag)
t.parse_cache = make(map[parse_cache_key]parsed_tag)
}
existing, found := t.parsed[sig]
if found {
return existing.tag, existing.err
}
var key parse_cache_key
defer func() {
t.parsed[sig] = parsed_tag{ans, err}
t.parse_cache[key] = parsed_tag{ans, err}
}()
re := t.entries[sig]
if re.data == nil {
return nil, &not_found{sig}
}
key = parse_cache_key{re.offset, len(re.data)}
if cached, ok := t.parse_cache[key]; ok {
return cached.tag, cached.err
}
return parse_tag(sig, re.data, input_colorspace, output_colorspace)
}
func (t *TagTable) getDescription(s Signature) (string, error) {
q, err := t.get_parsed(s, ColorSpaceRGB, ColorSpaceXYZ)
if err != nil {
return "", fmt.Errorf("could not get description for %s with error: %w", s, err)
}
if t, ok := q.(TextTag); ok {
return t.BestGuessValue(), nil
} else {
return "", fmt.Errorf("tag for %s is not a text tag", s)
}
}
func (t *TagTable) getProfileDescription() (string, error) {
return t.getDescription(DescSignature)
}
func (t *TagTable) getDeviceManufacturerDescription() (string, error) {
return t.getDescription(DeviceManufacturerDescriptionSignature)
}
func (t *TagTable) getDeviceModelDescription() (string, error) {
return t.getDescription(DeviceModelDescriptionSignature)
}
func (t *TagTable) load_curve_tag(s Signature) (Curve1D, error) {
r, err := t.get_parsed(s, ColorSpaceRGB, ColorSpaceXYZ)
if err != nil {
return nil, fmt.Errorf("could not load %s tag from profile with error: %w", s, err)
}
if ans, ok := r.(Curve1D); !ok {
return nil, fmt.Errorf("could not load %s tag from profile as it is of unsupported type: %T", s, r)
} else {
if _, ok := r.(*IdentityCurve); ok {
return nil, nil
}
return ans, nil
}
}
func (t *TagTable) load_rgb_matrix(forward bool) (ans *Matrix3, err error) {
r, err := t.get_parsed(RedMatrixColumnTagSignature, ColorSpaceRGB, ColorSpaceXYZ)
if err != nil {
return nil, err
}
g, err := t.get_parsed(GreenMatrixColumnTagSignature, ColorSpaceRGB, ColorSpaceXYZ)
if err != nil {
return nil, err
}
b, err := t.get_parsed(BlueMatrixColumnTagSignature, ColorSpaceRGB, ColorSpaceXYZ)
if err != nil {
return nil, err
}
rc, bc, gc := r.(*XYZType), g.(*XYZType), b.(*XYZType)
var m Matrix3
m[0][0], m[0][1], m[0][2] = rc.X, bc.X, gc.X
m[1][0], m[1][1], m[1][2] = rc.Y, bc.Y, gc.Y
m[2][0], m[2][1], m[2][2] = rc.Z, bc.Z, gc.Z
// stored in 2.15 format so need to scale, see
// BuildRGBInputMatrixShaper in lcms
m.Scale(MAX_ENCODEABLE_XYZ_INVERSE)
if is_identity_matrix(&m) {
return nil, nil
}
if forward {
return &m, nil
}
inv, err := m.Inverted()
if err != nil {
return nil, fmt.Errorf("the colorspace conversion matrix is not invertible: %w", err)
}
return &inv, nil
}
func array_to_matrix(a []unit_float) *Matrix3 {
_ = a[8]
m := Matrix3{}
copy(m[0][:], a[:3])
copy(m[1][:], a[3:6])
copy(m[2][:], a[6:9])
if is_identity_matrix(&m) {
return nil
}
return &m
}
func (p *TagTable) get_chromatic_adaption() (*Matrix3, error) {
x, err := p.get_parsed(ChromaticAdaptationTagSignature, ColorSpaceRGB, ColorSpaceXYZ)
if err != nil {
var nf *not_found
if errors.As(err, &nf) {
return nil, nil
}
return nil, err
}
a, ok := x.([]unit_float)
if !ok {
return nil, fmt.Errorf("chad tag is not an ArrayType")
}
return array_to_matrix(a), nil
}
func emptyTagTable() TagTable {
return TagTable{
entries: make(map[Signature]raw_tag_entry),
}
}
type Debug_callback = func(r, g, b, x, y, z unit_float, t ChannelTransformer)
type ChannelTransformer interface {
Transform(r, g, b unit_float) (x, y, z unit_float)
TransformGeneral(out, in []unit_float)
IOSig() (num_inputs, num_outputs int)
// Should yield only itself unless it is a container, in which case it should yield its contained transforms
Iter(func(ChannelTransformer) bool)
String() string
}