diff --git a/datatypes.go b/datatypes.go index 08f8bb6..729c910 100644 --- a/datatypes.go +++ b/datatypes.go @@ -69,6 +69,10 @@ type ImageDescriptor struct { Packed byte // image and color table data information } +/********************/ +/* EXTENSION BLOCKS */ +/********************/ + /* GraphicsControlPacked { 0: TransparentColorFlag @@ -78,10 +82,6 @@ GraphicsControlPacked { } */ -/********************/ -/* EXTENSION BLOCKS */ -/********************/ - // Only available on 89a and comes after the global color table and before the 1st image type GraphicsControlBlock struct { Packed byte // method of graphics disposal to use diff --git a/main.go b/main.go index 7f3580a..8941e13 100644 --- a/main.go +++ b/main.go @@ -31,6 +31,18 @@ func (v Palette) UnmarshalBinary(data []byte) error { return nil } +func (v Palette) MarshalBinary() []byte { + data := make([]byte, len(v)*3) + + for i := 0; i < len(v); i++ { + data[i*3] = v[i].Red + data[i*3+1] = v[i].Green + data[i*3+2] = v[i].Blue + } + + return data +} + func deinterlace() { // TODO: } @@ -59,7 +71,7 @@ func main() { fmt.Print("WARNING: some features are unimplemented for this version. Some images may not extract correctly\n\n") } fmt.Printf("GIF aspect ratio is: %v\n", header.AspectRatio) - fmt.Printf("GIF background color is: %v\n", header.BackgroundColor) + fmt.Printf("GIF background color index is: %v\n", header.BackgroundColor) fmt.Printf("GIF height is: %v\n", header.ScreenHeight) fmt.Printf("GIF width is: %v\n", header.ScreenWidth) @@ -78,20 +90,20 @@ func main() { } dirName := strings.Split(st.Name(), ".gif")[0] - fmt.Printf("%v\n", dirName) os.Mkdir(dirName, os.FileMode(0755)) - var palette Palette + var globalPalette Palette + var currentPalette Palette + transparencyIndex := -1 - if GlobalColorTableFlag == 0 { // no global color table, use default one (???) - // TODO: do we skip it. no idea if this is accurate - file.Seek(int64(GlobalColorTableSize), os.SEEK_CUR) - } else { // global color table exists, use it + if GlobalColorTableFlag == 1 { // global color table exists, use it globalColorTable := make([]byte, GlobalColorTableSize) file.Read(globalColorTable) - palette = make(Palette, GlobalColorTableEntries) - palette.UnmarshalBinary(globalColorTable) + globalPalette = make(Palette, GlobalColorTableEntries) + globalPalette.UnmarshalBinary(globalColorTable) + + currentPalette = globalPalette } counter := 1 @@ -100,16 +112,12 @@ func main() { nextByte := make([]byte, 1) file.Read(nextByte) - // fmt.Printf("next byte is: 0x%x\n", nextByte[0]) - switch nextByte[0] { case EXTENSION_BLOCK: { nextSecondByte := make([]byte, 1) file.Read(nextSecondByte) - // fmt.Printf("next second byte is: 0x%x\n", nextSecondByte[0]) - switch nextSecondByte[0] { case PLAINTEXT_BLOCK: { @@ -124,9 +132,23 @@ func main() { } case GRAPHICS_CONTROL_BLOCK: { - file.Seek(GRAPHICS_CONTROL_BLOCK_SIZE+1, os.SEEK_CUR) + // skip the length byte + file.Seek(1, os.SEEK_CUR) - // TODO: read graphics control block and do something with it ??? + // TODO: use the delayTime for something ??? + + block := GraphicsControlBlock{} + data := make([]byte, GRAPHICS_CONTROL_BLOCK_SIZE) + + file.Read(data) + + binary.Read(bytes.NewBuffer(data), binary.LittleEndian, &block) + + transparencyFlag := block.Packed & 1 + + if transparencyFlag == 1 { + transparencyIndex = int(block.TransparentColorIndex) + } break } @@ -168,27 +190,18 @@ func main() { SortFlag := (imageDescriptor.Packed & 0x20) >> 5 LocalColorTableEntries := math.Pow(2, float64((imageDescriptor.Packed&7)+1)) - // fmt.Printf("Local Color Table Flag: %v\n", LocalColorTableFlag) - // fmt.Printf("Interlace Flag: %v\n", InterlaceFlag) - // fmt.Printf("Sort Flag: %v\n", SortFlag) - // fmt.Printf("Local Color Table Entries: %v\n", LocalColorTableEntries) - - fmt.Printf("Left Offset: %v\n", imageDescriptor.Left) - fmt.Printf("Top Offset: %v\n", imageDescriptor.Top) - - // TODO: use color tables for correct colors on pixels - // TODO: write the image data to PNG + // TODO: use offsets when writing image ?? // local color table comes first - var LocalColorTable Palette + var localPalette Palette if LocalColorTableFlag == 1 { tempLocalColorTable := make([]byte, int(LocalColorTableEntries*3)) file.Read(tempLocalColorTable) - LocalColorTable = make(Palette, int(LocalColorTableEntries)) - LocalColorTable.UnmarshalBinary(tempLocalColorTable) + localPalette = make(Palette, int(LocalColorTableEntries)) + localPalette.UnmarshalBinary(tempLocalColorTable) - // TODO: use local color table + currentPalette = localPalette } if InterlaceFlag == 1 { @@ -213,17 +226,14 @@ func main() { panic(err) } - outFile, err := os.OpenFile(fmt.Sprintf("./%s/%s-raw.%v", dirName, st.Name(), counter), os.O_CREATE|os.O_RDWR, os.FileMode(0755)) - if err != nil { - panic(err) - } - - outFile.Write(decompressedFrameData) + fileName := fmt.Sprintf("./%s/%s-%v.png", dirName, dirName, counter) + WriteToPNG(decompressedFrameData, currentPalette, fileName, imageDescriptor.Width, imageDescriptor.Height, transparencyIndex) counter++ break } } + currentPalette = globalPalette // skip the terminator byte file.Seek(1, os.SEEK_CUR) diff --git a/png.go b/png.go new file mode 100644 index 0000000..09a5766 --- /dev/null +++ b/png.go @@ -0,0 +1,194 @@ +package main + +import ( + "bytes" + "compress/zlib" + "encoding/binary" + "hash/crc32" + "io/fs" + "os" +) + +func writeHeader(pngFile *os.File) { + header := []byte{0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a} + _, err := pngFile.Write(header) + if err != nil { + panic(err) + } +} + +func writeIHDR(pngFile *os.File, width uint16, height uint16) { + chunkLength := []byte{0x00, 0x00, 0x00, 0x0D} // always 13 + IHDR := []byte{0x49, 0x48, 0x44, 0x52} // IHDR string + bitDepth := byte(0x8) // number of bits per sample or per palette index (always 8 for indexed-color) + colorType := byte(0x3) // 3 is Indexed-color which is what's used for GIFs. this also means a PLTE chunk needs to exist + compressionMethod := byte(0x0) // always 0 + filterMethod := byte(0x0) // always 0 + interlaceMethod := byte(0x0) // 0 or 1 + hash := make([]byte, 4) // crc-32 hash of the previous 13 bytes + + chunkData := make([]byte, 13) + + binary.BigEndian.PutUint32(chunkData[0:4], uint32(width)) + binary.BigEndian.PutUint32(chunkData[4:8], uint32(height)) + chunkData[8] = bitDepth + chunkData[9] = colorType + chunkData[10] = compressionMethod + chunkData[11] = filterMethod + chunkData[12] = interlaceMethod + + binary.BigEndian.PutUint32(hash, crc32.Update(crc32.ChecksumIEEE(IHDR), crc32.IEEETable, chunkData)) + + _, err := pngFile.Write(chunkLength) + if err != nil { + panic(err) + } + _, err = pngFile.Write(IHDR) + if err != nil { + panic(err) + } + _, err = pngFile.Write(chunkData) + if err != nil { + panic(err) + } + _, err = pngFile.Write(hash) + if err != nil { + panic(err) + } +} + +func writePLTE(pngFile *os.File, palette Palette) { + chunkLength := make([]byte, 4) + PLTE := []byte{0x50, 0x4C, 0x54, 0x45} + hash := make([]byte, 4) + + binary.BigEndian.PutUint32(chunkLength, uint32(len(palette)*3)) + binary.BigEndian.PutUint32(hash, crc32.Update(crc32.ChecksumIEEE(PLTE), crc32.IEEETable, palette.MarshalBinary())) + + _, err := pngFile.Write(chunkLength) + if err != nil { + panic(err) + } + _, err = pngFile.Write(PLTE) + if err != nil { + panic(err) + } + _, err = pngFile.Write(palette.MarshalBinary()) + if err != nil { + panic(err) + } + _, err = pngFile.Write(hash) + if err != nil { + panic(err) + } +} + +func writetRNS(pngFile *os.File, transparencyIndex int) { + chunkLength := make([]byte, 4) + tRNS := []byte{0x74, 0x52, 0x4E, 0x53} + hash := make([]byte, 4) + + binary.BigEndian.PutUint32(chunkLength, uint32(transparencyIndex)+1) + + data := make([]byte, transparencyIndex+1) + for i := 0; i < transparencyIndex; i++ { + data[i] = 0xFF + } + + data[transparencyIndex] = 0 + + binary.BigEndian.PutUint32(hash, crc32.Update(crc32.ChecksumIEEE(tRNS), crc32.IEEETable, data)) + + _, err := pngFile.Write(chunkLength) + if err != nil { + panic(err) + } + _, err = pngFile.Write(tRNS) + if err != nil { + panic(err) + } + _, err = pngFile.Write(data) + if err != nil { + panic(err) + } + _, err = pngFile.Write(hash) + if err != nil { + panic(err) + } +} + +func serialize(data []byte, width int, height int) []byte { + b := make([]byte, 0, (width+1)*height) + for i := 0; i < height; i++ { + b = append(b, 0) + b = append(b, data[width*i:width*(i+1)]...) + } + return b +} + +func writeIDAT(pngFile *os.File, data []byte, width uint16, height uint16) { + chunkLength := make([]byte, 4) + IDAT := []byte{0x49, 0x44, 0x41, 0x54} + hash := make([]byte, 4) + + binary.BigEndian.PutUint32(chunkLength, uint32(len(data))) + binary.BigEndian.PutUint32(hash, crc32.Update(crc32.ChecksumIEEE(IDAT), crc32.IEEETable, data)) + + _, err := pngFile.Write(chunkLength) + if err != nil { + panic(err) + } + _, err = pngFile.Write(IDAT) + if err != nil { + panic(err) + } + var buf bytes.Buffer + writer, err := zlib.NewWriterLevel(&buf, zlib.BestCompression) + if err != nil { + panic(err) + } + defer writer.Close() + _, err = writer.Write(serialize(data, int(width), int(height))) + if err != nil { + panic(err) + } + writer.Flush() + _, err = pngFile.Write(buf.Bytes()) + if err != nil { + panic(err) + } + _, err = pngFile.Write(hash) + if err != nil { + panic(err) + } +} + +func writeIEND(pngFile *os.File) { + chunkLength := []byte{0x00, 0x00, 0x00, 0x00} + IEND := []byte{0x49, 0x45, 0x4E, 0x44} + hash := make([]byte, 4) + binary.BigEndian.PutUint32(hash, crc32.Update(crc32.ChecksumIEEE(IEND), crc32.IEEETable, nil)) + + IENDChunk := append(chunkLength, append(IEND, hash...)...) + _, err := pngFile.Write(IENDChunk) + if err != nil { + panic(err) + } +} + +func WriteToPNG(data []byte, palette Palette, fileName string, width uint16, height uint16, transparenyIndex int) { + pngFile, err := os.OpenFile(fileName, os.O_CREATE|os.O_WRONLY, fs.FileMode(0755)) + if err != nil { + panic(err) + } + + writeHeader(pngFile) + writeIHDR(pngFile, width, height) + writePLTE(pngFile, palette) + if transparenyIndex != -1 { + writetRNS(pngFile, transparenyIndex) + } + writeIDAT(pngFile, data, width, height) + writeIEND(pngFile) + pngFile.Close() +}