///|
/// Image-based visual regression testing on top of the gfx pipeline.
///
/// crater's existing VRT compares *paint trees* (geometry). This package
/// closes the loop to *pixels*: it drives the same paint tree through the
/// `gfx_bridge` assembler onto the `gfx_software` CPU backend and exposes
/// the resulting RGBA framebuffer plus a deterministic ASCII rendering that
/// is convenient to snapshot.
///
/// `render_html_to_image` reuses the existing `vrt` HTML -> paint tree
/// entry point, so image VRT runs against the very same input as the
/// paint-tree VRT.
///|
/// A rendered RGBA framebuffer: row-major, four 0..255 channels per pixel.
pub(all) struct RenderedImage {
width : Int
height : Int
pixels : Array[Int]
}
///|
/// Read back a single pixel as (r, g, b, a).
pub fn RenderedImage::pixel_at(
self : RenderedImage,
x : Int,
y : Int,
) -> (Int, Int, Int, Int) {
if x < 0 || y < 0 || x >= self.width || y >= self.height {
return (0, 0, 0, 0)
}
let base = (y * self.width + x) * 4
(
self.pixels[base],
self.pixels[base + 1],
self.pixels[base + 2],
self.pixels[base + 3],
)
}
///|
/// Render a paint tree to an RGBA image of the given size via the software
/// backend. The tree is expected to be laid out in the same pixel space
/// (its root box matching the viewport).
pub fn render_paint_tree_to_image(
root : @paint_model.PaintNode,
width : Int,
height : Int,
) -> RenderedImage {
let driver = @gfx_software.SoftwareDriver::new(width, height)
// The software backend never actually raises; keep the public API total.
@gfx_bridge.render_paint_tree(driver, root, width~, height~) catch {
_ => ()
}
let pixels = try {
match @gfx.GraphicsDriver::read_pixels(driver, 0, 0, width, height) {
Some(p) => p
None => Array::make(width * height * 4, 0)
}
} catch {
_ => Array::make(width * height * 4, 0)
}
{ width, height, pixels }
}
///|
/// Render an HTML document to an RGBA image at `viewport`, reusing the
/// existing VRT HTML -> paint tree pipeline.
pub fn render_html_to_image(
html : String,
viewport : @types.Size[Double],
) -> RenderedImage {
let root = @vrt.render_html_to_paint_tree(html, viewport)
render_paint_tree_to_image(
root,
viewport.width.to_int(),
viewport.height.to_int(),
)
}
///|
/// Render the image as ASCII art for snapshotting: each pixel maps to a
/// ramp character by brightness (light -> space, dark -> '@'). Fully
/// transparent pixels render as a space.
pub fn RenderedImage::to_ascii(self : RenderedImage) -> String {
let ramp = [' ', '.', ':', '-', '=', '+', '*', '#', '%', '@']
let last = ramp.length() - 1
let buf = StringBuilder::new()
for y in 0.. RenderedImage {
let driver = @gfx_software.SoftwareDriver::new(width, height)
let pixels = try {
@gfx.GraphicsDriver::initialize(driver)
let dst = @gfx.GraphicsDriver::new_image(driver, width, height)
let shader = @gfx.GraphicsDriver::new_shader(driver, "s")
@gfx.GraphicsDriver::begin(
driver,
@gfx.new_render_pass_desc(@gfx.new_color(1.0, 1.0, 1.0, 1.0), true),
)
let frame = @gfx_frame.RenderFrame2D::new(
dst~,
shader~,
screen_w=width,
screen_h=height,
)
for
cmd in @gfx_text.bitmap_text_commands(
frame,
text,
x,
y,
scale,
@gfx.new_color(0.0, 0.0, 0.0, 1.0),
) {
@gfx.GraphicsDriver::draw_triangles(driver, cmd)
}
@gfx.GraphicsDriver::end(driver, false)
match @gfx.GraphicsDriver::read_pixels(driver, 0, 0, width, height) {
Some(p) => p
None => Array::make(width * height * 4, 0)
}
} catch {
_ => Array::make(width * height * 4, 0)
}
{ width, height, pixels }
}