blob: 79adca3735379aca388d32c7b5fcd644dca553dd [file] [edit]
// Copyright 2026 The Wuffs Authors.
//
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// https://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.
//
// SPDX-License-Identifier: Apache-2.0 OR MIT
//go:build ignore
// +build ignore
package main
// convert-nie-to-png.go decodes NIA from stdin and encodes (animated) PNG to
// stdout.
//
// Usage: go run convert-nia-to-apng.go < foo.nia > foo.png
import (
"bytes"
"errors"
"io"
"os"
"github.com/google/wuffs/lib/uncompng"
)
func main() {
if err := main1(); err != nil {
os.Stderr.WriteString(err.Error() + "\n")
os.Exit(1)
}
}
func main1() error {
// When writing APNG, we need to know (at the start of the file) the
// animation's number of frames, but when reading NIA, we only know that at
// the end of the file.
//
// We therefore use io.ReadAll, slurping the entire NIA data into memory
// instead of using the io.Reader streaming API directly, so that we can
// make two passes over the NIA data.
niaBytes, err := io.ReadAll(os.Stdin)
if err != nil {
return err
} else if len(niaBytes) < 24 {
return errors.New("input is not in the NIA format")
}
premultiplied := niaBytes[6] == 'p'
depth8 := niaBytes[7] == '8'
width := u32le(niaBytes[8:])
height := u32le(niaBytes[12:])
padded := !depth8 && ((width & 1) != 0) && ((height & 1) != 0)
if (u32le(niaBytes[:]) != 0x41AF_C36E) || // 0x41AF_C36E is 'nïA'le.
(niaBytes[4] != 0xFF) ||
(niaBytes[5] != 'b') ||
((niaBytes[6] != 'n') && (niaBytes[6] != 'p')) ||
((niaBytes[7] != '4') && (niaBytes[7] != '8')) ||
(width >= 0x8000_0000) ||
(height >= 0x8000_0000) {
return errors.New("input is not in the NIA format")
} else if (width > 0xFF_FFFF) || (height > 0xFF_FFFF) {
return errors.New("input is in an unsupported NIA format")
}
nieSize := calculateNIESize(depth8, width, height)
if nieSize < 0 {
return errors.New("input image dimensions are unsupported (too large)")
} else if nieSize < 16 {
panic("unreachable")
}
// NIA's depth8 is measured in *bytes* per *pixel*, but uncompng's depth is
// measured *bits* per *channel* (times four channels, for BGRA).
depth := uncompng.Depth8
if depth8 {
depth = uncompng.Depth16
}
expectedNieHeader := append([]byte(nil), niaBytes[:16]...)
expectedNieHeader[3] = 'E'
numFrames := uint64(0)
prevCDD := uint64(0)
lastCDD := uint64(0)
for buf := niaBytes[16:]; len(buf) >= 8; numFrames++ {
cdd := u64le(buf)
if (cdd >> 32) == 0x8000_0000 {
lastCDD = cdd
break
} else if ((cdd >> 32) > 0x8000_0000) || (cdd < prevCDD) {
return errors.New("bad CDD (Cumulative Display Duration)")
}
buf = buf[8:]
if int64(len(buf)) < nieSize {
return errors.New("bad NIE frame")
} else if !bytes.Equal(buf[:16], expectedNieHeader) {
return errors.New("bad NIE header")
}
buf = buf[nieSize:]
if padded {
if len(buf) < 4 {
return errors.New("bad NIE padding")
}
buf = buf[4:]
}
prevCDD = cdd
}
enc := uncompng.AnimationEncoder{}
if err := enc.EncodeHeader(
os.Stdout, depth, uncompng.ColorTypeNRGBA,
int(width), int(height), uint32(numFrames), uint32(lastCDD)); err != nil {
return err
}
prevCDD = 0
for buf := niaBytes[16:]; len(buf) >= 8; {
cdd := u64le(buf)
if (cdd >> 32) >= 0x8000_0000 {
break
}
buf = buf[8:]
if depth8 {
swapBlueRedEndian8(buf[16:nieSize])
if premultiplied {
unpremultiply8(buf[16:nieSize])
}
} else {
swapBlueRedEndian4(buf[16:nieSize])
if premultiplied {
unpremultiply4(buf[16:nieSize])
}
}
numer, denom := calculateDurationRatio(cdd - prevCDD)
if err := enc.EncodeFrame(
os.Stdout, buf[16:nieSize], 4*int(width), numer, denom); err != nil {
return err
}
buf = buf[nieSize:]
if padded {
buf = buf[4:]
}
prevCDD = cdd
}
return nil
}
// swapBlueRedEndian4 corrects for the fact that NIA uses little-endian BGRA
// order but the *Go* lib/uncompng library (unlike the *C* snippet/uncompng.c)
// uses big-endian RGBA order, the same as the Go standard library (and PNG).
func swapBlueRedEndian4(buf []byte) {
for ; len(buf) >= 4; buf = buf[4:] {
buf[0], buf[2] = buf[2], buf[0]
}
}
func swapBlueRedEndian8(buf []byte) {
for ; len(buf) >= 8; buf = buf[8:] {
buf[0], buf[1], buf[4], buf[5] = buf[5], buf[4], buf[1], buf[0]
buf[2], buf[3], buf[6], buf[7] = buf[3], buf[2], buf[7], buf[6]
}
}
func unpremultiply4(buf []byte) {
for ; len(buf) >= 4; buf = buf[4:] {
r := 0x101 * uint32(buf[0])
g := 0x101 * uint32(buf[1])
b := 0x101 * uint32(buf[2])
a := 0x101 * uint32(buf[3])
if (a == 0xFFFF) || (a == 0x0000) {
continue
}
r = (r * 0xFFFF) / a
g = (g * 0xFFFF) / a
b = (b * 0xFFFF) / a
buf[0] = byte(r >> 8)
buf[1] = byte(g >> 8)
buf[2] = byte(b >> 8)
}
}
func unpremultiply8(buf []byte) {
for ; len(buf) >= 4; buf = buf[4:] {
r := (uint32(buf[0]) << 8) | uint32(buf[1])
g := (uint32(buf[2]) << 8) | uint32(buf[3])
b := (uint32(buf[4]) << 8) | uint32(buf[5])
a := (uint32(buf[6]) << 8) | uint32(buf[7])
if (a == 0xFFFF) || (a == 0x0000) {
continue
}
r = (r * 0xFFFF) / a
g = (g * 0xFFFF) / a
b = (b * 0xFFFF) / a
buf[0] = byte(r >> 8)
buf[1] = byte(r >> 0)
buf[2] = byte(g >> 8)
buf[3] = byte(g >> 0)
buf[4] = byte(b >> 8)
buf[5] = byte(b >> 0)
}
}
// calculateDurationRatio converts from a time duration measured in flicks
// (frame-ticks, 1 / 705600000 of a second, to APNG's representation, which is
// a ratio of two uint16 values.
//
// TODO: be smarter about how to express a durationInFlicks that isn't an
// integer number of milliseconds (or is over 65.535 seconds). For now, just
// hard-code the denominator to 1000, meaning milliseconds, and round to
// nearest (capped at 65535) to get the numerator.
func calculateDurationRatio(durationInFlicks uint64) (numerator uint16, denominator uint16) {
const flicksPerMillisecond = 705600
millis := (durationInFlicks + (flicksPerMillisecond / 2)) / flicksPerMillisecond
return uint16(min(0xFFFF, millis)), 1000
}
func calculateNIESize(depth8 bool, width uint32, height uint32) int64 {
const maxInt64 = (1 << 63) - 1
const maxUint64 = (1 << 64) - 1
n := uint64(width) * uint64(height) * 4
if depth8 {
if n > (maxUint64 / 2) {
return -1
}
n *= 2
}
if n > (maxInt64 - 16) {
return -1
}
return int64(n + 16)
}
func u32le(b []byte) uint32 {
return (uint32(b[0]) << 0) |
(uint32(b[1]) << 8) |
(uint32(b[2]) << 16) |
(uint32(b[3]) << 24)
}
func u64le(b []byte) uint64 {
return (uint64(b[0]) << 0) |
(uint64(b[1]) << 8) |
(uint64(b[2]) << 16) |
(uint64(b[3]) << 24) |
(uint64(b[4]) << 32) |
(uint64(b[5]) << 40) |
(uint64(b[6]) << 48) |
(uint64(b[7]) << 56)
}