| // Copyright 2021 The Wuffs Authors. | |
| // | |
| // Licensed under the Apache License, Version 2.0 (the "License"); | |
| // you may not use this file except in compliance with the License. | |
| // You may obtain a copy of the License at | |
| // | |
| // https://www.apache.org/licenses/LICENSE-2.0 | |
| // | |
| // Unless required by applicable law or agreed to in writing, software | |
| // distributed under the License is distributed on an "AS IS" BASIS, | |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| // See the License for the specific language governing permissions and | |
| // limitations under the License. | |
| //go:build ignore | |
| // +build ignore | |
| package main | |
| // make-red-blue-gradient.go makes a simple PNG image with optional color | |
| // correction chunks. | |
| // | |
| // Usage: go run make-red-blue-gradient.go > foo.png | |
| import ( | |
| "bytes" | |
| "compress/zlib" | |
| "errors" | |
| "flag" | |
| "hash/crc32" | |
| "image" | |
| "image/color" | |
| "image/png" | |
| "math" | |
| "os" | |
| "path/filepath" | |
| "strings" | |
| ) | |
| var ( | |
| checkerboard = flag.Bool("checkerboard", false, "Create a checkerboard pattern instead") | |
| gama = flag.Float64("gama", 0, "Gamma correction value (e.g. 2.2)") | |
| iccp = flag.String("iccp", "", "ICC filename (e.g. foo/bar.icc)") | |
| ) | |
| func main() { | |
| if err := main1(); err != nil { | |
| os.Stderr.WriteString(err.Error() + "\n") | |
| os.Exit(1) | |
| } | |
| } | |
| func main1() error { | |
| flag.Parse() | |
| // Make a checkerboard or red-blue gradient. | |
| src := image.Image(nil) | |
| if *checkerboard { | |
| // With -gama=1.0 (or -gama=2.2), the two halves of this image should | |
| // have roughly equal (or un-equal) brightness. | |
| m := image.NewGray(image.Rect(0, 0, 256, 256)) | |
| for y := 0; y < 256; y++ { | |
| for x := 0; x < 256; x++ { | |
| if x < 128 { | |
| m.SetGray(x, y, color.Gray{0x80}) | |
| } else if ((x ^ y) & 1) == 1 { | |
| m.SetGray(x, y, color.Gray{0xFF}) | |
| } else { | |
| m.SetGray(x, y, color.Gray{0x00}) | |
| } | |
| } | |
| } | |
| src = m | |
| } else { | |
| m := image.NewRGBA(image.Rect(0, 0, 256, 256)) | |
| for y := 0; y < 256; y++ { | |
| for x := 0; x < 256; x++ { | |
| m.SetRGBA(x, y, color.RGBA{ | |
| R: uint8(x), | |
| G: 0x00, | |
| B: uint8(y), | |
| A: 0xFF, | |
| }) | |
| } | |
| } | |
| src = m | |
| } | |
| // Encode to PNG. | |
| buf := &bytes.Buffer{} | |
| png.Encode(buf, src) | |
| original := buf.Bytes() | |
| // Split the encoding into a magic+IHDR prefix and an IDAT+IEND suffix. The | |
| // starting magic number is 8 bytes long. The IHDR chunk is 25 bytes long. | |
| prefix, suffix := original[:33], original[33:] | |
| out := []byte(nil) | |
| out = append(out, prefix...) | |
| // Insert the optional chunks between the prefix and suffix. | |
| if *gama > 0 { | |
| g := uint32(math.Round(100000 / *gama)) | |
| out = appendPNGChunk(out, "gAMA", | |
| byte(g>>24), | |
| byte(g>>16), | |
| byte(g>>8), | |
| byte(g>>0), | |
| ) | |
| } | |
| if *iccp != "" { | |
| raw, err := os.ReadFile(*iccp) | |
| if err != nil { | |
| return err | |
| } | |
| name, err := iccpName(*iccp) | |
| if err != nil { | |
| return err | |
| } | |
| data := &bytes.Buffer{} | |
| data.WriteString(name) | |
| data.WriteByte(0x00) // The name is NUL terminated. | |
| data.WriteByte(0x00) // 0x00 means zlib compression. | |
| w := zlib.NewWriter(data) | |
| w.Write(raw) | |
| w.Close() | |
| out = appendPNGChunk(out, "iCCP", data.Bytes()...) | |
| } | |
| // Append the suffix and write to stdout. | |
| out = append(out, suffix...) | |
| _, err := os.Stdout.Write(out) | |
| return err | |
| } | |
| func appendPNGChunk(b []byte, name string, chunk ...byte) []byte { | |
| n := uint32(len(chunk)) | |
| b = append(b, | |
| byte(n>>24), | |
| byte(n>>16), | |
| byte(n>>8), | |
| byte(n>>0), | |
| ) | |
| b = append(b, name...) | |
| b = append(b, chunk...) | |
| hasher := crc32.NewIEEE() | |
| hasher.Write([]byte(name)) | |
| hasher.Write(chunk) | |
| c := hasher.Sum32() | |
| return append(b, | |
| byte(c>>24), | |
| byte(c>>16), | |
| byte(c>>8), | |
| byte(c>>0), | |
| ) | |
| } | |
| // iccpName determines a "bar" iCCP profile name (that satisifes the PNG | |
| // specification re the iCCP chunk) based on the "foo/bar.icc" filename. | |
| func iccpName(filename string) (string, error) { | |
| s := filepath.Base(filename) | |
| if i := strings.IndexByte(s, '.'); i >= 0 { | |
| s = s[:i] | |
| } | |
| s = strings.TrimSpace(s) | |
| if strings.Index(s, " ") >= 0 { | |
| return "", errors.New("ICCP name cannot contain consecutive spaces") | |
| } else if (len(s) < 1) || (79 < len(s)) { | |
| return "", errors.New("ICCP name has invalid length.") | |
| } | |
| for i := 0; i < len(s); i++ { | |
| if (s[i] < ' ') || ('~' < s[i]) { | |
| // Strictly speaking, the PNG specification allows the iCCP name to | |
| // be printable Latin-1, not just printable ASCII, but printable | |
| // ASCII is easier and avoids any later Latin-1 vs UTF-8 ambiguity. | |
| return "", errors.New("ICCP name is not printable ASCII") | |
| } | |
| } | |
| return s, nil | |
| } |