saville_encode 1.1.2

Encode UTF-8 text into Peter Saville's colour code
Documentation
// saville-encode -- Encode UTF-8 text into Peter Saville's colour code.
// Copyright (C) 2025  Benjamin Alford <ssolar5.dev@gmail.com>
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along
// with this program; if not, see <https://www.gnu.org/licenses/>.

use args::{Args, ArgsError};
use getopts::Occur;
use saville_encode::{encode_saville, SavilleLayout, SavilleSpaceShape};
use std::{
    io::{self, Read},
    process::exit,
};

fn main() {
    let (layout, space_shape, output_file, symbol_height) = parse_args().unwrap();
    let mut input: Vec<u8> = Vec::new();
    if let Err(err) = io::stdin().read_to_end(&mut input) {
        panic!("Could not read from stdin: {}", err);
    }
    input.pop();
    let input = match String::from_utf8(input) {
        Ok(s) => s,
        Err(err) => panic!("Input is not valid UTF-8: {}", err),
    };
    let document = encode_saville(&input, layout, space_shape, symbol_height);
    svg::save(output_file, &document).unwrap();
}

fn parse_args() -> Result<(SavilleLayout, SavilleSpaceShape, String, i32), ArgsError> {
    let mut args = Args::new(
        "saville_encode",
        concat!(
            "Encode UTF-8 text into Peter Saville's colour code.\n",
            "\n",
            "saville_encode reads UTF-8 text from standard input (to EOF) and generates an SVG file of the encoded text which is outputted.\n",
            "\n",
            "saville_encode encodes '0'-'9' and 'a'-'z' (case-insensitive) as a sequence of colourful square-shaped symbols as per the code, and encodes ' ' (space) as a grey octagon.\n",
            "Custom two-colour symbols may be entered as \"|73\", where the colour for the left and right sides of the symbol are the (single) colours '7' and '3' encode as, respectively. Accepted characters are '0'-'9' and 'a'-'i'.\n",
            "A newline character begins a new line of symbols.\n",
            "Any UTF-8 character which is not in any aforementioned range (such as '.' [full stop/period], ',' [comma], '?' [question mark], and '\\'' [apostrophe]) will be encoded as a blank symbol space.\n",
            "\n",
            "Examples:\n",
            "$ echo \"fac|73 ,blue monday and\" | ./saville_encode -o blue_monday.svg\n",
            "$ echo \"but, if it wasn't for your misfortune,\n",
            "  i'd be a heavenly person today\" | ./saville_encode -l RightDown -s Regular -r 500\n",
            "$ ./saville_encode < input.txt"
        ),
    );
    args.flag("h", "help", "Print this help menu and exit.")
        .option("l", "layout",
            concat!(
                "(Default: DownRight) The layout of the design to be outputted. May be one of:\n",
                "  - DownRight: mimics the vertical design found on the cover of FAC 73 Blue Monday and The Beach by New Order.\n",
                "  - RightDown: mimics the horizontal design found in the official lyric video for Blue Monday by New Order.\n",
                "  - RightDownThin: RightDown but thin (same height as the top trapezoid of a regular octagon).",
            ),
            "LAYOUT", Occur::Optional, Some("DownRight".to_owned())
        )
        .option("s", "space-shape",
            concat!(
                "(Default: Fuller) The shape of the ' ' [space] character. May be one of:\n",
                "  - Fuller: corners are smaller than for Regular.\n",
                "  - Regular: regular octagon.",
            ),
            "SHAPE", Occur::Optional, Some("Fuller".to_owned())
        )
        .option("o", "output",
            "(Default: out.svg) The file to output the generated SVG to.", "FILE",
            Occur::Optional, Some("out.svg".to_owned())
        )
        .option("r", "resolution",
            "(Default: 1000) The height of each symbol in the SVG coordinate system. In other words, the length of a side of a square-shaped symbol (such as for 'a').",
            "INTEGER", Occur::Optional, Some("1000".to_owned())
        );

    args.parse_from_cli()?;

    if args.value_of("help")? {
        print!("{}", args.full_usage());
        exit(0);
    }

    Ok((
        match args.value_of::<String>("layout")?.as_str() {
            "RightDown" => SavilleLayout::RightDown,
            "DownRight" => SavilleLayout::DownRight,
            "RightDownThin" => SavilleLayout::RightDownThin,
            a => return Err(ArgsError::new("Invalid argument for option --layout", a)),
        },
        match args.value_of::<String>("space-shape")?.as_str() {
            "Fuller" => SavilleSpaceShape::Fuller,
            "Regular" => SavilleSpaceShape::Regular,
            a => {
                return Err(ArgsError::new(
                    "Invalid argument for option --space-shape",
                    a,
                ))
            }
        },
        args.value_of("output")?,
        match args.value_of::<i32>("resolution")? {
            a @ 0.. => a,
            a => {
                return Err(ArgsError::new(
                    format!(
                        "Argument for option --resolution is too large (max is {})",
                        i32::MAX
                    )
                    .as_str(),
                    format!("{}", a).as_str(),
                ))
            }
        },
    ))
}