~jpl8/text-to-piet

136caa446efc799ba1a5f71bb0b1ac9de277bc1d — jpl 1 year, 6 months ago 9fe7790
Start separating image creating functions into it's own file.

Also moved search_for_minimum_satisfier into utils.rs.
6 files changed, 334 insertions(+), 307 deletions(-)

M src/colors.rs
M src/instructions.rs
M src/main.rs
A src/piet_image.rs
A src/utils.rs
M test.png
M src/colors.rs => src/colors.rs +1 -1
@@ 1,6 1,6 @@
use std::fmt;

use crate::Instructions;
use crate::instructions::Instructions;

#[allow(dead_code)]
#[derive(PartialEq, Eq, Copy, Clone, Debug)]

M src/instructions.rs => src/instructions.rs +11 -25
@@ 12,6 12,7 @@ use std::fs::OpenOptions;
use anyhow::{anyhow, Result};

use crate::spiral;
use crate::utils;

#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)]
pub enum Instructions {


@@ 149,20 150,21 @@ pub fn expected_size(instructions: &Vec<Instructions>) -> Result<(u32, u32)> {
    // TODO: Make the actual corner_turn vec accessible here
    let corner_turn_len = 2;

    let num_corners = |side: &usize| if *side > 3 { side - 3 } else { 0 };

    let num_corners = |side: &usize| spiral::Spiral2D::num_of_turns(*side as u32) as usize;
    let eq_to_minimize = |side: &usize| {
        num_blocks + corner_turn_len * num_corners(side)
            <= (spiral::Spiral2D::length(*side as u32) as usize)
    };

    let min_img_size_range = match search_for_minimum_satisfier(eq_to_minimize, 1..usize::MAX) {
        Some(lower_bound) => {
            let upper_bound = lower_bound + std::cmp::min(num_pushes, num_corners(&lower_bound));
            Ok((lower_bound as u32, upper_bound as u32))
        }
        None => Err(anyhow!("Unable to find a lower bound on the image size!")),
    };
    let min_img_size_range =
        match utils::search_for_minimum_satisfier(eq_to_minimize, 1..usize::MAX) {
            Some(lower_bound) => {
                let upper_bound =
                    lower_bound + std::cmp::min(num_pushes, num_corners(&lower_bound));
                Ok((lower_bound as u32, upper_bound as u32))
            }
            None => Err(anyhow!("Unable to find a lower bound on the image size!")),
        };

    min_img_size_range



@@ 170,22 172,6 @@ pub fn expected_size(instructions: &Vec<Instructions>) -> Result<(u32, u32)> {
    //     as u32
    //     + corner_turn_len as u32
}

fn search_for_minimum_satisfier<T, F, I>(predicate: F, search_space: I) -> Option<T>
where
    F: Fn(&T) -> bool,
    T: Sized + PartialEq + Clone,
    I: IntoIterator<Item = T>,
{
    for (satisfies, val) in search_space.into_iter().map(|v| (predicate(&v), v)) {
        if satisfies {
            return Some(val.clone());
        }
    }

    None
}

pub fn gen_num_bfs(n: i64, initial_stack: Vec<i64>) -> Vec<Instructions> {
    eprintln!("BFS({n})");
    let moves = vec![

M src/main.rs => src/main.rs +12 -281
@@ 1,18 1,15 @@
use std::collections::HashSet;
use std::fs;

use clap::Parser;
use image::{Rgb, RgbImage};
use rand::seq::SliceRandom;

use anyhow::{bail, Context, Result};

use colors::Color;
use instructions::{Instructions::*, *};

mod colors;
mod instructions;
mod piet_image;
mod spiral;
mod utils;

use crate::instructions::PietEncoder;
use anyhow::{Context, Result};

use spiral::*;

/// A text to piet encoder


@@ 38,285 35,19 @@ fn main() -> Result<()> {
    let input = fs::read_to_string(args.input_file).context("Failed to read input file")?;

    let encoder = instructions::EncoderBFS::new("instruction_cache/bfs_instructions.ron");

    let instrs = encoder.encode_text(&input);

    let mut img = piet_image::PietImager::with_instructions(instrs);

    if let Some(img_size) = args.img_size {
        try_image_with_size(img_size, &instrs)
            .unwrap()
        // TODO: add image_size
        img.to_image()? // something
            .save(args.output.unwrap_or("test.png".to_string()))
            .context("Failed to write ouput image")
    } else {
        let (min_img_size, max_img_size) = expected_size(&instrs)?;
        eprintln!(
            "(min_img_size, max_img_size: {:?}",
            (min_img_size, max_img_size)
        );

        let sat_img_size: u32 = search_for_minimum_satisfier(
            |size| try_image_with_size(*size, &instrs).is_ok(),
            min_img_size..=max_img_size,
        )
        .unwrap();

        try_image_with_size(sat_img_size, &instrs)
            .unwrap()
        img.to_image()?
            .save(args.output.unwrap_or("test.png".to_string()))
            .context("Failed to write ouput image")
    }
}

fn search_for_minimum_satisfier<T, F, I>(predicate: F, search_space: I) -> Option<T>
where
    F: Fn(&T) -> bool,
    T: Sized + PartialEq + Clone,
    I: IntoIterator<Item = T>,
{
    for (satisfies, val) in search_space.into_iter().map(|v| (predicate(&v), v)) {
        if satisfies {
            return Some(val.clone());
        }
    }

    None
}

fn try_image_with_size(img_size: u32, instrs: &Vec<Instructions>) -> Result<RgbImage> {
    eprintln!("spiral_len: {:?}", Spiral2D::length(img_size));
    eprintln!("img_size: {:?}", img_size);
    let colors = instructions_to_blocks(instrs, img_size)?;

    colors_to_img(colors, img_size)
        .context("Failed to convert colors to an image during main execution")
}

fn update_x_y_spiral(x: u32, y: u32, step: u32, dir: &Direction) -> (u32, u32) {
    match dir {
        Direction::Up => (x, y - step),
        Direction::Down => (x, y + step),
        Direction::Right => (x + step, y),
        Direction::Left => (x - step, y),
    }
}
fn instructions_to_blocks(instructions: &Vec<Instructions>, img_size: u32) -> Result<Vec<Color>> {
    let mut colors = vec![];
    let mut prev = Color::Red;

    let corner_turn = [PUSH(1), POINTER];
    let mut space_in_row: u32 = img_size;
    let mut remaining_space: u32 = img_size - 1;

    let mut direction = Direction::Right;

    for instr in instructions {
        //eprintln!("remaining_space: {}", remaining_space);
        match instr {
            PUSH(n) => {
                // If there isn't enough room for the entire push instruction
                // then split it up into two pushes, a corner_turn and an add.

                if remaining_space < (*n as u32) + corner_turn.len() as u32 {
                    //eprintln!(
                    //    "SPLIT PUSH - remaining_space: {:?}, n: {:?}, prev: {:?}",
                    //    remaining_space, n, prev
                    //);
                    let first_push = remaining_space as usize - corner_turn.len();
                    let second_push = n - first_push;
                    //eprintln!("first: {}, second: {}", first_push, second_push);
                    for _ in 0..first_push {
                        colors.push(prev);
                    }
                    prev = colors::instruction_to_color(PUSH(first_push), prev);
                    //eprintln!("COLOR: {:?}", prev);

                    for _instr in corner_turn {
                        colors.push(prev);
                        prev = colors::instruction_to_color(_instr, prev);
                        //eprintln!("COLOR: {:?}", prev);
                    }

                    direction = direction.clockwise();
                    match direction {
                        Direction::Down | Direction::Up => space_in_row -= 2,
                        _ => (),
                    }
                    remaining_space = space_in_row;

                    for _ in 0..second_push {
                        colors.push(prev);
                    }
                    remaining_space -= second_push as u32;
                    prev = colors::instruction_to_color(PUSH(second_push), prev);
                    //eprintln!("COLOR: {:?}", prev);

                    colors.push(prev);
                    prev = colors::instruction_to_color(ADD, prev);
                    //eprintln!("COLOR: {:?}", prev);
                    remaining_space -= 1;
                    continue;
                } else {
                    for _ in 0..*n {
                        colors.push(prev);
                    }
                    remaining_space -= *n as u32;
                }
            }
            _ => {
                colors.push(prev);
                remaining_space = remaining_space
                    .checked_sub(1)
                    .context("Ran out of space to place more blocks in the image!")?;
            }
        }
        prev = colors::instruction_to_color(*instr, prev);

        // If there is just enough space for a corner_turn,
        // then just turn
        if remaining_space == corner_turn.len() as u32 {
            for _instr in corner_turn {
                colors.push(prev);
                prev = colors::instruction_to_color(_instr, prev);
            }

            direction = direction.clockwise();
            match direction {
                Direction::Down | Direction::Up => space_in_row -= 2,
                _ => (),
            }

            remaining_space = space_in_row;
        }
    }

    colors.push(prev);

    Ok(colors)
}

fn colors_to_img(colors: Vec<Color>, img_size: u32) -> Result<RgbImage> {
    let mut img = RgbImage::new(img_size, img_size);

    let mut final_pos = (0, 0);
    let mut final_dir = Direction::Right;

    let sp = Spiral2D::new(0, 0, Direction::Right, img_size, colors.len() as u32);

    for ((x, y, dir), color) in sp.zip(colors.iter()) {
        img.put_pixel(x, y, Rgb(color.to_bytes()));
        final_pos = (x, y);
        final_dir = dir;
    }

    // Fill in remaining colors
    let filler_spiral = Spiral2D::new(
        img_size - 1,
        img_size - 1,
        Direction::Left,
        img_size,
        colors.len() as u32,
    );

    let all_colors: HashSet<[u8; 3]> = vec![
        Color::LightRed.to_bytes(),
        Color::LightYellow.to_bytes(),
        Color::LightGreen.to_bytes(),
        Color::LightCyan.to_bytes(),
        Color::LightBlue.to_bytes(),
        Color::LightMagenta.to_bytes(),
        Color::Red.to_bytes(),
        Color::Yellow.to_bytes(),
        Color::Green.to_bytes(),
        Color::Cyan.to_bytes(),
        Color::Blue.to_bytes(),
        Color::Magenta.to_bytes(),
        Color::White.to_bytes(),
        Color::Black.to_bytes(),
        Color::DarkRed.to_bytes(),
        Color::DarkYellow.to_bytes(),
        Color::DarkGreen.to_bytes(),
        Color::DarkCyan.to_bytes(),
        Color::DarkBlue.to_bytes(),
        Color::DarkMagenta.to_bytes(),
    ]
    .into_iter()
    .collect();

    for (x, y, _) in filler_spiral {
        let neighbors: HashSet<[u8; 3]> = vec![
            img.get_pixel_checked(if x == 0 { 0 } else { x - 1 }, y),
            img.get_pixel_checked(x + 1, y),
            img.get_pixel_checked(x, if y == 0 { 0 } else { y - 1 }),
            img.get_pixel_checked(x, y + 1),
        ]
        .into_iter()
        .map(|c| if let Some(rgb) = c { rgb.0 } else { [0, 0, 0] })
        .collect();

        let legal_colors: Vec<&[u8; 3]> = all_colors.difference(&neighbors).collect();
        let picked_color = *legal_colors.choose(&mut rand::thread_rng()).unwrap();

        img.put_pixel(x, y, Rgb(*picked_color));
    }

    let (x, y) = final_pos;

    // SAFE: There is always the starting color in colors
    let color = colors.last().unwrap();

    // Check if it is safe to add the terminator
    {
        let terminator_region_corner = {
            let (square_x, square_y) = update_x_y_spiral(x, y, 2, &final_dir);
            update_x_y_spiral(square_x, square_y, 1, &final_dir.clockwise())
        };

        let terminator_region = get_rect((5, 4), terminator_region_corner, &final_dir.clockwise());

        let all_pixels_in_region_are_black = terminator_region
            .iter()
            .all(|&(x, y)| img.get_pixel(x, y) == &Rgb(Color::Black.to_bytes()));

        if !all_pixels_in_region_are_black {
            bail!("Unable to add terminator to piet image due to insuficient space");
        }
    }
    // Add terminator
    add_terminator(&mut img, final_pos, color, &final_dir);

    Ok(img)
}

/// Creates a square that starts at the given position, clockwise
/// with respect to the start position.
fn get_rect(sides: (u32, u32), start: (u32, u32), dir: &Direction) -> Vec<(u32, u32)> {
    let mut blocks = vec![];
    for i in 0..sides.0 {
        let (x, y) = update_x_y_spiral(start.0, start.1, i, &dir.clockwise());
        for j in 0..sides.1 {
            blocks.push(update_x_y_spiral(x, y, j, dir));
        }
    }
    blocks
}

fn add_terminator(img: &mut RgbImage, pos: (u32, u32), color: &Color, dir: &Direction) {
    let (mut x, mut y) = pos;
    img.put_pixel(x, y, Rgb(color.to_bytes()));

    let color = colors::instruction_to_color(PUSH(1), *color);
    (x, y) = update_x_y_spiral(x, y, 1, &dir.clockwise());
    img.put_pixel(x, y, Rgb(color.to_bytes()));

    let color = colors::instruction_to_color(PUSH(1), color);
    (x, y) = update_x_y_spiral(x, y, 1, &dir.clockwise());
    img.put_pixel(x, y, Rgb(color.to_bytes()));

    (x, y) = update_x_y_spiral(x, y, 1, &dir);
    img.put_pixel(x, y, Rgb(color.to_bytes()));

    (x, y) = update_x_y_spiral(x, y, 2, &dir.clockwise().clockwise());
    img.put_pixel(x, y, Rgb(color.to_bytes()));

    (x, y) = update_x_y_spiral(x, y, 1, &dir);
    (x, y) = update_x_y_spiral(x, y, 1, &dir.clockwise());
    img.put_pixel(x, y, Rgb(color.to_bytes()));
}

A src/piet_image.rs => src/piet_image.rs +296 -0
@@ 0,0 1,296 @@
use std::collections::HashSet;

use image::{Rgb, RgbImage};
use rand::seq::SliceRandom;

use anyhow::{bail, Context, Result};

use crate::colors::{self, Color};
use crate::instructions;
use crate::instructions::{Instructions::*, *};
use crate::spiral::Spiral2D;
use crate::utils;
use crate::Direction;

pub struct PietImager {
    img: RgbImage,
    instructions: Vec<Instructions>,
}

impl PietImager {
    pub fn new() -> Self {
        Self {
            img: RgbImage::new(0, 0),
            instructions: vec![],
        }
    }

    pub fn with_instructions(instructions: Vec<Instructions>) -> Self {
        Self {
            instructions,
            img: RgbImage::new(0, 0),
        }
    }

    pub fn to_image(&mut self) -> Result<RgbImage> {
        let (min_img_size, max_img_size) = instructions::expected_size(&self.instructions)?;
        eprintln!(
            "(min_img_size, max_img_size: {:?}",
            (min_img_size, max_img_size)
        );

        let sat_img_size: u32 = utils::search_for_minimum_satisfier(
            |size| self.try_image_with_size(*size).is_ok(),
            min_img_size..=max_img_size,
        )
        .unwrap();

        self.try_image_with_size(sat_img_size)
            .context("Failed to create a piet image!")
    }

    fn instructions_to_blocks(&self, img_size: u32) -> Result<Vec<Color>> {
        let mut colors = vec![];
        let mut prev = Color::Red;

        let corner_turn = [PUSH(1), POINTER];
        let mut space_in_row: u32 = img_size;
        let mut remaining_space: u32 = img_size - 1;

        let mut direction = Direction::Right;

        for instr in &self.instructions {
            //eprintln!("remaining_space: {}", remaining_space);
            match instr {
                PUSH(n) => {
                    // If there isn't enough room for the entire push instruction
                    // then split it up into two pushes, a corner_turn and an add.

                    if remaining_space < (*n as u32) + corner_turn.len() as u32 {
                        //eprintln!(
                        //    "SPLIT PUSH - remaining_space: {:?}, n: {:?}, prev: {:?}",
                        //    remaining_space, n, prev
                        //);
                        let first_push = remaining_space as usize - corner_turn.len();
                        let second_push = n - first_push;
                        //eprintln!("first: {}, second: {}", first_push, second_push);
                        for _ in 0..first_push {
                            colors.push(prev);
                        }
                        prev = colors::instruction_to_color(PUSH(first_push), prev);
                        //eprintln!("COLOR: {:?}", prev);

                        for _instr in corner_turn {
                            colors.push(prev);
                            prev = colors::instruction_to_color(_instr, prev);
                            //eprintln!("COLOR: {:?}", prev);
                        }

                        direction = direction.clockwise();
                        match direction {
                            Direction::Down | Direction::Up => space_in_row -= 2,
                            _ => (),
                        }
                        remaining_space = space_in_row;

                        for _ in 0..second_push {
                            colors.push(prev);
                        }
                        remaining_space -= second_push as u32;
                        prev = colors::instruction_to_color(PUSH(second_push), prev);
                        //eprintln!("COLOR: {:?}", prev);

                        colors.push(prev);
                        prev = colors::instruction_to_color(ADD, prev);
                        //eprintln!("COLOR: {:?}", prev);
                        remaining_space -= 1;
                        continue;
                    } else {
                        for _ in 0..*n {
                            colors.push(prev);
                        }
                        remaining_space -= *n as u32;
                    }
                }
                _ => {
                    colors.push(prev);
                    remaining_space = remaining_space
                        .checked_sub(1)
                        .context("Ran out of space to place more blocks in the image!")?;
                }
            }
            prev = colors::instruction_to_color(*instr, prev);

            // If there is just enough space for a corner_turn,
            // then just turn
            if remaining_space == corner_turn.len() as u32 {
                for _instr in corner_turn {
                    colors.push(prev);
                    prev = colors::instruction_to_color(_instr, prev);
                }

                direction = direction.clockwise();
                match direction {
                    Direction::Down | Direction::Up => space_in_row -= 2,
                    _ => (),
                }

                remaining_space = space_in_row;
            }
        }

        colors.push(prev);

        Ok(colors)
    }

    fn try_image_with_size(&mut self, img_size: u32) -> Result<RgbImage> {
        eprintln!("spiral_len: {:?}", Spiral2D::length(img_size));
        eprintln!("img_size: {:?}", img_size);
        let colors = self.instructions_to_blocks(img_size)?;

        self.colors_to_img(colors, img_size)
            .context("Failed to convert colors to an image during main execution")
    }

    fn colors_to_img(&mut self, colors: Vec<Color>, img_size: u32) -> Result<RgbImage> {
        self.img = RgbImage::new(img_size, img_size);

        let mut final_pos = (0, 0);
        let mut final_dir = Direction::Right;

        let sp = Spiral2D::new(0, 0, Direction::Right, img_size, colors.len() as u32);

        for ((x, y, dir), color) in sp.zip(colors.iter()) {
            self.img.put_pixel(x, y, Rgb(color.to_bytes()));
            final_pos = (x, y);
            final_dir = dir;
        }

        // Fill in remaining colors
        let filler_spiral = Spiral2D::new(
            img_size - 1,
            img_size - 1,
            Direction::Left,
            img_size,
            colors.len() as u32,
        );

        let all_colors: HashSet<[u8; 3]> = vec![
            Color::LightRed.to_bytes(),
            Color::LightYellow.to_bytes(),
            Color::LightGreen.to_bytes(),
            Color::LightCyan.to_bytes(),
            Color::LightBlue.to_bytes(),
            Color::LightMagenta.to_bytes(),
            Color::Red.to_bytes(),
            Color::Yellow.to_bytes(),
            Color::Green.to_bytes(),
            Color::Cyan.to_bytes(),
            Color::Blue.to_bytes(),
            Color::Magenta.to_bytes(),
            Color::White.to_bytes(),
            Color::Black.to_bytes(),
            Color::DarkRed.to_bytes(),
            Color::DarkYellow.to_bytes(),
            Color::DarkGreen.to_bytes(),
            Color::DarkCyan.to_bytes(),
            Color::DarkBlue.to_bytes(),
            Color::DarkMagenta.to_bytes(),
        ]
        .into_iter()
        .collect();

        for (x, y, _) in filler_spiral {
            let neighbors: HashSet<[u8; 3]> = vec![
                self.img
                    .get_pixel_checked(if x == 0 { 0 } else { x - 1 }, y),
                self.img.get_pixel_checked(x + 1, y),
                self.img
                    .get_pixel_checked(x, if y == 0 { 0 } else { y - 1 }),
                self.img.get_pixel_checked(x, y + 1),
            ]
            .into_iter()
            .map(|c| if let Some(rgb) = c { rgb.0 } else { [0, 0, 0] })
            .collect();

            let legal_colors: Vec<&[u8; 3]> = all_colors.difference(&neighbors).collect();
            let picked_color = *legal_colors.choose(&mut rand::thread_rng()).unwrap();

            self.img.put_pixel(x, y, Rgb(*picked_color));
        }

        let (x, y) = final_pos;

        // SAFE: There is always the starting color in colors
        let color = colors.last().unwrap();

        // Check if it is safe to add the terminator
        {
            let terminator_region_corner = {
                let (square_x, square_y) = self.update_x_y(x, y, 2, &final_dir);
                self.update_x_y(square_x, square_y, 1, &final_dir.clockwise())
            };

            let terminator_region =
                self.get_rect((5, 4), terminator_region_corner, &final_dir.clockwise());

            let all_pixels_in_region_are_black = terminator_region
                .iter()
                .all(|&(x, y)| self.img.get_pixel(x, y) == &Rgb(Color::Black.to_bytes()));

            if !all_pixels_in_region_are_black {
                bail!("Unable to add terminator to piet image due to insuficient space");
            }
        }
        // Add terminator
        self.add_terminator(final_pos, color, &final_dir);

        Ok(self.img.clone())
    }

    /// Creates a square that starts at the given position, clockwise
    /// with respect to the start position.
    fn get_rect(&self, sides: (u32, u32), start: (u32, u32), dir: &Direction) -> Vec<(u32, u32)> {
        let mut blocks = vec![];
        for i in 0..sides.0 {
            let (x, y) = self.update_x_y(start.0, start.1, i, &dir.clockwise());
            for j in 0..sides.1 {
                blocks.push(self.update_x_y(x, y, j, dir));
            }
        }
        blocks
    }

    fn add_terminator(&mut self, pos: (u32, u32), color: &Color, dir: &Direction) {
        let (mut x, mut y) = pos;
        self.img.put_pixel(x, y, Rgb(color.to_bytes()));

        let color = colors::instruction_to_color(PUSH(1), *color);
        (x, y) = self.update_x_y(x, y, 1, &dir.clockwise());
        self.img.put_pixel(x, y, Rgb(color.to_bytes()));

        let color = colors::instruction_to_color(PUSH(1), color);
        (x, y) = self.update_x_y(x, y, 1, &dir.clockwise());
        self.img.put_pixel(x, y, Rgb(color.to_bytes()));

        (x, y) = self.update_x_y(x, y, 1, &dir);
        self.img.put_pixel(x, y, Rgb(color.to_bytes()));

        (x, y) = self.update_x_y(x, y, 2, &dir.clockwise().clockwise());
        self.img.put_pixel(x, y, Rgb(color.to_bytes()));

        (x, y) = self.update_x_y(x, y, 1, &dir);
        (x, y) = self.update_x_y(x, y, 1, &dir.clockwise());
        self.img.put_pixel(x, y, Rgb(color.to_bytes()));
    }

    fn update_x_y(&self, x: u32, y: u32, step: u32, dir: &Direction) -> (u32, u32) {
        match dir {
            Direction::Up => (x, y - step),
            Direction::Down => (x, y + step),
            Direction::Right => (x + step, y),
            Direction::Left => (x - step, y),
        }
    }
}

A src/utils.rs => src/utils.rs +14 -0
@@ 0,0 1,14 @@
pub fn search_for_minimum_satisfier<T, F, I>(mut predicate: F, search_space: I) -> Option<T>
where
    F: FnMut(&T) -> bool,
    T: Sized + PartialEq + Clone,
    I: IntoIterator<Item = T>,
{
    for (satisfies, val) in search_space.into_iter().map(|v| (predicate(&v), v)) {
        if satisfies {
            return Some(val.clone());
        }
    }

    None
}

M test.png => test.png +0 -0