Skip to content

Commit 775927f

Browse files
committed
feat: padding, scaling customization, better tracing & encoder handling
1 parent 7292e00 commit 775927f

File tree

6 files changed

+448
-274
lines changed

6 files changed

+448
-274
lines changed

src/cli.rs

Lines changed: 68 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@ use std::io::BufWriter;
99
use std::path::PathBuf;
1010
use tracing::Level;
1111

12-
use crate::{github, image};
12+
use crate::{
13+
encode::{create_encoder, Encoder, ImageFormat},
14+
github,
15+
};
1316

1417
/// Command-line arguments for livecards.
1518
#[derive(Parser)]
@@ -41,6 +44,41 @@ pub struct Cli {
4144
pub log_level: Level,
4245
}
4346

47+
/// Formats the SVG template with repository data.
48+
///
49+
/// # Arguments
50+
/// * `name` - Repository name
51+
/// * `description` - Repository description
52+
/// * `language` - Primary programming language
53+
/// * `stars` - Star count as string
54+
/// * `forks` - Fork count as string
55+
///
56+
/// # Returns
57+
/// Formatted SVG string
58+
fn format_svg_template(
59+
name: &str,
60+
description: &str,
61+
language: &str,
62+
stars: &str,
63+
forks: &str,
64+
) -> String {
65+
let svg_template = include_str!("../card.svg");
66+
let wrapped_description = crate::image::wrap_text(description, 65);
67+
let language_color =
68+
crate::colors::get_color(language).unwrap_or_else(|| "#f1e05a".to_string());
69+
70+
let formatted_stars = crate::image::format_count(stars);
71+
let formatted_forks = crate::image::format_count(forks);
72+
73+
svg_template
74+
.replace("{{name}}", name)
75+
.replace("{{description}}", &wrapped_description)
76+
.replace("{{language}}", language)
77+
.replace("{{language_color}}", &language_color)
78+
.replace("{{stars}}", &formatted_stars)
79+
.replace("{{forks}}", &formatted_forks)
80+
}
81+
4482
/// Executes the CLI command to generate a repository card.
4583
///
4684
/// # Arguments
@@ -61,16 +99,41 @@ pub async fn run(cli: Cli) -> Result<()> {
6199
};
62100

63101
let file = File::create(&output_path)?;
64-
let writer = BufWriter::new(file);
102+
let mut writer = BufWriter::new(file);
103+
104+
// Start timing for image generation
105+
let start_time = std::time::Instant::now();
65106

66-
image::generate_image(
107+
// Format the SVG template
108+
let formatted_svg = format_svg_template(
67109
&repo.name,
68110
&repo.description.unwrap_or_default(),
69111
&repo.language.unwrap_or_default(),
70112
&repo.stargazers_count.to_string(),
71113
&repo.forks_count.to_string(),
72-
writer,
73-
)?;
114+
);
115+
116+
// Create encoder and encode
117+
let encoder = create_encoder(ImageFormat::Png);
118+
encoder.encode(&formatted_svg, &mut writer, None)?;
119+
120+
// Calculate timing
121+
let duration = start_time.elapsed();
122+
let duration_ms = duration.as_millis();
123+
124+
tracing::debug!(
125+
"CLI image generation completed in {}ms for {}",
126+
duration_ms,
127+
repo_path
128+
);
129+
130+
if duration_ms > 1000 {
131+
tracing::warn!(
132+
"CLI image generation took {}ms (>1000ms) for {}",
133+
duration_ms,
134+
repo_path
135+
);
136+
}
74137

75138
tracing::info!("Successfully generated {}.", output_path.to_string_lossy());
76139

src/encode.rs

Lines changed: 59 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,12 @@ use tracing::instrument;
1010

