// Copyright (C) 2026 Elias Wendland // // 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, version 3 exclusively. // // 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 . use convertis_plugin_api::{Conversion, ConversionRequest, OptionSpec, Plugin, PluginMetadata}; const FORMATS: &[&str] = &["png", "jpeg", "gif", "webp", "bmp", "tiff", "ico"]; struct ImageAscii; impl Plugin for ImageAscii { fn metadata(&self) -> PluginMetadata { PluginMetadata { id: "image-ascii".into(), package: "convertis-image-ascii".into(), description: "Render images as ASCII text".into(), conversions: FORMATS .iter() .map(|from| Conversion::file(from, "text", (255, 180, 230))) .collect(), options: vec![ OptionSpec { name: "width".into(), help: "Output width in characters".into(), default: Some("80".into()), }, OptionSpec { name: "characters".into(), help: "Dark-to-light character ramp".into(), default: Some("@%#*+=-:. ".into()), }, OptionSpec { name: "invert".into(), help: "Reverse the character ramp".into(), default: Some("false".into()), }, ], } } fn convert(&self, request: &ConversionRequest) -> Result<(), String> { let image = image::open(&request.input).map_err(|error| error.to_string())?; let width: u32 = request .options .get("width") .map(String::as_str) .unwrap_or("80") .parse() .map_err(|_| "width must be a positive integer")?; if width == 0 { return Err("width must be greater than zero".into()); } let height = ((image.height() as f32 / image.width() as f32) * width as f32 * 0.5) .round() .max(1.0) as u32; let grayscale = image .resize_exact(width, height, image::imageops::FilterType::Triangle) .to_luma8(); let mut ramp: Vec = request .options .get("characters") .map(String::as_str) .unwrap_or("@%#*+=-:. ") .chars() .collect(); if ramp.len() < 2 { return Err("characters must contain at least two characters".into()); } if request .options .get("invert") .is_some_and(|value| value == "true") { ramp.reverse(); } let mut output = String::with_capacity((width as usize + 1) * height as usize); for y in 0..height { for x in 0..width { let value = grayscale.get_pixel(x, y)[0] as usize; output.push(ramp[value * (ramp.len() - 1) / 255]); } output.push('\n'); } std::fs::write(&request.output, output).map_err(|error| error.to_string()) } } convertis_plugin_api::export_plugin!(ImageAscii, "image-ascii");