2020-06-08 03:29:51 +03:00
|
|
|
package png
|
|
|
|
|
|
|
|
// http://www.libpng.org/pub/png/spec/1.2/PNG-Contents.html
|
|
|
|
// https://ftp-osl.osuosl.org/pub/libpng/documents/pngext-1.5.0.html
|
|
|
|
// https://wiki.mozilla.org/APNG_Specification
|
|
|
|
// TODO: color types
|
|
|
|
|
|
|
|
import (
|
2021-09-19 21:49:15 +03:00
|
|
|
"compress/zlib"
|
2020-06-08 03:29:51 +03:00
|
|
|
"hash/crc32"
|
2021-08-17 13:06:32 +03:00
|
|
|
|
|
|
|
"github.com/wader/fq/format"
|
|
|
|
"github.com/wader/fq/format/registry"
|
|
|
|
"github.com/wader/fq/pkg/decode"
|
|
|
|
"github.com/wader/fq/pkg/ranges"
|
2020-06-08 03:29:51 +03:00
|
|
|
)
|
|
|
|
|
|
|
|
var iccProfileFormat []*decode.Format
|
|
|
|
var exifFormat []*decode.Format
|
|
|
|
|
|
|
|
func init() {
|
|
|
|
registry.MustRegister(&decode.Format{
|
|
|
|
Name: format.PNG,
|
|
|
|
Description: "Portable Network Graphics file",
|
|
|
|
Groups: []string{format.PROBE, format.IMAGE},
|
|
|
|
DecodeFn: pngDecode,
|
|
|
|
Dependencies: []decode.Dependency{
|
|
|
|
{Names: []string{format.ICC_PROFILE}, Formats: &iccProfileFormat},
|
|
|
|
{Names: []string{format.EXIF}, Formats: &exifFormat},
|
|
|
|
},
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
const (
|
|
|
|
compressionDeflate = 0
|
|
|
|
)
|
|
|
|
|
|
|
|
var compressionNames = map[uint64]string{
|
|
|
|
compressionDeflate: "deflate",
|
|
|
|
}
|
|
|
|
|
|
|
|
const (
|
|
|
|
disposeOpNone = 0
|
|
|
|
disposeOpBackground = 1
|
|
|
|
disposeOpPrevious = 2
|
|
|
|
)
|
|
|
|
|
|
|
|
var disposeOpNames = map[uint64]string{
|
|
|
|
disposeOpNone: "None",
|
|
|
|
disposeOpBackground: "Background",
|
|
|
|
disposeOpPrevious: "Previous",
|
|
|
|
}
|
|
|
|
|
|
|
|
const (
|
|
|
|
blendOpNone = 0
|
|
|
|
blendOpBackground = 1
|
|
|
|
)
|
|
|
|
|
|
|
|
var blendOpNames = map[uint64]string{
|
|
|
|
blendOpNone: "Source",
|
|
|
|
blendOpBackground: "Over",
|
|
|
|
}
|
|
|
|
|
|
|
|
func pngDecode(d *decode.D, in interface{}) interface{} {
|
|
|
|
iEndFound := false
|
|
|
|
|
|
|
|
d.FieldValidateUTF8("signature", "\x89PNG\r\n\x1a\n")
|
|
|
|
d.FieldStructArrayLoopFn("chunks", "chunk", func() bool { return d.NotEnd() && !iEndFound }, func(d *decode.D) {
|
|
|
|
chunkLength := int(d.FieldU32("length"))
|
|
|
|
crcStartPos := d.Pos()
|
|
|
|
chunkType := d.FieldStrFn("type", func() (string, string) {
|
|
|
|
chunkType := d.UTF8(4)
|
|
|
|
// upper/lower case in chunk type is used to set flags
|
|
|
|
d.SeekRel(-4 * 8)
|
|
|
|
d.SeekRel(3)
|
|
|
|
d.FieldBool("ancillary")
|
|
|
|
d.SeekRel(7)
|
|
|
|
d.FieldBool("private")
|
|
|
|
d.SeekRel(7)
|
|
|
|
d.FieldBool("reserved")
|
|
|
|
d.SeekRel(7)
|
|
|
|
d.FieldBool("safe_to_copy")
|
|
|
|
d.SeekRel(4)
|
|
|
|
return chunkType, ""
|
|
|
|
})
|
|
|
|
|
|
|
|
d.DecodeLenFn(int64(chunkLength)*8, func(d *decode.D) {
|
|
|
|
switch chunkType {
|
|
|
|
case "IHDR":
|
|
|
|
d.FieldU32("width")
|
|
|
|
d.FieldU32("height")
|
|
|
|
d.FieldU8("bit_depth")
|
|
|
|
d.FieldU8("color_type")
|
|
|
|
d.FieldStringMapFn("compression_method", compressionNames, "unknown", d.U8, decode.NumberDecimal)
|
|
|
|
d.FieldStringMapFn("filter_method", map[uint64]string{
|
|
|
|
0: "Adaptive filtering",
|
|
|
|
}, "unknown", d.U8, decode.NumberDecimal)
|
|
|
|
d.FieldStringMapFn("interlace_method", map[uint64]string{
|
|
|
|
0: "No interlace",
|
|
|
|
1: "Adam7 interlace",
|
|
|
|
}, "unknown", d.U8, decode.NumberDecimal)
|
|
|
|
case "tEXt":
|
|
|
|
// TODO: latin1
|
|
|
|
keywordLen := int(d.PeekFindByte(0, 80))
|
|
|
|
d.FieldUTF8("keyword", keywordLen)
|
|
|
|
d.FieldUTF8("null", 1)
|
|
|
|
d.FieldUTF8("text", chunkLength-keywordLen-1)
|
|
|
|
case "zTXt":
|
|
|
|
// TODO: latin1
|
|
|
|
keywordLen := int(d.PeekFindByte(0, 80))
|
|
|
|
d.FieldUTF8("keyword", keywordLen)
|
|
|
|
d.FieldUTF8("null", 1)
|
|
|
|
compressionMethod, _ := d.FieldStringMapFn("compression_method", compressionNames, "unknown", d.U8, decode.NumberDecimal)
|
|
|
|
// +2 to skip null and compression_method
|
|
|
|
dataLen := (chunkLength - (keywordLen + 2)) * 8
|
|
|
|
|
|
|
|
switch compressionMethod {
|
|
|
|
case compressionDeflate:
|
|
|
|
dd := d.FieldStructFn("data", func(d *decode.D) {
|
2021-09-19 21:49:15 +03:00
|
|
|
d.FieldFormatReaderLen("uncompressed", int64(dataLen), zlib.NewReader, decode.FormatFn(func(d *decode.D, in interface{}) interface{} {
|
2020-06-08 03:29:51 +03:00
|
|
|
d.FieldUTF8("text", int(d.BitsLeft()/8))
|
|
|
|
return nil
|
|
|
|
}))
|
|
|
|
})
|
|
|
|
// TODO: depends on isRoot in postProcess
|
|
|
|
dd.Value.Range = ranges.Range{Start: d.Pos() - int64(dataLen), Len: int64(dataLen)}
|
|
|
|
default:
|
|
|
|
d.FieldBitBufLen("data", int64(dataLen))
|
|
|
|
}
|
|
|
|
case "iCCP":
|
|
|
|
profileNameLen := int(d.PeekFindByte(0, 80))
|
|
|
|
d.FieldUTF8("profile_name", profileNameLen)
|
|
|
|
d.FieldUTF8("null", 1)
|
|
|
|
compressionMethod, _ := d.FieldStringMapFn("compression_method", compressionNames, "unknown", d.U8, decode.NumberDecimal)
|
|
|
|
// +2 to skip null and compression_method
|
|
|
|
dataLen := (chunkLength - (profileNameLen + 2)) * 8
|
|
|
|
|
|
|
|
switch compressionMethod {
|
|
|
|
case compressionDeflate:
|
|
|
|
dd := d.FieldStructFn("data", func(d *decode.D) {
|
2021-09-19 21:49:15 +03:00
|
|
|
d.FieldFormatReaderLen("uncompressed", int64(dataLen), zlib.NewReader, iccProfileFormat)
|
2020-06-08 03:29:51 +03:00
|
|
|
})
|
|
|
|
dd.Value.Range = ranges.Range{Start: d.Pos() - int64(dataLen), Len: int64(dataLen)}
|
|
|
|
default:
|
|
|
|
d.FieldBitBufLen("data", int64(dataLen))
|
|
|
|
}
|
|
|
|
case "pHYs":
|
|
|
|
d.FieldU32("x_pixels_per_unit")
|
|
|
|
d.FieldU32("y_pixels_per_unit")
|
|
|
|
d.FieldU8("unit")
|
|
|
|
case "bKGD":
|
|
|
|
d.FieldU16("value")
|
|
|
|
case "gAMA":
|
|
|
|
d.FieldU32("value")
|
|
|
|
case "cHRM":
|
|
|
|
df := func() (float64, string) { return float64(d.U32()) / 1000.0, "" }
|
|
|
|
d.FieldFloatFn("white_point_x", df)
|
|
|
|
d.FieldFloatFn("white_point_y", df)
|
|
|
|
d.FieldFloatFn("red_x", df)
|
|
|
|
d.FieldFloatFn("red_y", df)
|
|
|
|
d.FieldFloatFn("green_x", df)
|
|
|
|
d.FieldFloatFn("green_y", df)
|
|
|
|
d.FieldFloatFn("blue_x", df)
|
|
|
|
d.FieldFloatFn("blue_y", df)
|
|
|
|
case "eXIf":
|
2021-09-16 16:29:11 +03:00
|
|
|
d.FieldFormatLen("exif", int64(chunkLength)*8, exifFormat, nil)
|
2020-06-08 03:29:51 +03:00
|
|
|
case "acTL":
|
|
|
|
d.FieldU32("num_frames")
|
|
|
|
d.FieldU32("num_plays")
|
|
|
|
case "fcTL":
|
|
|
|
d.FieldU32("sequence_number")
|
|
|
|
d.FieldU32("width")
|
|
|
|
d.FieldU32("height")
|
|
|
|
d.FieldU32("x_offset")
|
|
|
|
d.FieldU32("y_offset")
|
|
|
|
d.FieldU16("delay_num")
|
|
|
|
d.FieldU16("delay_sep")
|
|
|
|
d.FieldStringMapFn("dispose_op", disposeOpNames, "Unknown", d.U8, decode.NumberDecimal)
|
|
|
|
d.FieldStringMapFn("blend_op", blendOpNames, "Unknown", d.U8, decode.NumberDecimal)
|
|
|
|
case "fdAT":
|
|
|
|
d.FieldU32("sequence_number")
|
|
|
|
d.FieldBitBufLen("data", int64(chunkLength-4)*8)
|
|
|
|
default:
|
|
|
|
if chunkType == "IEND" {
|
|
|
|
iEndFound = true
|
|
|
|
} else {
|
|
|
|
d.FieldBitBufLen("data", int64(chunkLength)*8)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
chunkCRC := crc32.NewIEEE()
|
2021-09-14 19:54:59 +03:00
|
|
|
decode.MustCopy(d, chunkCRC, d.BitBufRange(crcStartPos, d.Pos()-crcStartPos))
|
2020-06-08 03:29:51 +03:00
|
|
|
d.FieldChecksumLen("crc", 32, chunkCRC.Sum(nil), decode.BigEndian)
|
|
|
|
})
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|