From 5d977ae02865446cb4acbc136f055ab1eb076827 Mon Sep 17 00:00:00 2001
From: mehbark <terezi@pyrope.net>
Date: Tue, 11 Mar 2025 00:33:28 -0400
Subject: [PATCH] png output

---
 Cargo.lock  | 98 +++++++++++++++++++++++++++++++++++++++++++++++++++--
 Cargo.toml  |  1 +
 src/main.rs | 51 ++++++++++++++--------------
 3 files changed, 123 insertions(+), 27 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock
index c30458e..126a457 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2,6 +2,12 @@
 # It is not intended for manual editing.
 version = 4
 
+[[package]]
+name = "adler2"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
+
 [[package]]
 name = "anstream"
 version = "0.6.18"
@@ -58,6 +64,12 @@ version = "1.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
 
+[[package]]
+name = "bitflags"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+
 [[package]]
 name = "bitflags"
 version = "2.9.0"
@@ -74,6 +86,18 @@ dependencies = [
  "serde",
 ]
 
+[[package]]
+name = "bytemuck"
+version = "1.22.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6b1fc10dbac614ebc03540c9dbd60e83887fda27794998c6528f1782047d540"
+
+[[package]]
+name = "byteorder-lite"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
+
 [[package]]
 name = "cc"
 version = "1.2.16"
@@ -135,6 +159,15 @@ version = "1.0.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990"
 
+[[package]]
+name = "crc32fast"
+version = "1.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3"
+dependencies = [
+ "cfg-if",
+]
+
 [[package]]
 name = "either"
 version = "1.15.0"
@@ -157,11 +190,31 @@ dependencies = [
  "windows-sys",
 ]
 
+[[package]]
+name = "fdeflate"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c"
+dependencies = [
+ "simd-adler32",
+]
+
+[[package]]
+name = "flate2"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "11faaf5a5236997af9848be0bef4db95824b1d534ebc64d0f0c6cf3e67bd38dc"
+dependencies = [
+ "crc32fast",
+ "miniz_oxide",
+]
+
 [[package]]
 name = "grapher"
 version = "0.1.0"
 dependencies = [
  "clap",
+ "image",
  "mlua",
 ]
 
@@ -171,6 +224,18 @@ version = "0.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
 
+[[package]]
+name = "image"
+version = "0.25.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cd6f44aed642f18953a158afeb30206f4d50da59fbc66ecb53c66488de73563b"
+dependencies = [
+ "bytemuck",
+ "byteorder-lite",
+ "num-traits",
+ "png",
+]
+
 [[package]]
 name = "is_terminal_polyfill"
 version = "1.70.1"
@@ -224,6 +289,16 @@ version = "2.7.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
 
+[[package]]
+name = "miniz_oxide"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e3e04debbb59698c15bacbb6d93584a8c0ca9cc3213cb423d31f760d8843ce5"
+dependencies = [
+ "adler2",
+ "simd-adler32",
+]
+
 [[package]]
 name = "mlua"
 version = "0.10.3"
@@ -295,6 +370,19 @@ version = "0.3.32"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
 
+[[package]]
+name = "png"
+version = "0.17.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526"
+dependencies = [
+ "bitflags 1.3.2",
+ "crc32fast",
+ "fdeflate",
+ "flate2",
+ "miniz_oxide",
+]
+
 [[package]]
 name = "proc-macro2"
 version = "1.0.94"
@@ -319,7 +407,7 @@ version = "0.5.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0b8c0c260b63a8219631167be35e6a988e9554dbd323f8bd08439c8ed1302bd1"
 dependencies = [
- "bitflags",
+ "bitflags 2.9.0",
 ]
 
 [[package]]
