1package png
2
3// http://www.libpng.org/pub/png/spec/1.2/PNG-Contents.html
4// https://ftp-osl.osuosl.org/pub/libpng/documents/pngext-1.5.0.html
5// https://wiki.mozilla.org/APNG_Specification
6
7import (
8	"compress/zlib"
9	"hash/crc32"
10
11	"github.com/wader/fq/format"
12	"github.com/wader/fq/format/registry"
13	"github.com/wader/fq/pkg/decode"
14	"github.com/wader/fq/pkg/scalar"
15)
16
17var iccProfileFormat decode.Group
18var exifFormat decode.Group
19
20func init() {
21	registry.MustRegister(decode.Format{
22		Name:        format.PNG,
23		Description: "Portable Network Graphics file",
24		Groups:      []string{format.PROBE, format.IMAGE},
25		DecodeFn:    pngDecode,
26		Dependencies: []decode.Dependency{
27			{Names: []string{format.ICC_PROFILE}, Group: &iccProfileFormat},
28			{Names: []string{format.EXIF}, Group: &exifFormat},
29		},
30	})
31}
32
33const (
34	compressionDeflate = 0
35)
36
37var compressionNames = scalar.UToSymStr{
38	compressionDeflate: "deflate",
39}
40
41const (
42	disposeOpNone       = 0
43	disposeOpBackground = 1
44	disposeOpPrevious   = 2
45)
46
47var disposeOpNames = scalar.UToSymStr{
48	disposeOpNone:       "none",
49	disposeOpBackground: "background",
50	disposeOpPrevious:   "previous",
51}
52
53const (
54	blendOpNone       = 0
55	blendOpBackground = 1
56)
57
58var blendOpNames = scalar.UToSymStr{
59	blendOpNone:       "source",
60	blendOpBackground: "over",
61}
62
63const (
64	colorTypeGrayscale          = 0
65	colorTypeRGB                = 2
66	colorTypePalette            = 3
67	colorTypeGrayscaleWithAlpha = 4
68	colorTypeRGBA               = 6
69)
70
71var colorTypeMap = scalar.UToScalar{
72	colorTypeGrayscale:          {Sym: "g", Description: "Grayscale"},
73	colorTypeRGB:                {Sym: "rgb", Description: "RGB"},
74	colorTypePalette:            {Sym: "p", Description: "Palette"},
75	colorTypeGrayscaleWithAlpha: {Sym: "ga", Description: "Grayscale with alpha"},
76	colorTypeRGBA:               {Sym: "rgba", Description: "RGBA"},
77}
78
79func pngDecode(d *decode.D, in interface{}) interface{} {
80	iEndFound := false
81	var colorType uint64
82
83	d.FieldRawLen("signature", 8*8, d.AssertBitBuf([]byte("\x89PNG\r\n\x1a\n")))
84	d.FieldStructArrayLoop("chunks", "chunk", func() bool { return d.NotEnd() && !iEndFound }, func(d *decode.D) {
85		chunkLength := d.FieldU32("length")
86		crcStartPos := d.Pos()
87		// TODO: this is a bit weird, use struct?
88		chunkType := d.FieldStrFn("type", func(d *decode.D) string {
89			chunkType := d.UTF8(4)
90			// upper/lower case in chunk type is used to set flags
91			d.SeekRel(-4 * 8)
92			d.SeekRel(3)
93			d.FieldBool("ancillary")
94			d.SeekRel(7)
95			d.FieldBool("private")
96			d.SeekRel(7)
97			d.FieldBool("reserved")
98			d.SeekRel(7)
99			d.FieldBool("safe_to_copy")
100			d.SeekRel(4)
101			return chunkType
102		})
103
104		d.LenFn(int64(chunkLength)*8, func(d *decode.D) {
105			switch chunkType {
106			case "IHDR":
107				d.FieldU32("width")
108				d.FieldU32("height")
109				d.FieldU8("bit_depth")
110				colorType = d.FieldU8("color_type", colorTypeMap)
111				d.FieldU8("compression_method", compressionNames)
112				d.FieldU8("filter_method", scalar.UToSymStr{
113					0: "Adaptive filtering",
114				})
115				d.FieldU8("interlace_method", scalar.UToSymStr{
116					0: "No interlace",
117					1: "Adam7 interlace",
118				})
119			case "tEXt":
120				d.FieldUTF8Null("keyword")
121				d.FieldUTF8("text", int(d.BitsLeft())/8)
122			case "zTXt":
123				d.FieldUTF8Null("keyword")
124				compressionMethod := d.FieldU8("compression_method", compressionNames)
125				dataLen := d.BitsLeft()
126
127				// TODO: make nicer
128				d.FieldRawLen("compressed", dataLen)
129				d.SeekRel(-dataLen)
130
131				switch compressionMethod {
132				case compressionDeflate:
133					d.FieldFormatReaderLen("uncompressed", dataLen, zlib.NewReader, decode.FormatFn(func(d *decode.D, in interface{}) interface{} {
134						d.FieldUTF8("text", int(d.BitsLeft()/8))
135						return nil
136					}))
137				default:
138					d.FieldRawLen("data", dataLen)
139				}
140			case "iCCP":
141				d.FieldUTF8Null("profile_name")
142				compressionMethod := d.FieldU8("compression_method", compressionNames)
143				dataLen := d.BitsLeft()
144
145				d.FieldRawLen("compressed", dataLen)
146				d.SeekRel(-dataLen)
147
148				switch compressionMethod {
149				case compressionDeflate:
150					d.FieldFormatReaderLen("uncompressed", dataLen, zlib.NewReader, iccProfileFormat)
151				default:
152					d.FieldRawLen("data", dataLen)
153				}
154			case "pHYs":
155				d.FieldU32("x_pixels_per_unit")
156				d.FieldU32("y_pixels_per_unit")
157				d.FieldU8("unit")
158			case "bKGD":
159				switch colorType {
160				case colorTypePalette:
161					d.FieldU8("index")
162				case colorTypeGrayscale, colorTypeGrayscaleWithAlpha:
163					d.FieldU16("gray")
164				case colorTypeRGB, colorTypeRGBA:
165					d.FieldU16("r")
166					d.FieldU16("g")
167					d.FieldU16("b")
168				}
169			case "gAMA":
170				d.FieldU32("value")
171			case "cHRM":
172				df := func(d *decode.D) float64 { return float64(d.U32()) / 1000.0 }
173				d.FieldFFn("white_point_x", df)
174				d.FieldFFn("white_point_y", df)
175				d.FieldFFn("red_x", df)
176				d.FieldFFn("red_y", df)
177				d.FieldFFn("green_x", df)
178				d.FieldFFn("green_y", df)
179				d.FieldFFn("blue_x", df)
180				d.FieldFFn("blue_y", df)
181			case "eXIf":
182				d.FieldFormatLen("exif", d.BitsLeft(), exifFormat, nil)
183			case "acTL":
184				d.FieldU32("num_frames")
185				d.FieldU32("num_plays")
186			case "fcTL":
187				d.FieldU32("sequence_number")
188				d.FieldU32("width")
189				d.FieldU32("height")
190				d.FieldU32("x_offset")
191				d.FieldU32("y_offset")
192				d.FieldU16("delay_num")
193				d.FieldU16("delay_sep")
194				d.FieldU8("dispose_op", disposeOpNames)
195				d.FieldU8("blend_op", blendOpNames)
196			case "fdAT":
197				d.FieldU32("sequence_number")
198				d.FieldRawLen("data", d.BitsLeft()-32)
199			default:
200				if chunkType == "IEND" {
201					iEndFound = true
202				} else {
203					d.FieldRawLen("data", d.BitsLeft())
204				}
205			}
206		})
207
208		chunkCRC := crc32.NewIEEE()
209		d.MustCopy(chunkCRC, d.BitBufRange(crcStartPos, d.Pos()-crcStartPos))
210		d.FieldU32("crc", d.ValidateUBytes(chunkCRC.Sum(nil)), scalar.Hex)
211	})
212
213	return nil
214}
215