blob: a704efeca2ab1ec50521248b2e0523451ac96b35 [file] [log] [blame]
Nigel Taodbc209d2021-10-14 15:58:03 +11001// Copyright 2021 The Wuffs Authors.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// https://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//go:build ignore
16// +build ignore
17
18package main
19
20// make-red-blue-gradient.go makes a simple PNG image with optional color
21// correction chunks.
22//
23// Usage: go run make-red-blue-gradient.go > foo.png
24
25import (
26 "bytes"
Nigel Taoe6733ae2021-10-15 10:30:09 +110027 "compress/zlib"
Nigel Taof40ef012021-10-31 15:35:14 +110028 "errors"
Nigel Taodbc209d2021-10-14 15:58:03 +110029 "flag"
30 "hash/crc32"
31 "image"
32 "image/color"
33 "image/png"
34 "math"
35 "os"
Nigel Taof40ef012021-10-31 15:35:14 +110036 "path/filepath"
37 "strings"
Nigel Taodbc209d2021-10-14 15:58:03 +110038)
39
40var (
Nigel Tao6ea31442021-10-19 22:19:01 +110041 checkerboard = flag.Bool("checkerboard", false, "Create a checkerboard pattern instead")
42 gama = flag.Float64("gama", 0, "Gamma correction value (e.g. 2.2)")
43 iccp = flag.String("iccp", "", "ICC filename (e.g. foo/bar.icc)")
Nigel Taodbc209d2021-10-14 15:58:03 +110044)
45
46func main() {
47 if err := main1(); err != nil {
48 os.Stderr.WriteString(err.Error() + "\n")
49 os.Exit(1)
50 }
51}
52
53func main1() error {
54 flag.Parse()
55
Nigel Tao6ea31442021-10-19 22:19:01 +110056 // Make a checkerboard or red-blue gradient.
57 src := image.Image(nil)
58 if *checkerboard {
59 // With -gama=1.0 (or -gama=2.2), the two halves of this image should
60 // have roughly equal (or un-equal) brightness.
61 m := image.NewGray(image.Rect(0, 0, 256, 256))
62 for y := 0; y < 256; y++ {
63 for x := 0; x < 256; x++ {
64 if x < 128 {
65 m.SetGray(x, y, color.Gray{0x80})
66 } else if ((x ^ y) & 1) == 1 {
67 m.SetGray(x, y, color.Gray{0xFF})
68 } else {
69 m.SetGray(x, y, color.Gray{0x00})
70 }
71 }
Nigel Taodbc209d2021-10-14 15:58:03 +110072 }
Nigel Tao6ea31442021-10-19 22:19:01 +110073 src = m
74 } else {
Nigel Taod557bc32021-10-22 09:23:49 +110075 m := image.NewRGBA(image.Rect(0, 0, 256, 256))
76 for y := 0; y < 256; y++ {
77 for x := 0; x < 256; x++ {
Nigel Tao6ea31442021-10-19 22:19:01 +110078 m.SetRGBA(x, y, color.RGBA{
Nigel Taod557bc32021-10-22 09:23:49 +110079 R: uint8(x),
Nigel Tao6ea31442021-10-19 22:19:01 +110080 G: 0x00,
Nigel Taod557bc32021-10-22 09:23:49 +110081 B: uint8(y),
Nigel Tao6ea31442021-10-19 22:19:01 +110082 A: 0xFF,
83 })
84 }
85 }
86 src = m
Nigel Taodbc209d2021-10-14 15:58:03 +110087 }
88
89 // Encode to PNG.
90 buf := &bytes.Buffer{}
Nigel Tao6ea31442021-10-19 22:19:01 +110091 png.Encode(buf, src)
Nigel Taodbc209d2021-10-14 15:58:03 +110092 original := buf.Bytes()
93
94 // Split the encoding into a magic+IHDR prefix and an IDAT+IEND suffix. The
95 // starting magic number is 8 bytes long. The IHDR chunk is 25 bytes long.
96 prefix, suffix := original[:33], original[33:]
97 out := []byte(nil)
98 out = append(out, prefix...)
99
Nigel Taoe6733ae2021-10-15 10:30:09 +1100100 // Insert the optional chunks between the prefix and suffix.
101
Nigel Taodbc209d2021-10-14 15:58:03 +1100102 if *gama > 0 {
103 g := uint32(math.Round(100000 / *gama))
104 out = appendPNGChunk(out, "gAMA",
105 byte(g>>24),
106 byte(g>>16),
107 byte(g>>8),
108 byte(g>>0),
109 )
110 }
111
Nigel Taoe6733ae2021-10-15 10:30:09 +1100112 if *iccp != "" {
113 raw, err := os.ReadFile(*iccp)
114 if err != nil {
115 return err
116 }
Nigel Taof40ef012021-10-31 15:35:14 +1100117 name, err := iccpName(*iccp)
118 if err != nil {
119 return err
120 }
Nigel Taoe6733ae2021-10-15 10:30:09 +1100121 data := &bytes.Buffer{}
Nigel Taof40ef012021-10-31 15:35:14 +1100122 data.WriteString(name)
123 data.WriteByte(0x00) // The name is NUL terminated.
124 data.WriteByte(0x00) // 0x00 means zlib compression.
Nigel Taoe6733ae2021-10-15 10:30:09 +1100125 w := zlib.NewWriter(data)
126 w.Write(raw)
127 w.Close()
128 out = appendPNGChunk(out, "iCCP", data.Bytes()...)
129 }
130
Nigel Taodbc209d2021-10-14 15:58:03 +1100131 // Append the suffix and write to stdout.
132 out = append(out, suffix...)
133 _, err := os.Stdout.Write(out)
134 return err
135}
136
137func appendPNGChunk(b []byte, name string, chunk ...byte) []byte {
138 n := uint32(len(chunk))
139 b = append(b,
140 byte(n>>24),
141 byte(n>>16),
142 byte(n>>8),
143 byte(n>>0),
144 )
145 b = append(b, name...)
146 b = append(b, chunk...)
147 hasher := crc32.NewIEEE()
148 hasher.Write([]byte(name))
149 hasher.Write(chunk)
150 c := hasher.Sum32()
151 return append(b,
152 byte(c>>24),
153 byte(c>>16),
154 byte(c>>8),
155 byte(c>>0),
156 )
157}
Nigel Taof40ef012021-10-31 15:35:14 +1100158
159// iccpName determines a "bar" iCCP profile name (that satisifes the PNG
160// specification re the iCCP chunk) based on the "foo/bar.icc" filename.
161func iccpName(filename string) (string, error) {
162 s := filepath.Base(filename)
163 if i := strings.IndexByte(s, '.'); i >= 0 {
164 s = s[:i]
165 }
166 s = strings.TrimSpace(s)
167 if strings.Index(s, " ") >= 0 {
168 return "", errors.New("ICCP name cannot contain consecutive spaces")
169 } else if (len(s) < 1) || (79 < len(s)) {
170 return "", errors.New("ICCP name has invalid length.")
171 }
172 for i := 0; i < len(s); i++ {
173 if (s[i] < ' ') || ('~' < s[i]) {
174 // Strictly speaking, the PNG specification allows the iCCP name to
175 // be printable Latin-1, not just printable ASCII, but printable
176 // ASCII is easier and avoids any later Latin-1 vs UTF-8 ambiguity.
177 return "", errors.New("ICCP name is not printable ASCII")
178 }
179 }
180 return s, nil
181}