Initial QSfera import
This commit is contained in:
+50
@@ -0,0 +1,50 @@
|
||||
package autometa
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"github.com/kovidgoyal/imaging/prism/meta"
|
||||
"github.com/kovidgoyal/imaging/prism/meta/gifmeta"
|
||||
"github.com/kovidgoyal/imaging/prism/meta/jpegmeta"
|
||||
"github.com/kovidgoyal/imaging/prism/meta/netpbmmeta"
|
||||
"github.com/kovidgoyal/imaging/prism/meta/pngmeta"
|
||||
"github.com/kovidgoyal/imaging/prism/meta/tiffmeta"
|
||||
"github.com/kovidgoyal/imaging/prism/meta/webpmeta"
|
||||
"github.com/kovidgoyal/imaging/streams"
|
||||
)
|
||||
|
||||
var loaders = []func(io.Reader) (*meta.Data, error){
|
||||
jpegmeta.ExtractMetadata,
|
||||
pngmeta.ExtractMetadata,
|
||||
gifmeta.ExtractMetadata,
|
||||
webpmeta.ExtractMetadata,
|
||||
tiffmeta.ExtractMetadata,
|
||||
netpbmmeta.ExtractMetadata,
|
||||
}
|
||||
|
||||
// Load loads the metadata for an image stream, which may be one of the
|
||||
// supported image formats.
|
||||
//
|
||||
// Only as much of the stream is consumed as necessary to extract the metadata;
|
||||
// the returned stream contains a buffered copy of the consumed data such that
|
||||
// reading from it will produce the same results as fully reading the input
|
||||
// stream. This provides a convenient way to load the full image after loading
|
||||
// the metadata.
|
||||
//
|
||||
// Returns nil if the no image format was recognized. Returns an error if there
|
||||
// was an error decoding metadata.
|
||||
func Load(r io.Reader) (md *meta.Data, imgStream io.Reader, err error) {
|
||||
for _, loader := range loaders {
|
||||
r, err = streams.CallbackWithSeekable(r, func(r io.Reader) (err error) {
|
||||
md, err = loader(r)
|
||||
return
|
||||
})
|
||||
switch {
|
||||
case err != nil:
|
||||
return nil, r, err
|
||||
case md != nil:
|
||||
return md, r, nil
|
||||
}
|
||||
}
|
||||
return nil, r, nil
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
// Package autometa provides support for embedded metadata and automatic
|
||||
// detection of image formats.
|
||||
package autometa
|
||||
Generated
Vendored
BIN
Binary file not shown.
+452
@@ -0,0 +1,452 @@
|
||||
package meta
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
|
||||
"github.com/kovidgoyal/imaging/prism/meta/icc"
|
||||
)
|
||||
|
||||
var _ = fmt.Print
|
||||
|
||||
type CodingIndependentCodePoints struct {
|
||||
ColorPrimaries, TransferCharacteristics, MatrixCoefficients, VideoFullRange uint8
|
||||
IsSet bool
|
||||
}
|
||||
|
||||
var SRGB = CodingIndependentCodePoints{1, 13, 0, 1, true}
|
||||
var DISPLAY_P3 = CodingIndependentCodePoints{12, 13, 0, 1, true}
|
||||
|
||||
func (c CodingIndependentCodePoints) String() string {
|
||||
return fmt.Sprintf("CodingIndependentCodePoints{ColorPrimaries: %d, TransferCharacteristics: %d, MatrixCoefficients: %d, VideoFullRange: %d}", c.ColorPrimaries, c.TransferCharacteristics, c.MatrixCoefficients, c.VideoFullRange)
|
||||
}
|
||||
|
||||
func (c CodingIndependentCodePoints) IsSRGB() bool {
|
||||
return c == SRGB
|
||||
}
|
||||
|
||||
func (c CodingIndependentCodePoints) VideoFullRangeIsValid() bool {
|
||||
return c.VideoFullRange == 0 || c.VideoFullRange == 1
|
||||
}
|
||||
|
||||
// See https://www.w3.org/TR/png-3/#cICP-chunk for why we do this
|
||||
func extend_over_full_range(f func(float64) float64) func(float64) float64 {
|
||||
return func(x float64) float64 {
|
||||
return math.Copysign(1, x) * f(math.Abs(x))
|
||||
}
|
||||
}
|
||||
|
||||
func (src CodingIndependentCodePoints) PipelineTo(dest CodingIndependentCodePoints) *icc.Pipeline {
|
||||
if src == dest {
|
||||
return nil
|
||||
}
|
||||
if src.MatrixCoefficients != 0 || dest.MatrixCoefficients != 0 {
|
||||
return nil // TODO: Add support for these
|
||||
}
|
||||
if !src.VideoFullRangeIsValid() || !dest.VideoFullRangeIsValid() {
|
||||
return nil
|
||||
}
|
||||
p := primaries[int(src.ColorPrimaries)]
|
||||
if p.Name == "" {
|
||||
return nil
|
||||
}
|
||||
tc := transfer_functions[int(src.TransferCharacteristics)]
|
||||
if tc.Name == "" {
|
||||
return nil
|
||||
}
|
||||
to_linear := icc.NewUniformFunctionTransformer(tc.Name, icc.IfElse(src.VideoFullRange == SRGB.VideoFullRange, tc.EOTF, extend_over_full_range(tc.EOTF)))
|
||||
if tc.Name == "Identity" {
|
||||
to_linear = nil
|
||||
}
|
||||
linear_to_xyz := p.CalculateRGBtoXYZMatrix()
|
||||
p = primaries[int(dest.ColorPrimaries)]
|
||||
if p.Name == "" {
|
||||
return nil
|
||||
}
|
||||
tc = transfer_functions[int(dest.TransferCharacteristics)]
|
||||
if tc.Name == "" {
|
||||
return nil
|
||||
}
|
||||
xyz_to_linear := p.CalculateRGBtoXYZMatrix()
|
||||
xyz_to_linear, err := xyz_to_linear.Inverted()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
f := icc.IfElse(dest.VideoFullRange == SRGB.VideoFullRange, tc.OETF, extend_over_full_range(tc.OETF))
|
||||
from_linear := icc.NewUniformFunctionTransformer(tc.Name, func(x float64) float64 {
|
||||
// TODO: Gamut mapping for white point of dest, re-use code from colorconv
|
||||
return max(0, min(f(x), 1))
|
||||
})
|
||||
if tc.Name == "Identity" {
|
||||
from_linear = nil
|
||||
}
|
||||
ans := &icc.Pipeline{}
|
||||
ans.Append(to_linear, &linear_to_xyz, &xyz_to_linear, from_linear)
|
||||
ans.Finalize(true)
|
||||
return ans
|
||||
}
|
||||
|
||||
func (c CodingIndependentCodePoints) PipelineToSRGB() *icc.Pipeline {
|
||||
return c.PipelineTo(SRGB)
|
||||
}
|
||||
|
||||
// XY holds CIE xy chromaticity coordinates.
|
||||
type XY struct {
|
||||
X, Y float64
|
||||
}
|
||||
|
||||
// ColorSpace defines the primaries and white point of a color space.
|
||||
type Primaries struct {
|
||||
Name string
|
||||
Red XY
|
||||
Green XY
|
||||
Blue XY
|
||||
White XY
|
||||
}
|
||||
|
||||
// xyToXYZ converts xy chromaticity to XYZ coordinates, assuming Y=1.
|
||||
func xyToXYZ(p XY) [3]float64 {
|
||||
if p.Y == 0 {
|
||||
return [3]float64{0, 0, 0}
|
||||
}
|
||||
return [3]float64{
|
||||
p.X / p.Y,
|
||||
1.0,
|
||||
(1.0 - p.X - p.Y) / p.Y,
|
||||
}
|
||||
}
|
||||
|
||||
// CalculateRGBtoXYZMatrix computes the matrix to convert from a linear RGB color space to CIE XYZ.
|
||||
func (cs *Primaries) CalculateRGBtoXYZMatrix() icc.Matrix3 {
|
||||
// Convert primaries to XYZ space (normalized to Y=1)
|
||||
r := xyToXYZ(cs.Red)
|
||||
g := xyToXYZ(cs.Green)
|
||||
b := xyToXYZ(cs.Blue)
|
||||
|
||||
// Form the matrix of primaries
|
||||
M := icc.Matrix3{
|
||||
{r[0], g[0], b[0]},
|
||||
{r[1], g[1], b[1]},
|
||||
{r[2], g[2], b[2]},
|
||||
}
|
||||
|
||||
// Calculate the scaling factors (S_r, S_g, S_b)
|
||||
invM, err := M.Inverted()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
whiteXYZ := xyToXYZ(cs.White)
|
||||
|
||||
s_r := invM[0][0]*whiteXYZ[0] + invM[0][1]*whiteXYZ[1] + invM[0][2]*whiteXYZ[2]
|
||||
s_g := invM[1][0]*whiteXYZ[0] + invM[1][1]*whiteXYZ[1] + invM[1][2]*whiteXYZ[2]
|
||||
s_b := invM[2][0]*whiteXYZ[0] + invM[2][1]*whiteXYZ[1] + invM[2][2]*whiteXYZ[2]
|
||||
|
||||
// Scale the primaries matrix to get the final conversion matrix
|
||||
finalMatrix := icc.Matrix3{
|
||||
{M[0][0] * s_r, M[0][1] * s_g, M[0][2] * s_b},
|
||||
{M[1][0] * s_r, M[1][1] * s_g, M[1][2] * s_b},
|
||||
{M[2][0] * s_r, M[2][1] * s_g, M[2][2] * s_b},
|
||||
}
|
||||
return finalMatrix
|
||||
}
|
||||
|
||||
type WellKnownPrimaries int
|
||||
|
||||
// These come from ITU-T H.273 spec
|
||||
var primaries = map[int]Primaries{
|
||||
1: {
|
||||
Name: "sRGB",
|
||||
Green: XY{X: 0.30, Y: 0.60},
|
||||
Blue: XY{X: 0.15, Y: 0.06},
|
||||
Red: XY{X: 0.64, Y: 0.33},
|
||||
White: XY{X: 0.3127, Y: 0.3290}, // D65
|
||||
},
|
||||
4: {
|
||||
Name: "BT-470M",
|
||||
Green: XY{X: 0.21, Y: 0.71},
|
||||
Blue: XY{X: 0.14, Y: 0.08},
|
||||
Red: XY{X: 0.67, Y: 0.33},
|
||||
White: XY{X: 0.310, Y: 0.316},
|
||||
},
|
||||
5: {
|
||||
Name: "BT-470B",
|
||||
Green: XY{X: 0.29, Y: 0.69},
|
||||
Blue: XY{X: 0.15, Y: 0.06},
|
||||
Red: XY{X: 0.64, Y: 0.33},
|
||||
White: XY{X: 0.310, Y: 0.316},
|
||||
},
|
||||
6: {
|
||||
Name: "BT-601",
|
||||
Green: XY{0.310, 0.595},
|
||||
Blue: XY{0.155, 0.070},
|
||||
Red: XY{0.630, 0.340},
|
||||
White: XY{0.3127, 0.3290},
|
||||
},
|
||||
7: {
|
||||
Name: "BT-601",
|
||||
Green: XY{0.310, 0.595},
|
||||
Blue: XY{0.155, 0.070},
|
||||
Red: XY{0.630, 0.340},
|
||||
White: XY{0.3127, 0.3290},
|
||||
},
|
||||
8: {
|
||||
Name: "Generic film",
|
||||
Green: XY{0.243, 0.692},
|
||||
Blue: XY{0.145, 0.049},
|
||||
Red: XY{0.681, 0.319},
|
||||
White: XY{0.310, 0.316},
|
||||
},
|
||||
9: {
|
||||
Name: "BT-2020",
|
||||
Green: XY{0.170, 0.797},
|
||||
Blue: XY{0.131, 0.046},
|
||||
Red: XY{0.708, 0.292},
|
||||
White: XY{0.3127, 0.3290},
|
||||
},
|
||||
10: { // 10
|
||||
Name: "SMPTE ST 428-1",
|
||||
Green: XY{0.0, 1.0},
|
||||
Blue: XY{0.0, 0.0},
|
||||
Red: XY{1.0, 0.0},
|
||||
White: XY{1 / 3., 1 / 3.},
|
||||
},
|
||||
11: { // 11
|
||||
Name: "DCI-P3",
|
||||
Green: XY{0.265, 0.690},
|
||||
Blue: XY{0.150, 0.060},
|
||||
Red: XY{0.680, 0.320},
|
||||
White: XY{0.314, 0.351}, // DCI White
|
||||
},
|
||||
12: { // 12
|
||||
Name: "Diplay P3",
|
||||
Green: XY{0.265, 0.690},
|
||||
Blue: XY{0.150, 0.060},
|
||||
Red: XY{0.680, 0.320},
|
||||
White: XY{0.3127, 0.3290}, // D65
|
||||
},
|
||||
22: { // 22
|
||||
Name: "Unnamed",
|
||||
Green: XY{0.295, 0.605},
|
||||
Blue: XY{0.155, 0.077},
|
||||
Red: XY{0.630, 0.340},
|
||||
White: XY{0.3127, 0.3290}, // D65
|
||||
},
|
||||
}
|
||||
|
||||
// TransferFunction defines an Opto-Electronic Transfer Function (OETF)
|
||||
// and its inverse Electro-Optical Transfer Function (EOTF).
|
||||
type TransferFunction struct {
|
||||
ID int
|
||||
Name string
|
||||
OETF func(float64) float64 // To non-linear
|
||||
EOTF func(float64) float64 // To linear
|
||||
}
|
||||
|
||||
// Constants from various specifications used in the transfer functions.
|
||||
const (
|
||||
// BT.709, BT.2020, BT.601
|
||||
alpha709 = 1.099
|
||||
beta709 = 0.018
|
||||
gamma709 = 0.45
|
||||
delta709 = 4.5
|
||||
|
||||
// SMPTE ST 240M
|
||||
alpha240M = 1.1115
|
||||
beta240M = 0.0228
|
||||
gamma240M = 0.45
|
||||
delta240M = 4.0
|
||||
|
||||
// SMPTE ST 428-1
|
||||
gamma428 = 1.0 / 2.6
|
||||
|
||||
// PQ (Perceptual Quantizer) - SMPTE ST 2084
|
||||
m1PQ = 2610.0 / 16384.0 // (2610 / 4096) * (1/4)
|
||||
m2PQ = 2523.0 / 32.0 // (2523 / 4096) * 128
|
||||
c1PQ = 3424.0 / 4096.0
|
||||
c2PQ = 2413.0 / 4096.0 * 32.0
|
||||
c3PQ = 2392.0 / 4096.0 * 32.0
|
||||
|
||||
// HLG (Hybrid Log-Gamma) - ARIB STD-B67
|
||||
aHLG = 0.17883277
|
||||
bHLG = 1.0 - 4.0*aHLG // 0.28466892
|
||||
cHLG = 0.55991073 // 0.5 - aHLG*math.Log(4.0*aHLG)
|
||||
)
|
||||
|
||||
// holds all the H.273 transfer characteristics.
|
||||
var transfer_functions = make(map[int]TransferFunction)
|
||||
|
||||
func init() {
|
||||
tf1 := TransferFunction{
|
||||
ID: 1, Name: "BT.709",
|
||||
OETF: func(L float64) float64 {
|
||||
if L < beta709 {
|
||||
return delta709 * L
|
||||
}
|
||||
return alpha709*math.Pow(L, gamma709) - (alpha709 - 1)
|
||||
},
|
||||
EOTF: func(V float64) float64 {
|
||||
if V < delta709*beta709 {
|
||||
return V / delta709
|
||||
}
|
||||
return math.Pow((V+(alpha709-1))/alpha709, 1.0/gamma709)
|
||||
},
|
||||
}
|
||||
transfer_functions[1] = tf1
|
||||
transfer_functions[6] = tf1 // BT.601, BT.2020 share this with BT.709
|
||||
transfer_functions[14] = tf1 // BT.2020 10-bit
|
||||
transfer_functions[15] = tf1 // BT.2020 12-bit
|
||||
|
||||
// 2: Identity
|
||||
transfer_functions[2] = TransferFunction{
|
||||
ID: 2, Name: "Identity",
|
||||
OETF: func(v float64) float64 { return v },
|
||||
EOTF: func(v float64) float64 { return v },
|
||||
}
|
||||
transfer_functions[8] = transfer_functions[2]
|
||||
|
||||
// 4: Gamma 2.2
|
||||
tf4 := TransferFunction{
|
||||
ID: 4, Name: "Gamma 2.2",
|
||||
OETF: func(L float64) float64 { return math.Pow(L, 1.0/2.2) },
|
||||
EOTF: func(V float64) float64 { return math.Pow(V, 2.2) },
|
||||
}
|
||||
transfer_functions[4] = tf4
|
||||
transfer_functions[5] = tf4 // BT.470BG also uses Gamma 2.2 approx.
|
||||
|
||||
// 5: Gamma 2.8
|
||||
transfer_functions[5] = TransferFunction{
|
||||
ID: 5, Name: "Gamma 2.8",
|
||||
OETF: func(L float64) float64 { return math.Pow(L, 1.0/2.8) },
|
||||
EOTF: func(V float64) float64 { return math.Pow(V, 2.8) },
|
||||
}
|
||||
|
||||
// 7: SMPTE 240M
|
||||
tf7 := TransferFunction{
|
||||
ID: 7, Name: "SMPTE 240M",
|
||||
OETF: func(L float64) float64 {
|
||||
if L < beta240M {
|
||||
return delta240M * L
|
||||
}
|
||||
return alpha240M*math.Pow(L, gamma240M) - (alpha240M - 1)
|
||||
},
|
||||
EOTF: func(V float64) float64 {
|
||||
if V < delta240M*beta240M {
|
||||
return V / delta240M
|
||||
}
|
||||
return math.Pow((V+(alpha240M-1))/alpha240M, 1.0/gamma240M)
|
||||
},
|
||||
}
|
||||
transfer_functions[7] = tf7
|
||||
// 9: Logarithmic (100:1)
|
||||
transfer_functions[9] = TransferFunction{
|
||||
ID: 9, Name: "Logarithmic (100:1)",
|
||||
OETF: func(L float64) float64 {
|
||||
return 1.0 - math.Log10(1.0-L*(1.0-math.Pow(10.0, -2.0)))/2.0
|
||||
},
|
||||
EOTF: func(V float64) float64 {
|
||||
return (1.0 - math.Pow(10.0, -2.0*V)) / (1.0 - math.Pow(10.0, -2.0))
|
||||
},
|
||||
}
|
||||
|
||||
// 10: Logarithmic (100 * sqrt(10):1)
|
||||
transfer_functions[10] = TransferFunction{
|
||||
ID: 10, Name: "Logarithmic (100*sqrt(10):1)",
|
||||
OETF: func(L float64) float64 {
|
||||
return 1.0 - math.Log10(1.0-L*(1.0-math.Pow(10.0, -2.5)))/2.5
|
||||
},
|
||||
EOTF: func(V float64) float64 {
|
||||
return (1.0 - math.Pow(10.0, -2.5*V)) / (1.0 - math.Pow(10.0, -2.5))
|
||||
},
|
||||
}
|
||||
|
||||
// 11: IEC 61966-2-4
|
||||
transfer_functions[11] = TransferFunction{
|
||||
ID: 11, Name: "IEC 61966-2-4",
|
||||
OETF: func(L float64) float64 {
|
||||
if L < -beta709 {
|
||||
return -delta709 * -L
|
||||
}
|
||||
if L > beta709 {
|
||||
return alpha709*math.Pow(L, gamma709) - (alpha709 - 1)
|
||||
}
|
||||
return delta709 * L
|
||||
},
|
||||
EOTF: func(V float64) float64 {
|
||||
if V < -delta709*beta709 {
|
||||
return -math.Pow((-V+(alpha709-1))/alpha709, 1.0/gamma709)
|
||||
}
|
||||
if V > delta709*beta709 {
|
||||
return math.Pow((V+(alpha709-1))/alpha709, 1.0/gamma709)
|
||||
}
|
||||
return V / delta709
|
||||
},
|
||||
}
|
||||
|
||||
// 12: BT.1361 extended gamut
|
||||
tf12 := tf1 // It's based on BT.709
|
||||
tf12.ID = 12
|
||||
tf12.Name = "BT.1361"
|
||||
transfer_functions[12] = tf12
|
||||
|
||||
// 13: sRGB/IEC 61966-2-1
|
||||
transfer_functions[13] = TransferFunction{
|
||||
ID: 13, Name: "sRGB",
|
||||
OETF: func(L float64) float64 {
|
||||
if L <= 0.0031308 {
|
||||
return 12.92 * L
|
||||
}
|
||||
return 1.055*math.Pow(L, 1.0/2.4) - 0.055
|
||||
},
|
||||
EOTF: func(V float64) float64 {
|
||||
if V <= 0.04045 {
|
||||
return V / 12.92
|
||||
}
|
||||
return math.Pow((V+0.055)/1.055, 2.4)
|
||||
},
|
||||
}
|
||||
// 16: SMPTE ST 2084 (PQ)
|
||||
transfer_functions[16] = TransferFunction{
|
||||
ID: 16, Name: "SMPTE ST 2084 (PQ)",
|
||||
OETF: func(L float64) float64 { // EOTF^-1, L is normalized to 10000 cd/m^2
|
||||
Lp := math.Pow(L, m1PQ)
|
||||
return math.Pow((c1PQ+c2PQ*Lp)/(1.0+c3PQ*Lp), m2PQ)
|
||||
},
|
||||
EOTF: func(V float64) float64 { // V is non-linear signal
|
||||
Vp := math.Pow(V, 1.0/m2PQ)
|
||||
num := math.Max(Vp-c1PQ, 0.0)
|
||||
den := math.Max(c2PQ-c3PQ*Vp, 1e-6) // Avoid division by zero
|
||||
return math.Pow(num/den, 1.0/m1PQ)
|
||||
},
|
||||
}
|
||||
|
||||
// 17: SMPTE ST 428-1
|
||||
transfer_functions[17] = TransferFunction{
|
||||
ID: 17, Name: "SMPTE ST 428-1",
|
||||
OETF: func(L float64) float64 { // OOTF^-1, from linear scene light to D-cinema
|
||||
// Input L is assumed to be scene linear (48 cd/m^2 peak)
|
||||
// The spec normalizes by 52.37
|
||||
return math.Pow((L*48.0)/52.37, gamma428)
|
||||
},
|
||||
EOTF: func(V float64) float64 { // OOTF
|
||||
// Output is linear light, normalized to 1.0 for peak white (48 cd/m^2)
|
||||
return (52.37 / 48.0) * math.Pow(V, 1.0/gamma428)
|
||||
},
|
||||
}
|
||||
|
||||
// 18: ARIB STD-B67 (HLG)
|
||||
transfer_functions[18] = TransferFunction{
|
||||
ID: 18, Name: "ARIB STD-B67 (HLG)",
|
||||
OETF: func(L float64) float64 { // L is scene linear light, display-referred
|
||||
if L <= 1.0/12.0 {
|
||||
return math.Sqrt(3.0 * L)
|
||||
}
|
||||
return aHLG*math.Log(12.0*L-bHLG) + cHLG
|
||||
},
|
||||
EOTF: func(V float64) float64 { // V is the non-linear signal
|
||||
if V <= 0.5 {
|
||||
return (V * V) / 3.0
|
||||
}
|
||||
return (math.Exp((V-cHLG)/aHLG) + bHLG) / 12.0
|
||||
},
|
||||
}
|
||||
}
|
||||
+151
@@ -0,0 +1,151 @@
|
||||
package meta
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"slices"
|
||||
"sync"
|
||||
|
||||
"github.com/kovidgoyal/imaging/prism/meta/icc"
|
||||
"github.com/kovidgoyal/imaging/types"
|
||||
"github.com/rwcarlsen/goexif/exif"
|
||||
)
|
||||
|
||||
var _ = fmt.Println
|
||||
|
||||
// Data represents the metadata for an image.
|
||||
type Data struct {
|
||||
Format types.Format
|
||||
PixelWidth uint32
|
||||
PixelHeight uint32
|
||||
BitsPerComponent uint32
|
||||
HasFrames bool
|
||||
NumFrames, NumPlays int
|
||||
CICP CodingIndependentCodePoints
|
||||
|
||||
mutex sync.Mutex
|
||||
exifData []byte
|
||||
exif *exif.Exif
|
||||
exifErr error
|
||||
iccProfileData []byte
|
||||
iccProfileErr error
|
||||
iccProfile *icc.Profile
|
||||
}
|
||||
|
||||
func (s *Data) Clone() *Data {
|
||||
return &Data{
|
||||
Format: s.Format, PixelWidth: s.PixelWidth, PixelHeight: s.PixelHeight, BitsPerComponent: s.BitsPerComponent,
|
||||
HasFrames: s.HasFrames, NumFrames: s.NumFrames, NumPlays: s.NumPlays, CICP: s.CICP,
|
||||
exifData: slices.Clone(s.exifData), exifErr: s.exifErr, iccProfileData: slices.Clone(s.iccProfileData),
|
||||
iccProfileErr: s.iccProfileErr,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Data) IsSRGB() bool {
|
||||
if s.CICP.IsSet {
|
||||
return s.CICP.IsSRGB()
|
||||
}
|
||||
if p, err := s.ICCProfile(); p != nil && err == nil {
|
||||
return p.IsSRGB()
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Returns an extracted EXIF metadata object from this metadata.
|
||||
//
|
||||
// An error is returned if the EXIF profile could not be correctly parsed.
|
||||
//
|
||||
// If no EXIF data was found, nil is returned without an error.
|
||||
func (md *Data) Exif() (*exif.Exif, error) {
|
||||
md.mutex.Lock()
|
||||
defer md.mutex.Unlock()
|
||||
|
||||
if md.exifErr != nil {
|
||||
return nil, md.exifErr
|
||||
}
|
||||
if md.exif != nil {
|
||||
return md.exif, nil
|
||||
}
|
||||
if len(md.exifData) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
md.exif, md.exifErr = exif.Decode(bytes.NewReader(md.exifData))
|
||||
return md.exif, md.exifErr
|
||||
}
|
||||
|
||||
func (md *Data) SetExifData(data []byte) {
|
||||
md.mutex.Lock()
|
||||
defer md.mutex.Unlock()
|
||||
md.exifData = data
|
||||
md.exifErr = nil
|
||||
md.exif = nil
|
||||
}
|
||||
|
||||
func (md *Data) SetExif(e *exif.Exif) {
|
||||
md.mutex.Lock()
|
||||
defer md.mutex.Unlock()
|
||||
md.exifData = nil
|
||||
md.exifErr = nil
|
||||
md.exif = e
|
||||
}
|
||||
|
||||
func (md *Data) SetExifError(e error) {
|
||||
md.mutex.Lock()
|
||||
defer md.mutex.Unlock()
|
||||
md.exifData = nil
|
||||
md.exifErr = e
|
||||
md.exif = nil
|
||||
}
|
||||
|
||||
func (md *Data) ExifData() []byte {
|
||||
md.mutex.Lock()
|
||||
defer md.mutex.Unlock()
|
||||
return md.exifData
|
||||
}
|
||||
|
||||
// ICCProfile returns an extracted ICC profile from this metadata.
|
||||
//
|
||||
// An error is returned if the ICC profile could not be correctly parsed.
|
||||
//
|
||||
// If no profile data was found, nil is returned without an error.
|
||||
func (md *Data) ICCProfile() (*icc.Profile, error) {
|
||||
md.mutex.Lock()
|
||||
defer md.mutex.Unlock()
|
||||
|
||||
if md.iccProfileErr != nil {
|
||||
return nil, md.iccProfileErr
|
||||
}
|
||||
if len(md.iccProfileData) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
md.iccProfile, md.iccProfileErr = icc.NewProfileReader(bytes.NewReader(md.iccProfileData)).ReadProfile()
|
||||
return md.iccProfile, md.iccProfileErr
|
||||
}
|
||||
|
||||
// ICCProfileData returns the raw ICC profile data from this metadata.
|
||||
//
|
||||
// An error is returned if the ICC profile could not be correctly extracted from
|
||||
// the image.
|
||||
//
|
||||
// If no profile data was found, nil is returned without an error.
|
||||
func (md *Data) ICCProfileData() ([]byte, error) {
|
||||
md.mutex.Lock()
|
||||
defer md.mutex.Unlock()
|
||||
return md.iccProfileData, md.iccProfileErr
|
||||
}
|
||||
|
||||
func (md *Data) SetICCProfileData(data []byte) {
|
||||
md.mutex.Lock()
|
||||
defer md.mutex.Unlock()
|
||||
md.iccProfileData = data
|
||||
md.iccProfileErr = nil
|
||||
md.iccProfile = nil
|
||||
}
|
||||
|
||||
func (md *Data) SetICCProfileError(err error) {
|
||||
md.mutex.Lock()
|
||||
defer md.mutex.Unlock()
|
||||
md.iccProfileData = nil
|
||||
md.iccProfile = nil
|
||||
md.iccProfileErr = err
|
||||
}
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
// Package meta and its subpackages provide support for embedded image metadata.
|
||||
package meta
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
package gifmeta
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image/gif"
|
||||
"io"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/kovidgoyal/imaging/prism/meta"
|
||||
"github.com/kovidgoyal/imaging/types"
|
||||
)
|
||||
|
||||
var _ = fmt.Print
|
||||
|
||||
func ExtractMetadata(r io.Reader) (md *meta.Data, err error) {
|
||||
c, err := gif.DecodeConfig(r)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "gif: can't recognize format") {
|
||||
err = nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
md = &meta.Data{
|
||||
Format: types.GIF, PixelWidth: uint32(c.Width), PixelHeight: uint32(c.Height),
|
||||
BitsPerComponent: 8, HasFrames: true,
|
||||
}
|
||||
return md, nil
|
||||
}
|
||||
|
||||
func CalcMinimumGap(gaps []int) (min_gap int) {
|
||||
// Some broken GIF images have all zero gaps, browsers with their usual
|
||||
// idiot ideas render these with a default 100ms gap https://bugzilla.mozilla.org/show_bug.cgi?id=125137
|
||||
// Browsers actually force a 100ms gap at any zero gap frame, but that
|
||||
// just means it is impossible to deliberately use zero gap frames for
|
||||
// sophisticated blending, so we dont do that.
|
||||
max_gap := 0
|
||||
for _, g := range gaps {
|
||||
max_gap = max(max_gap, g)
|
||||
}
|
||||
if max_gap <= 0 {
|
||||
min_gap = 10
|
||||
}
|
||||
return min_gap
|
||||
}
|
||||
|
||||
func CalculateFrameDelay(delay, min_gap int) time.Duration {
|
||||
delay_ms := max(min_gap, delay)
|
||||
return time.Duration(delay_ms) * 10 * time.Millisecond
|
||||
}
|
||||
+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.
+2
@@ -0,0 +1,2 @@
|
||||
// Package jpegmeta provides support for working with embedded JPEG metadata.
|
||||
package jpegmeta
|
||||
+165
@@ -0,0 +1,165 @@
|
||||
package jpegmeta
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/kovidgoyal/go-parallel"
|
||||
"github.com/kovidgoyal/imaging/prism/meta"
|
||||
"github.com/kovidgoyal/imaging/streams"
|
||||
"github.com/kovidgoyal/imaging/types"
|
||||
)
|
||||
|
||||
const exifSignature = "Exif\x00\x00"
|
||||
|
||||
var iccProfileIdentifier = []byte("ICC_PROFILE\x00")
|
||||
|
||||
// Load loads the metadata for a JPEG image stream.
|
||||
//
|
||||
// Only as much of the stream is consumed as necessary to extract the metadata;
|
||||
// the returned stream contains a buffered copy of the consumed data such that
|
||||
// reading from it will produce the same results as fully reading the input
|
||||
// stream. This provides a convenient way to load the full image after loading
|
||||
// the metadata.
|
||||
func Load(r io.Reader) (md *meta.Data, imgStream io.Reader, err error) {
|
||||
imgStream, err = streams.CallbackWithSeekable(r, func(r io.Reader) (err error) {
|
||||
md, err = ExtractMetadata(r)
|
||||
return
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Same as Load() except that no new stream is provided
|
||||
func ExtractMetadata(r io.Reader) (md *meta.Data, err error) {
|
||||
metadataExtracted := false
|
||||
md = &meta.Data{Format: types.JPEG}
|
||||
segReader := NewSegmentReader(r)
|
||||
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
if !metadataExtracted {
|
||||
md = nil
|
||||
}
|
||||
err = parallel.Format_stacktrace_on_panic(r, 1)
|
||||
}
|
||||
}()
|
||||
|
||||
var iccProfileChunks [][]byte
|
||||
var iccProfileChunksExtracted int
|
||||
var exif []byte
|
||||
|
||||
allMetadataExtracted := func() bool {
|
||||
return metadataExtracted &&
|
||||
iccProfileChunks != nil &&
|
||||
iccProfileChunksExtracted == len(iccProfileChunks) &&
|
||||
exif != nil
|
||||
}
|
||||
|
||||
soiSegment, err := segReader.ReadSegment()
|
||||
if err != nil {
|
||||
if q := err.Error(); strings.Contains(q, "invalid marker identifier") || strings.Contains(q, "unrecognised marker type") {
|
||||
err = nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
if soiSegment.Marker.Type != markerTypeStartOfImage {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
parseSegments:
|
||||
for {
|
||||
segment, err := segReader.ReadSegment()
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
return nil, fmt.Errorf("unexpected EOF")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch segment.Marker.Type {
|
||||
|
||||
case markerTypeStartOfFrameBaseline,
|
||||
markerTypeStartOfFrameProgressive:
|
||||
md.BitsPerComponent = uint32(segment.Data[0])
|
||||
md.PixelHeight = uint32(segment.Data[1])<<8 | uint32(segment.Data[2])
|
||||
md.PixelWidth = uint32(segment.Data[3])<<8 | uint32(segment.Data[4])
|
||||
metadataExtracted = true
|
||||
|
||||
if allMetadataExtracted() {
|
||||
break parseSegments
|
||||
}
|
||||
|
||||
case markerTypeStartOfScan,
|
||||
markerTypeEndOfImage:
|
||||
break parseSegments
|
||||
|
||||
case markerTypeApp1:
|
||||
if bytes.HasPrefix(segment.Data, []byte(exifSignature)) {
|
||||
exif = segment.Data
|
||||
}
|
||||
case markerTypeApp2:
|
||||
if len(segment.Data) < len(iccProfileIdentifier)+2 {
|
||||
continue
|
||||
}
|
||||
|
||||
for i := range iccProfileIdentifier {
|
||||
if segment.Data[i] != iccProfileIdentifier[i] {
|
||||
continue parseSegments
|
||||
}
|
||||
}
|
||||
|
||||
iccData, iccErr := md.ICCProfileData()
|
||||
if iccData != nil || iccErr != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
chunkTotal := segment.Data[len(iccProfileIdentifier)+1]
|
||||
if iccProfileChunks == nil {
|
||||
iccProfileChunks = make([][]byte, chunkTotal)
|
||||
} else if int(chunkTotal) != len(iccProfileChunks) {
|
||||
md.SetICCProfileError(fmt.Errorf("inconsistent ICC profile chunk count"))
|
||||
continue
|
||||
}
|
||||
|
||||
chunkNum := segment.Data[len(iccProfileIdentifier)]
|
||||
if chunkNum == 0 || int(chunkNum) > len(iccProfileChunks) {
|
||||
md.SetICCProfileError(fmt.Errorf("invalid ICC profile chunk number"))
|
||||
continue
|
||||
}
|
||||
if iccProfileChunks[chunkNum-1] != nil {
|
||||
md.SetICCProfileError(fmt.Errorf("duplicated ICC profile chunk"))
|
||||
continue
|
||||
}
|
||||
iccProfileChunksExtracted++
|
||||
iccProfileChunks[chunkNum-1] = segment.Data[len(iccProfileIdentifier)+2:]
|
||||
|
||||
if allMetadataExtracted() {
|
||||
break parseSegments
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !metadataExtracted {
|
||||
return nil, fmt.Errorf("no metadata found")
|
||||
}
|
||||
md.SetExifData(exif)
|
||||
|
||||
// Incomplete or missing ICC profile
|
||||
if len(iccProfileChunks) != iccProfileChunksExtracted {
|
||||
_, iccErr := md.ICCProfileData()
|
||||
if iccErr == nil {
|
||||
md.SetICCProfileError(fmt.Errorf("incomplete ICC profile data"))
|
||||
}
|
||||
return md, nil
|
||||
}
|
||||
|
||||
iccProfileData := bytes.Buffer{}
|
||||
for i := range iccProfileChunks {
|
||||
iccProfileData.Write(iccProfileChunks[i])
|
||||
}
|
||||
md.SetICCProfileData(iccProfileData.Bytes())
|
||||
|
||||
return md, nil
|
||||
}
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
package jpegmeta
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/kovidgoyal/imaging/streams"
|
||||
)
|
||||
|
||||
var invalidMarker = marker{Type: markerTypeInvalid}
|
||||
|
||||
type marker struct {
|
||||
Type markerType
|
||||
DataLength int
|
||||
}
|
||||
|
||||
func makeMarker(mType byte, r io.Reader) (marker, error) {
|
||||
var length uint16
|
||||
switch mType {
|
||||
|
||||
case
|
||||
byte(markerTypeRestart0),
|
||||
byte(markerTypeRestart1),
|
||||
byte(markerTypeRestart2),
|
||||
byte(markerTypeRestart3),
|
||||
byte(markerTypeRestart4),
|
||||
byte(markerTypeRestart5),
|
||||
byte(markerTypeRestart6),
|
||||
byte(markerTypeRestart7),
|
||||
byte(markerTypeStartOfImage),
|
||||
byte(markerTypeEndOfImage):
|
||||
|
||||
length = 2
|
||||
|
||||
case byte(markerTypeStartOfFrameBaseline),
|
||||
byte(markerTypeStartOfFrameProgressive),
|
||||
byte(markerTypeDefineHuffmanTable),
|
||||
byte(markerTypeStartOfScan),
|
||||
byte(markerTypeDefineQuantisationTable),
|
||||
byte(markerTypeDefineRestartInterval),
|
||||
byte(markerTypeApp0),
|
||||
byte(markerTypeApp1),
|
||||
byte(markerTypeApp2),
|
||||
byte(markerTypeApp3),
|
||||
byte(markerTypeApp4),
|
||||
byte(markerTypeApp5),
|
||||
byte(markerTypeApp6),
|
||||
byte(markerTypeApp7),
|
||||
byte(markerTypeApp8),
|
||||
byte(markerTypeApp9),
|
||||
byte(markerTypeApp10),
|
||||
byte(markerTypeApp11),
|
||||
byte(markerTypeApp12),
|
||||
byte(markerTypeApp13),
|
||||
byte(markerTypeApp14),
|
||||
byte(markerTypeApp15),
|
||||
byte(markerTypeComment):
|
||||
|
||||
var err error
|
||||
if err = binary.Read(r, binary.BigEndian, &length); err != nil {
|
||||
return invalidMarker, err
|
||||
}
|
||||
|
||||
default:
|
||||
return invalidMarker, fmt.Errorf("unrecognised marker type %0x", mType)
|
||||
}
|
||||
|
||||
return marker{
|
||||
Type: markerType(mType),
|
||||
DataLength: int(length) - 2,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func readMarker(r io.Reader) (marker, error) {
|
||||
b, err := streams.ReadByte(r)
|
||||
if err != nil {
|
||||
return invalidMarker, err
|
||||
}
|
||||
|
||||
if b != 0xff {
|
||||
return invalidMarker, fmt.Errorf("invalid marker identifier %0x", b)
|
||||
}
|
||||
if b, err = streams.ReadByte(r); err != nil {
|
||||
return invalidMarker, err
|
||||
}
|
||||
|
||||
return makeMarker(b, r)
|
||||
}
|
||||
+115
@@ -0,0 +1,115 @@
|
||||
package jpegmeta
|
||||
|
||||
import "fmt"
|
||||
|
||||
type markerType int
|
||||
|
||||
const (
|
||||
markerTypeInvalid markerType = 0x00
|
||||
markerTypeStartOfFrameBaseline markerType = 0xc0
|
||||
markerTypeStartOfFrameProgressive markerType = 0xc2
|
||||
markerTypeDefineHuffmanTable markerType = 0xc4
|
||||
markerTypeRestart0 markerType = 0xd0
|
||||
markerTypeRestart1 markerType = 0xd1
|
||||
markerTypeRestart2 markerType = 0xd2
|
||||
markerTypeRestart3 markerType = 0xd3
|
||||
markerTypeRestart4 markerType = 0xd4
|
||||
markerTypeRestart5 markerType = 0xd5
|
||||
markerTypeRestart6 markerType = 0xd6
|
||||
markerTypeRestart7 markerType = 0xd7
|
||||
markerTypeStartOfImage markerType = 0xd8
|
||||
markerTypeEndOfImage markerType = 0xd9
|
||||
markerTypeStartOfScan markerType = 0xda
|
||||
markerTypeDefineQuantisationTable markerType = 0xdb
|
||||
markerTypeDefineRestartInterval markerType = 0xdd
|
||||
markerTypeApp0 markerType = 0xe0
|
||||
markerTypeApp1 markerType = 0xe1
|
||||
markerTypeApp2 markerType = 0xe2
|
||||
markerTypeApp3 markerType = 0xe3
|
||||
markerTypeApp4 markerType = 0xe4
|
||||
markerTypeApp5 markerType = 0xe5
|
||||
markerTypeApp6 markerType = 0xe6
|
||||
markerTypeApp7 markerType = 0xe7
|
||||
markerTypeApp8 markerType = 0xe8
|
||||
markerTypeApp9 markerType = 0xe9
|
||||
markerTypeApp10 markerType = 0xea
|
||||
markerTypeApp11 markerType = 0xeb
|
||||
markerTypeApp12 markerType = 0xec
|
||||
markerTypeApp13 markerType = 0xed
|
||||
markerTypeApp14 markerType = 0xee
|
||||
markerTypeApp15 markerType = 0xef
|
||||
markerTypeComment markerType = 0xfe
|
||||
)
|
||||
|
||||
func (mt markerType) String() string {
|
||||
switch mt {
|
||||
case markerTypeStartOfFrameBaseline:
|
||||
return "SOF0"
|
||||
case markerTypeStartOfFrameProgressive:
|
||||
return "SOF2"
|
||||
case markerTypeDefineHuffmanTable:
|
||||
return "DHT"
|
||||
case markerTypeRestart0:
|
||||
return "RST0"
|
||||
case markerTypeRestart1:
|
||||
return "RST1"
|
||||
case markerTypeRestart2:
|
||||
return "RST2"
|
||||
case markerTypeRestart3:
|
||||
return "RST3"
|
||||
case markerTypeRestart4:
|
||||
return "RST4"
|
||||
case markerTypeRestart5:
|
||||
return "RST5"
|
||||
case markerTypeRestart6:
|
||||
return "RST6"
|
||||
case markerTypeRestart7:
|
||||
return "RST7"
|
||||
case markerTypeStartOfImage:
|
||||
return "SOI"
|
||||
case markerTypeEndOfImage:
|
||||
return "EOI"
|
||||
case markerTypeStartOfScan:
|
||||
return "SOS"
|
||||
case markerTypeDefineQuantisationTable:
|
||||
return "DQT"
|
||||
case markerTypeDefineRestartInterval:
|
||||
return "DRI"
|
||||
case markerTypeApp0:
|
||||
return "APP0"
|
||||
case markerTypeApp1:
|
||||
return "APP1"
|
||||
case markerTypeApp2:
|
||||
return "APP2"
|
||||
case markerTypeApp3:
|
||||
return "APP3"
|
||||
case markerTypeApp4:
|
||||
return "APP4"
|
||||
case markerTypeApp5:
|
||||
return "APP5"
|
||||
case markerTypeApp6:
|
||||
return "APP6"
|
||||
case markerTypeApp7:
|
||||
return "APP7"
|
||||
case markerTypeApp8:
|
||||
return "APP8"
|
||||
case markerTypeApp9:
|
||||
return "APP9"
|
||||
case markerTypeApp10:
|
||||
return "APP10"
|
||||
case markerTypeApp11:
|
||||
return "APP11"
|
||||
case markerTypeApp12:
|
||||
return "APP12"
|
||||
case markerTypeApp13:
|
||||
return "APP13"
|
||||
case markerTypeApp14:
|
||||
return "APP14"
|
||||
case markerTypeApp15:
|
||||
return "APP15"
|
||||
case markerTypeComment:
|
||||
return "COM"
|
||||
default:
|
||||
return fmt.Sprintf("Unknown (%0x)", byte(mt))
|
||||
}
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
package jpegmeta
|
||||
|
||||
import (
|
||||
"io"
|
||||
)
|
||||
|
||||
var invalidSegment = segment{Marker: invalidMarker}
|
||||
|
||||
type segment struct {
|
||||
Marker marker
|
||||
Data []byte
|
||||
}
|
||||
|
||||
func makeSegment(markerType byte, r io.Reader) (segment, error) {
|
||||
m, err := makeMarker(markerType, r)
|
||||
return segment{Marker: m}, err
|
||||
}
|
||||
|
||||
func readSegment(r io.Reader) (segment, error) {
|
||||
m, err := readMarker(r)
|
||||
if err != nil {
|
||||
return invalidSegment, err
|
||||
}
|
||||
|
||||
seg := segment{
|
||||
Marker: m,
|
||||
}
|
||||
if m.DataLength > 0 {
|
||||
seg.Data = make([]byte, m.DataLength)
|
||||
|
||||
_, err := io.ReadFull(r, seg.Data)
|
||||
if err != nil {
|
||||
return invalidSegment, err
|
||||
}
|
||||
}
|
||||
|
||||
return seg, nil
|
||||
}
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
package jpegmeta
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"github.com/kovidgoyal/imaging/streams"
|
||||
)
|
||||
|
||||
type segmentReader struct {
|
||||
reader io.Reader
|
||||
inEntropyCodedData bool
|
||||
}
|
||||
|
||||
func (sr *segmentReader) ReadSegment() (segment, error) {
|
||||
if sr.inEntropyCodedData {
|
||||
for {
|
||||
b, err := streams.ReadByte(sr.reader)
|
||||
if err != nil {
|
||||
return segment{}, err
|
||||
}
|
||||
|
||||
if b == 0xFF {
|
||||
if b, err = streams.ReadByte(sr.reader); err != nil {
|
||||
return segment{}, err
|
||||
}
|
||||
|
||||
if b != 0x00 {
|
||||
seg, err := makeSegment(b, sr.reader)
|
||||
if err != nil {
|
||||
return segment{}, err
|
||||
}
|
||||
|
||||
sr.inEntropyCodedData = seg.Marker.Type == markerTypeStartOfScan ||
|
||||
(seg.Marker.Type >= markerTypeRestart0 && seg.Marker.Type <= markerTypeRestart7)
|
||||
|
||||
return seg, err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
seg, err := readSegment(sr.reader)
|
||||
if err != nil {
|
||||
return seg, err
|
||||
}
|
||||
|
||||
sr.inEntropyCodedData = seg.Marker.Type == markerTypeStartOfScan
|
||||
|
||||
return seg, nil
|
||||
}
|
||||
|
||||
func NewSegmentReader(r io.Reader) *segmentReader {
|
||||
return &segmentReader{
|
||||
reader: r,
|
||||
}
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
package netpbmmeta
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/kovidgoyal/imaging/netpbm"
|
||||
"github.com/kovidgoyal/imaging/prism/meta"
|
||||
"github.com/kovidgoyal/imaging/prism/meta/tiffmeta"
|
||||
)
|
||||
|
||||
var _ = fmt.Print
|
||||
|
||||
func ExtractMetadata(r io.Reader) (md *meta.Data, err error) {
|
||||
c, fmt, err := netpbm.DecodeConfigAndFormat(r)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "unsupported netPBM format") {
|
||||
err = nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
md = &meta.Data{
|
||||
Format: fmt, PixelWidth: uint32(c.Width), PixelHeight: uint32(c.Height),
|
||||
BitsPerComponent: tiffmeta.BitsPerComponent(c.ColorModel),
|
||||
}
|
||||
return md, nil
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
package pngmeta
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
type chunkHeader struct {
|
||||
Length uint32
|
||||
ChunkType [4]byte
|
||||
}
|
||||
|
||||
func (ch chunkHeader) String() string {
|
||||
return fmt.Sprintf("%c%c%c%c(%d)", ch.ChunkType[0], ch.ChunkType[1], ch.ChunkType[2], ch.ChunkType[3], ch.Length)
|
||||
}
|
||||
|
||||
func readChunkHeader(r io.Reader) (ch chunkHeader, err error) {
|
||||
err = binary.Read(r, binary.BigEndian, &ch)
|
||||
return
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
package pngmeta
|
||||
|
||||
const (
|
||||
chunkTypeiCCP = "iCCP"
|
||||
chunkTypeIDAT = "IDAT"
|
||||
chunkTypeIEND = "IEND"
|
||||
chunkTypeIHDR = "IHDR"
|
||||
chunkTypeacTL = "acTL"
|
||||
chunkTypeeXIf = "eXIf"
|
||||
chunkTypecICP = "eICP"
|
||||
)
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
// Package pngmeta provides support for working with embedded PNG metadata.
|
||||
package pngmeta
|
||||
+199
@@ -0,0 +1,199 @@
|
||||
package pngmeta
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/zlib"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"unsafe"
|
||||
|
||||
"github.com/kovidgoyal/imaging/prism/meta"
|
||||
"github.com/kovidgoyal/imaging/streams"
|
||||
"github.com/kovidgoyal/imaging/types"
|
||||
)
|
||||
|
||||
var pngSignature = [8]byte{0x89, 'P', 'N', 'G', 0x0D, 0x0A, 0x1A, 0x0A}
|
||||
|
||||
// Load loads the metadata for a PNG image stream.
|
||||
//
|
||||
// Only as much of the stream is consumed as necessary to extract the metadata;
|
||||
// the returned stream contains a buffered copy of the consumed data such that
|
||||
// reading from it will produce the same results as fully reading the input
|
||||
// stream. This provides a convenient way to load the full image after loading
|
||||
// the metadata.
|
||||
//
|
||||
// An error is returned if basic metadata could not be extracted. The returned
|
||||
// stream still provides the full image data.
|
||||
func Load(r io.Reader) (md *meta.Data, imgStream io.Reader, err error) {
|
||||
imgStream, err = streams.CallbackWithSeekable(r, func(r io.Reader) (err error) {
|
||||
md, err = ExtractMetadata(r)
|
||||
return
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
func read_chunk(r io.Reader, length uint32) (ans []byte, err error) {
|
||||
ans = make([]byte, length+4)
|
||||
_, err = io.ReadFull(r, ans)
|
||||
ans = ans[:len(ans)-4] // we dont care about the chunk CRC
|
||||
return
|
||||
}
|
||||
|
||||
func skip_chunk(r io.Reader, length uint32) (err error) {
|
||||
return streams.Skip(r, int64(length)+4)
|
||||
}
|
||||
|
||||
// Same as Load() except that no new stream is provided
|
||||
func ExtractMetadata(r io.Reader) (md *meta.Data, err error) {
|
||||
metadataExtracted := false
|
||||
md = &meta.Data{Format: types.PNG}
|
||||
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
if !metadataExtracted {
|
||||
md = nil
|
||||
}
|
||||
err = fmt.Errorf("panic while extracting image metadata: %v", r)
|
||||
}
|
||||
}()
|
||||
found_exif := false
|
||||
allMetadataExtracted := func() bool {
|
||||
iccData, iccErr := md.ICCProfileData()
|
||||
return metadataExtracted && md.HasFrames && found_exif && (iccData != nil || iccErr != nil) && md.CICP.IsSet
|
||||
}
|
||||
|
||||
pngSig := [8]byte{}
|
||||
if _, err := io.ReadFull(r, pngSig[:]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if pngSig != pngSignature {
|
||||
return nil, nil
|
||||
}
|
||||
var chunk []byte
|
||||
|
||||
decode := func(target any) error {
|
||||
if n, err := binary.Decode(chunk, binary.BigEndian, target); err == nil {
|
||||
chunk = chunk[n:]
|
||||
return nil
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
parseChunks:
|
||||
for {
|
||||
ch, err := readChunkHeader(r)
|
||||
if err != nil {
|
||||
if errors.Is(err, io.EOF) {
|
||||
break
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch unsafe.String(unsafe.SliceData(ch.ChunkType[:]), 4) {
|
||||
|
||||
case chunkTypeIHDR:
|
||||
if chunk, err = read_chunk(r, ch.Length); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = decode(&md.PixelWidth); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = decode(&md.PixelHeight); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
md.BitsPerComponent = uint32(chunk[0])
|
||||
metadataExtracted = true
|
||||
if allMetadataExtracted() {
|
||||
break parseChunks
|
||||
}
|
||||
|
||||
case chunkTypeeXIf:
|
||||
if chunk, err = read_chunk(r, ch.Length); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
found_exif = true
|
||||
md.SetExifData(chunk)
|
||||
if allMetadataExtracted() {
|
||||
break parseChunks
|
||||
}
|
||||
|
||||
case chunkTypecICP:
|
||||
if chunk, err = read_chunk(r, ch.Length); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
md.CICP.ColorPrimaries, md.CICP.TransferCharacteristics = chunk[0], chunk[1]
|
||||
md.CICP.MatrixCoefficients, md.CICP.VideoFullRange = chunk[2], chunk[3]
|
||||
md.CICP.IsSet = true
|
||||
if allMetadataExtracted() {
|
||||
break parseChunks
|
||||
}
|
||||
case chunkTypeacTL:
|
||||
if chunk, err = read_chunk(r, ch.Length); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
md.HasFrames = true
|
||||
var num_frames, num_plays uint32
|
||||
if err = decode(&num_frames); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = decode(&num_plays); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
md.NumFrames, md.NumPlays = int(num_frames), int(num_plays)
|
||||
if allMetadataExtracted() {
|
||||
break parseChunks
|
||||
}
|
||||
|
||||
case chunkTypeiCCP:
|
||||
if chunk, err = read_chunk(r, ch.Length); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
idx := bytes.IndexByte(chunk, 0)
|
||||
if idx < 0 || idx > 80 {
|
||||
return nil, fmt.Errorf("null terminator not found reading ICC profile name")
|
||||
}
|
||||
chunk = chunk[idx+1:]
|
||||
if len(chunk) < 1 {
|
||||
return nil, fmt.Errorf("incomplete ICCP chunk in PNG file")
|
||||
}
|
||||
if compressionMethod := chunk[0]; compressionMethod != 0x00 {
|
||||
return nil, fmt.Errorf("unknown compression method (%d)", compressionMethod)
|
||||
}
|
||||
chunk = chunk[1:]
|
||||
// Decompress ICC profile data
|
||||
zReader, err := zlib.NewReader(bytes.NewReader(chunk))
|
||||
if err != nil {
|
||||
md.SetICCProfileError(err)
|
||||
break
|
||||
}
|
||||
defer zReader.Close()
|
||||
profileData := &bytes.Buffer{}
|
||||
_, err = io.Copy(profileData, zReader)
|
||||
if err == nil {
|
||||
md.SetICCProfileData(profileData.Bytes())
|
||||
if allMetadataExtracted() {
|
||||
break parseChunks
|
||||
}
|
||||
} else {
|
||||
md.SetICCProfileError(err)
|
||||
}
|
||||
|
||||
case chunkTypeIDAT, chunkTypeIEND:
|
||||
break parseChunks
|
||||
|
||||
default:
|
||||
if err = skip_chunk(r, ch.Length); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !metadataExtracted {
|
||||
return nil, fmt.Errorf("no metadata found")
|
||||
}
|
||||
|
||||
return md, nil
|
||||
}
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
package tiffmeta
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image/color"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/kovidgoyal/imaging/prism/meta"
|
||||
"github.com/kovidgoyal/imaging/types"
|
||||
"github.com/rwcarlsen/goexif/exif"
|
||||
"golang.org/x/image/tiff"
|
||||
)
|
||||
|
||||
var _ = fmt.Print
|
||||
|
||||
func BitsPerComponent(c color.Model) uint32 {
|
||||
switch c {
|
||||
case color.RGBAModel, color.NRGBAModel, color.YCbCrModel, color.CMYKModel:
|
||||
return 8
|
||||
case color.GrayModel:
|
||||
return 8
|
||||
case color.Gray16Model:
|
||||
return 16
|
||||
case color.AlphaModel:
|
||||
return 8
|
||||
case color.Alpha16Model:
|
||||
return 16
|
||||
default:
|
||||
// This handles paletted images and other custom color models.
|
||||
// For a palette, each color in the palette has its own depth.
|
||||
// We can check the bit depth by converting a color from the model to RGBA.
|
||||
// The `Convert` method is part of the color.Model interface.
|
||||
// A fully opaque red color is used for this check.
|
||||
r, g, b, a := c.Convert(color.RGBA{R: 255, A: 255}).RGBA()
|
||||
|
||||
// The values returned by RGBA() are 16-bit alpha-premultiplied values (0-65535).
|
||||
// If the highest value is <= 255, it's an 8-bit model.
|
||||
if r|g|b|a <= 0xff {
|
||||
return 8
|
||||
} else {
|
||||
return 16
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func ExtractMetadata(r_ io.Reader) (md *meta.Data, err error) {
|
||||
r := r_.(io.ReadSeeker)
|
||||
pos, err := r.Seek(0, io.SeekCurrent)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c, err := tiff.DecodeConfig(r)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "malformed header") {
|
||||
err = nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
md = &meta.Data{
|
||||
Format: types.TIFF, PixelWidth: uint32(c.Width), PixelHeight: uint32(c.Height),
|
||||
BitsPerComponent: BitsPerComponent(c.ColorModel),
|
||||
}
|
||||
if _, err = r.Seek(pos, io.SeekStart); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if e, err := exif.Decode(r); err == nil {
|
||||
md.SetExif(e)
|
||||
} else {
|
||||
md.SetExifError(err)
|
||||
}
|
||||
return md, nil
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
package webpmeta
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
type chunkHeader struct {
|
||||
ChunkType [4]byte
|
||||
Length uint32
|
||||
}
|
||||
|
||||
func (ch chunkHeader) String() string {
|
||||
return fmt.Sprintf("%c%c%c%c(%d)", ch.ChunkType[0], ch.ChunkType[1], ch.ChunkType[2], ch.ChunkType[3], ch.Length)
|
||||
}
|
||||
|
||||
func readChunkHeader(r io.Reader) (ch chunkHeader, err error) {
|
||||
err = binary.Read(r, binary.LittleEndian, &ch)
|
||||
return
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
package webpmeta
|
||||
|
||||
var (
|
||||
chunkTypeRIFF = [4]byte{'R', 'I', 'F', 'F'}
|
||||
chunkTypeWEBP = [4]byte{'W', 'E', 'B', 'P'}
|
||||
chunkTypeVP8 = [4]byte{'V', 'P', '8', ' '}
|
||||
chunkTypeVP8L = [4]byte{'V', 'P', '8', 'L'}
|
||||
chunkTypeVP8X = [4]byte{'V', 'P', '8', 'X'}
|
||||
chunkTypeICCP = [4]byte{'I', 'C', 'C', 'P'}
|
||||
chunkTypeEXIF = [4]byte{'E', 'X', 'I', 'F'}
|
||||
)
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
// Package webpmeta provides support for working with embedded WebP metadata.
|
||||
package webpmeta
|
||||
Generated
Vendored
BIN
Binary file not shown.
|
After Width: | Height: | Size: 294 B |
+243
@@ -0,0 +1,243 @@
|
||||
package webpmeta
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/kovidgoyal/imaging/prism/meta"
|
||||
"github.com/kovidgoyal/imaging/streams"
|
||||
"github.com/kovidgoyal/imaging/types"
|
||||
)
|
||||
|
||||
// Signature is FourCC bytes in the RIFF chunk, "RIFF????WEBP"
|
||||
var webpSignature = [4]byte{'W', 'E', 'B', 'P'}
|
||||
|
||||
type webpFormat int
|
||||
|
||||
const (
|
||||
webpFormatSimple = webpFormat(iota)
|
||||
webpFormatLossless
|
||||
webpFormatExtended
|
||||
)
|
||||
|
||||
// Bits per component is fixed in WebP
|
||||
const bitsPerComponent = 8
|
||||
|
||||
// Load loads the metadata for a WebP image stream.
|
||||
//
|
||||
// Only as much of the stream is consumed as necessary to extract the metadata;
|
||||
// the returned stream contains a buffered copy of the consumed data such that
|
||||
// reading from it will produce the same results as fully reading the input
|
||||
// stream. This provides a convenient way to load the full image after loading
|
||||
// the metadata.
|
||||
//
|
||||
// An error is returned if basic metadata could not be extracted. The returned
|
||||
// stream still provides the full image data.
|
||||
func Load(r io.Reader) (md *meta.Data, imgStream io.Reader, err error) {
|
||||
imgStream, err = streams.CallbackWithSeekable(r, func(r io.Reader) (err error) {
|
||||
md, err = ExtractMetadata(r)
|
||||
return
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Same as Load() except that no new stream is provided
|
||||
func ExtractMetadata(r io.Reader) (md *meta.Data, err error) {
|
||||
md = &meta.Data{Format: types.WEBP}
|
||||
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
err = fmt.Errorf("panic while extracting image metadata: %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
if is_webp, err := verifySignature(r); err != nil {
|
||||
return nil, err
|
||||
} else if !is_webp {
|
||||
return nil, nil
|
||||
}
|
||||
format, chunkLen, err := readWebPFormat(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = parseFormat(r, md, format, chunkLen)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return md, nil
|
||||
}
|
||||
|
||||
func parseFormat(r io.Reader, md *meta.Data, format webpFormat, chunkLen uint32) error {
|
||||
switch format {
|
||||
case webpFormatExtended:
|
||||
return parseWebpExtended(r, md, chunkLen)
|
||||
case webpFormatSimple:
|
||||
return parseWebpSimple(r, md, chunkLen)
|
||||
case webpFormatLossless:
|
||||
return parseWebpLossless(r, md, chunkLen)
|
||||
default:
|
||||
return errors.New("unknown WebP format")
|
||||
}
|
||||
}
|
||||
|
||||
func parseWebpSimple(r io.Reader, md *meta.Data, chunkLen uint32) error {
|
||||
var buf [10]byte
|
||||
b := buf[:]
|
||||
if _, err := io.ReadFull(r, b); err != nil {
|
||||
return err
|
||||
}
|
||||
b = b[3:]
|
||||
if b[0] != 0x9d || b[1] != 0x01 || b[2] != 0x2a {
|
||||
return errors.New("corrupted WebP VP8 frame")
|
||||
}
|
||||
md.PixelWidth = uint32(b[4]&((1<<6)-1))<<8 | uint32(b[3])
|
||||
md.PixelWidth = uint32(b[6]&((1<<6)-1))<<8 | uint32(b[5])
|
||||
md.BitsPerComponent = bitsPerComponent
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseWebpLossless(r io.Reader, md *meta.Data, chunkLen uint32) error {
|
||||
var b [5]byte
|
||||
if _, err := io.ReadFull(r, b[:]); err != nil {
|
||||
return err
|
||||
}
|
||||
if b[0] != 0x2f {
|
||||
return errors.New("corrupted lossless WebP")
|
||||
}
|
||||
// Next 28 bits are width-1 and height-1.
|
||||
w := uint32(b[1])
|
||||
w |= uint32(b[2]&((1<<6)-1)) << 8
|
||||
w &= 0x3FFF
|
||||
|
||||
h := uint32((b[2] >> 6) & ((1 << 2) - 1))
|
||||
h |= uint32(b[3]) << 2
|
||||
h |= uint32(b[4]&((1<<4)-1)) << 10
|
||||
h &= 0x3FFF
|
||||
|
||||
md.PixelWidth = w + 1
|
||||
md.PixelHeight = h + 1
|
||||
md.BitsPerComponent = bitsPerComponent
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseWebpExtended(r io.Reader, md *meta.Data, chunkLen uint32) error {
|
||||
if chunkLen != 10 {
|
||||
return fmt.Errorf("unexpected VP8X chunk length: %d", chunkLen)
|
||||
}
|
||||
var hb [10]byte
|
||||
h := hb[:]
|
||||
if _, err := io.ReadFull(r, h); err != nil {
|
||||
return err
|
||||
}
|
||||
hasProfile := h[0]&(1<<5) != 0
|
||||
hasExif := h[0]&(1<<3) != 0
|
||||
animated := h[0]&(1<<1) != 0
|
||||
h = h[4:]
|
||||
w := uint32(h[0]) | uint32(h[1])<<8 | uint32(h[2])<<16
|
||||
ht := uint32(h[3]) | uint32(h[4])<<8 | uint32(h[5])<<16
|
||||
md.PixelWidth = w + 1
|
||||
md.PixelHeight = ht + 1
|
||||
md.BitsPerComponent = bitsPerComponent
|
||||
md.HasFrames = animated
|
||||
if !hasProfile && !hasExif {
|
||||
return nil
|
||||
}
|
||||
if err := skip(r, chunkLen-10); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if hasProfile {
|
||||
// ICCP must be next
|
||||
data, err := readICCP(r)
|
||||
if err != nil {
|
||||
md.SetICCProfileError(err)
|
||||
} else {
|
||||
md.SetICCProfileData(data)
|
||||
}
|
||||
}
|
||||
|
||||
if hasExif {
|
||||
for {
|
||||
ch, err := readChunkHeader(r)
|
||||
if err != nil {
|
||||
if errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF) {
|
||||
err = nil
|
||||
break
|
||||
}
|
||||
return err
|
||||
}
|
||||
if ch.ChunkType == chunkTypeEXIF {
|
||||
data := make([]byte, ch.Length)
|
||||
if _, err := io.ReadFull(r, data); err != nil {
|
||||
return err
|
||||
}
|
||||
md.SetExifData(data)
|
||||
break
|
||||
} else {
|
||||
if err = skip(r, ch.Length); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func readICCP(r io.Reader) ([]byte, error) {
|
||||
// ICCP _must_ be the next chunk.
|
||||
ch, err := readChunkHeader(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if ch.ChunkType != chunkTypeICCP {
|
||||
return nil, errors.New("no expected ICCP chunk")
|
||||
}
|
||||
|
||||
// Extract ICCP.
|
||||
data := make([]byte, ch.Length)
|
||||
if _, err := io.ReadFull(r, data); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func verifySignature(r io.Reader) (bool, error) {
|
||||
ch, err := readChunkHeader(r)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if ch.ChunkType != chunkTypeRIFF {
|
||||
return false, nil
|
||||
}
|
||||
var fourcc [4]byte
|
||||
if _, err := io.ReadFull(r, fourcc[:]); err != nil {
|
||||
return false, err
|
||||
}
|
||||
if fourcc != webpSignature {
|
||||
return false, nil
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func readWebPFormat(r io.Reader) (format webpFormat, length uint32, err error) {
|
||||
ch, err := readChunkHeader(r)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
switch ch.ChunkType {
|
||||
case chunkTypeVP8:
|
||||
return webpFormatSimple, ch.Length, nil
|
||||
case chunkTypeVP8L:
|
||||
return webpFormatLossless, ch.Length, nil
|
||||
case chunkTypeVP8X:
|
||||
return webpFormatExtended, ch.Length, nil
|
||||
default:
|
||||
return 0, 0, fmt.Errorf("unexpected WEBP format: %s", string(ch.ChunkType[:]))
|
||||
}
|
||||
}
|
||||
|
||||
func skip(r io.Reader, length uint32) error {
|
||||
return streams.Skip(r, int64(length))
|
||||
}
|
||||
Reference in New Issue
Block a user