You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.02 22:33:09