mirror of
https://github.com/mfontanini/presenterm.git
synced 2025-05-05 15:32:58 +00:00
feat: make HTML export self contained
This commit is contained in:
parent
afb0f0797f
commit
5565d420f5
5
.github/workflows/merge.yaml
vendored
5
.github/workflows/merge.yaml
vendored
@ -45,7 +45,7 @@ jobs:
|
||||
source ./.venv/bin/activate
|
||||
uv pip install weasyprint
|
||||
|
||||
- name: Export demo presentation as PDF
|
||||
- name: Export demo presentation as PDF and HTML
|
||||
run: |
|
||||
cat >/tmp/config.yaml <<EOL
|
||||
export:
|
||||
@ -54,7 +54,8 @@ jobs:
|
||||
columns: 135
|
||||
EOL
|
||||
source ./.venv/bin/activate
|
||||
cargo run -- -e -c /tmp/config.yaml examples/demo.md
|
||||
cargo run -- --export-pdf -c /tmp/config.yaml examples/demo.md
|
||||
cargo run -- --export-html -c /tmp/config.yaml examples/demo.md
|
||||
|
||||
nix-flake:
|
||||
name: Validate nix flake
|
||||
|
@ -8,15 +8,11 @@ use crate::{
|
||||
presentation::Slide,
|
||||
render::{engine::RenderEngine, properties::WindowSize},
|
||||
terminal::{
|
||||
image::{
|
||||
Image, ImageSource,
|
||||
printer::{ImageProperties, TerminalImage},
|
||||
},
|
||||
image::printer::TerminalImage,
|
||||
virt::{TerminalGrid, VirtualTerminal},
|
||||
},
|
||||
tools::ThirdPartyTools,
|
||||
};
|
||||
use image::{ImageEncoder, codecs::png::PngEncoder};
|
||||
use std::{
|
||||
fs, io,
|
||||
path::{Path, PathBuf},
|
||||
@ -37,7 +33,7 @@ struct HtmlSlide {
|
||||
}
|
||||
|
||||
impl HtmlSlide {
|
||||
fn new(grid: TerminalGrid, content_manager: &mut ContentManager) -> Result<Self, ExportError> {
|
||||
fn new(grid: TerminalGrid) -> Result<Self, ExportError> {
|
||||
let mut rows = Vec::new();
|
||||
rows.push(String::from("<div class=\"container\">"));
|
||||
for (y, row) in grid.rows.into_iter().enumerate() {
|
||||
@ -58,11 +54,11 @@ impl HtmlSlide {
|
||||
other => current_string.push(other),
|
||||
}
|
||||
if let Some(image) = grid.images.get(&(y as u16, x as u16)) {
|
||||
let image_path = content_manager.persist_image(&image.image)?;
|
||||
let image_path_str = image_path.display();
|
||||
let TerminalImage::Raw(raw_image) = image.image.image() else { panic!("not in raw image mode") };
|
||||
let image_contents = raw_image.to_inline_html();
|
||||
let width_pixels = (image.width_columns as f64 * FONT_SIZE as f64 * FONT_SIZE_WIDTH).ceil();
|
||||
let image_tag = format!(
|
||||
"<img width=\"{width_pixels}\" src=\"file://{image_path_str}\" style=\"position: absolute\" />"
|
||||
"<img width=\"{width_pixels}\" src=\"{image_contents}\" style=\"position: absolute\" />"
|
||||
);
|
||||
current_string.push_str(&image_tag);
|
||||
}
|
||||
@ -86,35 +82,11 @@ impl HtmlSlide {
|
||||
|
||||
pub(crate) struct ContentManager {
|
||||
output_directory: OutputDirectory,
|
||||
image_count: usize,
|
||||
}
|
||||
|
||||
impl ContentManager {
|
||||
pub(crate) fn new(output_directory: OutputDirectory) -> Self {
|
||||
Self { output_directory, image_count: 0 }
|
||||
}
|
||||
|
||||
fn persist_image(&mut self, image: &Image) -> Result<PathBuf, ExportError> {
|
||||
match image.source.clone() {
|
||||
ImageSource::Filesystem(path) => Ok(path),
|
||||
ImageSource::Generated => {
|
||||
let mut buffer = Vec::new();
|
||||
let dimensions = image.image().dimensions();
|
||||
let TerminalImage::Ascii(image) = image.image() else { panic!("not in ascii mode") };
|
||||
let image = image.image();
|
||||
PngEncoder::new(&mut buffer).write_image(
|
||||
image.as_bytes(),
|
||||
dimensions.0,
|
||||
dimensions.1,
|
||||
image.color().into(),
|
||||
)?;
|
||||
let name = format!("img-{}.png", self.image_count);
|
||||
let path = self.output_directory.path().join(name);
|
||||
fs::write(&path, buffer)?;
|
||||
self.image_count += 1;
|
||||
Ok(path)
|
||||
}
|
||||
}
|
||||
Self { output_directory }
|
||||
}
|
||||
|
||||
fn persist_file(&self, name: &str, data: &[u8]) -> io::Result<PathBuf> {
|
||||
@ -155,7 +127,7 @@ impl ExportRenderer {
|
||||
engine.render(slide.iter_operations())?;
|
||||
|
||||
let grid = terminal.into_contents();
|
||||
let slide = HtmlSlide::new(grid, &mut self.content_manager)?;
|
||||
let slide = HtmlSlide::new(grid)?;
|
||||
if self.background_color.is_none() {
|
||||
self.background_color.clone_from(&slide.background_color);
|
||||
}
|
||||
|
@ -293,8 +293,8 @@ impl CoreComponents {
|
||||
}
|
||||
|
||||
fn select_graphics_mode(cli: &Cli, config: &Config) -> GraphicsMode {
|
||||
if cli.export_pdf {
|
||||
GraphicsMode::AsciiBlocks
|
||||
if cli.export_pdf | cli.export_html {
|
||||
GraphicsMode::Raw
|
||||
} else {
|
||||
let protocol = cli.image_protocol.as_ref().unwrap_or(&config.defaults.image_protocol);
|
||||
match GraphicsMode::try_from(protocol) {
|
||||
|
@ -1,7 +1,6 @@
|
||||
use self::printer::{ImageProperties, TerminalImage};
|
||||
use image::DynamicImage;
|
||||
use protocols::ascii::AsciiImage;
|
||||
|
||||
use self::printer::{ImageProperties, TerminalImage};
|
||||
use std::{
|
||||
fmt::Debug,
|
||||
ops::Deref,
|
||||
@ -43,6 +42,7 @@ impl Image {
|
||||
TerminalImage::Ascii(image) => image.clone(),
|
||||
TerminalImage::Kitty(image) => DynamicImage::from(image.as_rgba8()).into(),
|
||||
TerminalImage::Iterm(image) => DynamicImage::from(image.as_rgba8()).into(),
|
||||
TerminalImage::Raw(_) => unreachable!("raw is only used for exports"),
|
||||
#[cfg(feature = "sixel")]
|
||||
TerminalImage::Sixel(image) => DynamicImage::from(image.as_rgba8()).into(),
|
||||
};
|
||||
|
@ -4,6 +4,7 @@ use super::{
|
||||
ascii::{AsciiImage, AsciiPrinter},
|
||||
iterm::{ItermImage, ItermPrinter},
|
||||
kitty::{KittyImage, KittyMode, KittyPrinter},
|
||||
raw::{RawImage, RawPrinter},
|
||||
},
|
||||
};
|
||||
use crate::{
|
||||
@ -54,6 +55,7 @@ pub(crate) enum TerminalImage {
|
||||
Kitty(KittyImage),
|
||||
Iterm(ItermImage),
|
||||
Ascii(AsciiImage),
|
||||
Raw(RawImage),
|
||||
#[cfg(feature = "sixel")]
|
||||
Sixel(super::protocols::sixel::SixelImage),
|
||||
}
|
||||
@ -64,6 +66,7 @@ impl ImageProperties for TerminalImage {
|
||||
Self::Kitty(image) => image.dimensions(),
|
||||
Self::Iterm(image) => image.dimensions(),
|
||||
Self::Ascii(image) => image.dimensions(),
|
||||
Self::Raw(image) => image.dimensions(),
|
||||
#[cfg(feature = "sixel")]
|
||||
Self::Sixel(image) => image.dimensions(),
|
||||
}
|
||||
@ -74,6 +77,7 @@ pub enum ImagePrinter {
|
||||
Kitty(KittyPrinter),
|
||||
Iterm(ItermPrinter),
|
||||
Ascii(AsciiPrinter),
|
||||
Raw(RawPrinter),
|
||||
Null,
|
||||
#[cfg(feature = "sixel")]
|
||||
Sixel(super::protocols::sixel::SixelPrinter),
|
||||
@ -91,6 +95,7 @@ impl ImagePrinter {
|
||||
GraphicsMode::Kitty { mode, inside_tmux } => Self::new_kitty(mode, inside_tmux)?,
|
||||
GraphicsMode::Iterm2 => Self::new_iterm(),
|
||||
GraphicsMode::AsciiBlocks => Self::new_ascii(),
|
||||
GraphicsMode::Raw => Self::new_raw(),
|
||||
#[cfg(feature = "sixel")]
|
||||
GraphicsMode::Sixel => Self::new_sixel()?,
|
||||
};
|
||||
@ -109,6 +114,10 @@ impl ImagePrinter {
|
||||
Self::Ascii(AsciiPrinter)
|
||||
}
|
||||
|
||||
fn new_raw() -> Self {
|
||||
Self::Raw(RawPrinter)
|
||||
}
|
||||
|
||||
#[cfg(feature = "sixel")]
|
||||
fn new_sixel() -> Result<Self, CreatePrinterError> {
|
||||
Ok(Self::Sixel(super::protocols::sixel::SixelPrinter::new()?))
|
||||
@ -124,6 +133,7 @@ impl PrintImage for ImagePrinter {
|
||||
Self::Iterm(printer) => TerminalImage::Iterm(printer.register(spec)?),
|
||||
Self::Ascii(printer) => TerminalImage::Ascii(printer.register(spec)?),
|
||||
Self::Null => return Err(RegisterImageError::Unsupported),
|
||||
Self::Raw(printer) => TerminalImage::Raw(printer.register(spec)?),
|
||||
#[cfg(feature = "sixel")]
|
||||
Self::Sixel(printer) => TerminalImage::Sixel(printer.register(spec)?),
|
||||
};
|
||||
@ -139,6 +149,7 @@ impl PrintImage for ImagePrinter {
|
||||
(Self::Iterm(printer), TerminalImage::Iterm(image)) => printer.print(image, options, terminal),
|
||||
(Self::Ascii(printer), TerminalImage::Ascii(image)) => printer.print(image, options, terminal),
|
||||
(Self::Null, _) => Ok(()),
|
||||
(Self::Raw(printer), TerminalImage::Raw(image)) => printer.print(image, options, terminal),
|
||||
#[cfg(feature = "sixel")]
|
||||
(Self::Sixel(printer), TerminalImage::Sixel(image)) => printer.print(image, options, terminal),
|
||||
_ => Err(PrintImageError::Unsupported),
|
||||
@ -165,6 +176,7 @@ impl fmt::Debug for ImageRegistry {
|
||||
ImagePrinter::Iterm(_) => "Iterm",
|
||||
ImagePrinter::Ascii(_) => "Ascii",
|
||||
ImagePrinter::Null => "Null",
|
||||
ImagePrinter::Raw(_) => "Raw",
|
||||
#[cfg(feature = "sixel")]
|
||||
ImagePrinter::Sixel(_) => "Sixel",
|
||||
};
|
||||
|
@ -36,10 +36,6 @@ impl AsciiImage {
|
||||
cached_sizes.insert(cache_key, image.into_rgba8());
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn image(&self) -> &DynamicImage {
|
||||
&self.inner.image
|
||||
}
|
||||
}
|
||||
|
||||
impl ImageProperties for AsciiImage {
|
||||
|
@ -1,5 +1,6 @@
|
||||
pub(crate) mod ascii;
|
||||
pub(crate) mod iterm;
|
||||
pub(crate) mod kitty;
|
||||
pub(crate) mod raw;
|
||||
#[cfg(feature = "sixel")]
|
||||
pub(crate) mod sixel;
|
||||
|
61
src/terminal/image/protocols/raw.rs
Normal file
61
src/terminal/image/protocols/raw.rs
Normal file
@ -0,0 +1,61 @@
|
||||
use crate::terminal::{
|
||||
image::printer::{ImageProperties, ImageSpec, PrintImage, PrintImageError, PrintOptions, RegisterImageError},
|
||||
printer::TerminalIo,
|
||||
};
|
||||
use base64::{Engine, engine::general_purpose::STANDARD};
|
||||
use image::{GenericImageView, ImageEncoder, ImageFormat, codecs::png::PngEncoder};
|
||||
use std::fs;
|
||||
|
||||
pub(crate) struct RawImage {
|
||||
contents: Vec<u8>,
|
||||
format: ImageFormat,
|
||||
width: u32,
|
||||
height: u32,
|
||||
}
|
||||
|
||||
impl RawImage {
|
||||
pub(crate) fn to_inline_html(&self) -> String {
|
||||
let mime_type = self.format.to_mime_type();
|
||||
let data = STANDARD.encode(&self.contents);
|
||||
format!("data:{mime_type};base64,{data}")
|
||||
}
|
||||
}
|
||||
|
||||
impl ImageProperties for RawImage {
|
||||
fn dimensions(&self) -> (u32, u32) {
|
||||
(self.width, self.height)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct RawPrinter;
|
||||
|
||||
impl PrintImage for RawPrinter {
|
||||
type Image = RawImage;
|
||||
|
||||
fn register(&self, spec: ImageSpec) -> Result<Self::Image, RegisterImageError> {
|
||||
let image = match spec {
|
||||
ImageSpec::Generated(image) => {
|
||||
let mut contents = Vec::new();
|
||||
let encoder = PngEncoder::new(&mut contents);
|
||||
let (width, height) = image.dimensions();
|
||||
encoder.write_image(image.as_bytes(), width, height, image.color().into())?;
|
||||
RawImage { contents, format: ImageFormat::Png, width, height }
|
||||
}
|
||||
ImageSpec::Filesystem(path) => {
|
||||
let contents = fs::read(path)?;
|
||||
let format = image::guess_format(&contents)?;
|
||||
let image = image::load_from_memory_with_format(&contents, format)?;
|
||||
let (width, height) = image.dimensions();
|
||||
RawImage { contents, format, width, height }
|
||||
}
|
||||
};
|
||||
Ok(image)
|
||||
}
|
||||
|
||||
fn print<T>(&self, _image: &Self::Image, _options: &PrintOptions, _terminal: &mut T) -> Result<(), PrintImageError>
|
||||
where
|
||||
T: TerminalIo,
|
||||
{
|
||||
Err(PrintImageError::Other("raw images can't be printed".into()))
|
||||
}
|
||||
}
|
@ -15,6 +15,7 @@ pub enum GraphicsMode {
|
||||
inside_tmux: bool,
|
||||
},
|
||||
AsciiBlocks,
|
||||
Raw,
|
||||
#[cfg(feature = "sixel")]
|
||||
Sixel,
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user