///|
/// 纯 MoonBit PNG 像素画布:渲染计划 → RGBA 位图 → PNG 字节流。
/// 借用 std 的 @base64 作为文本面;配合 native 目标可以直接打开。
struct RgbaBuf {
width : Int
height : Int
pixels : FixedArray[Int] // ARGB
}
///|
fn rgba_new(width : Int, height : Int) -> RgbaBuf {
{ width, height, pixels: FixedArray::make(width * height, 0xFF000000), }
}
///|
fn rgba_set(buf : RgbaBuf, x : Int, y : Int, argb : Int) -> Unit {
if x >= 0 && x < buf.width && y >= 0 && y < buf.height {
buf.pixels[y * buf.width + x] = argb
}
}
///|
fn fill_rect_rgba(
buf : RgbaBuf,
x : Int,
y : Int,
w : Int,
h : Int,
argb : Int,
) -> Unit {
let x0 = if x < 0 { 0 } else { x }
let y0 = if y < 0 { 0 } else { y }
let x1 = if x + w > buf.width { buf.width } else { x + w }
let y1 = if y + h > buf.height { buf.height } else { y + h }
for yy in y0.. Unit {
let x1 = if x + w > buf.width { buf.width } else { x + w }
let y1 = if y + h > buf.height { buf.height } else { y + h }
for xx in x.. Bool {
s.length() >= prefix.length() &&
s.substring(start=0, end=prefix.length()) == prefix
}
///|
fn hex_digit_c(s : String, i : Int) -> Int? {
match s.get_char(i) {
Some(c) => hex_digit(c)
None => None
}
}
///|
fn hex_digit(c : Char) -> Int? {
let code = c.to_int()
if code >= 48 && code <= 57 {
Some(code - 48)
} else if code >= 97 && code <= 102 {
Some(code - 87)
} else if code >= 65 && code <= 70 {
Some(code - 55)
} else {
None
}
}
///|
fn hex2(s : String, i : Int) -> Int {
let a = match hex_digit_c(s, i) {
Some(x) => x
None => 0
}
let b = match hex_digit_c(s, i + 1) {
Some(x) => x
None => 0
}
a * 16 + b
}
///|
fn color_from_hex(hex : String) -> Int {
let body = if has_pref(hex, "#") { hex.substring(start=1) } else { hex }
0xFF000000 + (hex2(body, 0) << 16) + (hex2(body, 2) << 8) + hex2(body, 4)
}
///|
fn push_b(arr : Array[Byte], v : Int) -> Unit {
arr.push((v & 0xFF).to_byte())
}
///|
fn push_be32(arr : Array[Byte], v : Int) -> Unit {
push_b(arr, (v >> 24) & 0xFF)
push_b(arr, (v >> 16) & 0xFF)
push_b(arr, (v >> 8) & 0xFF)
push_b(arr, v & 0xFF)
}
///|
fn push_tag(arr : Array[Byte], tag : String) -> Unit {
for i in 0.. push_b(arr, c.to_int())
None => ()
}
}
}
///|
/// CRC32-IEEE:UInt 语义,poly 0xEDB88320;(初始 0xFFFFFFFF>xor-final)
fn crc32(data : Array[Byte], start : Int, len : Int) -> Int {
// 64 位承载 32 位 CRC 位模式,避免有符号溢出与算术右移
let crc = crc32_data(data, start, len)
let fin : Int64 = crc ^ 4294967295L
fin.to_int()
}
///|
fn crc32_data(data : Array[Byte], start : Int, len : Int) -> Int64 {
let mut crc : Int64 = 4294967295L
let poly : Int64 = 3988292384L // 0xEDB88320
for i in 0.. b.to_int().to_int64()
None => 0L
}
crc = crc ^ (b0 & 255L)
for _bit in 0..<8 {
if (crc & 1L) == 1L {
crc = (crc >> 1) ^ poly
} else {
crc = crc >> 1
}
}
}
crc
}
///|
fn adler32(data : Array[Byte]) -> Int {
let mut a = 1
let mut b = 0
for i in 0.. x.to_int() & 0xFF
None => 0
}
a = (a + byte0) % 65521
b = (b + a) % 65521
}
(b << 16) + a
}
///|
fn zlib_store(data : Array[Byte]) -> Array[Byte] {
let out : Array[Byte] = []
push_b(out, 0x78)
push_b(out, 0x01)
let n : Int = if data.length() == 0 {
1
} else {
(data.length() + 65534) / 65535
}
for i in 0.. data.length() { data.length() } else { st + 65535 }
let sz = en - st
let final_flag = if i == n - 1 { 1 } else { 0 }
push_b(out, final_flag)
push_b(out, sz % 256)
push_b(out, (sz >> 8) & 0xFF)
push_b(out, (0xFFFF - sz) & 0xFF)
push_b(out, ((0xFFFF - sz) >> 8) & 0xFF)
for k in st.. out.push(x)
None => ()
}
}
}
push_be32(out, adler32(data))
out
}
///|
fn png_chunk(tag : String, data : Array[Byte]) -> Array[Byte] {
let body : Array[Byte] = []
push_tag(body, tag)
for x in data.iter() {
body.push(x)
}
let out : Array[Byte] = []
push_be32(out, data.length())
for x in body.iter() {
out.push(x)
}
push_be32(out, crc32(body, 0, body.length()))
out
}
///|
pub fn png_encode(buf : RgbaBuf) -> Array[Byte] {
let out : Array[Byte] = []
push_b(out, 137)
push_b(out, 80)
push_b(out, 78)
push_b(out, 71)
push_b(out, 13)
push_b(out, 10)
push_b(out, 26)
push_b(out, 10)
let ihdr : Array[Byte] = []
push_be32(ihdr, buf.width)
push_be32(ihdr, buf.height)
push_b(ihdr, 8)
push_b(ihdr, 6)
push_b(ihdr, 0)
push_b(ihdr, 0)
push_b(ihdr, 0)
let ihdr_chunk = png_chunk("IHDR", ihdr)
for x in ihdr_chunk.iter() {
out.push(x)
}
let raw : Array[Byte] = []
for y in 0..> 16) & 0xFF)
push_b(raw, (px >> 8) & 0xFF)
push_b(raw, px & 0xFF)
push_b(raw, (px >> 24) & 0xFF)
}
}
let idat = zlib_store(raw)
for x in png_chunk("IDAT", idat).iter() {
out.push(x)
}
let iend = png_chunk("IEND", [])
for x in iend.iter() {
out.push(x)
}
out
}
///|
fn base64_encode(data : Array[Byte]) -> String {
let bytes = Bytes::from_iter(data.iter())
@base64.encode(bytes)
}
///|
pub fn render_png(
doc : @core.Document,
width : Int,
height : Int,
) -> Array[Byte] {
let layout = @core.solve(doc)
let buf = rgba_new(width, height)
fill_rect_rgba(buf, 0, 0, width, height, color_from_hex("#FFFFFF"))
walk_png(buf, doc, doc.root_id, layout)
png_encode(buf)
}
///|
fn walk_png(
buf : RgbaBuf,
doc : @core.Document,
id : String,
layout : @core.SolvedLayout,
) -> Unit {
match doc.nodes.get(id) {
None => ()
Some(n) =>
match layout.rect_of(id) {
None => ()
Some(r) => {
let fill_hex : String = if n.style.fill == "none" {
"#FFFFFF"
} else {
n.style.fill
}
fill_rect_rgba(
buf,
r.x.to_int(),
r.y.to_int(),
r.width.to_int(),
r.height.to_int(),
color_from_hex(fill_hex),
)
stroke_rect_rgba(
buf,
r.x.to_int(),
r.y.to_int(),
r.width.to_int(),
r.height.to_int(),
color_from_hex("#CCCCCC"),
)
for cid in n.children.iter() {
walk_png(buf, doc, cid, layout)
}
}
}
}
}