mirror of
https://github.com/mfontanini/presenterm.git
synced 2025-05-05 15:32:58 +00:00
feat: make HTML export self contained (#575)
Some checks are pending
Deploy docs / build-and-deploy (push) Waiting to run
Merge checks / Checks (push) Waiting to run
Merge checks / Validate nix flake (push) Waiting to run
Merge checks / Validate bat assets (push) Waiting to run
Merge checks / Validate JSON schemas (push) Waiting to run
Some checks are pending
Deploy docs / build-and-deploy (push) Waiting to run
Merge checks / Checks (push) Waiting to run
Merge checks / Validate nix flake (push) Waiting to run
Merge checks / Validate bat assets (push) Waiting to run
Merge checks / Validate JSON schemas (push) Waiting to run
#566 did the heavy work to allow HTML exports but the one thing missing is that images were still referenced on their external paths, which caused 2 issues: * Files were not redistributable unless you included images and fixed their paths. * Generated images (e.g. mermaid diagrams) were likely pointing to a temporary directory so they'd be lost. This changes that so HTML exports are now fully self contained, using `data:...` notation to inline images within the HTML.
This commit is contained in:
commit
725312e71c
5
.github/workflows/merge.yaml
vendored
5
.github/workflows/merge.yaml
vendored
@ -45,7 +45,7 @@ jobs:
|
|||||||
source ./.venv/bin/activate
|
source ./.venv/bin/activate
|
||||||
uv pip install weasyprint
|
uv pip install weasyprint
|
||||||
|
|
||||||
- name: Export demo presentation as PDF
|
- name: Export demo presentation as PDF and HTML
|
||||||
run: |
|
run: |
|
||||||
cat >/tmp/config.yaml <<EOL
|
cat >/tmp/config.yaml <<EOL
|
||||||
export:
|
export:
|
||||||
@ -54,7 +54,8 @@ jobs:
|
|||||||
columns: 135
|
columns: 135
|
||||||
EOL
|
EOL
|
||||||
source ./.venv/bin/activate
|
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:
|
nix-flake:
|
||||||
name: Validate nix flake
|
name: Validate nix flake
|
||||||
|
@ -8,15 +8,11 @@ use crate::{
|
|||||||
presentation::Slide,
|
presentation::Slide,
|
||||||
render::{engine::RenderEngine, properties::WindowSize},
|
render::{engine::RenderEngine, properties::WindowSize},
|
||||||
terminal::{
|
terminal::{
|
||||||
image::{
|
image::printer::TerminalImage,
|
||||||
Image, ImageSource,
|
|
||||||
printer::{ImageProperties, TerminalImage},
|
|
||||||
},
|
|
||||||
virt::{TerminalGrid, VirtualTerminal},
|
virt::{TerminalGrid, VirtualTerminal},
|
||||||
},
|
},
|
||||||
tools::ThirdPartyTools,
|
tools::ThirdPartyTools,
|
||||||
};
|
};
|
||||||
use image::{ImageEncoder, codecs::png::PngEncoder};
|
|
||||||
use std::{
|
use std::{
|
||||||
fs, io,
|
fs, io,
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
@ -37,7 +33,7 @@ struct HtmlSlide {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl 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();
|
let mut rows = Vec::new();
|
||||||
rows.push(String::from("<div class=\"container\">"));
|
rows.push(String::from("<div class=\"container\">"));
|
||||||
for (y, row) in grid.rows.into_iter().enumerate() {
|
for (y, row) in grid.rows.into_iter().enumerate() {
|
||||||
@ -58,11 +54,11 @@ impl HtmlSlide {
|
|||||||
other => current_string.push(other),
|
other => current_string.push(other),
|
||||||
}
|
}
|
||||||
if let Some(image) = grid.images.get(&(y as u16, x as u16)) {
|
if let Some(image) = grid.images.get(&(y as u16, x as u16)) {
|
||||||
let image_path = content_manager.persist_image(&image.image)?;
|
let TerminalImage::Raw(raw_image) = image.image.image() else { panic!("not in raw image mode") };
|
||||||
let image_path_str = image_path.display();
|
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 width_pixels = (image.width_columns as f64 * FONT_SIZE as f64 * FONT_SIZE_WIDTH).ceil();
|
||||||
let image_tag = format!(
|
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);
|
current_string.push_str(&image_tag);
|
||||||
}
|
}
|
||||||
@ -86,35 +82,11 @@ impl HtmlSlide {
|
|||||||
|
|
||||||
pub(crate) struct ContentManager {
|
pub(crate) struct ContentManager {
|
||||||
output_directory: OutputDirectory,
|
output_directory: OutputDirectory,
|
||||||
image_count: usize,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ContentManager {
|
impl ContentManager {
|
||||||
pub(crate) fn new(output_directory: OutputDirectory) -> Self {
|
pub(crate) fn new(output_directory: OutputDirectory) -> Self {
|
||||||
Self { output_directory, image_count: 0 }
|
Self { output_directory }
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn persist_file(&self, name: &str, data: &[u8]) -> io::Result<PathBuf> {
|
fn persist_file(&self, name: &str, data: &[u8]) -> io::Result<PathBuf> {
|
||||||
@ -155,7 +127,7 @@ impl ExportRenderer {
|
|||||||
engine.render(slide.iter_operations())?;
|
engine.render(slide.iter_operations())?;
|
||||||
|
|
||||||
let grid = terminal.into_contents();
|
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() {
|
if self.background_color.is_none() {
|
||||||
self.background_color.clone_from(&slide.background_color);
|
self.background_color.clone_from(&slide.background_color);
|
||||||
}
|
}
|
||||||
|
@ -293,8 +293,8 @@ impl CoreComponents {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn select_graphics_mode(cli: &Cli, config: &Config) -> GraphicsMode {
|
fn select_graphics_mode(cli: &Cli, config: &Config) -> GraphicsMode {
|
||||||
if cli.export_pdf {
|
if cli.export_pdf | cli.export_html {
|
||||||
GraphicsMode::AsciiBlocks
|
GraphicsMode::Raw
|
||||||
} else {
|
} else {
|
||||||
let protocol = cli.image_protocol.as_ref().unwrap_or(&config.defaults.image_protocol);
|
let protocol = cli.image_protocol.as_ref().unwrap_or(&config.defaults.image_protocol);
|
||||||
match GraphicsMode::try_from(protocol) {
|
match GraphicsMode::try_from(protocol) {
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
|
use self::printer::{ImageProperties, TerminalImage};
|
||||||
use image::DynamicImage;
|
use image::DynamicImage;
|
||||||
use protocols::ascii::AsciiImage;
|
use protocols::ascii::AsciiImage;
|
||||||
|
|
||||||
use self::printer::{ImageProperties, TerminalImage};
|
|
||||||
use std::{
|
use std::{
|
||||||
fmt::Debug,
|
fmt::Debug,
|
||||||
ops::Deref,
|
ops::Deref,
|
||||||
@ -43,6 +42,7 @@ impl Image {
|
|||||||
TerminalImage::Ascii(image) => image.clone(),
|
TerminalImage::Ascii(image) => image.clone(),
|
||||||
TerminalImage::Kitty(image) => DynamicImage::from(image.as_rgba8()).into(),
|
TerminalImage::Kitty(image) => DynamicImage::from(image.as_rgba8()).into(),
|
||||||
TerminalImage::Iterm(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")]
|
#[cfg(feature = "sixel")]
|
||||||
TerminalImage::Sixel(image) => DynamicImage::from(image.as_rgba8()).into(),
|
TerminalImage::Sixel(image) => DynamicImage::from(image.as_rgba8()).into(),
|
||||||
};
|
};
|
||||||
|
@ -4,6 +4,7 @@ use super::{
|
|||||||
ascii::{AsciiImage, AsciiPrinter},
|
ascii::{AsciiImage, AsciiPrinter},
|
||||||
iterm::{ItermImage, ItermPrinter},
|
iterm::{ItermImage, ItermPrinter},
|
||||||
kitty::{KittyImage, KittyMode, KittyPrinter},
|
kitty::{KittyImage, KittyMode, KittyPrinter},
|
||||||
|
raw::{RawImage, RawPrinter},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use crate::{
|
use crate::{
|
||||||
@ -54,6 +55,7 @@ pub(crate) enum TerminalImage {
|
|||||||
Kitty(KittyImage),
|
Kitty(KittyImage),
|
||||||
Iterm(ItermImage),
|
Iterm(ItermImage),
|
||||||
Ascii(AsciiImage),
|
Ascii(AsciiImage),
|
||||||
|
Raw(RawImage),
|
||||||
#[cfg(feature = "sixel")]
|
#[cfg(feature = "sixel")]
|
||||||
Sixel(super::protocols::sixel::SixelImage),
|
Sixel(super::protocols::sixel::SixelImage),
|
||||||
}
|
}
|
||||||
@ -64,6 +66,7 @@ impl ImageProperties for TerminalImage {
|
|||||||
Self::Kitty(image) => image.dimensions(),
|
Self::Kitty(image) => image.dimensions(),
|
||||||
Self::Iterm(image) => image.dimensions(),
|
Self::Iterm(image) => image.dimensions(),
|
||||||
Self::Ascii(image) => image.dimensions(),
|
Self::Ascii(image) => image.dimensions(),
|
||||||
|
Self::Raw(image) => image.dimensions(),
|
||||||
#[cfg(feature = "sixel")]
|
#[cfg(feature = "sixel")]
|
||||||
Self::Sixel(image) => image.dimensions(),
|
Self::Sixel(image) => image.dimensions(),
|
||||||
}
|
}
|
||||||
@ -74,6 +77,7 @@ pub enum ImagePrinter {
|
|||||||
Kitty(KittyPrinter),
|
Kitty(KittyPrinter),
|
||||||
Iterm(ItermPrinter),
|
Iterm(ItermPrinter),
|
||||||
Ascii(AsciiPrinter),
|
Ascii(AsciiPrinter),
|
||||||
|
Raw(RawPrinter),
|
||||||
Null,
|
Null,
|
||||||
#[cfg(feature = "sixel")]
|
#[cfg(feature = "sixel")]
|
||||||
Sixel(super::protocols::sixel::SixelPrinter),
|
Sixel(super::protocols::sixel::SixelPrinter),
|
||||||
@ -91,6 +95,7 @@ impl ImagePrinter {
|
|||||||
GraphicsMode::Kitty { mode, inside_tmux } => Self::new_kitty(mode, inside_tmux)?,
|
GraphicsMode::Kitty { mode, inside_tmux } => Self::new_kitty(mode, inside_tmux)?,
|
||||||
GraphicsMode::Iterm2 => Self::new_iterm(),
|
GraphicsMode::Iterm2 => Self::new_iterm(),
|
||||||
GraphicsMode::AsciiBlocks => Self::new_ascii(),
|
GraphicsMode::AsciiBlocks => Self::new_ascii(),
|
||||||
|
GraphicsMode::Raw => Self::new_raw(),
|
||||||
#[cfg(feature = "sixel")]
|
#[cfg(feature = "sixel")]
|
||||||
GraphicsMode::Sixel => Self::new_sixel()?,
|
GraphicsMode::Sixel => Self::new_sixel()?,
|
||||||
};
|
};
|
||||||
@ -109,6 +114,10 @@ impl ImagePrinter {
|
|||||||
Self::Ascii(AsciiPrinter)
|
Self::Ascii(AsciiPrinter)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn new_raw() -> Self {
|
||||||
|
Self::Raw(RawPrinter)
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(feature = "sixel")]
|
#[cfg(feature = "sixel")]
|
||||||
fn new_sixel() -> Result<Self, CreatePrinterError> {
|
fn new_sixel() -> Result<Self, CreatePrinterError> {
|
||||||
Ok(Self::Sixel(super::protocols::sixel::SixelPrinter::new()?))
|
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::Iterm(printer) => TerminalImage::Iterm(printer.register(spec)?),
|
||||||
Self::Ascii(printer) => TerminalImage::Ascii(printer.register(spec)?),
|
Self::Ascii(printer) => TerminalImage::Ascii(printer.register(spec)?),
|
||||||
Self::Null => return Err(RegisterImageError::Unsupported),
|
Self::Null => return Err(RegisterImageError::Unsupported),
|
||||||
|
Self::Raw(printer) => TerminalImage::Raw(printer.register(spec)?),
|
||||||
#[cfg(feature = "sixel")]
|
#[cfg(feature = "sixel")]
|
||||||
Self::Sixel(printer) => TerminalImage::Sixel(printer.register(spec)?),
|
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::Iterm(printer), TerminalImage::Iterm(image)) => printer.print(image, options, terminal),
|
||||||
(Self::Ascii(printer), TerminalImage::Ascii(image)) => printer.print(image, options, terminal),
|
(Self::Ascii(printer), TerminalImage::Ascii(image)) => printer.print(image, options, terminal),
|
||||||
(Self::Null, _) => Ok(()),
|
(Self::Null, _) => Ok(()),
|
||||||
|
(Self::Raw(printer), TerminalImage::Raw(image)) => printer.print(image, options, terminal),
|
||||||
#[cfg(feature = "sixel")]
|
#[cfg(feature = "sixel")]
|
||||||
(Self::Sixel(printer), TerminalImage::Sixel(image)) => printer.print(image, options, terminal),
|
(Self::Sixel(printer), TerminalImage::Sixel(image)) => printer.print(image, options, terminal),
|
||||||
_ => Err(PrintImageError::Unsupported),
|
_ => Err(PrintImageError::Unsupported),
|
||||||
@ -165,6 +176,7 @@ impl fmt::Debug for ImageRegistry {
|
|||||||
ImagePrinter::Iterm(_) => "Iterm",
|
ImagePrinter::Iterm(_) => "Iterm",
|
||||||
ImagePrinter::Ascii(_) => "Ascii",
|
ImagePrinter::Ascii(_) => "Ascii",
|
||||||
ImagePrinter::Null => "Null",
|
ImagePrinter::Null => "Null",
|
||||||
|
ImagePrinter::Raw(_) => "Raw",
|
||||||
#[cfg(feature = "sixel")]
|
#[cfg(feature = "sixel")]
|
||||||
ImagePrinter::Sixel(_) => "Sixel",
|
ImagePrinter::Sixel(_) => "Sixel",
|
||||||
};
|
};
|
||||||
|
@ -36,10 +36,6 @@ impl AsciiImage {
|
|||||||
cached_sizes.insert(cache_key, image.into_rgba8());
|
cached_sizes.insert(cache_key, image.into_rgba8());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn image(&self) -> &DynamicImage {
|
|
||||||
&self.inner.image
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ImageProperties for AsciiImage {
|
impl ImageProperties for AsciiImage {
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
pub(crate) mod ascii;
|
pub(crate) mod ascii;
|
||||||
pub(crate) mod iterm;
|
pub(crate) mod iterm;
|
||||||
pub(crate) mod kitty;
|
pub(crate) mod kitty;
|
||||||
|
pub(crate) mod raw;
|
||||||
#[cfg(feature = "sixel")]
|
#[cfg(feature = "sixel")]
|
||||||
pub(crate) mod 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,
|
inside_tmux: bool,
|
||||||
},
|
},
|
||||||
AsciiBlocks,
|
AsciiBlocks,
|
||||||
|
Raw,
|
||||||
#[cfg(feature = "sixel")]
|
#[cfg(feature = "sixel")]
|
||||||
Sixel,
|
Sixel,
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user