Initial QSfera import
This commit is contained in:
+104
@@ -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}
|
||||
}
|
||||
+104
@@ -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))
|
||||
}
|
||||
}
|
||||
+36
@@ -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
@@ -0,0 +1,2 @@
|
||||
// Package icc provides support for working with ICC colour profile data.
|
||||
package icc
|
||||
+57
@@ -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())
|
||||
}
|
||||
+193
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
+30
@@ -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
@@ -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
|
||||
}
|
||||
+104
@@ -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,
|
||||
}
|
||||
}
|
||||
+13
@@ -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)
|
||||
}
|
||||
+27
@@ -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
@@ -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) + "'"
|
||||
}
|
||||
+183
@@ -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
@@ -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))
|
||||
}
|
||||
+668
@@ -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())
|
||||
})
|
||||
+209
@@ -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
@@ -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
|
||||
}
|
||||
+180
@@ -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
@@ -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, ¬_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, ¬_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
|
||||
}
|
||||
Generated
Vendored
BIN
Binary file not shown.
Reference in New Issue
Block a user