// Package rasterimage decodes a deterministic still image from supported // raster formats. For animated inputs, the returned image is the first // animation frame composited onto the format's logical canvas.
package rasterimage import ( _ _ ) // Config validates the bounded container structure and reports the logical // canvas decoded by DecodeFirst. JPEG dimensions include EXIF Orientation. // Animated is true when the input contains an animation rather than a still. func ( context.Context, []byte, string) ( image.Config, bool, error) { if == nil { = context.Background() } if := contextError(); != nil { return image.Config{}, false, } switch { case "gif": , = inspectGIF(, ) if != nil { return image.Config{}, false, } , = gif.DecodeConfig(contextReader{ctx: , reader: bytes.NewReader()}) case "png": , := inspectPNG(, ) if != nil { return image.Config{}, false, } = .animated , _, = image.DecodeConfig(contextReader{ctx: , reader: bytes.NewReader()}) case "webp": , := inspectWebP(, , false) if != nil { return image.Config{}, false, } = .animated if ! { , _, = image.DecodeConfig(contextReader{ctx: , reader: bytes.NewReader()}) break } = image.Config{ColorModel: color.NRGBAModel, Width: .canvas.Dx(), Height: .canvas.Dy()} case "jpeg": , := jpegEXIFOrientation(, ) if != nil { return image.Config{}, false, } , _, = image.DecodeConfig(contextReader{ctx: , reader: bytes.NewReader()}) if == nil { = orientedConfig(, ) } default: , _, = image.DecodeConfig(contextReader{ctx: , reader: bytes.NewReader()}) } if != nil { if := contextError(); != nil { return image.Config{}, false, } return image.Config{}, false, } if .Width <= 0 || .Height <= 0 { return image.Config{}, false, fmt.Errorf("invalid decoded dimensions %dx%d", .Width, .Height) } return , , nil } // DecodeFirst returns the first animation frame on the logical canvas and // applies JPEG EXIF Orientation. It validates container framing but does not // decode later animation frames. func ( context.Context, []byte, string) (image.Image, error) { if == nil { = context.Background() } if := contextError(); != nil { return nil, } switch { case "gif": if , := inspectGIF(, ); != nil { return nil, } , := gif.DecodeConfig(contextReader{ctx: , reader: bytes.NewReader()}) if != nil { return nil, decodeError(, ) } , := gif.Decode(contextReader{ctx: , reader: bytes.NewReader()}) if != nil { return nil, decodeError(, ) } := image.Rect(0, 0, .Width, .Height) if .Bounds() == { return , nil } if .Bounds().Empty() || !.Bounds().In() { return nil, fmt.Errorf("GIF first-frame bounds %v fall outside logical canvas %v", .Bounds(), ) } return logicalImage{Image: , bounds: }, nil case "png": , := inspectPNG(, ) if != nil { return nil, } if !.animated || .pngFrameUsesIDAT { , := png.Decode(contextReader{ctx: , reader: bytes.NewReader()}) if != nil { return nil, decodeError(, ) } return , nil } , := buildFramePNG(, , ) if != nil { return nil, } , := png.Decode(contextReader{ctx: , reader: bytes.NewReader()}) if != nil { return nil, decodeError(, ) } return compositeFrame(, .canvas, .frame, , .sourceBlend) case "webp": , := inspectWebP(, , true) if != nil { return nil, } := if .animated { = .firstFrame } , , := image.Decode(contextReader{ctx: , reader: bytes.NewReader()}) if != nil { return nil, decodeError(, ) } if !.animated { return , nil } return compositeFrame(, .canvas, .frame, , .sourceBlend) case "jpeg": , := jpegEXIFOrientation(, ) if != nil { return nil, } , , := image.Decode(contextReader{ctx: , reader: bytes.NewReader()}) if != nil { return nil, decodeError(, ) } if := contextError(); != nil { return nil, } return orientImage(, ), nil default: , , := image.Decode(contextReader{ctx: , reader: bytes.NewReader()}) if != nil { return nil, decodeError(, ) } return , nil } } func decodeError( context.Context, error) error { if := contextError(); != nil { return } return } type logicalImage struct { image.Image bounds image.Rectangle } func ( logicalImage) () image.Rectangle { return .bounds } func ( logicalImage) (, int) color.Color { if !(image.Point{X: , Y: }).In(.Image.Bounds()) { return color.NRGBA{} } return .Image.At(, ) } func compositeFrame( context.Context, , image.Rectangle, image.Image, bool) (image.Image, error) { if := contextError(); != nil { return nil, } if == && .Bounds().Dx() == .Dx() && .Bounds().Dy() == .Dy() { return , nil } := image.NewNRGBA() := draw.Over if { = draw.Src } const = 64 << 10 := / max(1, .Dx()) if < 1 { = 1 } else if > 64 { = 64 } for := .Min.Y; < .Max.Y; += { if := contextError(); != nil { return nil, } := + if > .Max.Y { = .Max.Y } := image.Rect(.Min.X, , .Max.X, ) := .Bounds().Min.Add(image.Pt(0, -.Min.Y)) draw.Draw(, , , , ) } if := contextError(); != nil { return nil, } return , nil } func inspectGIF( context.Context, []byte) (bool, error) { if len() < 13 { return false, errors.New("truncated logical screen descriptor") } := 13 if [10]&0x80 != 0 { += 3 * (1 << (([10] & 0x07) + 1)) if > len() { return false, errors.New("truncated global color table") } } := 0 for { if := contextError(); != nil { return false, } if >= len() { return false, errors.New("missing GIF trailer") } := [] ++ switch { case 0x3b: if == 0 { return false, errors.New("GIF has no image frame") } return > 1, nil case 0x21: if >= len() { return false, errors.New("truncated GIF extension") } ++ var error , = skipGIFSubBlocks(, , ) if != nil { return false, } case 0x2c: ++ if len()- < 9 { return false, errors.New("truncated GIF image descriptor") } := [+8] += 9 if &0x80 != 0 { += 3 * (1 << (( & 0x07) + 1)) if > len() { return false, errors.New("truncated local color table") } } if >= len() { return false, errors.New("missing GIF LZW code size") } ++ var error , = skipGIFSubBlocks(, , ) if != nil { return false, } default: return false, fmt.Errorf("unexpected GIF block marker 0x%02x", ) } } } func skipGIFSubBlocks( context.Context, []byte, int) (int, error) { for { if := contextError(); != nil { return 0, } if >= len() { return 0, errors.New("truncated GIF sub-blocks") } := int([]) ++ if == 0 { return , nil } if > len()- { return 0, errors.New("truncated GIF sub-block data") } += } } type animationPlan struct { animated bool firstFrameSeen bool canvas image.Rectangle frame image.Rectangle firstFrame []byte sourceBlend bool pngPrefixBytes int pngFrameBytes int pngIHDR [13]byte pngFrameUsesIDAT bool } const ( pngChunkIHDR = uint32('I')<<24 | uint32('H')<<16 | uint32('D')<<8 | uint32('R') pngChunkAcTL = uint32('a')<<24 | uint32('c')<<16 | uint32('T')<<8 | uint32('L') pngChunkFcTL = uint32('f')<<24 | uint32('c')<<16 | uint32('T')<<8 | uint32('L') pngChunkIDAT = uint32('I')<<24 | uint32('D')<<16 | uint32('A')<<8 | uint32('T') pngChunkFdAT = uint32('f')<<24 | uint32('d')<<16 | uint32('A')<<8 | uint32('T') pngChunkIEND = uint32('I')<<24 | uint32('E')<<16 | uint32('N')<<8 | uint32('D') ) func inspectPNG( context.Context, []byte) (animationPlan, error) { if len() < 8 || !bytes.Equal([:8], []byte("\x89PNG\r\n\x1a\n")) { return animationPlan{}, errors.New("invalid PNG signature") } var animationPlan var []byte var uint32 var uint64 var uint64 := false := false := false := false := false := false := false := 8 for { if := contextError(); != nil { return animationPlan{}, } if len()- < 12 { return animationPlan{}, errors.New("truncated PNG chunk") } := uint64(binary.BigEndian.Uint32([ : +4])) := + 12 if > uint64(len()-) { return animationPlan{}, errors.New("truncated PNG chunk data") } := [ : +int()] := binary.BigEndian.Uint32([4:8]) := [8 : 8+int()] , := validPNGChunkChecksum(, ) if != nil { return animationPlan{}, } if ! { return animationPlan{}, fmt.Errorf("invalid PNG %s checksum", [4:8]) } switch { case pngChunkIHDR: if || != 8 || len() != 13 { return animationPlan{}, errors.New("invalid PNG IHDR") } = true copy(.pngIHDR[:], ) := binary.BigEndian.Uint32([0:4]) := binary.BigEndian.Uint32([4:8]) if == 0 || == 0 || uint64() > uint64(math.MaxInt) || uint64() > uint64(math.MaxInt) { return animationPlan{}, errors.New("invalid PNG canvas dimensions") } .canvas = image.Rect(0, 0, int(), int()) case pngChunkAcTL: if ! || || || len() != 8 || binary.BigEndian.Uint32([:4]) == 0 { return animationPlan{}, errors.New("invalid APNG animation control chunk") } = true .animated = true = binary.BigEndian.Uint32([:4]) if > uint64() { return animationPlan{}, errors.New("APNG frame count exceeds acTL declaration") } case pngChunkFcTL: // APNG permits the default image's fcTL on either side of acTL, // provided both precede IDAT. No later frame may begin before acTL. if ! || len() != 26 || (! && ( || != 0)) { return animationPlan{}, errors.New("invalid APNG frame control chunk") } if && ! { return animationPlan{}, errors.New("APNG frame has no data") } if := consumeAPNGSequence([:4], &); != nil { return animationPlan{}, } ++ if && > uint64() { return animationPlan{}, errors.New("APNG frame count exceeds acTL declaration") } , , := apngFrame(, .canvas) if != nil { return animationPlan{}, } = true = false = == 1 && ! = == 1 if { = .firstFrameSeen = true .frame = .sourceBlend = .pngFrameUsesIDAT = if && != .canvas { return animationPlan{}, errors.New("APNG IDAT first frame does not cover its logical canvas") } } case pngChunkIDAT: if ! { return animationPlan{}, errors.New("PNG IDAT precedes IHDR") } if && ! { return animationPlan{}, errors.New("APNG IDAT data precedes animation control") } if && ! { return animationPlan{}, errors.New("APNG IDAT data follows a non-IDAT frame control") } = true if && len() != 0 { = true } case pngChunkFdAT: if ! || ! { return animationPlan{}, errors.New("APNG frame data precedes frame control") } if len() < 4 || { return animationPlan{}, errors.New("invalid APNG frame data chunk") } if := consumeAPNGSequence([:4], &); != nil { return animationPlan{}, } if len() > 4 { = true } if { .pngFrameBytes, = checkedAddInt(.pngFrameBytes, len()-4, "APNG first-frame data bytes") if != nil { return animationPlan{}, } } case pngChunkIEND: if len() != 0 { return animationPlan{}, errors.New("invalid PNG IEND") } if ! { return animationPlan{}, errors.New("PNG is missing IHDR") } if != 0 && ! { return animationPlan{}, errors.New("APNG is missing an animation control chunk") } if !.animated { return , nil } if == nil { return animationPlan{}, errors.New("APNG is missing a first frame control chunk") } if ! { return animationPlan{}, errors.New("APNG frame has no data") } if != uint64() { return animationPlan{}, fmt.Errorf("APNG contains %d frames, acTL declares %d", , ) } if !.pngFrameUsesIDAT && .pngFrameBytes == 0 { return animationPlan{}, errors.New("APNG first frame has no data") } return , nil default: if ! { .pngPrefixBytes, = checkedAddInt(.pngPrefixBytes, len(), "APNG prefix bytes") if != nil { return animationPlan{}, } } } += int() } } func apngFrame( []byte, image.Rectangle) (image.Rectangle, bool, error) { := binary.BigEndian.Uint32([4:8]) := binary.BigEndian.Uint32([8:12]) := binary.BigEndian.Uint32([12:16]) := binary.BigEndian.Uint32([16:20]) if == 0 || == 0 || uint64()+uint64() > uint64(.Dx()) || uint64()+uint64() > uint64(.Dy()) { return image.Rectangle{}, false, errors.New("APNG frame falls outside its logical canvas") } if [24] > 2 || [25] > 1 { return image.Rectangle{}, false, errors.New("APNG frame has invalid dispose or blend operation") } return image.Rect(int(), int(), int(+), int(+)), [25] == 0, nil } // buildFramePNG makes a still PNG for an APNG whose default image is not part // of the animation. inspectPNG is the first pass: it validates the complete // container and computes the exact amount of prefix and first-frame data. This // second pass uses one allocation and never retains a slice per input chunk. func buildFramePNG( context.Context, []byte, animationPlan) ([]byte, error) { if := contextError(); != nil { return nil, } if !.animated || !.firstFrameSeen || .pngFrameUsesIDAT || .frame.Empty() { return nil, errors.New("invalid APNG first-frame materialization plan") } , := firstFramePNGSize(.pngPrefixBytes, .pngFrameBytes) if != nil { return nil, } if .pngPrefixBytes > len() || .pngFrameBytes > len() { return nil, errors.New("APNG first-frame materialization plan exceeds input") } := make([]byte, 0, ) , = appendContext(, , []byte("\x89PNG\r\n\x1a\n")) if != nil { return nil, } := .pngIHDR binary.BigEndian.PutUint32([0:4], uint32(.frame.Dx())) binary.BigEndian.PutUint32([4:8], uint32(.frame.Dy())) = appendPNGChunk(, "IHDR", [:]) := false := false := false := 0 var uint32 := 8 for < len() { if := contextError(); != nil { return nil, } if len()- < 12 { return nil, errors.New("truncated PNG chunk during first-frame materialization") } := uint64(binary.BigEndian.Uint32([ : +4])) := + 12 if > uint64(len()-) { return nil, errors.New("truncated PNG chunk data during first-frame materialization") } := [ : +int()] := binary.BigEndian.Uint32([4:8]) := [8 : 8+int()] switch { case pngChunkIHDR, pngChunkAcTL: case pngChunkIDAT: = true case pngChunkFcTL: if ! { return nil, errors.New("APNG first-frame materialization unexpectedly uses IDAT") } if { // inspectPNG already validated the remaining frames. The second // pass only needs to materialize the first one. = len() continue } else { = true = true if cap()-len() < 8 { return nil, errors.New("APNG first-frame output size overflow") } = binary.BigEndian.AppendUint32(, uint32(.pngFrameBytes)) = append(, "IDAT"...) = crc32.Update(0, crc32.IEEETable, []byte("IDAT")) } case pngChunkFdAT: if { if len() < 4 { return nil, errors.New("invalid APNG frame data during first-frame materialization") } := [4:] , = checkedAddInt(, len(), "APNG copied first-frame data bytes") if != nil { return nil, } if > .pngFrameBytes { return nil, errors.New("APNG first-frame data exceeds materialization plan") } , , = appendAndChecksumContext(, , , ) if != nil { return nil, } } case pngChunkIEND: = len() continue default: if ! { , = appendContext(, , ) if != nil { return nil, } } } += int() } if ! || != .pngFrameBytes { return nil, fmt.Errorf("APNG copied %d first-frame bytes, expected %d", , .pngFrameBytes) } if cap()-len() < 4 { return nil, errors.New("APNG first-frame output size overflow") } = binary.BigEndian.AppendUint32(, ) = appendPNGChunk(, "IEND", nil) if len() != || cap() != { return nil, fmt.Errorf("APNG first-frame output is %d bytes, expected %d", len(), ) } if := contextError(); != nil { return nil, } return , nil } func firstFramePNGSize(, int) (int, error) { if < 0 || < 0 || uint64() > uint64(math.MaxUint32) { return 0, errors.New("APNG first-frame output size overflow") } const = 8 + 25 + 12 + 12 // signature, IHDR, IDAT framing, IEND := uint64() + uint64() + uint64() if > uint64(math.MaxInt) { return 0, errors.New("APNG first-frame output size overflow") } return int(), nil } func appendContext( context.Context, , []byte) ([]byte, error) { if len() > cap()-len() { return nil, errors.New("APNG first-frame output size overflow") } := len() = [:+len()] const = 32 << 10 for := 0; < len(); { if := contextError(); != nil { return nil, } := + if > len() { = len() } copy([+:+], [:]) = } return , contextError() } func appendAndChecksumContext( context.Context, , []byte, uint32) ([]byte, uint32, error) { if len() > cap()-len() { return nil, 0, errors.New("APNG first-frame output size overflow") } := len() = [:+len()] const = 32 << 10 for := 0; < len(); { if := contextError(); != nil { return nil, 0, } := + if > len() { = len() } := [:] copy([+:+], ) = crc32.Update(, crc32.IEEETable, ) = } if := contextError(); != nil { return nil, 0, } return , , nil } func validPNGChunkChecksum( context.Context, []byte) (bool, error) { if len() < 12 { return false, errors.New("truncated PNG chunk") } := uint32(0) := [4 : len()-4] const = 32 << 10 for := 0; < len(); { if := contextError(); != nil { return false, } := + if > len() { = len() } = crc32.Update(, crc32.IEEETable, [:]) = } if := contextError(); != nil { return false, } return == binary.BigEndian.Uint32([len()-4:]), nil } func consumeAPNGSequence( []byte, *uint64) error { if len() != 4 { return errors.New("invalid APNG sequence number") } := uint64(binary.BigEndian.Uint32()) if != * { return fmt.Errorf("APNG sequence number is %d, expected %d", , *) } (*)++ return nil } func checkedAddInt(, int, string) (int, error) { if < 0 || < 0 || > math.MaxInt- { return 0, fmt.Errorf("%s overflow", ) } return + , nil } func appendPNGChunk( []byte, string, []byte) []byte { := len() = binary.BigEndian.AppendUint32(, uint32(len())) = append(, ...) = append(, ...) := crc32.ChecksumIEEE([+4:]) return binary.BigEndian.AppendUint32(, ) } func inspectWebP( context.Context, []byte, bool) (animationPlan, error) { if len() < 12 || !bytes.Equal([:4], []byte("RIFF")) || !bytes.Equal([8:12], []byte("WEBP")) { return animationPlan{}, errors.New("invalid WebP RIFF header") } := uint64(binary.LittleEndian.Uint32([4:8])) + 8 if != uint64(len()) || < 12 { return animationPlan{}, errors.New("invalid WebP RIFF length") } var animationPlan var , bool := 0 := -1 var []byte := false := 0 for := 12; uint64() < ; { if := contextError(); != nil { return animationPlan{}, } , , , := webPChunk(, , int()) if != nil { return animationPlan{}, } switch { case "VP8X": if || len() != 10 { return animationPlan{}, errors.New("invalid WebP VP8X chunk") } = true = [0]&0x02 != 0 := readUint24([4:7]) + 1 := readUint24([7:10]) + 1 if <= 0 || <= 0 { return animationPlan{}, errors.New("invalid WebP canvas dimensions") } .canvas = image.Rect(0, 0, , ) case "ANIM": ++ if == 1 { = len() } case "ANMF": if ! { = = true } case "VP8 ", "VP8L": ++ } = } if { if ! || != 1 || != 6 || ! { return animationPlan{}, errors.New("incomplete animated WebP container") } , , , := extractWebPFrame(, , ) if != nil { return animationPlan{}, } .animated = true .firstFrameSeen = true .frame = .sourceBlend = .firstFrame = if .frame.Min.X < 0 || .frame.Min.Y < 0 || .frame.Max.X > .canvas.Dx() || .frame.Max.Y > .canvas.Dy() { return animationPlan{}, errors.New("WebP first frame falls outside its logical canvas") } return , nil } if != 1 { return animationPlan{}, fmt.Errorf("expected exactly one static WebP image bitstream, got %d", ) } return , nil } func webPChunk( []byte, , int) (string, []byte, int, error) { if - < 8 { return "", nil, 0, errors.New("truncated WebP chunk header") } := uint64(binary.LittleEndian.Uint32([+4 : +8])) := + %2 := uint64() + 8 + if > uint64() { return "", nil, 0, errors.New("truncated WebP chunk data") } return string([ : +4]), [+8 : +8+int()], int(), nil } func extractWebPFrame( context.Context, []byte, bool) (image.Rectangle, bool, []byte, error) { if len() < 16 { return image.Rectangle{}, false, nil, errors.New("truncated WebP ANMF header") } := readUint24([0:3]) * 2 := readUint24([3:6]) * 2 := readUint24([6:9]) + 1 := readUint24([9:12]) + 1 if <= 0 || <= 0 || > math.MaxInt- || > math.MaxInt- { return image.Rectangle{}, false, nil, errors.New("invalid WebP first-frame dimensions") } := image.Rect(, , +, +) := [15]&0x02 != 0 var , []byte var []byte := "" for := 16; < len(); { if := contextError(); != nil { return image.Rectangle{}, false, nil, } , , , := webPChunk(, , len()) if != nil { return image.Rectangle{}, false, nil, } switch { case "ALPH": if != nil { return image.Rectangle{}, false, nil, errors.New("multiple WebP ALPH chunks in first frame") } if { = appendWebPChunk(nil, , ) } else { = } case "VP8 ", "VP8L": if != nil { return image.Rectangle{}, false, nil, errors.New("multiple WebP bitstreams in first frame") } = = if { = appendWebPChunk(nil, , ) } else { = } default: // The WebP container permits future or application-specific chunks. // webPChunk has already validated and skipped their padded extent. } = } if == nil || ( != nil && != "VP8 ") { return image.Rectangle{}, false, nil, errors.New("invalid WebP first-frame bitstream") } , , := webPBitstreamDimensions(, ) if != nil { return image.Rectangle{}, false, nil, } if != || != { return image.Rectangle{}, false, nil, fmt.Errorf("WebP first-frame header is %dx%d but its bitstream is %dx%d", , , , ) } if ! { return , , nil, nil } := make([]byte, 0, len()+len()+18) if != nil { := make([]byte, 10) [0] = 0x10 writeUint24([4:7], -1) writeUint24([7:10], -1) = appendWebPChunk(, "VP8X", ) = append(, ...) } = append(, ...) := make([]byte, 12, 12+len()) copy([:4], "RIFF") copy([8:12], "WEBP") = append(, ...) binary.LittleEndian.PutUint32([4:8], uint32(len()-8)) return , , , nil } func webPBitstreamDimensions( string, []byte) (int, int, error) { switch { case "VP8 ": if len() < 10 || !bytes.Equal([3:6], []byte{0x9d, 0x01, 0x2a}) { return 0, 0, errors.New("invalid WebP VP8 first-frame header") } return int(binary.LittleEndian.Uint16([6:8]) & 0x3fff), int(binary.LittleEndian.Uint16([8:10]) & 0x3fff), nil case "VP8L": if len() < 5 || [0] != 0x2f { return 0, 0, errors.New("invalid WebP VP8L first-frame header") } := binary.LittleEndian.Uint32([1:5]) return int(&0x3fff) + 1, int((>>14)&0x3fff) + 1, nil default: return 0, 0, errors.New("missing WebP first-frame bitstream") } } func appendWebPChunk( []byte, string, []byte) []byte { = append(, ...) = binary.LittleEndian.AppendUint32(, uint32(len())) = append(, ...) if len()%2 != 0 { = append(, 0) } return } func readUint24( []byte) int { return int([0]) | int([1])<<8 | int([2])<<16 } func writeUint24( []byte, int) { [0] = byte() [1] = byte( >> 8) [2] = byte( >> 16) } type contextReader struct { ctx context.Context reader *bytes.Reader } func ( contextReader) ( []byte) (int, error) { if := contextError(.ctx); != nil { return 0, } if len() > 32<<10 { = [:32<<10] } , := .reader.Read() if := contextError(.ctx); != nil { return , } return , } var _ io.Reader = contextReader{} func contextError( context.Context) error { select { case <-.Done(): if := .Err(); != nil { return } return context.Canceled default: return .Err() } }