1111
/// Helper function to rasterize SVG and convert to RgbaImage.
1212
/// This eliminates code duplication across encoders.
13-
fn rasterize_svg_to_rgba(svg_data: &str) -> Result<RgbaImage> {
14-
let rasterizer = crate::image::Rasterizer::new();
15-
let pixmap = rasterizer.render(svg_data)?;
13+
fn rasterize_svg_to_rgba(
14+
rasterizer: &crate::image::Rasterizer,
15+
svg_data: &str,
16+
scale: Option<f64>,
17+
) -> Result<RgbaImage> {
18+
let pixmap = rasterizer.render_with_scale(svg_data, scale)?;
1619

1720
let width = pixmap.width();
1821
let height = pixmap.height();
@@ -82,10 +85,11 @@ pub trait Encoder {
8285
/// # Arguments
8386
/// * `svg_data` - The SVG data to encode
8487
/// * `writer` - Output writer for the encoded data
88+
/// * `scale` - Optional scale factor for the image
8589
///
8690
/// # Returns
8791
/// Result indicating success or failure
88-
fn encode(&self, svg_data: &str, writer: &mut dyn Write) -> Result<()>;
92+
fn encode(&self, svg_data: &str, writer: &mut dyn Write, scale: Option<f64>) -> Result<()>;
8993
}
9094

9195
/// PNG encoder using the resvg library.
@@ -103,9 +107,11 @@ impl PngEncoder {
103107
}
104108

105109
impl Encoder for PngEncoder {
106-
#[instrument(skip(self, writer))]
107-
fn encode(&self, svg_data: &str, writer: &mut dyn Write) -> Result<()> {
108-
let pixmap = self.rasterizer.render(svg_data)?;
110+
#[instrument(skip(self, writer, svg_data))]
111+
fn encode(&self, svg_data: &str, writer: &mut dyn Write, scale: Option<f64>) -> Result<()> {
112+
let start_time = std::time::Instant::now();
113+
114+
let pixmap = self.rasterizer.render_with_scale(svg_data, scale)?;
109115

110116
let mut png_encoder = png::Encoder::new(writer, pixmap.width(), pixmap.height());
111117
png_encoder.set_color(png::ColorType::Rgba);
@@ -123,6 +129,27 @@ impl Encoder for PngEncoder {
123129
.finish()
124130
.map_err(|e| LivecardsError::Image(ImageError::PngWrite(e.to_string())))?;
125131

132+
let duration = start_time.elapsed();
133+
let duration_ms = duration.as_millis();
134+
135+
tracing::debug!(
136+
"PNG encoding completed in {}ms (scale: {:?}, size: {}x{})",
137+
duration_ms,
138+
scale,
139+
pixmap.width(),
140+
pixmap.height()
141+
);
142+
143+
if duration_ms > 1000 {
144+
tracing::warn!(
145+
"PNG encoding took {}ms (>1000ms) (scale: {:?}, size: {}x{})",
146+
duration_ms,
147+
scale,
148+
pixmap.width(),
149+
pixmap.height()
150+
);
151+
}
152+
126153
Ok(())
127154
}
128155
}
@@ -144,9 +171,9 @@ impl WebPEncoder {
144171
}
145172

146173
impl Encoder for WebPEncoder {
147-
#[instrument(skip(writer))]
148-
fn encode(&self, svg_data: &str, writer: &mut dyn Write) -> Result<()> {
149-
let img = rasterize_svg_to_rgba(svg_data)?;
174+
#[instrument(skip(writer, svg_data))]
175+
fn encode(&self, svg_data: &str, writer: &mut dyn Write, scale: Option<f64>) -> Result<()> {
176+
let img = rasterize_svg_to_rgba(&crate::image::Rasterizer::new(), svg_data, scale)?;
150177

151178
// Encode as WebP
152179
img.write_with_encoder(image::codecs::webp::WebPEncoder::new_lossless(writer))
@@ -173,9 +200,9 @@ impl JpegEncoder {
173200
}
174201

175202
impl Encoder for JpegEncoder {
176-
#[instrument(skip(writer))]
177-
fn encode(&self, svg_data: &str, writer: &mut dyn Write) -> Result<()> {
178-
let img = rasterize_svg_to_rgba(svg_data)?;
203+
#[instrument(skip(writer, svg_data))]
204+
fn encode(&self, svg_data: &str, writer: &mut dyn Write, scale: Option<f64>) -> Result<()> {
205+
let img = rasterize_svg_to_rgba(&crate::image::Rasterizer::new(), svg_data, scale)?;
179206

180207
// Convert RGBA to RGB for JPEG encoding
181208
let rgb_img = image::DynamicImage::ImageRgba8(img).into_rgb8();
@@ -206,11 +233,12 @@ impl SvgEncoder {
206233
}
207234

208235
impl Encoder for SvgEncoder {
209-
#[instrument(skip(writer))]
210-
fn encode(&self, svg_data: &str, writer: &mut dyn Write) -> Result<()> {
236+
#[instrument(skip(writer, svg_data))]
237+
fn encode(&self, svg_data: &str, writer: &mut dyn Write, _scale: Option<f64>) -> Result<()> {
211238
writer
212239
.write_all(svg_data.as_bytes())
213240
.map_err(|e| LivecardsError::Image(ImageError::SvgWrite(e.to_string())))?;
241+
214242
Ok(())
215243
}
216244
}
@@ -232,9 +260,9 @@ impl AvifEncoder {
232260
}
233261

234262
impl Encoder for AvifEncoder {
235-
#[instrument(skip(writer))]
236-
fn encode(&self, svg_data: &str, writer: &mut dyn Write) -> Result<()> {
237-
let img = rasterize_svg_to_rgba(svg_data)?;
263+
#[instrument(skip(writer, svg_data))]
264+
fn encode(&self, svg_data: &str, writer: &mut dyn Write, scale: Option<f64>) -> Result<()> {
265+
let img = rasterize_svg_to_rgba(&crate::image::Rasterizer::new(), svg_data, scale)?;
238266

239267
// Encode as AVIF with maximum speed settings (speed 10, quality 60)
240268
img.write_with_encoder(image::codecs::avif::AvifEncoder::new_with_speed_quality(
@@ -265,10 +293,10 @@ impl GifEncoder {
265293

266294
impl Encoder for GifEncoder {
267295
#[instrument(skip(_svg_data, _writer))]
268-
fn encode(&self, _svg_data: &str, _writer: &mut dyn Write) -> Result<()> {
296+
fn encode(&self, _svg_data: &str, _writer: &mut dyn Write, _scale: Option<f64>) -> Result<()> {
269297
// GIF encoding is not currently supported
270298
Err(LivecardsError::Image(ImageError::GifWrite(
271-
"GIF encoding not supported".to_string(),
299+
"GIF encoding is not implemented".to_string(),
272300
)))
273301
}
274302
}
@@ -290,9 +318,9 @@ impl IcoEncoder {
290318
}
291319

292320
impl Encoder for IcoEncoder {
293-
#[instrument(skip(writer))]
294-
fn encode(&self, svg_data: &str, writer: &mut dyn Write) -> Result<()> {
295-
let img = rasterize_svg_to_rgba(svg_data)?;
321+
#[instrument(skip(writer, svg_data))]
322+
fn encode(&self, svg_data: &str, writer: &mut dyn Write, scale: Option<f64>) -> Result<()> {
323+
let img = rasterize_svg_to_rgba(&crate::image::Rasterizer::new(), svg_data, scale)?;
296324

297325
// Resize image to fit ICO requirements (max 256x256)
298326
let width = img.width();
@@ -339,15 +367,15 @@ pub enum EncoderType {
339367
}
340368

341369
impl Encoder for EncoderType {
342-
fn encode(&self, svg_data: &str, writer: &mut dyn Write) -> Result<()> {
370+
fn encode(&self, svg_data: &str, writer: &mut dyn Write, scale: Option<f64>) -> Result<()> {
343371
match self {
344-
EncoderType::Png(encoder) => encoder.encode(svg_data, writer),
345-
EncoderType::WebP(encoder) => encoder.encode(svg_data, writer),
346-
EncoderType::Jpeg(encoder) => encoder.encode(svg_data, writer),
347-
EncoderType::Svg(encoder) => encoder.encode(svg_data, writer),
348-
EncoderType::Avif(encoder) => encoder.encode(svg_data, writer),
349-
EncoderType::Gif(encoder) => encoder.encode(svg_data, writer),
350-
EncoderType::Ico(encoder) => encoder.encode(svg_data, writer),
372+
EncoderType::Png(encoder) => encoder.encode(svg_data, writer, scale),
373+
EncoderType::WebP(encoder) => encoder.encode(svg_data, writer, scale),
374+
EncoderType::Jpeg(encoder) => encoder.encode(svg_data, writer, scale),
375+
EncoderType::Svg(encoder) => encoder.encode(svg_data, writer, scale),
376+
EncoderType::Avif(encoder) => encoder.encode(svg_data, writer, scale),
377+
EncoderType::Gif(encoder) => encoder.encode(svg_data, writer, scale),
378+
EncoderType::Ico(encoder) => encoder.encode(svg_data, writer, scale),
351379
}
352380
}
353381
}
@@ -364,46 +392,3 @@ pub fn create_encoder(format: ImageFormat) -> EncoderType {
364392
ImageFormat::Ico => EncoderType::Ico(IcoEncoder::new()),
365393
}
366394
}
367-
368-
/// Generate an image in the specified format.
369-
///
370-
/// # Arguments
371-
/// * `name` - Repository name
372-
/// * `description` - Repository description
373-
/// * `language` - Primary programming language
374-
/// * `stars` - Star count as string
375-
/// * `forks` - Fork count as string
376-
/// * `format` - Target image format
377-
/// * `writer` - Output writer for the encoded data
378-
///
379-
/// # Returns
380-
/// Result indicating success or failure
381-
#[instrument(skip(writer))]
382-
pub fn generate_image<W: Write>(
383-
name: &str,
384-
description: &str,
385-
language: &str,
386-
stars: &str,
387-
forks: &str,
388-
format: ImageFormat,
389-
mut writer: W,
390-
) -> Result<()> {
391-
let svg_template = include_str!("../card.svg");
392-
let wrapped_description = crate::image::wrap_text(description, 65);
393-
let language_color =
394-
crate::colors::get_color(language).unwrap_or_else(|| "#f1e05a".to_string());
395-
396-
let formatted_stars = crate::image::format_count(stars);
397-
let formatted_forks = crate::image::format_count(forks);
398-
399-
let svg_filled = svg_template
400-
.replace("{{name}}", name)
401-
.replace("{{description}}", &wrapped_description)
402-
.replace("{{language}}", language)
403-
.replace("{{language_color}}", &language_color)
404-
.replace("{{stars}}", &formatted_stars)
405-
.replace("{{forks}}", &formatted_forks);
406-
407-
let encoder = create_encoder(format);
408-
encoder.encode(&svg_filled, &mut writer)
409-
}

src/errors.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,8 +74,8 @@ pub enum GitHubError {
7474
#[derive(Error, Debug)]
7575
pub enum ImageError {
7676
/// Failed to create pixmap
77-
#[error("Failed to create pixmap")]
78-
PixmapCreation,
77+
#[error("Failed to create pixmap: {0}")]
78+
PixmapCreation(String),
7979

8080
/// Failed to render SVG
8181
#[error("Failed to render SVG: {0}")]

0 commit comments

Comments
 (0)