@@ -334,7 +422,7 @@ version = "0.38.44"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154"
 dependencies = [
- "bitflags",
+ "bitflags 2.9.0",
  "errno",
  "libc",
  "linux-raw-sys",
@@ -373,6 +461,12 @@ version = "1.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
 
+[[package]]
+name = "simd-adler32"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe"
+
 [[package]]
 name = "smallvec"
 version = "1.14.0"
diff --git a/Cargo.toml b/Cargo.toml
index dc0f7ab..a074cdb 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -5,4 +5,5 @@ edition = "2021"
 
 [dependencies]
 clap = { version = "4.5.32", features = ["derive"] }
+image = { version = "0.25.5", features = ["png"], default-features = false }
 mlua = { version = "0.10.3", features = ["lua54", "vendored"] }
diff --git a/src/main.rs b/src/main.rs
index 9d78218..277a145 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,7 +1,9 @@
-use std::{num::NonZero, process};
+use std::{path::PathBuf, process};
 
 use clap::Parser;
+use image::{ImageBuffer, Pixel};
 use mlua::Lua;
+
 fn main() {
     let Args {
         width,
@@ -11,9 +13,13 @@ fn main() {
         ymin,
         xmax,
         ymax,
+        output_file,
     } = Args::parse();
+    if output_file.extension().is_none_or(|s| s != "png") {
+        eprintln!("i'm only doing pngs");
+        process::exit(2)
+    }
     let lua = Lua::new();
-
     // so we can write unqualified sin, cos, etc
     lua.load("setmetatable(_G, {__index = math})")
         .exec()
@@ -41,7 +47,6 @@ fn main() {
 
     let pos_to_sample = |ix: u16, iy: u16| -> (f32, f32) {
         // TODO: bad
-        let (width, height): (u16, u16) = (width.into(), height.into());
         let (width, height): (f32, f32) = (width.into(), height.into());
         let (x, y) = (f32::from(ix), f32::from(iy));
         let (xt, yt) = (x / width, y / height);
@@ -51,8 +56,8 @@ fn main() {
     let (mut min, mut max) = (-1.0f32, 1.0f32);
     // first, find the min and max of the function
     // (literally doubles our runtime :D)
-    for iy in 0..(height.into()) {
-        for ix in 0..(width.into()) {
+    for iy in 0..height {
+        for ix in 0..width {
             let (x, y) = pos_to_sample(ix, iy);
             let i = f(x, y);
             min = min.min(i);
@@ -61,35 +66,31 @@ fn main() {
     }
     let (min, max) = (min, max);
 
-    for iy in (0..(height.into())).rev() {
-        for ix in 0..(width.into()) {
-            let (x, y) = pos_to_sample(ix, iy);
-            let i = f(x, y);
-            let scaled = i / (max - min);
-            #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
-            // cube rooting makes things sharper
-            let channel = (scaled.cbrt() * 256.) as u8;
-            put_pixel(channel, channel, channel);
-            put_pixel(channel, channel, channel);
-        }
-        println!();
+    #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
+    let img = ImageBuffer::from_fn(width.into(), height.into(), |x, y| {
+        let (x, y) = pos_to_sample(x as u16, y as u16);
+        let i = f(x, y);
+        let scaled = i / (max - min);
+        // cube rooting makes things sharper
+        let channel = (scaled.cbrt() * 256.) as u8;
+        image::Luma([channel]).to_rgb()
+    });
+    if let Err(e) = img.save(output_file) {
+        eprintln!("error saving image: {e}");
+        process::exit(4);
     }
-    // reset style
-    print!("\x1b[0m");
-}
-
-fn put_pixel(r: u8, g: u8, b: u8) {
-    print!("\x1b[48;2;{r};{g};{b}m ");
 }
 
 #[derive(Debug, Parser)]
 struct Args {
     #[arg(long)]
-    width: NonZero<u16>,
+    width: u16,
     #[arg(long)]
-    height: NonZero<u16>,
+    height: u16,
     #[arg(short, long)]
     expr: String,
+    #[arg(short, long)]
+    output_file: PathBuf,
     #[arg(default_value_t = -1.0)]
     xmin: f32,
     #[arg(default_value_t = -1.0)]