use base64::{Engine, engine::general_purpose};
use chromiumoxide::{Browser, BrowserConfig, BrowserFetcher, BrowserFetcherOptions};
use ffmpeg_sidecar::{command::FfmpegCommand, event::OutputVideoFrame};
use futures::stream::StreamExt;
use image::{DynamicImage, ImageBuffer, Rgba};
use resvg::{
tiny_skia,
usvg::{self, Options, Tree},
};
use std::{
error, fs,
io::Read,
path::{Path, PathBuf},
};
use comrak::{
ComrakOptions, ComrakPlugins, markdown_to_html_with_plugins, plugins::syntect::SyntectAdapter,
};
use std::io::Write;
use crate::rasteroid;
pub fn image_to_base64(img: &Vec<u8>) -> String {
general_purpose::STANDARD.encode(&img)
}
pub fn offset_to_terminal(offset: Option<u16>) -> String {
match offset {
Some(offset) => format!("\x1b[{}C", offset),
None => "".to_string(),
}
}
pub fn svg_to_image(
mut reader: impl Read,
width: Option<&str>,
height: Option<&str>,
) -> Result<DynamicImage, Box<dyn std::error::Error>> {
let mut svg_data = Vec::new();
reader.read_to_end(&mut svg_data)?;
let mut opt = Options::default();
let mut fontdb = fontdb::Database::new();
fontdb.load_system_fonts();
opt.fontdb = std::sync::Arc::new(fontdb);
opt.text_rendering = usvg::TextRendering::OptimizeLegibility;
let tree = Tree::from_data(&svg_data, &opt)?;
let pixmap_size = tree.size();
let src_width = pixmap_size.width();
let src_height = pixmap_size.height();
let width = match width {
Some(w) => rasteroid::term_misc::dim_to_px(w, rasteroid::term_misc::SizeDirection::Width)?,
None => src_width as u32,
};
let height = match height {
Some(h) => rasteroid::term_misc::dim_to_px(h, rasteroid::term_misc::SizeDirection::Height)?,
None => src_height as u32,
};
let (target_width, target_height) =
rasteroid::image_extended::calc_fit(src_width as u32, src_height as u32, width, height);
let scale_x = target_width as f32 / src_width;
let scale_y = target_height as f32 / src_height;
let scale = scale_x.min(scale_y);
let mut pixmap = tiny_skia::Pixmap::new(target_width, target_height)
.ok_or("Failed to create pixmap for svg")?;
let transform = tiny_skia::Transform::from_scale(scale, scale);
resvg::render(&tree, transform, &mut pixmap.as_mut());
let image_buffer = ImageBuffer::<Rgba<u8>, _>::from_raw(
target_width as u32,
target_height as u32,
pixmap.data().to_vec(),
)
.ok_or("Failed to create image buffer for svg")?;
Ok(DynamicImage::ImageRgba8(image_buffer))
}
fn get_chromium_install_path() -> PathBuf {
let base_dir = dirs::cache_dir()
.or_else(dirs::data_dir)
.unwrap_or_else(|| std::env::temp_dir());
let p = base_dir.join("chromiumoxide").join("chromium");
if !p.exists() {
eprintln!("couldn't find chromium installed, trying to install.. it may take a little.");
let _ = fs::create_dir_all(p.clone());
}
p
}
pub fn html_to_image(html: &str) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
let encoded_html = urlencoding::encode(&html);
let data_uri = format!("data:text/html;charset=utf-8,{}", encoded_html);
let data = screenshot_uri(&data_uri)?;
Ok(data)
}
fn screenshot_uri(data_uri: &str) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()?;
rt.block_on(async {
let config = match BrowserConfig::builder().new_headless_mode().build() {
Ok(c) => c,
Err(_) => {
let download_path = get_chromium_install_path();
let fetcher = BrowserFetcher::new(
BrowserFetcherOptions::builder()
.with_path(&download_path)
.build()?,
);
let info = fetcher.fetch().await?;
BrowserConfig::builder()
.chrome_executable(info.executable_path)
.new_headless_mode()
.build()?
}
};
let (browser, mut handler) = Browser::launch(config).await.map_err(|e| format!("failed to launch chromium\neiter you need to kill chrome/edge process or\nremove: {} and rerun. or install chrome\noriginal error: {}", get_chromium_install_path().display(), e))?;
tokio::spawn(async move { while let Some(_) = handler.next().await {} });
let page = browser.new_page(data_uri).await?;
let mut prms = chromiumoxide::page::ScreenshotParams::default();
prms.full_page = Some(true);
prms.omit_background = Some(true);
let screenshot = page.screenshot(prms).await?;
Ok(screenshot)
})
}
pub fn md_to_html(markdown: &str, css_path: Option<&str>, raw_html: bool) -> String {
let mut options = ComrakOptions::default();
let mut plugins = ComrakPlugins::default();
let adapter = SyntectAdapter::new(None);
plugins.render.codefence_syntax_highlighter = Some(&adapter);
options.extension.strikethrough = true;
options.extension.tagfilter = true;
options.extension.table = true;
options.extension.autolink = true;
options.extension.tasklist = true;
options.extension.footnotes = true;
options.extension.description_lists = true;
options.parse.smart = true;
options.render.unsafe_ = raw_html;
options.render.hardbreaks = false;
options.render.github_pre_lang = true; options.render.full_info_string = true;
let css_content = match css_path {
Some("makurai") => Some(include_str!("../styles/makurai.css").to_string()),
Some("default") => Some(include_str!("../styles/default.css").to_string()),
Some(path) => std::fs::read_to_string(path).ok(),
None => None,
};
let html = markdown_to_html_with_plugins(markdown, &options, &plugins);
match css_content {
Some(css) => format!(
r#"
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>{}</style>
</head>
<body>
{}
</body>
</html>
"#,
css, html
),
None => html,
}
}
pub fn inline_a_video(
input: impl AsRef<str>,
out: &mut impl Write,
inline_encoder: &rasteroid::InlineEncoder,
center: bool,
) -> Result<(), Box<dyn error::Error>> {
match inline_encoder {
rasteroid::InlineEncoder::Kitty => {
let frames = video_to_frames(input)?;
let id = rand::random::<u32>();
rasteroid::kitty_encoder::encode_frames(frames, out, id, center)?;
Ok(())
}
rasteroid::InlineEncoder::Iterm => {
let gif = video_to_gif(input)?;
let dyn_img = image::load_from_memory_with_format(&gif, image::ImageFormat::Gif)?;
let offset = match center {
true => Some(rasteroid::term_misc::center_image(dyn_img.width() as u16)),
false => None,
};
rasteroid::iterm_encoder::encode_image(&gif, out, offset)?;
Ok(())
}
rasteroid::InlineEncoder::Sixel => return Err("Cannot view videos in sixel".into()),
}
}
fn video_to_gif(input: impl AsRef<str>) -> Result<Vec<u8>, Box<dyn error::Error>> {
let input = input.as_ref();
if input.ends_with(".gif") {
let path = Path::new(input);
let bytes = fs::read(path)?;
return Ok(bytes);
}
if !ffmpeg_sidecar::command::ffmpeg_is_installed() {
eprintln!("ffmpeg isn't installed, installing.. it may take a little");
ffmpeg_sidecar::download::auto_download()?;
}
let mut command = FfmpegCommand::new();
command
.hwaccel("auto")
.input(input)
.format("gif")
.output("-");
let mut child = command.spawn()?;
let mut stdout = child
.take_stdout()
.ok_or("failed to get stdout for ffmpeg")?;
let mut output_bytes = Vec::new();
stdout.read_to_end(&mut output_bytes)?;
child.wait()?;
Ok(output_bytes)
}
fn video_to_frames(
input: impl AsRef<str>,
) -> Result<Box<dyn Iterator<Item = OutputVideoFrame>>, Box<dyn error::Error>> {
let input = input.as_ref();
if !ffmpeg_sidecar::command::ffmpeg_is_installed() {
eprintln!("ffmpeg isn't installed, installing.. it may take a little");
ffmpeg_sidecar::download::auto_download()?;
}
let mut command = FfmpegCommand::new();
command.hwaccel("auto").input(input).rawvideo();
let mut child = command.spawn()?;
let frames = child.iter()?.filter_frames();
Ok(Box::new(frames))
}