///|
/// Reusable image operations for command-line and embedding integrations.
pub(all) enum PipelineStage {
Gray
InvertColors
Blur(radius~ : Int)
GaussianBlur
Sharpen
Edge
Posterize(levels~ : Int)
Threshold(level~ : Byte)
Normalize
Dither(levels~ : Int)
Pixelate(block~ : Int)
Grain(amount~ : Int, seed~ : Int)
Tint(color~ : Rgba, strength~ : Int)
Adjust(adjustment~ : ColorAdjustment)
Convolve(kernel~ : ConvolutionKernel)
Dilate(radius~ : Int, mode~ : MorphologyMode)
Erode(radius~ : Int, mode~ : MorphologyMode)
ChromaKey(key~ : Rgba, tolerance~ : Int)
Crop(x~ : Int, y~ : Int, width~ : Int, height~ : Int)
Resize(width~ : Int, height~ : Int)
FlipHorizontal
FlipVertical
} derive(Debug)
///|
pub(all) struct PipelineReport {
stage_count : Int
input_pixels : Int
output_pixels : Int
labels : Array[String]
} derive(Debug)
///|
pub fn PipelineReport::stage_count(self : PipelineReport) -> Int {
self.stage_count
}
///|
pub fn PipelineReport::input_pixels(self : PipelineReport) -> Int {
self.input_pixels
}
///|
pub fn PipelineReport::output_pixels(self : PipelineReport) -> Int {
self.output_pixels
}
///|
pub fn PipelineReport::labels(self : PipelineReport) -> ArrayView[String] {
self.labels[:]
}
///|
pub(all) struct CanvasPipeline {
stages : Array[PipelineStage]
} derive(Debug)
///|
pub fn CanvasPipeline::new() -> CanvasPipeline {
{ stages: [] }
}
///|
pub fn CanvasPipeline::stage_count(self : CanvasPipeline) -> Int {
self.stages.length()
}
///|
pub fn CanvasPipeline::add(
self : CanvasPipeline,
stage : PipelineStage,
) -> CanvasPipeline {
self.stages.push(stage)
self
}
///|
pub fn CanvasPipeline::gray(self : CanvasPipeline) -> CanvasPipeline {
self.add(Gray)
}
///|
pub fn CanvasPipeline::invert(self : CanvasPipeline) -> CanvasPipeline {
self.add(InvertColors)
}
///|
pub fn CanvasPipeline::blur(
self : CanvasPipeline,
radius : Int,
) -> CanvasPipeline {
self.add(Blur(radius~))
}
///|
pub fn CanvasPipeline::posterize(
self : CanvasPipeline,
levels : Int,
) -> CanvasPipeline {
self.add(Posterize(levels~))
}
///|
pub fn CanvasPipeline::threshold(
self : CanvasPipeline,
level : Byte,
) -> CanvasPipeline {
self.add(Threshold(level~))
}
///|
pub fn CanvasPipeline::resize(
self : CanvasPipeline,
width : Int,
height : Int,
) -> CanvasPipeline {
self.add(Resize(width~, height~))
}
///|
fn apply_stage(canvas : Canvas, stage : PipelineStage) -> Canvas {
match stage {
Gray => canvas.grayscale()
InvertColors => canvas.invert()
Blur(radius~) => canvas.blur(radius~)
GaussianBlur => canvas.gaussian_blur()
Sharpen => canvas.sharpen()
Edge => canvas.edge_map()
Posterize(levels~) => canvas.posterize(levels)
Threshold(level~) => canvas.threshold(level.to_int())
Normalize => canvas.normalize_luma()
Dither(levels~) => canvas.ordered_dither(levels~)
Pixelate(block~) => canvas.pixelate(block)
Grain(amount~, seed~) => canvas.grain(amount, seed~)
Tint(color~, strength~) => canvas.tint(tint=color, strength~)
Adjust(adjustment~) => canvas.adjust(adjustment)
Convolve(kernel~) => canvas.convolve(kernel)
Dilate(radius~, mode~) => canvas.dilate(radius, mode~)
Erode(radius~, mode~) => canvas.erode(radius, mode~)
ChromaKey(key~, tolerance~) => canvas.chroma_key(key, tolerance)
Crop(x~, y~, width~, height~) => canvas.crop(x~, y~, width~, height~)
Resize(width~, height~) => canvas.resize_nearest(width~, height~)
FlipHorizontal => canvas.flip_horizontal()
FlipVertical => canvas.flip_vertical()
}
}
///|
fn stage_label(stage : PipelineStage) -> String {
match stage {
Gray => "grayscale"
InvertColors => "invert"
Blur(_) => "blur"
GaussianBlur => "gaussian-blur"
Sharpen => "sharpen"
Edge => "edge"
Posterize(_) => "posterize"
Threshold(_) => "threshold"
Normalize => "normalize-luma"
Dither(_) => "ordered-dither"
Pixelate(_) => "pixelate"
Grain(..) => "grain"
Tint(..) => "tint"
Adjust(..) => "adjust"
Convolve(..) => "convolution"
Dilate(..) => "dilate"
Erode(..) => "erode"
ChromaKey(..) => "chroma-key"
Crop(..) => "crop"
Resize(..) => "resize"
FlipHorizontal => "flip-horizontal"
FlipVertical => "flip-vertical"
}
}
///|
/// Apply all stages in insertion order.
pub fn CanvasPipeline::apply(self : CanvasPipeline, canvas : Canvas) -> Canvas {
let mut result = canvas
for stage in self.stages {
result = apply_stage(result, stage)
}
result
}
///|
/// Return an auditable report for a single pipeline execution.
pub fn CanvasPipeline::report(
self : CanvasPipeline,
canvas : Canvas,
) -> PipelineReport {
let labels : Array[String] = []
let mut result = canvas
for stage in self.stages {
labels.push(stage_label(stage))
result = apply_stage(result, stage)
}
PipelineReport::{
stage_count: self.stages.length(),
input_pixels: canvas.len(),
output_pixels: result.len(),
labels,
}
}
///|
/// Apply the same visual pipeline to every animation frame.
pub fn CanvasPipeline::apply_animation(
self : CanvasPipeline,
animation : Animation,
) -> Animation {
let result = Animation::new(width=animation.width, height=animation.height)
.with_loop_count(loop_count=animation.loop_count)
.with_background(background=animation.background)
for frame in animation.frames {
ignore(result.add_frame(frame.with_canvas(self.apply(frame.canvas()))))
}
result
}
///|
/// A pipeline variant that can resize the logical screen to the final output.
pub fn CanvasPipeline::apply_animation_fit(
self : CanvasPipeline,
animation : Animation,
width : Int,
height : Int,
) -> Animation {
let resized = self.resize(width, height).apply_animation(animation)
let result = Animation::new(width~, height~)
.with_loop_count(loop_count=resized.loop_count)
.with_background(background=resized.background)
for frame in resized.frames {
ignore(result.add_frame(frame.with_offset(left=0, top=0)))
}
result
}
///|
pub(all) struct AnimationPipeline {
stages : Array[PipelineStage]
delay_override : Int?
} derive(Debug)
///|
pub fn AnimationPipeline::new() -> AnimationPipeline {
{ stages: [], delay_override: None }
}
///|
pub fn AnimationPipeline::add(
self : AnimationPipeline,
stage : PipelineStage,
) -> AnimationPipeline {
self.stages.push(stage)
self
}
///|
pub fn AnimationPipeline::with_delay(
self : AnimationPipeline,
delay_cs : Int?,
) -> AnimationPipeline {
{ ..self, delay_override: delay_cs }
}
///|
pub fn AnimationPipeline::apply(
self : AnimationPipeline,
animation : Animation,
) -> Animation {
let canvas_pipeline : CanvasPipeline = { stages: self.stages }
let result = canvas_pipeline.apply_animation(animation)
match self.delay_override {
None => result
Some(delay) => result.with_delay(delay)
}
}
///|
/// Apply stages while keeping the timeline and GIF frame metadata intact.
pub fn AnimationPipeline::apply_preserving_metadata(
self : AnimationPipeline,
animation : Animation,
) -> Animation {
let canvas_pipeline : CanvasPipeline = { stages: self.stages }
let result = Animation::new(width=animation.width, height=animation.height)
.with_loop_count(loop_count=animation.loop_count)
.with_background(background=animation.background)
for frame in animation.frames {
let transformed = canvas_pipeline.apply(frame.canvas())
let next = match self.delay_override {
None => frame.with_canvas(transformed)
Some(delay) => frame.with_canvas(transformed).with_delay(delay_cs=delay)
}
ignore(result.add_frame(next))
}
result
}
///|
/// Compute a simple deterministic checksum after all stages.
pub fn CanvasPipeline::output_hash(
self : CanvasPipeline,
canvas : Canvas,
) -> UInt {
self.apply(canvas).hash()
}
///|
/// Compose independent pipelines without exposing mutable internals.
pub fn CanvasPipeline::then(
self : CanvasPipeline,
other : CanvasPipeline,
) -> CanvasPipeline {
let result = CanvasPipeline::new()
for stage in self.stages {
result.stages.push(stage)
}
for stage in other.stages {
result.stages.push(stage)
}
result
}
///|
/// Return a human-readable stage list for CLI help and diagnostics.
pub fn CanvasPipeline::describe(self : CanvasPipeline) -> String {
let mut result = ""
for index, stage in self.stages {
if index > 0 {
result = result + " -> "
}
result = result + stage_label(stage)
}
result
}