// Copyright 2025 International Digital Economy Academy
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
///|
/// PNG helpers (scaffold).
///
/// Upstream swash decodes embedded PNG glyph bitmaps and relies on `yazi` for
/// zlib inflation of IDAT chunks. We keep a minimal shared building block here
/// so the rest of the bitmap pipeline can be ported incrementally.
///|
/// Returns true if the data begins with the PNG signature.
pub fn is_png(data : Bytes) -> Bool {
data.length() >= 8 &&
data.get(0) == Some(b'\x89') &&
data.get(1) == Some(b'P') &&
data.get(2) == Some(b'N') &&
data.get(3) == Some(b'G') &&
data.get(4) == Some(b'\r') &&
data.get(5) == Some(b'\n') &&
data.get(6) == Some(b'\x1A') &&
data.get(7) == Some(b'\n')
}
///|
/// Inflates a zlib stream.
///
/// In PNG this is used for concatenated IDAT data.
pub fn inflate_zlib(data : Bytes) -> Array[Byte] raise @yazi.YaziError {
let (out, _) = @yazi.decompress(data, @yazi.Format::Zlib)
out
}
// ---- Internal PNG decoder (ported from upstream swash) --------------------
///|
const PNG_IHDR_LEN : Int = 13
///|
const CHUNK_IHDR : UInt = 0x49484452 // "IHDR"
///|
const CHUNK_PLTE : UInt = 0x504c5445 // "PLTE"
///|
const CHUNK_TRNS : UInt = 0x74524e53 // "tRNS"
///|
const CHUNK_IDAT : UInt = 0x49444154 // "IDAT"
///|
const CHUNK_GAMA : UInt = 0x67414d41 // "gAMA"
///|
const CHUNK_IEND : UInt = 0x49454e44 // "IEND"
// Color types.
///|
const COLOR_GREYSCALE : UInt = 0
///|
const COLOR_TRUECOLOR : UInt = 2
///|
const COLOR_INDEXED : UInt = 3
///|
const COLOR_GREYSCALE_ALPHA : UInt = 4
///|
const COLOR_TRUECOLOR_ALPHA : UInt = 6
///|
let adam7_row_start : Array[Int] = [0, 0, 4, 0, 2, 0, 1]
///|
let adam7_row_inc : Array[Int] = [8, 8, 8, 4, 4, 2, 2]
///|
let adam7_col_start : Array[Int] = [0, 4, 0, 2, 0, 1, 0]
///|
let adam7_col_inc : Array[Int] = [8, 8, 4, 4, 2, 2, 1]
///|
fn get_u32be(buf : Bytes, offset : Int) -> UInt? {
let b = buf
guard offset >= 0 && b.length() - offset >= 4 else { None }
match
(b.get(offset), b.get(offset + 1), b.get(offset + 2), b.get(offset + 3)) {
(Some(b0), Some(b1), Some(b2), Some(b3)) => {
let u0 = b0.to_int().reinterpret_as_uint()
let u1 = b1.to_int().reinterpret_as_uint()
let u2 = b2.to_int().reinterpret_as_uint()
let u3 = b3.to_int().reinterpret_as_uint()
Some((u0 << 24) | (u1 << 16) | (u2 << 8) | u3)
}
_ => None
}
}
///|
fn paeth(a : UInt, b : UInt, c : UInt) -> UInt {
let pa = (b.reinterpret_as_int() - c.reinterpret_as_int()).abs()
let pb = (a.reinterpret_as_int() - c.reinterpret_as_int()).abs()
let pc = (a.reinterpret_as_int() +
b.reinterpret_as_int() -
c.reinterpret_as_int() -
c.reinterpret_as_int()).abs()
if pc < pa && pc < pb {
c
} else if pb < pa {
b
} else {
a
}
}
///|
fn defilter(
ty : UInt,
source : Array[Byte],
dest : Array[Byte],
last : Array[Byte],
bwidth : Int,
) -> Bool {
let len = source.length()
if dest.length() < len || last.length() < len {
return false
}
match ty {
0 =>
for i in 0.. {
for i in 0..
for i in 0.. {
for i in 0.. {
for i in 0.. return false
}
true
}
///|
fn normalize(
source : Array[Byte],
dest : Array[Byte],
depth : UInt,
palette : Bool,
width : Int,
trunc_16 : Bool,
) -> Bool {
match depth {
16 => {
let needed = width
if dest.length() < needed {
return false
}
if trunc_16 {
// Take the high byte for each 16-bit sample.
for i in 0.. {
if dest.length() < source.length() {
return false
}
for i in 0.. {
if dest.length() < width {
return false
}
let conv = if !palette { 17 } else { 1 }
for i in 0..> shift) & 15) * conv.reinterpret_as_uint()
dest[i] = v.reinterpret_as_int().to_byte()
}
}
2 => {
if dest.length() < width {
return false
}
let conv = if !palette { 85 } else { 1 }
for i in 0..> shift) & 3) * conv.reinterpret_as_uint()
dest[i] = v.reinterpret_as_int().to_byte()
}
}
1 => {
if dest.length() < width {
return false
}
let conv = if !palette { 255 } else { 1 }
for i in 0..> shift) & 1) * conv.reinterpret_as_uint()
dest[i] = v.reinterpret_as_int().to_byte()
}
}
_ => return false
}
true
}
///|
fn emit_rgba8(
color_type : UInt,
palette : Array[Byte],
trans : Array[Byte],
source : Array[Byte],
image : Array[Byte],
x : Int,
y : Int,
width : Int,
inc : Int,
len : Int,
) -> Bool {
let mut out = y * width * 4 + x * 4
let mut i = 0
match color_type {
COLOR_INDEXED => {
let palette_len = palette.length()
let trans_len = trans.length()
for _ in 0..= palette_len {
image[out] = (0).to_byte()
image[out + 1] = (0).to_byte()
image[out + 2] = (0).to_byte()
} else {
image[out] = palette[p]
image[out + 1] = palette[p + 1]
image[out + 2] = palette[p + 2]
}
image[out + 3] = if t >= trans_len { (255).to_byte() } else { trans[t] }
i = i + 1
out = out + 4 * inc
}
}
COLOR_TRUECOLOR =>
for _ in 0..
for _ in 0..
for j in 0..
for _ in 0.. return false
}
true
}
///|
fn decode_png(
data : Bytes,
scratch : Array[Byte],
target : Array[Byte],
) -> (UInt, UInt)? {
if data.length() < 33 || !is_png(data) {
return None
}
// Validate IHDR.
let len = get_u32be(data, 8)
let ty = get_u32be(data, 12)
if len != Some(PNG_IHDR_LEN.reinterpret_as_uint()) || ty != Some(CHUNK_IHDR) {
return None
}
let width = match get_u32be(data, 16) {
None => return None
Some(v) => v
}
let height = match get_u32be(data, 20) {
None => return None
Some(v) => v
}
let depth = match data.get(24) {
None => return None
Some(v) => v.to_int().reinterpret_as_uint()
}
let color_type = match data.get(25) {
None => return None
Some(v) => v.to_int().reinterpret_as_uint()
}
let compression_method = match data.get(26) {
None => return None
Some(v) => v.to_int()
}
let filter_method = match data.get(27) {
None => return None
Some(v) => v.to_int()
}
let interlace_method = match data.get(28) {
None => return None
Some(v) => v.to_int()
}
if compression_method != 0 ||
filter_method != 0 ||
(interlace_method != 0 && interlace_method != 1) {
return None
}
// Validate depth/color_type.
match color_type {
COLOR_GREYSCALE | COLOR_INDEXED => {
if depth != 1 && depth != 2 && depth != 4 && depth != 8 && depth != 16 {
return None
}
if color_type == COLOR_INDEXED && depth == 16 {
return None
}
}
COLOR_TRUECOLOR | COLOR_TRUECOLOR_ALPHA | COLOR_GREYSCALE_ALPHA =>
if depth != 8 && depth != 16 {
return None
}
_ => return None
}
let w = width.reinterpret_as_int()
let h = height.reinterpret_as_int()
if w < 0 || h < 0 {
return None
}
if w == 0 || h == 0 {
return Some((width, height))
}
if target.length() < (width * height * 4U).reinterpret_as_int() {
return None
}
let channels = match color_type {
COLOR_TRUECOLOR => 3
COLOR_TRUECOLOR_ALPHA => 4
COLOR_GREYSCALE_ALPHA => 2
COLOR_GREYSCALE => 1
COLOR_INDEXED => 1
_ => return None
}
let bpp = depth.reinterpret_as_int() * channels
let pitch = (w * bpp + 7) / 8
let bwidth = (bpp + 7) / 8
// Collect palette, transparency and concatenated IDAT.
let palette : Array[Byte] = []
let trans : Array[Byte] = []
scratch.clear()
let limit = data.length()
let mut offset = 33
while true {
if offset + 8 > limit {
return None
}
let clen = match get_u32be(data, offset) {
None => return None
Some(v) => v.reinterpret_as_int()
}
let cty = match get_u32be(data, offset + 4) {
None => return None
Some(v) => v
}
offset = offset + 8
if clen < 0 || offset + clen > limit {
return None
}
// chunk data in data[offset:offset+clen]
match cty {
CHUNK_PLTE => {
palette.clear()
for i in 0.. return None
Some(b) => palette.push(b)
}
}
}
CHUNK_TRNS => {
trans.clear()
for i in 0.. return None
Some(b) => trans.push(b)
}
}
}
CHUNK_IDAT =>
for i in 0.. return None
Some(b) => scratch.push(b)
}
}
CHUNK_GAMA =>
// Ignored in swash's RGBA8 output.
()
CHUNK_IEND => break
_ => ()
}
offset = offset + clen + 4 // skip CRC
}
// Inflate concatenated IDAT zlib.
let idat = Bytes::from_array(scratch.op_as_view())
let decomp_opt = Some(inflate_zlib(idat)) catch { _ => None }
let decomp = match decomp_opt {
None => return None
Some(v) => v
}
if color_type == COLOR_INDEXED && palette.length() == 0 {
return None
}
// Row buffers.
let line : Array[Byte] = Array::makei(pitch, _ => b'\x00')
let prev_line : Array[Byte] = Array::makei(pitch, _ => b'\x00')
let out_line : Array[Byte] = Array::makei(w * 8, _ => b'\x00')
if interlace_method == 0 {
// Non-interlaced.
for y in 0.. decomp.length() {
return None
}
let ty = decomp[base].to_int().reinterpret_as_uint()
let scan : Array[Byte] = Array::makei(pitch, i => decomp[base + 1 + i])
if !defilter(ty, scan, line, prev_line, bwidth) {
return None
}
if depth == 8U {
if !emit_rgba8(color_type, palette, trans, line, target, 0, y, w, 1, w) {
return None
}
} else {
let has_palette = palette.length() != 0
let needed = w * channels
if !normalize(line, out_line, depth, has_palette, needed, true) {
return None
}
if !emit_rgba8(
color_type, palette, trans, out_line, target, 0, y, w, 1, w,
) {
return None
}
}
for i in 0.. decomp.length() {
return None
}
let row : Array[Byte] = Array::makei(pass_pitch + 1, i => {
decomp[off + i]
})
off = off + pass_pitch + 1
let ty = row[0].to_int().reinterpret_as_uint()
let scan : Array[Byte] = Array::makei(pass_pitch, i => row[i + 1])
if !defilter(ty, scan, line, prev_line, bwidth) {
return None
}
if depth == 8U {
if !emit_rgba8(
color_type, palette, trans, line, target, start, y, w, inc, cols,
) {
return None
}
} else {
let has_palette = palette.length() != 0
let needed = cols *
(if color_type == COLOR_TRUECOLOR {
3
} else if color_type == COLOR_TRUECOLOR_ALPHA {
4
} else if color_type == COLOR_GREYSCALE_ALPHA {
2
} else {
1
})
if !normalize(line, out_line, depth, has_palette, needed, true) {
return None
}
if !emit_rgba8(
color_type, palette, trans, out_line, target, start, y, w, inc, cols,
) {
return None
}
}
// swap prev and line
for i in 0..