如何将PNG的IDAT块数据解码为原始像素?附4x4示例解析
Got it, let's walk through decoding your 4x4 PNG's IDAT block into raw pixel data—since you know Swift, I’ll structure this with pseudocode and steps that map directly to Swift’s capabilities.
First, let's pull the compressed data from your hex dump. Your IDAT block starts at offset 0000003F:
- Block length:
0000003F(63 bytes) - Block type:
49444152(IDAT) - Compressed data:
081D013400CBFF01CC96B1134FE120C0CECDF15101FFA56000000000000E0403201DFE59286DF6D0000000000004EDB11F002E007A2193EDB11F3063136F4733525A - CRC: (we can ignore this since you already validated it)
PNG uses the DEFLATE compression algorithm (part of zlib). In Swift, you can use the zlib library (via Foundation or a lightweight wrapper) to inflate this data. Here’s pseudocode for this step:
// 1. Convert the hex string of compressed IDAT data to Data let compressedIDATHex = "081D013400CBFF01CC96B1134FE120C0CECDF15101FFA56000000000000E0403201DFE59286DF6D0000000000004EDB11F002E007A2193EDB11F3063136F4733525A" guard let compressedIDATData = Data(hexString: compressedIDATHex) else { fatalError("Failed to parse hex string to Data") } // 2. Inflate using zlib (Swift implementation with zlib bindings) func inflate(data: Data) -> Data? { guard !data.isEmpty else { return nil } // Add zlib header (0x78 0x9C) if your raw IDAT data doesn't include it let zlibHeader = Data([0x78, 0x9C]) let fullData = zlibHeader + data var buffer = [UInt8](repeating: 0, count: 1024) var stream = z_stream() stream.zalloc = nil stream.zfree = nil stream.opaque = nil stream.avail_in = UInt32(fullData.count) stream.next_in = UnsafeMutablePointer(mutating: (fullData as NSData).bytes.bindMemory(to: UInt8.self, capacity: fullData.count)) stream.avail_out = UInt32(buffer.count) stream.next_out = UnsafeMutablePointer(mutating: buffer) guard inflateInit2(&stream, MAX_WBITS + 16) == Z_OK else { return nil } var result = Data() repeat { if stream.avail_out == 0 { result.append(buffer) buffer = [UInt8](repeating: 0, count: 1024) stream.avail_out = UInt32(buffer.count) stream.next_out = UnsafeMutablePointer(mutating: buffer) } let status = inflate(&stream, Z_NO_FLUSH) guard status != Z_STREAM_ERROR else { inflateEnd(&stream) return nil } switch status { case Z_NEED_DICT, Z_DATA_ERROR, Z_MEM_ERROR: inflateEnd(&stream) return nil default: break } } while stream.avail_in > 0 || stream.avail_out == 0 let remainingBytes = buffer.count - Int(stream.avail_out) if remainingBytes > 0 { result.append(buffer[0..<remainingBytes]) } inflateEnd(&stream) return result } guard let inflatedData = inflate(data: compressedIDATData) else { fatalError("Failed to inflate IDAT data") }
Note: Raw IDAT DEFLATE data often lacks the zlib header, so adding 0x78 0x9C (standard zlib wrapper) is usually required for the inflate function to work correctly.
Once inflated, you’ll get a sequence of scanlines. Each scanline starts with a filter byte (1 byte) followed by pixel data. For your 4x4 RGB image:
- Each pixel uses 3 bytes (R, G, B)
- 4 pixels per scanline = 12 bytes of pixel data
- Total per scanline: 1 (filter byte) + 12 = 13 bytes
- 4 scanlines total: 4 * 13 = 52 bytes of inflated data
You need to reverse the filter for each scanline using PNG’s official filter rules. Here’s Swift-style pseudocode for this step:
// Define PNG filter types as an enum for clarity enum PNGFilterType: UInt8 { case none = 0 case sub = 1 case up = 2 case average = 3 case paeth = 4 } // 1. Split inflated data into individual scanlines let bytesPerPixel = 3 // RGB, 8 bits per channel let pixelsPerScanline = 4 let bytesPerScanline = 1 + (pixelsPerScanline * bytesPerPixel) // 13 bytes per line var scanlines = [[UInt8]]() var currentIndex = 0 while currentIndex < inflatedData.count { let endIndex = currentIndex + bytesPerScanline guard endIndex <= inflatedData.count else { break } let scanline = Array(inflatedData[currentIndex..<endIndex]) scanlines.append(scanline) currentIndex = endIndex } // 2. De-filter each scanline to get raw pixel data var rawPixels = [UInt8]() var previousScanlinePixelData = [UInt8]() // Empty for the first scanline for scanline in scanlines { let filterType = PNGFilterType(rawValue: scanline[0]) ?? .none let filteredPixelData = scanline[1...] // Skip the leading filter byte var deFilteredData = [UInt8]() for (i, byte) in filteredPixelData.enumerated() { // Get neighboring bytes based on filter rules let leftByte: UInt8 = i >= bytesPerPixel ? deFilteredData[i - bytesPerPixel] : 0 let upByte: UInt8 = i < previousScanlinePixelData.count ? previousScanlinePixelData[i] : 0 let upLeftByte: UInt8 = (i >= bytesPerPixel && i - bytesPerPixel < previousScanlinePixelData.count) ? previousScanlinePixelData[i - bytesPerPixel] : 0 let deFilteredByte: UInt8 switch filterType { case .none: deFilteredByte = byte case .sub: deFilteredByte = byte &+ leftByte // Use wrapping addition case .up: deFilteredByte = byte &+ upByte case .average: deFilteredByte = byte &+ UInt8((Int(leftByte) + Int(upByte)) / 2) case .paeth: // Paeth predictor: pick the closest of left/up/up-left let p = Int(leftByte) + Int(upByte) - Int(upLeftByte) let pa = abs(p - Int(leftByte)) let pb = abs(p - Int(upByte)) let pc = abs(p - Int(upLeftByte)) let predictor: UInt8 if pa <= pb && pa <= pc { predictor = leftByte } else if pb <= pc { predictor = upByte } else { predictor = upLeftByte } deFilteredByte = byte &+ predictor } deFilteredData.append(deFilteredByte) } rawPixels.append(contentsOf: deFilteredData) previousScanlinePixelData = deFilteredData } // Now rawPixels contains 48 bytes of raw RGB data (4x4 pixels × 3 bytes each) // Pixels are ordered left-to-right, top-to-bottom, each as (R, G, B)
Once you run this code, you can cross-check the raw pixel values against the original image you created in GIMP. For example, if your image had a solid color or simple pattern, the raw RGB bytes should match exactly what you designed.
内容的提问来源于stack exchange,提问作者ANoobSwiftly

