Add lib/nie Go library for decoding NIE images
diff --git a/lib/nie/nie.go b/lib/nie/nie.go
new file mode 100644
index 0000000..5c0d5d9
--- /dev/null
+++ b/lib/nie/nie.go
@@ -0,0 +1,146 @@
+// Copyright 2022 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.
+
+// ----------------
+
+// Package nie provides access to NIE (Naïve Image Format) files.
+//
+// The NIE specification is at
+// https://github.com/google/wuffs/blob/main/doc/spec/nie-spec.md
+package nie
+
+import (
+	"errors"
+	"image"
+	"image/color"
+	"io"
+)
+
+func u32le(b []byte) uint32 {
+	return (uint32(b[0]) << 0) |
+		(uint32(b[1]) << 8) |
+		(uint32(b[2]) << 16) |
+		(uint32(b[3]) << 24)
+}
+
+func init() {
+	image.RegisterFormat("nie", "nïE", Decode, DecodeConfig)
+}
+
+var (
+	errImageIsTooLarge = errors.New("nie: image is too large")
+	errInvalidHeader   = errors.New("nie: invalid header")
+)
+
+func decodeConfig(r io.Reader) (image.Config, byte, byte, error) {
+	b := [16]byte{}
+	_, err := io.ReadFull(r, b[:])
+	if err != nil {
+		return image.Config{}, 0, 0, err
+	}
+	if (u32le(b[:]) != 0x45afc36e) ||
+		(b[4] != 0xff) ||
+		(b[5] != 'b') ||
+		((b[6] != 'n') && (b[6] != 'p')) ||
+		((b[7] != '4') && (b[7] != '8')) {
+		return image.Config{}, 0, 0, errInvalidHeader
+	}
+
+	cm := color.Model(nil)
+	switch {
+	case (b[6] == 'n') && (b[7] == '4'):
+		cm = color.NRGBAModel
+	case (b[6] == 'n') && (b[7] == '8'):
+		cm = color.NRGBA64Model
+	case (b[6] == 'p') && (b[7] == '4'):
+		cm = color.RGBAModel
+	case (b[6] == 'p') && (b[7] == '8'):
+		cm = color.RGBA64Model
+	default:
+		return image.Config{}, 0, 0, errInvalidHeader
+	}
+
+	w := u32le(b[8:])
+	h := u32le(b[12:])
+	if (w > 0x7fffffff) || (h > 0x7fffffff) {
+		return image.Config{}, 0, 0, errInvalidHeader
+	}
+	return image.Config{
+		ColorModel: cm,
+		Width:      int(w),
+		Height:     int(h),
+	}, b[6], b[7], nil
+}
+
+// DecodeConfig returns the color model and dimensions of a NIE image without
+// decoding the entire image.
+func DecodeConfig(r io.Reader) (image.Config, error) {
+	cfg, _, _, err := decodeConfig(r)
+	return cfg, err
+}
+
+// Decode reads a NIE image from r.
+func Decode(r io.Reader) (image.Image, error) {
+	cfg, b6, b7, err := decodeConfig(r)
+	if err != nil {
+		return nil, err
+	}
+
+	const u64max = 0xffffffffffffffff
+	tooBig := false
+	wh := uint64(cfg.Width) * uint64(cfg.Height)
+	if b7 == '4' {
+		tooBig = (wh > (u64max / 4)) ||
+			((wh * 4) != uint64(int((wh * 4))))
+	} else {
+		tooBig = (wh > (u64max / 8)) ||
+			((wh * 8) != uint64(int((wh * 8))))
+	}
+	if tooBig {
+		return nil, errImageIsTooLarge
+	}
+
+	img, pix := image.Image(nil), []byte(nil)
+	switch {
+	case (b6 == 'n') && (b7 == '4'):
+		m := image.NewNRGBA(image.Rect(0, 0, int(cfg.Width), int(cfg.Height)))
+		img, pix = m, m.Pix
+	case (b6 == 'n') && (b7 == '8'):
+		m := image.NewNRGBA64(image.Rect(0, 0, int(cfg.Width), int(cfg.Height)))
+		img, pix = m, m.Pix
+	case (b6 == 'p') && (b7 == '4'):
+		m := image.NewRGBA(image.Rect(0, 0, int(cfg.Width), int(cfg.Height)))
+		img, pix = m, m.Pix
+	case (b6 == 'p') && (b7 == '8'):
+		m := image.NewRGBA64(image.Rect(0, 0, int(cfg.Width), int(cfg.Height)))
+		img, pix = m, m.Pix
+	}
+	if _, err := io.ReadFull(r, pix); err != nil {
+		return nil, err
+	}
+
+	// NIE is little-endian BGRA. Go is big-endian RGBA.
+	if b7 == '4' {
+		for i := 0; i < len(pix); i += 4 {
+			pix[i+0], pix[i+2] = pix[i+2], pix[i+0]
+		}
+	} else {
+		for i := 0; i < len(pix); i += 8 {
+			pix[i+0], pix[i+1], pix[i+2], pix[i+3], pix[i+4], pix[i+5], pix[i+6], pix[i+7] =
+				pix[i+5], pix[i+4], pix[i+3], pix[i+2], pix[i+1], pix[i+0], pix[i+7], pix[i+6]
+		}
+	}
+
+	return img, err
+}
diff --git a/script/convert-nie-to-png.go b/script/convert-nie-to-png.go
new file mode 100644
index 0000000..0e7918e
--- /dev/null
+++ b/script/convert-nie-to-png.go
@@ -0,0 +1,44 @@
+// Copyright 2022 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
+
+// convert-nie-to-png.go decodes NIE from stdin and encodes PNG to stdout.
+//
+// Usage: go run convert-nie-to-png.go < foo.nie > foo.png
+
+import (
+	"image/png"
+	"os"
+
+	"github.com/google/wuffs/lib/nie"
+)
+
+func main() {
+	if err := main1(); err != nil {
+		os.Stderr.WriteString(err.Error() + "\n")
+		os.Exit(1)
+	}
+}
+
+func main1() error {
+	img, err := nie.Decode(os.Stdin)
+	if err != nil {
+		return err
+	}
+	return png.Encode(os.Stdout, img)
+}