Luau中PNG解码去滤波步骤异常,请求技术修复
PNG解码去滤波逻辑问题修复
在Luau中实现PNG解码时,已完成文件块解析、IDAT解压及RGBA格式转换,但去滤波步骤存在异常:仅0号(无操作)滤波正常,1号(Sub)滤波红色通道异常,2-4号滤波结果严重失真。以下是问题排查和修复方案:
核心问题点及修复
1. 上一行扫描线引用错误
原代码使用未滤波的原始扫描线(ScanlineUnfiltered)作为上一行参考,而PNG滤波规则要求使用已完成去滤波的上一行扫描线作为ByteB和ByteC的数据源,这是Up、Average、Paeth滤波严重失真的核心原因。
修复代码:
将循环末尾的:
PreviousScanLine = ScanlineUnfiltered
改为:
PreviousScanLine = Scanline
同时将初始的PreviousScanLine赋值改为直接读取已处理的扫描线:
local PreviousScanLine:buffer? = Scanlines[Y - 1]
2. Sub滤波的ByteA判断条件错误
原代码中X > PixelBytes的判断会漏掉X == PixelBytes的情况,导致每个像素的第一个通道(如RGB的红色通道)无法正确获取左侧同通道字节,引发Sub滤波的红通道异常。
修复代码:
将ByteA的判断:
local ByteA:number = if (X > PixelBytes) then buffer.readu8(Scanline, X - PixelBytes) else 0
改为:
local ByteA:number = if (X >= PixelBytes) then buffer.readu8(Scanline, X - PixelBytes) else 0
同步修正ByteC的判断条件:
local ByteC:number = if (PreviousScanLine and (X >= PixelBytes)) then buffer.readu8(PreviousScanLine, X - PixelBytes) else 0
3. 字节值的溢出处理优化
原代码使用%256处理溢出,对于无符号字节,使用位运算bit32.band(result, 0xFF)更符合PNG的字节定义,避免负数模运算的潜在问题。
修复代码:
将DefilteredByte的计算:
local DefilteredByte:number = m:ApplyDefiltering(FilterByte, Byte, ByteA, ByteB, ByteC) % 256
改为:
local DefilteredByte:number = bit32.band(m:ApplyDefiltering(FilterByte, Byte, ByteA, ByteB, ByteC), 0xFF)
4. GetInt函数的位偏移计算错误(潜在问题)
原代码中BitOffset = (CurrentPos * BitDepth) % 8的计算逻辑错误,CurrentPos本身就是累计的位数,位偏移应为CurrentPos % 8,该错误当前因仅使用8位深未暴露,但需修正以支持其他位深。
修复代码:
将GetInt中的:
local BitOffset:number = (CurrentPos * BitDepth) % 8
改为:
local BitOffset:number = CurrentPos % 8
修复后的完整代码片段
主处理代码修复后
-- Util function to query a certain number of bits from the image data bytes -- Will be useful if support for bit depths other than 8 are introduced -- From here, CurrentPos shall be measured in bits instead of bytes CurrentPos = 0 local CurrentBuffer:buffer = DecompressedData local function GetInt(BitDepth:number):number local ByteIndex:number = math.floor(CurrentPos / 8) local BitOffset:number = CurrentPos % 8 -- 修复位偏移计算 local Int:number = 0 if BitDepth == 8 then Int = buffer.readu8(CurrentBuffer, ByteIndex) elseif BitDepth == 16 then Int = buffer.readu16(CurrentBuffer, ByteIndex) elseif BitDepth == 32 then Int = buffer.readu32(CurrentBuffer, ByteIndex) else -- For bit depths less than 8, you need to handle bit-wise extraction local Byte:number = buffer.readu8(CurrentBuffer, ByteIndex) local Shift:number = 8 - BitDepth - BitOffset return bit32.rshift(bit32.band(Byte, bit32.lshift(0xFF, BitOffset)), Shift) end CurrentPos += BitDepth return Int end -- Defilter process -- Start by splitting the bytes into scanlines -- How many bytes are in a scanline? (not including the filter byte) local ScanlineBytes:number = math.floor(ImageStruct.BitDepth * ImageStruct.Size.X / 8) local PixelBytes:number = 1 -- how many bytes in a single pixel? local Scanlines:{buffer} = {} -- List of scanlines processed if ImageStruct.ColorType == 2 then -- Truecolor PixelBytes = 3 elseif ImageStruct.ColorType == 3 then -- Indexed color PixelBytes = 1 -- lol.... elseif ImageStruct.ColorType == 6 then -- Truecolor with alpha PixelBytes = 4 end -- No support for greyscale color types yet sory ScanlineBytes *= PixelBytes -- The amount of bytes in a scanline is actually the width * how many bytes are in one pixel. -- For every scanline for Y:number = 1, ImageStruct.Size.Y do -- Get this scanline's filter byte local FilterByte:number = GetInt(8) local PreviousScanLine:buffer? = Scanlines[Y - 1] -- 修复:直接取已处理的上一行扫描线 local PreviousByte:number = 0 -- Currently unused variable local Scanline:buffer = buffer.create(ScanlineBytes) local ScanlineUnfiltered:buffer = buffer.create(ScanlineBytes) -- For every byte on the scanline (defiltering works with bytes, not pixels, so we can disregard bit depth for now) for X:number = 0, ScanlineBytes-1, 1 do -- Get this byte local Byte:number = GetInt(8) -- Get the byte in the previous pixel (the result should be 0 if we're on the first pixel) local ByteA:number = if (X >= PixelBytes) then buffer.readu8(Scanline, X - PixelBytes) else 0 -- 修复判断条件 -- Get the same byte in the previous scanline (the result should be 0 if we're on the first scanline) local ByteB:number = if PreviousScanLine then buffer.readu8(PreviousScanLine, X) else 0 -- Get the byte in the previous pixel on the previous scanline (the result should be 0 if blah blah blah you get the point) local ByteC:number = if (PreviousScanLine and (X >= PixelBytes)) then buffer.readu8(PreviousScanLine, X - PixelBytes) else 0 -- 同步修复ByteC的判断条件 -- Apply appropriate defiltering function local DefilteredByte:number = bit32.band(m:ApplyDefiltering(FilterByte, Byte, ByteA, ByteB, ByteC), 0xFF) -- 修复溢出处理 buffer.writeu8(ScanlineUnfiltered, X, Byte) buffer.writeu8(Scanline, X, DefilteredByte) PreviousByte = Byte end table.insert(Scanlines, Scanline) end -- Finally, we need to actually get the pixels local Pixels:{number} = {} for Y:number = 1, ImageStruct.Size.Y do local Scanline:buffer = Scanlines[Y] -- Set variables used by GetInt (we are reading from the current scanline) CurrentBuffer = Scanline CurrentPos = 0 for X:number = 1, ImageStruct.Size.X, 1 do local R:number, G:number, B:number, A:number if ImageStruct.ColorType == 2 then -- Truecolor R = GetInt(8) / 255 G = GetInt(8) / 255 B = GetInt(8) / 255 A = 1 elseif ImageStruct.ColorType == 3 then -- Indexed color local PaletteIndex:number = GetInt(8) local Color:Color3 = ImageStruct.Pallette[PaletteIndex] -- Get the color from the pallette (PLTE) R = Color.R G = Color.G B = Color.B A = 1 elseif ImageStruct.ColorType == 6 then -- Truecolor with alpha R = GetInt(8) / 255 G = GetInt(8) / 255 B = GetInt(8) / 255 A = GetInt(8) / 255 end table.insert(Pixels, R) table.insert(Pixels, G) table.insert(Pixels, B) table.insert(Pixels, A) end end
去滤波函数无需修改
function m:ApplyDefiltering(FilterByte:number, Byte:number, ByteA:number, ByteB:number, ByteC:number) if FilterByte == 0 then -- None return Byte elseif FilterByte == 1 then -- Sub return Byte + ByteA elseif FilterByte == 2 then -- Up return Byte + ByteB elseif FilterByte == 3 then -- Average -- Average(x) + floor((ByteA + ByteB) / 2) return Byte + math.floor((ByteA + ByteB) / 2) elseif FilterByte == 4 then -- Paeth Predictor local P:number = ByteA + ByteB - ByteC local PA:number = math.abs(P - ByteA) local PB:number = math.abs(P - ByteB) local PC:number = math.abs(P - ByteC) local PR:number if (PA <= PB) and (PA <= PC) then PR = ByteA elseif PB <= PC then PR = ByteB else PR = ByteC end return Byte + PR end end
内容的提问来源于stack exchange,提问作者Ethan Pixelate
相关产品推荐
相关产品推荐

