commit 10169910a6eeb278eda55853e0c8123f2c0b3cdb Author: IllusionMan1212 Date: Tue May 18 01:48:54 2021 +0300 initial commit diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..9316d6e --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 IllusionMan1212 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..a21c3d3 --- /dev/null +++ b/README.md @@ -0,0 +1,2 @@ +# gif-extractor-go +A GIF frames extractor written in go \ No newline at end of file diff --git a/blockreader.go b/blockreader.go new file mode 100644 index 0000000..ebbfe15 --- /dev/null +++ b/blockreader.go @@ -0,0 +1,72 @@ +package main + +import "io" + +func min(a, b int) int { + if a < b { + return a + } + return b +} + +func readByte(r io.Reader) (byte, error) { + var buf [1]byte + n, err := r.Read(buf[:]) + if n == 0 { + return 0, io.ErrUnexpectedEOF + } + return buf[0], err +} + +func (v *blockReader) readNextBlock() error { + blockSize, err := readByte(v.r) + if err == io.EOF { + return io.ErrUnexpectedEOF + } + if err != nil { + return err + } + if blockSize == 0 { + return io.EOF + } + _, err = io.ReadFull(v.r, v.buf[:blockSize]) + if err == io.EOF { + return io.ErrUnexpectedEOF + } + if err != nil { + return err + } + v.bufLen = int(blockSize) + v.bufNext = 0 + return nil +} + +func (v *blockReader) Read(p []byte) (n int, err error) { + if v.bufNext >= v.bufLen { + err = v.readNextBlock() + if err != nil { + return 0, err + } + } + n = min(len(p), v.bufLen-v.bufNext) + for i := 0; i < n; i++ { + p[i] = v.buf[i+v.bufNext] + } + v.bufNext += n + return +} + +type blockReader struct { + buf [255]byte + bufLen int + bufNext int + r io.Reader +} + +func newBlockReader(r io.Reader) *blockReader { + return &blockReader{ + r: r, + bufLen: 0, + bufNext: 0, + } +} diff --git a/datatypes.go b/datatypes.go new file mode 100644 index 0000000..9c4d1a4 --- /dev/null +++ b/datatypes.go @@ -0,0 +1,112 @@ +package main + +const ( + EXTENSION_BLOCK = 0x21 + + GRAPHICS_CONTROL_BLOCK = 0xF9 + GRAPHICS_CONTROL_BLOCK_SIZE = 0x04 + + PLAINTEXT_BLOCK = 0x01 + PLAINTEXT_BLOCK_SIZE = 0x0C + + APPLICATION_BLOCK = 0xFF + APPLICATION_BLOCK_SIZE = 0x0B + + COMMENT_BLOCK = 0xFE + IMAGE_DESCRIPTOR = 0x2C + TRAILER = 0x3B +) + +/* +HeaderPacked { + 0-2: GlobalColorTableSize + 3: ColorTableSortFlag | Only valid under 89a, 87a always sets it to 0 + 4-6: ColorResolution + 7: GlobalColorTableFlag +} +*/ + +type Header struct { + Signature [3]byte // "GIF" + Version [3]byte // "87a" or "89a" + + // Logical Screen Descriptor + ScreenWidth uint16 + ScreenHeight uint16 + Packed byte + BackgroundColor byte // unused if GlobalColorTableFlag is unset + AspectRatio byte +} + +// [OPTIONAL] +// Comes right after the logical screen descriptor +// Size of color table is always a power of 2, with a max of 256 entries in the table +// ColorTableEntries = 1 << ((Packed & 7) + 1) +// ColorTableSize = 3 * (1 << ((Packed & 7) + 1)) +type RGB struct { + Red byte + Green byte + Blue byte +} + +type Palette []RGB + +/* +ImageDescriptorPacked { + 0: LocalColorTableFlag | this flag is set (1) if the image contains a local color table + 1: InterlaceFlag | this flag is set (1) if the image is interlaced + 2: SortFlag | this flag is set (1) if the color table is sorted by importance (frequency of occurrence). only available on 89a + 3-4: Reserved + 5-7: LocalColorTableEntrySize +} +*/ + +type ImageDescriptor struct { + Left int16 // X position of image + Top int16 // Y position of image + Width uint16 // width of image in pixels + Height uint16 // height of image in pixels + Packed byte // image and color table data information +} + +/* +GraphicsControlPacked { + 0: TransparentColorFlag + 1: UserInputFlag + 2-4: DisposalMethod + 5-7: Reserved +} +*/ + +/********************/ +/* 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 + DelayTime int16 // delay to wait + TransparentColorIndex byte // transparent color index +} + +type PlainTextBlock struct { + TextGridLeft int16 // X position of text grid in pixels + TextGridTop int16 // Y position of the text grid in pixels + TextGridWidth int16 // width of text grid in pixels + TextGridHeight int16 // height of text grid in pixels + CellWidth byte // width of grid cell in pixels + CellHeight byte // height of grid cell in pixels + TextFgColorIndex byte // text foreground color index value + TextBgColorIndex byte // text background color index value + PlainTextData *byte // the plaintext data +} + +type ApplicationExtensionBlock struct { + Identifier [8]byte // application identifier + AuthentCode [3]byte // application authentication code + ApplicationData *byte // application data +} + +type CommentExtensionBlock struct { + CommentData *byte // comment data +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..29b7666 --- /dev/null +++ b/main.go @@ -0,0 +1,197 @@ +package main + +import ( + "bytes" + "compress/lzw" + "encoding/binary" + "fmt" + "io" + "os" + "strings" +) + +func readNextBytes(file *os.File) byte { + nextBytes := make([]byte, 1) + file.Read(nextBytes) + file.Seek(-1, os.SEEK_CUR) + + return nextBytes[0] +} + +func (v Palette) UnmarshalBinary(data []byte) error { + if len(v)*3 != len(data) { + return fmt.Errorf("len is not valid. required: %d, actual: %d", len(v)*3, len(data)) + } + for i := 0; i < len(v); i++ { + v[i].Red = data[i*3] + v[i].Green = data[i*3+1] + v[i].Blue = data[i*3+2] + } + return nil +} + +func main() { + inputFile := os.Args[1] + + file, err := os.OpenFile(inputFile, os.O_RDONLY, 0x755) + if err != nil { + panic(err) + } + + header := Header{} + headerData := make([]byte, 13) + file.Read(headerData) + binary.Read(bytes.NewBuffer(headerData), binary.LittleEndian, &header) + + if strings.ToUpper(string(header.Signature[:])) != "GIF" { + panic("Not a valid gif file") + } + + fmt.Print("Beginning extraction of gif frames\n\n") + + fmt.Printf("GIF version is: %v\n", string(header.Version[:])) + if string(header.Version[:]) == "89a" { + 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 height is: %v\n", header.ScreenHeight) + fmt.Printf("GIF width is: %v\n", header.ScreenWidth) + + GlobalColorTableFlag := (header.Packed & 0x80) >> 7 + GlobalColorTableEntries := 1 << ((header.Packed & 7) + 1) + GlobalColorTableSize := 3 * GlobalColorTableEntries // number of entries * 3 colors (r, g, b) + + fmt.Printf("\n") + fmt.Printf("Global color table flag: %v\n", GlobalColorTableFlag) + fmt.Printf("Global color table size: %v\n", GlobalColorTableSize) + fmt.Printf("global color table entries: %v\n", GlobalColorTableEntries) + + var palette Palette + + 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 + globalColorTable := make([]byte, GlobalColorTableSize) + file.Read(globalColorTable) + + palette = make(Palette, GlobalColorTableEntries) + palette.UnmarshalBinary(globalColorTable) + } + + for i := 1; readNextBytes(file) != TRAILER; i++ { + 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: + { + file.Seek(PLAINTEXT_BLOCK_SIZE+1, os.SEEK_CUR) + plaintextDataSize := make([]byte, 1) + file.Read(plaintextDataSize) + file.Seek(int64(plaintextDataSize[0]), os.SEEK_CUR) + + // TODO: read plaintext data and draw it to the frame (somehow idk ???) + + break + } + case GRAPHICS_CONTROL_BLOCK: + { + file.Seek(GRAPHICS_CONTROL_BLOCK_SIZE+1, os.SEEK_CUR) + + // TODO: read graphics control block and do something with it ??? + + break + } + case APPLICATION_BLOCK: + { + file.Seek(APPLICATION_BLOCK_SIZE+1, os.SEEK_CUR) + applicationDataSize := make([]byte, 1) + file.Read(applicationDataSize) + file.Seek(int64(applicationDataSize[0]), os.SEEK_CUR) + + // TODO: read application data and do something with it ??? + + break + } + case COMMENT_BLOCK: + { + commentDataSize := make([]byte, 1) + file.Read(commentDataSize) + file.Seek(int64(commentDataSize[0]), os.SEEK_CUR) + + // TODO: read comment data and output it to file ??? + + break + } + } + break + } + case IMAGE_DESCRIPTOR: + { + imageDescriptor := ImageDescriptor{} + imageDescriptorData := make([]byte, 9) + file.Read(imageDescriptorData) + binary.Read(bytes.NewBuffer(imageDescriptorData), binary.LittleEndian, &imageDescriptor) + + decompressedFrameSize := uint64(int64(imageDescriptor.Height) * int64(imageDescriptor.Width)) + + LocalColorTableFlag := imageDescriptor.Packed & 1 + InterlaceFlag := (imageDescriptor.Packed & 2) >> 1 + SortFlag := (imageDescriptor.Packed & 4) >> 2 + LocalColorTableSize := (imageDescriptor.Packed & 0xE0) >> 5 + + 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 Size: %v\n", LocalColorTableSize) + + // TODO: implement deinterlacing + // TODO: implement sort flag thingy ??? + // TODO: implement local color table + // TODO: use color tables for correct colors on pixels + // TODO: write the image data to PNG + + LZWCodeSize := make([]byte, 1) + file.Read(LZWCodeSize) + + reader := lzw.NewReader(newBlockReader(file), lzw.LSB, int(LZWCodeSize[0])) + defer reader.Close() + + decompressedFrameData := make([]byte, decompressedFrameSize) + + _, err = io.ReadFull(reader, decompressedFrameData) + if err != nil { + panic(err) + } + + outFile, err := os.OpenFile(fmt.Sprintf("%s-raw.%v", file.Name(), i), os.O_CREATE|os.O_RDWR, 0755) + if err != nil { + panic(err) + } + + outFile.Write(decompressedFrameData) + + break + } + } + + // skip the terminator byte + file.Seek(1, os.SEEK_CUR) + } + + fmt.Print("Extracted frames from gif successfully!\n") + + file.Close() +}