mirror of
https://github.com/blacktop/ipsw.git
synced 2026-06-07 12:27:36 +00:00
1070 lines
34 KiB
Go
1070 lines
34 KiB
Go
package car
|
||
|
||
import (
|
||
"bytes"
|
||
"compress/flate"
|
||
"compress/gzip"
|
||
"compress/zlib"
|
||
"encoding/binary"
|
||
"fmt"
|
||
"image"
|
||
"image/color"
|
||
"io"
|
||
"os"
|
||
"path/filepath"
|
||
"strings"
|
||
|
||
"github.com/apex/log"
|
||
"github.com/blacktop/go-macho/types"
|
||
"github.com/blacktop/ipsw/internal/magic"
|
||
"github.com/blacktop/ipsw/pkg/comp"
|
||
"github.com/blacktop/lzfse-cgo"
|
||
)
|
||
|
||
const (
|
||
PixFmtARGB = "ARGB" // Color image
|
||
PixFmtARGB16 = "RGBW" // Deep color image
|
||
PixFmtRGB555 = "RGB5" // Packed 16-bit per pixel opaque image
|
||
PixFmtGray = "GA8 " // Gray scale image with alpha
|
||
PixFmtGray16 = "GA16" // Deep gray scale image with alpha
|
||
PixFmtPDF = "PDF " // PDF raw bytes
|
||
PixFmtJPEG = "JPEG" // JPEG raw bytes
|
||
PixFmtHEIF = "HEIF" // HEIF raw bytes
|
||
PixFmtRawData = "DATA" // Raw bytes
|
||
)
|
||
|
||
type compressionType uint32
|
||
|
||
const (
|
||
Uncompressed compressionType = 0
|
||
RLE compressionType = 1
|
||
ZIP compressionType = 2
|
||
LZVN compressionType = 3
|
||
LZFSE compressionType = 4
|
||
JPEGLZFSE compressionType = 5
|
||
BlurredImage compressionType = 6
|
||
ASTCImage compressionType = 7
|
||
PaletteImage compressionType = 8
|
||
HEVC compressionType = 9
|
||
DeepmapLZFSE compressionType = 10
|
||
Deepmap2 compressionType = 11
|
||
DXTC compressionType = 12
|
||
)
|
||
|
||
type csiBitmapFlags uint32
|
||
|
||
func (f csiBitmapFlags) ChunksFollow() bool {
|
||
return types.ExtractBits(uint64(f), 0, 1) == 1
|
||
}
|
||
func (f csiBitmapFlags) IsOpaque() bool {
|
||
return types.ExtractBits(uint64(f), 1, 1) == 1
|
||
}
|
||
func (f csiBitmapFlags) String() string {
|
||
return fmt.Sprintf("chunks_follow: %t, is_opaque: %t", f.ChunksFollow(), f.IsOpaque())
|
||
}
|
||
|
||
type csiBitmap struct {
|
||
Signature [4]byte // 'PELM'
|
||
Flags csiBitmapFlags
|
||
Encoding compressionType
|
||
Length uint32
|
||
// Data []byte
|
||
}
|
||
|
||
type csiBitmapChunk struct {
|
||
Signature [4]byte // 'PECH' PELM Chunk
|
||
Flags uint32 // always 0
|
||
Version uint32
|
||
Rows uint32
|
||
Length uint32
|
||
// Data []byte
|
||
}
|
||
|
||
type csiRawData struct {
|
||
Signature [4]byte // 'RAWD'
|
||
Flags uint32
|
||
Length uint32
|
||
// Data []byte
|
||
}
|
||
|
||
type csiJpegLZFSEData struct {
|
||
Version uint32
|
||
ChucksFollowing uint32
|
||
LzfseAlphaSize uint32
|
||
LzfseDataRowBytes uint32
|
||
JpegDataSize uint32
|
||
}
|
||
|
||
type csiASTCData struct {
|
||
Version uint32 // 0 == raw ATSC Data; 1 == lzfse compressed ATSC Data
|
||
DataSize uint32
|
||
AstcDataSize uint32
|
||
// Data []byte
|
||
}
|
||
|
||
type csiHEVCData struct {
|
||
Version uint32
|
||
HevcDataSize uint32
|
||
}
|
||
|
||
type csiDeepmapData struct {
|
||
Version uint32
|
||
PixelFormat uint32
|
||
CompressedBytes uint64
|
||
}
|
||
|
||
type csiDeepmap2Data struct {
|
||
Version uint32
|
||
Encoding compressionType
|
||
Length uint64
|
||
}
|
||
|
||
type deepmapPixelFormat uint8
|
||
|
||
const (
|
||
ImageDeepmapPixelFormatG8 deepmapPixelFormat = 0x01
|
||
ImageDeepmapPixelFormatGA8 deepmapPixelFormat = 0x02
|
||
ImageDeepmapPixelFormatRGB8 deepmapPixelFormat = 0x03
|
||
ImageDeepmapPixelFormatRGBA8 deepmapPixelFormat = 0x04
|
||
ImageDeepmapPixelFormatRGBA16 deepmapPixelFormat = 0x0A // 16-bit per channel RGBA
|
||
ImageDeepmapPixelFormatG16F deepmapPixelFormat = 0x11
|
||
ImageDeepmapPixelFormatGA16F deepmapPixelFormat = 0x12
|
||
ImageDeepmapPixelFormatRGB16F deepmapPixelFormat = 0x13
|
||
ImageDeepmapPixelFormatRGBA16F deepmapPixelFormat = 0x14
|
||
)
|
||
|
||
type deepmapCompressionMethod uint8
|
||
|
||
const (
|
||
ImageDeepmapCompressionNone deepmapCompressionMethod = 1
|
||
ImageDeepmapCompressionDefault deepmapCompressionMethod = 2
|
||
ImageDeepmapCompressionLossless deepmapCompressionMethod = 3
|
||
ImageDeepmapCompressionPalette deepmapCompressionMethod = 4
|
||
)
|
||
|
||
type deepmap struct {
|
||
Signature [4]byte // 'dmap'
|
||
CompressionMethod deepmapCompressionMethod
|
||
Scale uint8
|
||
Unknown uint8 // 10 ?
|
||
PixelFormat deepmapPixelFormat
|
||
CompressedBlock uint32
|
||
}
|
||
|
||
type deepmap2 struct {
|
||
Signature [4]byte // 'dmp2'
|
||
Scale uint8
|
||
BlobVersion uint8 // 1
|
||
PixelFormat deepmapPixelFormat
|
||
CompressionMethod deepmapCompressionMethod
|
||
Width uint16
|
||
Height uint16
|
||
CompressedBlock uint32
|
||
}
|
||
|
||
// BGRA to RGBA
|
||
type BGRA struct {
|
||
image.RGBA
|
||
}
|
||
|
||
func (p *BGRA) RGBAAt(x, y int) color.RGBA {
|
||
c := p.RGBA.RGBAAt(x, y)
|
||
return color.RGBA{R: c.B, G: c.G, B: c.R, A: c.A}
|
||
}
|
||
|
||
func (p *BGRA) At(x, y int) color.Color {
|
||
return p.RGBAAt(x, y)
|
||
}
|
||
|
||
func (p *BGRA) SubImage(r image.Rectangle) image.Image {
|
||
c := p.RGBA.SubImage(r).(*image.RGBA)
|
||
return &BGRA{*c}
|
||
}
|
||
|
||
type GA8 struct {
|
||
Pix []uint8
|
||
Stride int
|
||
Rect image.Rectangle
|
||
}
|
||
|
||
func (p *GA8) ColorModel() color.Model { return color.RGBAModel }
|
||
|
||
func (p *GA8) Bounds() image.Rectangle { return p.Rect }
|
||
|
||
func (p *GA8) At(x, y int) color.Color {
|
||
return p.GA8At(x, y)
|
||
}
|
||
|
||
func (p *GA8) GA8At(x, y int) color.RGBA {
|
||
if !(image.Point{x, y}.In(p.Rect)) {
|
||
return color.RGBA{}
|
||
}
|
||
i := p.PixOffset(x, y)
|
||
if i+2 > len(p.Pix) {
|
||
return color.RGBA{}
|
||
}
|
||
s := p.Pix[i : i+2 : i+2] // Small cap improves performance, see https://golang.org/issue/27857
|
||
return color.RGBA{s[0], s[0], s[0], s[1]}
|
||
}
|
||
|
||
func (p *GA8) PixOffset(x, y int) int {
|
||
return (y-p.Rect.Min.Y)*p.Stride + (x-p.Rect.Min.X)*2
|
||
}
|
||
|
||
func unsupportedJPEGCompression() error {
|
||
return fmt.Errorf("unsupported JPEGLZFSE decode: JPEG color data with optional LZFSE alpha is not implemented")
|
||
}
|
||
|
||
func decodeImage(r io.Reader, ci csiHeader, conf *Config, rowBytesOverride int) (image.Image, error) {
|
||
var out bytes.Buffer
|
||
// Track Deepmap2 origin and pixel format so we can render without BGRA swap
|
||
fromDeepmap2 := false
|
||
var deepmap2PixFmt deepmapPixelFormat
|
||
|
||
var elem csiBitmap
|
||
if err := binary.Read(r, binary.LittleEndian, &elem); err != nil {
|
||
return nil, fmt.Errorf("failed to read CSIBitmap: %s", err)
|
||
}
|
||
|
||
// log.WithFields(log.Fields{
|
||
// "signature": string(elem.Signature[:]),
|
||
// "flags": elem.Flags.String(),
|
||
// "encoding": elem.Encoding,
|
||
// "length": elem.Length,
|
||
// }).Info("Reading CSIElement")
|
||
|
||
if elem.Flags.ChunksFollow() {
|
||
for i := uint32(0); i < elem.Length; i++ {
|
||
var chunk csiBitmapChunk
|
||
if err := binary.Read(r, binary.LittleEndian, &chunk); err != nil {
|
||
return nil, err
|
||
}
|
||
if chunk.Signature != [4]byte{'K', 'C', 'B', 'C'} {
|
||
return nil, fmt.Errorf("invalid chunk signature: %s", chunk.Signature)
|
||
}
|
||
data := make([]byte, chunk.Length)
|
||
if err := binary.Read(r, binary.LittleEndian, &data); err != nil {
|
||
return nil, err
|
||
}
|
||
switch elem.Encoding {
|
||
case Uncompressed:
|
||
out.Write(data)
|
||
case RLE:
|
||
out.Write(decodeRLE(data))
|
||
case ZIP:
|
||
// Try gzip first
|
||
gr, err := gzip.NewReader(bytes.NewReader(data))
|
||
if err == nil {
|
||
if _, err := io.Copy(&out, gr); err != nil {
|
||
return nil, fmt.Errorf("failed to decompress gzip: %v", err)
|
||
}
|
||
} else {
|
||
// Try zlib
|
||
zr, err := zlib.NewReader(bytes.NewReader(data))
|
||
if err == nil {
|
||
if _, err := io.Copy(&out, zr); err != nil {
|
||
return nil, fmt.Errorf("failed to decompress zlib: %v", err)
|
||
}
|
||
zr.Close()
|
||
} else {
|
||
// Try raw deflate
|
||
fr := flate.NewReader(bytes.NewReader(data))
|
||
if _, err := io.Copy(&out, fr); err != nil {
|
||
// Last resort - assume uncompressed
|
||
out.Write(data)
|
||
}
|
||
fr.Close()
|
||
}
|
||
}
|
||
case LZVN:
|
||
dec := make([]byte, len(data)*4)
|
||
if n := lzfse.DecodeLZVNBuffer(data, dec); n == 0 {
|
||
return nil, fmt.Errorf("failed to decompress lzvn data")
|
||
} else {
|
||
out.Write(dec[:n])
|
||
}
|
||
case LZFSE:
|
||
decompressed, err := comp.Decompress(data, comp.LZFSE)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("failed to decompress LZFSE data: %v", err)
|
||
}
|
||
out.Write(decompressed)
|
||
case JPEGLZFSE:
|
||
return nil, unsupportedJPEGCompression()
|
||
case HEVC:
|
||
// HEVC/H.265 video data
|
||
var hevcInfo csiHEVCData
|
||
r := bytes.NewReader(data)
|
||
if err := binary.Read(r, binary.LittleEndian, &hevcInfo); err != nil {
|
||
return nil, fmt.Errorf("failed to read HEVC header: %v", err)
|
||
}
|
||
// Read the HEVC data
|
||
hevcData := make([]byte, hevcInfo.HevcDataSize)
|
||
if err := binary.Read(r, binary.LittleEndian, &hevcData); err != nil {
|
||
return nil, fmt.Errorf("failed to read HEVC data: %v", err)
|
||
}
|
||
// Output raw HEVC data (would need HEVC decoder for actual image)
|
||
out.Write(hevcData)
|
||
case PaletteImage:
|
||
// Magic byte detection for compression format
|
||
if len(data) >= 4 {
|
||
magic := data[0:4]
|
||
if string(magic) == "lzvn" || (len(data) >= 4 && magic[0] == 0x68 && magic[1] == 0x01 && magic[2] == 0x00 && magic[3] == 0xf0) {
|
||
// LZVN compressed palette image
|
||
dec := make([]byte, len(data)*4)
|
||
if n := lzfse.DecodeLZVNBuffer(data, dec); n == 0 {
|
||
return nil, fmt.Errorf("failed to decompress PaletteImage LZVN data")
|
||
} else {
|
||
out.Write(dec[:n])
|
||
}
|
||
} else {
|
||
// Raw palette image data
|
||
out.Write(data)
|
||
}
|
||
} else {
|
||
out.Write(data)
|
||
}
|
||
case ASTCImage:
|
||
// ASTC texture compression
|
||
var astcInfo csiASTCData
|
||
r := bytes.NewReader(data)
|
||
if err := binary.Read(r, binary.LittleEndian, &astcInfo); err != nil {
|
||
return nil, fmt.Errorf("failed to read ASTC header: %v", err)
|
||
}
|
||
// Read the ASTC data
|
||
astcData := make([]byte, astcInfo.AstcDataSize)
|
||
if err := binary.Read(r, binary.LittleEndian, &astcData); err != nil {
|
||
return nil, fmt.Errorf("failed to read ASTC data: %v", err)
|
||
}
|
||
if astcInfo.Version == 1 {
|
||
// LZFSE compressed ASTC data
|
||
decompressed, err := comp.Decompress(astcData, comp.LZFSE)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("failed to decompress ASTC data: %v", err)
|
||
}
|
||
out.Write(decompressed)
|
||
} else {
|
||
// Raw ASTC data
|
||
out.Write(astcData)
|
||
}
|
||
case DeepmapLZFSE:
|
||
// Deepmap with LZFSE compression
|
||
var deepmapInfo csiDeepmapData
|
||
r := bytes.NewReader(data)
|
||
if err := binary.Read(r, binary.LittleEndian, &deepmapInfo); err != nil {
|
||
return nil, fmt.Errorf("failed to read Deepmap header: %v", err)
|
||
}
|
||
log.WithFields(log.Fields{
|
||
"version": deepmapInfo.Version,
|
||
"pixel_format": deepmapInfo.PixelFormat,
|
||
"compressed_bytes": deepmapInfo.CompressedBytes,
|
||
}).Debug("Reading Deepmap")
|
||
// Read the deepmap structure header
|
||
var dm deepmap
|
||
if err := binary.Read(r, binary.LittleEndian, &dm); err != nil {
|
||
return nil, fmt.Errorf("failed to read deepmap structure: %v", err)
|
||
}
|
||
if dm.Signature != [4]byte{'d', 'm', 'a', 'p'} {
|
||
return nil, fmt.Errorf("invalid deepmap signature: %s", dm.Signature)
|
||
}
|
||
// Read the compressed data
|
||
compressedData := make([]byte, dm.CompressedBlock)
|
||
if err := binary.Read(r, binary.LittleEndian, &compressedData); err != nil {
|
||
return nil, fmt.Errorf("failed to read Deepmap compressed data: %v", err)
|
||
}
|
||
log.WithFields(log.Fields{
|
||
"signature": string(dm.Signature[:]),
|
||
"compression_method": dm.CompressionMethod,
|
||
"scale": dm.Scale,
|
||
"pixel_format": dm.PixelFormat,
|
||
"compressed_block": dm.CompressedBlock,
|
||
}).Debug("Reading Deepmap")
|
||
if isLZFSE, _ := magic.IsLZFSE(compressedData); isLZFSE {
|
||
// Decompress the data
|
||
decompressed, err := comp.Decompress(compressedData, comp.LZFSE)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("failed to decompress DeepmapLZFSE data: %v", err)
|
||
}
|
||
out.Write(decompressed)
|
||
} else {
|
||
out.Write(compressedData)
|
||
}
|
||
// switch dm.CompressionMethod {
|
||
// case ImageDeepmapCompressionDefault:
|
||
// // Decompress the data
|
||
// decompressed, err := comp.Decompress(compressedData, comp.LZFSE)
|
||
// if err != nil {
|
||
// return nil, fmt.Errorf("failed to decompress DeepmapLZFSE data: %v", err)
|
||
// }
|
||
// out.Write(decompressed)
|
||
// case ImageDeepmapCompressionNone:
|
||
// out.Write(compressedData)
|
||
// default:
|
||
// out.Write(compressedData)
|
||
// // return nil, fmt.Errorf("unsupported deepmap compression method: %d", dm.CompressionMethod)
|
||
// }
|
||
case Deepmap2:
|
||
// Handle Deepmap2 inside chunk stream
|
||
rdm := bytes.NewReader(data)
|
||
var cdm2 csiDeepmap2Data
|
||
if err := binary.Read(rdm, binary.LittleEndian, &cdm2); err != nil {
|
||
return nil, err
|
||
}
|
||
var dm2 deepmap2
|
||
if err := binary.Read(rdm, binary.LittleEndian, &dm2); err != nil {
|
||
return nil, err
|
||
}
|
||
if dm2.Signature != [4]byte{'d', 'm', 'p', '2'} {
|
||
return nil, fmt.Errorf("invalid deepmap2 signature: %s", dm2.Signature)
|
||
}
|
||
// Override dimensions; keep CSI pixel format (authoritative for GA8 masks)
|
||
ci.Width = uint32(dm2.Width)
|
||
ci.Height = uint32(dm2.Height)
|
||
fromDeepmap2 = true
|
||
deepmap2PixFmt = dm2.PixelFormat
|
||
// Gather compressed payload which may span multiple KCBC chunks
|
||
need := int(dm2.CompressedBlock)
|
||
compressed := make([]byte, need)
|
||
readSoFar := 0
|
||
if rdm.Len() > 0 {
|
||
frag := make([]byte, rdm.Len())
|
||
if _, err := io.ReadFull(rdm, frag); err == nil {
|
||
toCopy := min(len(frag), need)
|
||
copy(compressed[0:toCopy], frag[:toCopy])
|
||
readSoFar += toCopy
|
||
}
|
||
}
|
||
// If not enough, consume following KCBC chunks from the outer reader 'r'.
|
||
// Cap to remaining chunks in the element to avoid reading past the end.
|
||
extraChunks := uint32(0)
|
||
maxExtra := elem.Length - 1 - i
|
||
for readSoFar < int(dm2.CompressedBlock) && extraChunks < maxExtra {
|
||
var next csiBitmapChunk
|
||
if err := binary.Read(r, binary.LittleEndian, &next); err != nil {
|
||
break
|
||
}
|
||
if next.Signature != [4]byte{'K', 'C', 'B', 'C'} {
|
||
break
|
||
}
|
||
buf := make([]byte, next.Length)
|
||
if err := binary.Read(r, binary.LittleEndian, &buf); err != nil {
|
||
break
|
||
}
|
||
remain := int(dm2.CompressedBlock) - readSoFar
|
||
toCopy := min(len(buf), remain)
|
||
copy(compressed[readSoFar:readSoFar+toCopy], buf[:toCopy])
|
||
readSoFar += toCopy
|
||
extraChunks++
|
||
}
|
||
// Skip accounting for consumed chunks in the outer loop
|
||
i += extraChunks
|
||
compressed = compressed[:readSoFar]
|
||
// Palette compression carries a palette before an LZFSE block; decode directly to image
|
||
if dm2.CompressionMethod == ImageDeepmapCompressionPalette {
|
||
magic := []byte("bvx2")
|
||
pos := bytes.Index(compressed, magic)
|
||
if pos > 0 {
|
||
palette := compressed[:pos]
|
||
indicesCompressed := compressed[pos:]
|
||
decomp, err := comp.Decompress(indicesCompressed, comp.LZFSE)
|
||
if err == nil {
|
||
// Compose [palette][indices] and decode
|
||
combo := append([]byte{}, append(palette, decomp...)...)
|
||
if palImg, err := decodePalettedImage(combo, int(ci.Width), int(ci.Height)); err == nil {
|
||
return palImg, nil
|
||
}
|
||
}
|
||
}
|
||
}
|
||
switch cdm2.Encoding {
|
||
case LZFSE:
|
||
if isLZFSE, _ := magic.IsLZFSE(compressed); isLZFSE {
|
||
decompressed, err := comp.Decompress(compressed, comp.LZFSE)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("failed to decompress Deepmap2 LZFSE data: %v", err)
|
||
}
|
||
out.Write(decompressed)
|
||
} else {
|
||
// Some payloads are raw when marked LZFSE
|
||
out.Write(compressed)
|
||
}
|
||
case ZIP:
|
||
if isLZFSE, _ := magic.IsLZFSE(compressed); isLZFSE {
|
||
decompressed, err := comp.Decompress(compressed, comp.LZFSE)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("failed to decompress Deepmap2 LZFSE-as-ZIP data: %v", err)
|
||
}
|
||
out.Write(decompressed)
|
||
break
|
||
}
|
||
if gr, err := gzip.NewReader(bytes.NewReader(compressed)); err == nil {
|
||
if _, err := io.Copy(&out, gr); err != nil {
|
||
return nil, fmt.Errorf("failed to decompress gzip data: %v", err)
|
||
}
|
||
} else if zr, err := zlib.NewReader(bytes.NewReader(compressed)); err == nil {
|
||
if _, err := io.Copy(&out, zr); err != nil {
|
||
return nil, fmt.Errorf("failed to decompress zlib data: %v", err)
|
||
}
|
||
zr.Close()
|
||
} else {
|
||
fr := flate.NewReader(bytes.NewReader(compressed))
|
||
if _, err := io.Copy(&out, fr); err != nil {
|
||
// Assume raw on failure
|
||
out.Write(compressed)
|
||
}
|
||
fr.Close()
|
||
}
|
||
case Deepmap2:
|
||
// Nested Deepmap2 often uses LZFSE after a small header
|
||
if len(data) > 16 {
|
||
decompressed, err := comp.Decompress(data[16:], comp.LZFSE)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("failed to decompress nested Deepmap2: %v", err)
|
||
}
|
||
out.Write(decompressed)
|
||
} else {
|
||
out.Write(compressed)
|
||
}
|
||
default:
|
||
return nil, fmt.Errorf("unsupported deepmap2 encoding: %s", cdm2.Encoding)
|
||
}
|
||
default:
|
||
return nil, fmt.Errorf("unknown encoding: %s (value: %d)", elem.Encoding, elem.Encoding)
|
||
}
|
||
}
|
||
} else {
|
||
data := make([]byte, elem.Length)
|
||
if err := binary.Read(r, binary.LittleEndian, &data); err != nil {
|
||
return nil, err
|
||
}
|
||
switch elem.Encoding {
|
||
case Uncompressed:
|
||
out.Write(data)
|
||
case RLE:
|
||
out.Write(decodeRLE(data))
|
||
case ZIP:
|
||
// Try gzip first
|
||
gr, err := gzip.NewReader(bytes.NewReader(data))
|
||
if err == nil {
|
||
if _, err := io.Copy(&out, gr); err != nil {
|
||
return nil, fmt.Errorf("failed to decompress gzip: %v", err)
|
||
}
|
||
} else {
|
||
// Try zlib
|
||
zr, err := zlib.NewReader(bytes.NewReader(data))
|
||
if err == nil {
|
||
if _, err := io.Copy(&out, zr); err != nil {
|
||
return nil, fmt.Errorf("failed to decompress zlib: %v", err)
|
||
}
|
||
zr.Close()
|
||
} else {
|
||
// Try raw deflate
|
||
fr := flate.NewReader(bytes.NewReader(data))
|
||
if _, err := io.Copy(&out, fr); err != nil {
|
||
// Last resort - assume uncompressed
|
||
out.Write(data)
|
||
}
|
||
fr.Close()
|
||
}
|
||
}
|
||
case LZVN:
|
||
dec := make([]byte, len(data)*4)
|
||
if n := lzfse.DecodeLZVNBuffer(data, dec); n == 0 {
|
||
return nil, fmt.Errorf("failed to decompress lzvn data")
|
||
} else {
|
||
out.Write(dec[:n])
|
||
}
|
||
case LZFSE:
|
||
decompressed, err := comp.Decompress(data, comp.LZFSE)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("failed to decompress LZFSE data: %v", err)
|
||
}
|
||
out.Write(decompressed)
|
||
case JPEGLZFSE:
|
||
return nil, unsupportedJPEGCompression()
|
||
case HEVC:
|
||
// HEVC/H.265 video data
|
||
var hevcInfo csiHEVCData
|
||
r := bytes.NewReader(data)
|
||
if err := binary.Read(r, binary.LittleEndian, &hevcInfo); err != nil {
|
||
return nil, fmt.Errorf("failed to read HEVC header: %v", err)
|
||
}
|
||
// Read the HEVC data
|
||
hevcData := make([]byte, hevcInfo.HevcDataSize)
|
||
if err := binary.Read(r, binary.LittleEndian, &hevcData); err != nil {
|
||
return nil, fmt.Errorf("failed to read HEVC data: %v", err)
|
||
}
|
||
// Output raw HEVC data (would need HEVC decoder for actual image)
|
||
out.Write(hevcData)
|
||
case ASTCImage:
|
||
// ASTC texture compression
|
||
var astcInfo csiASTCData
|
||
r := bytes.NewReader(data)
|
||
if err := binary.Read(r, binary.LittleEndian, &astcInfo); err != nil {
|
||
return nil, fmt.Errorf("failed to read ASTC header: %v", err)
|
||
}
|
||
// Read the ASTC data
|
||
astcData := make([]byte, astcInfo.AstcDataSize)
|
||
if err := binary.Read(r, binary.LittleEndian, &astcData); err != nil {
|
||
return nil, fmt.Errorf("failed to read ASTC data: %v", err)
|
||
}
|
||
if astcInfo.Version == 1 {
|
||
// LZFSE compressed ASTC data
|
||
decompressed, err := comp.Decompress(astcData, comp.LZFSE)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("failed to decompress ASTC data: %v", err)
|
||
}
|
||
out.Write(decompressed)
|
||
} else {
|
||
// Raw ASTC data
|
||
out.Write(astcData)
|
||
}
|
||
case PaletteImage:
|
||
if isLZFSE, _ := magic.IsLZFSE(data); isLZFSE {
|
||
// Decompress the data
|
||
decompressed, err := comp.Decompress(data, comp.LZFSE)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("failed to decompress DeepmapLZFSE data: %v", err)
|
||
}
|
||
out.Write(decompressed)
|
||
} else {
|
||
// Raw palette image data
|
||
out.Write(data)
|
||
}
|
||
// After decompression, the data should contain palette indices
|
||
// We'll handle this in the pixel format switch below
|
||
case DeepmapLZFSE:
|
||
// Deepmap with LZFSE compression
|
||
var deepmapInfo csiDeepmapData
|
||
r := bytes.NewReader(data)
|
||
if err := binary.Read(r, binary.LittleEndian, &deepmapInfo); err != nil {
|
||
return nil, fmt.Errorf("failed to read Deepmap header: %v", err)
|
||
}
|
||
var dm deepmap
|
||
if err := binary.Read(r, binary.LittleEndian, &dm); err != nil {
|
||
return nil, err
|
||
}
|
||
if dm.Signature != [4]byte{'d', 'm', 'a', 'p'} {
|
||
return nil, fmt.Errorf("invalid deepmap signature: %s", dm.Signature)
|
||
}
|
||
// Read the compressed data
|
||
compressedData := make([]byte, dm.CompressedBlock)
|
||
if err := binary.Read(r, binary.LittleEndian, &compressedData); err != nil {
|
||
return nil, fmt.Errorf("failed to read Deepmap compressed data: %v", err)
|
||
}
|
||
switch dm.CompressionMethod {
|
||
case ImageDeepmapCompressionLossless:
|
||
if isLZFSE, _ := magic.IsLZFSE(compressedData); isLZFSE {
|
||
// Decompress the data
|
||
decompressed, err := comp.Decompress(compressedData, comp.LZFSE)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("failed to decompress DeepmapLZFSE data: %v", err)
|
||
}
|
||
out.Write(decompressed)
|
||
} else {
|
||
return nil, fmt.Errorf("lzfse magic bytes not found in DeepmapLZFSE data")
|
||
}
|
||
case ImageDeepmapCompressionDefault:
|
||
out.Write(decodeRLE(compressedData))
|
||
case ImageDeepmapCompressionNone:
|
||
out.Write(compressedData)
|
||
case ImageDeepmapCompressionPalette:
|
||
return nil, fmt.Errorf("deepmap palette compression not implemented")
|
||
}
|
||
case Deepmap2:
|
||
dmr := bytes.NewReader(data)
|
||
var cdm2 csiDeepmap2Data
|
||
if err := binary.Read(dmr, binary.LittleEndian, &cdm2); err != nil {
|
||
return nil, err
|
||
}
|
||
if conf != nil && conf.Verbose {
|
||
log.WithFields(log.Fields{
|
||
"version": cdm2.Version,
|
||
"encoding": cdm2.Encoding,
|
||
"length": cdm2.Length,
|
||
}).Info("Reading Deepmap2 Data")
|
||
}
|
||
var dm2 deepmap2
|
||
if err := binary.Read(dmr, binary.LittleEndian, &dm2); err != nil {
|
||
return nil, err
|
||
}
|
||
if dm2.Signature != [4]byte{'d', 'm', 'p', '2'} {
|
||
return nil, fmt.Errorf("invalid deepmap2 signature: %s", dm2.Signature)
|
||
}
|
||
if conf != nil && conf.Verbose {
|
||
log.WithFields(log.Fields{
|
||
"signature": string(dm2.Signature[:]),
|
||
"blob_version": dm2.BlobVersion,
|
||
"pixel_format": dm2.PixelFormat,
|
||
"compression_method": dm2.CompressionMethod,
|
||
"width": dm2.Width,
|
||
"height": dm2.Height,
|
||
"scale": dm2.Scale,
|
||
"compressed_block": dm2.CompressedBlock,
|
||
}).Warn("Reading Deepmap2")
|
||
}
|
||
if dm2.BlobVersion != 1 {
|
||
return nil, fmt.Errorf("unsupported deepmap2 blob version: %d", dm2.BlobVersion)
|
||
}
|
||
|
||
// Override dimensions; keep CSI pixel format (authoritative for GA8 masks)
|
||
ci.Width = uint32(dm2.Width)
|
||
ci.Height = uint32(dm2.Height)
|
||
fromDeepmap2 = true
|
||
deepmap2PixFmt = dm2.PixelFormat
|
||
switch cdm2.Encoding {
|
||
case LZFSE, ZIP:
|
||
// Gather the full Deepmap2 compressed block, which may follow in KCBC chunks
|
||
need := int(dm2.CompressedBlock)
|
||
compressed := make([]byte, need)
|
||
readSoFar := 0
|
||
if dmr.Len() > 0 {
|
||
frag := make([]byte, dmr.Len())
|
||
if _, err := io.ReadFull(dmr, frag); err == nil {
|
||
toCopy := min(len(frag), need)
|
||
copy(compressed[0:], frag[:toCopy])
|
||
readSoFar += toCopy
|
||
}
|
||
}
|
||
// In the non-chunked path, the blob may be smaller than
|
||
// CompressedBlock claims — proceed with what we have.
|
||
compressed = compressed[:readSoFar]
|
||
|
||
// If palette compression, split palette and indices and decode paletted directly
|
||
if dm2.CompressionMethod == ImageDeepmapCompressionPalette {
|
||
// Look for LZFSE block header magic commonly seen as 'bvx2'
|
||
pos := bytes.Index(compressed, []byte("bvx2"))
|
||
if pos > 0 {
|
||
palette := compressed[:pos]
|
||
indicesCompressed := compressed[pos:]
|
||
// Some files mark ZIP but actually contain LZFSE
|
||
decomp, err := comp.Decompress(indicesCompressed, comp.LZFSE)
|
||
if err == nil {
|
||
combo := append([]byte{}, append(palette, decomp...)...)
|
||
if palImg, err := decodePalettedImage(combo, int(ci.Width), int(ci.Height)); err == nil {
|
||
return palImg, nil
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Otherwise, decompress the full block
|
||
if cdm2.Encoding == ZIP {
|
||
if isLZFSE, _ := magic.IsLZFSE(compressed); isLZFSE {
|
||
decompressed, err := comp.Decompress(compressed, comp.LZFSE)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("failed to decompress Deepmap2 LZFSE data: %v", err)
|
||
}
|
||
out.Write(decompressed)
|
||
} else if gr, err := gzip.NewReader(bytes.NewReader(compressed)); err == nil {
|
||
if _, err := io.Copy(&out, gr); err != nil {
|
||
return nil, fmt.Errorf("failed to decompress gzip data: %v", err)
|
||
}
|
||
} else if zr, err := zlib.NewReader(bytes.NewReader(compressed)); err == nil {
|
||
if _, err := io.Copy(&out, zr); err != nil {
|
||
return nil, fmt.Errorf("failed to decompress zlib data: %v", err)
|
||
}
|
||
zr.Close()
|
||
} else {
|
||
fr := flate.NewReader(bytes.NewReader(compressed))
|
||
if _, err := io.Copy(&out, fr); err != nil {
|
||
out.Write(compressed)
|
||
}
|
||
fr.Close()
|
||
}
|
||
} else {
|
||
if isLZFSE, _ := magic.IsLZFSE(compressed); isLZFSE {
|
||
decompressed, err := comp.Decompress(compressed, comp.LZFSE)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("failed to decompress LZFSE data: %v", err)
|
||
}
|
||
out.Write(decompressed)
|
||
} else {
|
||
out.Write(compressed)
|
||
}
|
||
}
|
||
case Deepmap2:
|
||
// Handle nested Deepmap2 encoding
|
||
if dmr.Len() > 16 {
|
||
payload := make([]byte, dmr.Len())
|
||
if _, err := io.ReadFull(dmr, payload); err == nil {
|
||
decompressed, err := comp.Decompress(payload[16:], comp.LZFSE)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("failed to decompress nested Deepmap2: %v", err)
|
||
}
|
||
out.Write(decompressed)
|
||
}
|
||
}
|
||
default:
|
||
return nil, fmt.Errorf("unsupported deepmap2 encoding: %s", cdm2.Encoding)
|
||
}
|
||
default:
|
||
return nil, fmt.Errorf("unknown encoding: %s (value: %d)", elem.Encoding, elem.Encoding)
|
||
}
|
||
}
|
||
|
||
// Check for invalid image dimensions
|
||
if ci.Width == 0 || ci.Height == 0 {
|
||
return nil, fmt.Errorf("invalid image dimensions: %dx%d", ci.Width, ci.Height)
|
||
}
|
||
|
||
// Check if this was a PaletteImage compression type
|
||
// If so, decode as a paletted image regardless of pixel format
|
||
if elem.Encoding == PaletteImage {
|
||
// Try to decode as a paletted image
|
||
if palImg, err := decodePalettedImage(out.Bytes(), int(ci.Width), int(ci.Height)); err == nil {
|
||
return palImg, nil
|
||
} else {
|
||
// If palette decoding fails, log and try normal pixel format handling as fallback
|
||
log.Debugf("Failed to decode as PaletteImage for %s: %v, falling back to pixel format", string(bytes.Trim(ci.Metadata.Name[:], "\x00")), err)
|
||
}
|
||
}
|
||
|
||
format := string(ci.PixelFormat[:])
|
||
switch format {
|
||
case PixFmtARGB, PixFmtARGB16:
|
||
// Special handling for IconImage layout which often uses channel-separated ARGB format (AAAA RRRR GGGG BBBB)
|
||
if ci.Metadata.Layout == IconImage {
|
||
pixelCount := int(ci.Width * ci.Height)
|
||
// Prefer exact-sized channel data, but allow >= to tolerate minor padding
|
||
if out.Len() >= pixelCount*4 {
|
||
if img, err := decodeAppIconARGB(out.Bytes(), int(ci.Width), int(ci.Height)); err == nil {
|
||
return img, nil
|
||
}
|
||
}
|
||
}
|
||
|
||
var offset int
|
||
bytesPerPixel := 4 // Default for ARGB (8-bit per channel)
|
||
if format == PixFmtARGB16 {
|
||
if !fromDeepmap2 {
|
||
return nil, fmt.Errorf("unsupported pixel format: %s outside Deepmap2", format)
|
||
}
|
||
bytesPerPixel = 8 // 16-bit per channel (2 bytes per channel × 4 channels)
|
||
}
|
||
|
||
expectedSize := int(ci.Width * ci.Height * uint32(bytesPerPixel))
|
||
actualSize := out.Len()
|
||
|
||
if actualSize < expectedSize {
|
||
// Dump raw bytes for debugging when verbose
|
||
if conf != nil && conf.Verbose && conf.Export && conf.Output != "" {
|
||
name := strings.Trim(string(bytes.Trim(ci.Metadata.Name[:], "\x00")), " ")
|
||
if name == "" {
|
||
name = fmt.Sprintf("asset_%dx%d_%s", ci.Width, ci.Height, format)
|
||
}
|
||
errPath := filepath.Join(conf.Output, fmt.Sprintf("%s.error", name))
|
||
_ = os.WriteFile(errPath, out.Bytes(), 0644)
|
||
}
|
||
return nil, fmt.Errorf("insufficient image data: got %d bytes, expected %d bytes for %dx%d %s", actualSize, expectedSize, ci.Width, ci.Height, format)
|
||
}
|
||
|
||
if v := actualSize - expectedSize; v != 0 {
|
||
offset = v / int(ci.Height*uint32(bytesPerPixel))
|
||
}
|
||
rect := image.Rectangle{
|
||
Min: image.Point{0, 0},
|
||
Max: image.Point{
|
||
X: int(ci.Width),
|
||
Y: int(ci.Height),
|
||
},
|
||
}
|
||
// rgba := image.NewRGBA(rect)
|
||
// rgba.Pix = out.Bytes()
|
||
// rgba.Stride = (rect.Dx() + offset) * 4
|
||
// return rgba, nil
|
||
// Deepmap2 emits RGBA ordering; do not apply BGRA swap, and downconvert 16-bit to 8-bit
|
||
if fromDeepmap2 {
|
||
if format == PixFmtARGB16 || deepmap2PixFmt == ImageDeepmapPixelFormatRGBA16 {
|
||
// downconvert RGBA16 (LE) to RGBA8
|
||
in := out.Bytes()
|
||
px := rect.Dx() * rect.Dy()
|
||
dst := make([]byte, px*4)
|
||
for i := range px {
|
||
j := i * 8
|
||
// take high byte of each 16-bit LE component
|
||
r := in[j+1]
|
||
g := in[j+3]
|
||
b := in[j+5]
|
||
a := in[j+7]
|
||
k := i * 4
|
||
dst[k+0] = r
|
||
dst[k+1] = g
|
||
dst[k+2] = b
|
||
dst[k+3] = a
|
||
}
|
||
stride := rect.Dx() * 4
|
||
img := &image.RGBA{Pix: dst, Stride: stride, Rect: rect}
|
||
return img, nil
|
||
}
|
||
// RGBA8
|
||
stride := rect.Dx() * 4
|
||
if rowBytesOverride > 0 {
|
||
stride = rowBytesOverride
|
||
}
|
||
img := &image.RGBA{Pix: out.Bytes(), Stride: stride, Rect: rect}
|
||
return img, nil
|
||
}
|
||
|
||
// Default path: treat as BGRA in memory and swap when reading
|
||
stride := (rect.Dx() + offset) * bytesPerPixel
|
||
if rowBytesOverride > 0 {
|
||
stride = rowBytesOverride
|
||
}
|
||
bgra := &BGRA{image.RGBA{
|
||
Pix: out.Bytes(),
|
||
Stride: stride,
|
||
Rect: rect,
|
||
}}
|
||
return bgra, nil
|
||
case PixFmtGray, PixFmtGray16:
|
||
if format == PixFmtGray16 {
|
||
return nil, fmt.Errorf("unsupported pixel format: %s", format)
|
||
}
|
||
var offset int
|
||
bytesPerPixel := 2 // GA8: gray + alpha (1 byte each)
|
||
|
||
expectedSize := int(ci.Width * ci.Height * uint32(bytesPerPixel))
|
||
actualSize := out.Len()
|
||
|
||
if actualSize < expectedSize {
|
||
// Not enough data for the image
|
||
return nil, fmt.Errorf("insufficient image data: got %d bytes, expected %d bytes for %dx%d %s", actualSize, expectedSize, ci.Width, ci.Height, format)
|
||
}
|
||
|
||
if v := actualSize - expectedSize; v != 0 {
|
||
offset = v / int(ci.Height*uint32(bytesPerPixel))
|
||
}
|
||
|
||
rect := image.Rectangle{
|
||
Min: image.Point{0, 0},
|
||
Max: image.Point{
|
||
X: int(ci.Width),
|
||
Y: int(ci.Height),
|
||
},
|
||
}
|
||
stride := (rect.Dx() + offset) * bytesPerPixel
|
||
if rowBytesOverride > 0 {
|
||
stride = rowBytesOverride
|
||
}
|
||
bgra := &GA8{
|
||
Pix: out.Bytes(),
|
||
Stride: stride,
|
||
Rect: rect,
|
||
}
|
||
return bgra, nil
|
||
default:
|
||
return nil, fmt.Errorf("unknown pixel format: %s", format)
|
||
}
|
||
}
|
||
|
||
// decodePalettedImage creates a paletted image from palette data and indices
|
||
// The format is typically: [palette colors (256 * 4 bytes RGBA)][pixel indices]
|
||
// cafef00dMagic identifies CoreUI palette rendition data.
|
||
const cafef00dMagic = 0xcafef00d
|
||
|
||
func decodePalettedImage(data []byte, width, height int) (image.Image, error) {
|
||
pixelCount := width * height
|
||
|
||
// CoreUI palette format: [uint32 magic 0xcafef00d][uint32 version]
|
||
// [uint16 palette_count][count*4 RGBA entries][width*height indices]
|
||
if len(data) >= 10 {
|
||
magic := binary.LittleEndian.Uint32(data[0:4])
|
||
if magic == cafef00dMagic {
|
||
count := int(binary.LittleEndian.Uint16(data[8:10]))
|
||
hdr := 10
|
||
palBytes := count * 4
|
||
if len(data) < hdr+palBytes+pixelCount {
|
||
return nil, fmt.Errorf("cafef00d palette data too short: got %d, need %d (hdr=%d palette=%d indices=%d)",
|
||
len(data), hdr+palBytes+pixelCount, hdr, palBytes, pixelCount)
|
||
}
|
||
palette := make(color.Palette, count)
|
||
for i := range count {
|
||
off := hdr + i*4
|
||
palette[i] = color.RGBA{
|
||
R: data[off],
|
||
G: data[off+1],
|
||
B: data[off+2],
|
||
A: data[off+3],
|
||
}
|
||
}
|
||
indices := data[hdr+palBytes:]
|
||
rect := image.Rect(0, 0, width, height)
|
||
img := image.NewPaletted(rect, palette)
|
||
copy(img.Pix, indices[:pixelCount])
|
||
return img, nil
|
||
}
|
||
}
|
||
|
||
// Fallback: fixed 256-entry palette + indices
|
||
const fixedPaletteSize = 256
|
||
fixedPalBytes := fixedPaletteSize * 4
|
||
if len(data) >= fixedPalBytes+pixelCount {
|
||
palette := make(color.Palette, fixedPaletteSize)
|
||
for i := range fixedPaletteSize {
|
||
off := i * 4
|
||
palette[i] = color.RGBA{
|
||
R: data[off],
|
||
G: data[off+1],
|
||
B: data[off+2],
|
||
A: data[off+3],
|
||
}
|
||
}
|
||
rect := image.Rect(0, 0, width, height)
|
||
img := image.NewPaletted(rect, palette)
|
||
copy(img.Pix, data[fixedPalBytes:fixedPalBytes+pixelCount])
|
||
return img, nil
|
||
}
|
||
|
||
// Fallback: indices only (grayscale palette)
|
||
if len(data) >= pixelCount {
|
||
palette := make(color.Palette, 256)
|
||
for i := range 256 {
|
||
palette[i] = color.RGBA{uint8(i), uint8(i), uint8(i), 255}
|
||
}
|
||
rect := image.Rect(0, 0, width, height)
|
||
img := image.NewPaletted(rect, palette)
|
||
copy(img.Pix, data[:pixelCount])
|
||
return img, nil
|
||
}
|
||
|
||
return nil, fmt.Errorf("insufficient data for paletted image: got %d bytes, need %d (width=%d height=%d)",
|
||
len(data), pixelCount, width, height)
|
||
}
|
||
|
||
// decodeAppIconARGB decodes AppIcon ARGB format where channels are separated
|
||
// Instead of interleaved ARGBARGBARGB, the data is stored as AAARRRGGGBBB
|
||
func decodeAppIconARGB(data []byte, width, height int) (image.Image, error) {
|
||
pixelCount := width * height
|
||
expectedSize := pixelCount * 4 // 4 bytes per pixel (ARGB)
|
||
|
||
if len(data) < expectedSize {
|
||
return nil, fmt.Errorf("insufficient data for AppIcon ARGB: got %d bytes, expected %d", len(data), expectedSize)
|
||
}
|
||
|
||
rect := image.Rectangle{
|
||
Min: image.Point{0, 0},
|
||
Max: image.Point{X: width, Y: height},
|
||
}
|
||
|
||
img := image.NewRGBA(rect)
|
||
|
||
// Extract the separate channels
|
||
// The format is: all alpha values, then all red, then all green, then all blue
|
||
alphaChannel := data[0:pixelCount]
|
||
redChannel := data[pixelCount : pixelCount*2]
|
||
greenChannel := data[pixelCount*2 : pixelCount*3]
|
||
blueChannel := data[pixelCount*3 : pixelCount*4]
|
||
|
||
// Reconstruct interleaved RGBA pixels
|
||
for i := range pixelCount {
|
||
pixelIndex := i * 4
|
||
img.Pix[pixelIndex+0] = redChannel[i] // R
|
||
img.Pix[pixelIndex+1] = greenChannel[i] // G
|
||
img.Pix[pixelIndex+2] = blueChannel[i] // B
|
||
img.Pix[pixelIndex+3] = alphaChannel[i] // A
|
||
}
|
||
|
||
return img, nil
|
||
}
|