97 lines
3.6 KiB
Rust
97 lines
3.6 KiB
Rust
// Copyright (C) 2026 Elias Wendland <eliaswendland@pm.me>
|
|
//
|
|
// 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 <https://www.gnu.org/licenses/>.
|
|
|
|
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<char> = 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");
|