Skip to content

Commit ba6b347

Browse files
authored
[Feat] support generate beautiful image snapshot (#113)
1 parent 9396bd3 commit ba6b347

File tree

11 files changed

+245
-114
lines changed

11 files changed

+245
-114
lines changed

cli/src/code.rs

Lines changed: 54 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -15,31 +15,12 @@ use codesnap::{
1515

1616
use crate::{highlight::HighlightLineRange, range::Range, CLI, STDIN_CODE_DEFAULT_CHAR};
1717

18-
pub fn create_code(cli: &CLI, code_config: Code) -> anyhow::Result<Content> {
18+
pub fn create_content(cli: &CLI, default_content: Code) -> anyhow::Result<Content> {
1919
let code = match cli.execute[..] {
20-
[] => {
21-
let range = Range::from_opt_string(cli.range.clone())?;
22-
let code_snippet = get_code_snippet(cli)?;
23-
let parsed_range = range.parse_range(&code_snippet)?;
24-
let parsed_code_snippet = parsed_range.cut_code_snippet(&code_snippet)?;
25-
let mut code = CodeBuilder::default()
26-
.content(parsed_code_snippet)
27-
.build()?;
28-
29-
code.start_line_number = cli
30-
.has_line_number
31-
.then_some(cli.start_line_number.unwrap_or(parsed_range.0 as u32));
32-
33-
code.file_path = cli
34-
.from_file
35-
.clone()
36-
.or(cli.file_path.clone())
37-
.or(code_config.file_path);
38-
code.language = cli.language.clone().or(code_config.language);
39-
code.highlight_lines = create_highlight_lines(&cli, parsed_range, &code_snippet)?;
40-
41-
Content::Code(code)
42-
}
20+
[] => match &cli.from_image {
21+
Some(image) => create_image(image),
22+
None => create_code(cli, default_content),
23+
}?,
4324
_ => {
4425
let command_content = cli
4526
.execute
@@ -132,17 +113,7 @@ fn get_code_snippet(cli: &CLI) -> anyhow::Result<String> {
132113
if let Some(ref code) = cli.from_code {
133114
// Read code from pipe if the code option is "-"
134115
return Ok(if code == STDIN_CODE_DEFAULT_CHAR {
135-
// If input come from terminal, print help and exit
136-
if stdin().is_terminal() {
137-
CLI::command().print_help()?;
138-
process::exit(2);
139-
}
140-
141-
let mut content = String::new();
142-
143-
BufReader::new(stdin().lock()).read_to_string(&mut content)?;
144-
145-
content
116+
String::from_utf8(read_from_stdin()?)?
146117
} else {
147118
code.clone()
148119
});
@@ -156,3 +127,51 @@ fn get_code_snippet(cli: &CLI) -> anyhow::Result<String> {
156127

157128
bail!("No code snippet provided");
158129
}
130+
131+
fn create_code(cli: &CLI, default_content: Code) -> anyhow::Result<Content> {
132+
let range = Range::from_opt_string(cli.range.clone())?;
133+
let code_snippet = get_code_snippet(cli)?;
134+
let parsed_range = range.parse_range(&code_snippet)?;
135+
let parsed_code_snippet = parsed_range.cut_code_snippet(&code_snippet)?;
136+
let mut code = CodeBuilder::default()
137+
.content(parsed_code_snippet)
138+
.build()?;
139+
140+
code.start_line_number = cli
141+
.has_line_number
142+
.then_some(cli.start_line_number.unwrap_or(parsed_range.0 as u32));
143+
144+
code.file_path = cli
145+
.from_file
146+
.clone()
147+
.or(cli.file_path.clone())
148+
.or(default_content.file_path);
149+
code.language = cli.language.clone().or(default_content.language);
150+
code.highlight_lines = create_highlight_lines(cli, parsed_range, &code_snippet)?;
151+
152+
Ok(Content::Code(code))
153+
}
154+
155+
fn create_image(image: &str) -> anyhow::Result<Content> {
156+
let image_data = if image == STDIN_CODE_DEFAULT_CHAR {
157+
read_from_stdin()?
158+
} else {
159+
image.as_bytes().to_owned()
160+
};
161+
162+
Ok(Content::Image(image_data))
163+
}
164+
165+
fn read_from_stdin() -> anyhow::Result<Vec<u8>> {
166+
let mut content = vec![];
167+
168+
// If input come from terminal, print help and exit
169+
if stdin().is_terminal() {
170+
CLI::command().print_help()?;
171+
process::exit(2);
172+
}
173+
174+
BufReader::new(stdin().lock()).read_to_end(&mut content)?;
175+
176+
Ok(content)
177+
}

cli/src/main.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,10 @@ use std::fs::read_to_string;
1313
use anyhow::bail;
1414
use clap::value_parser;
1515
use clap::Parser;
16-
use code::create_code;
16+
use code::create_content;
1717
use code_config::create_code_config;
1818
use codesnap::config::CodeSnap;
19+
use codesnap::config::Content;
1920
use codesnap::config::SnapshotConfig;
2021
use codesnap::themes::parse_code_theme;
2122
use config::CodeSnapCLIConfig;
@@ -44,6 +45,9 @@ struct CLI {
4445
#[arg(short = 'c', long, default_missing_value = STDIN_CODE_DEFAULT_CHAR, require_equals=false, num_args=0..=1, value_parser=value_parser!(String), value_name="Code")]
4546
from_code: Option<String>,
4647

48+
#[arg(short = 'i', long, default_missing_value = STDIN_CODE_DEFAULT_CHAR, require_equals=false, num_args=0..=1, value_parser=value_parser!(String), value_name="Image")]
49+
from_image: Option<String>,
50+
4751
#[arg(long)]
4852
from_clipboard: bool,
4953

@@ -292,7 +296,7 @@ async fn create_snapshot_config(
292296
// Build screenshot config
293297
let mut codesnap = codesnap
294298
.map_code_config(|code_config| create_code_config(cli, code_config))?
295-
.map_code(|raw_code| create_code(cli, raw_code))?
299+
.map_content(|default_content| create_content(cli, default_content))?
296300
.map_watermark(|watermark| create_watermark(cli, watermark))?
297301
.map_window(|window| create_window(cli, window))?
298302
.scale_factor(cli.scale_factor)

core/examples/image.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
use codesnap::config::{CodeSnap, Content};
2+
3+
pub fn main() -> anyhow::Result<()> {
4+
let data = std::fs::read("core/examples/screenshot.png").unwrap();
5+
let code_content = Content::Image(data);
6+
7+
let snapshot = CodeSnap::from_default_theme()?
8+
.content(code_content)
9+
.build()?
10+
.create_snapshot()?;
11+
12+
// Copy the snapshot data to the clipboard
13+
snapshot.raw_data()?.copy()
14+
}

core/examples/screenshot.png

103 KB
Loading

core/src/components.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ pub mod command_line;
55
pub mod container;
66
pub mod editor;
77
pub mod highlight_code_block;
8+
pub mod image;
89
pub mod interface;
910
pub mod layout;
1011
pub mod line_number;

core/src/components/image.rs

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
use tiny_skia::{Pixmap, PixmapPaint};
2+
3+
use crate::components::interface::{
4+
component::{Component, ComponentContext, RenderParams},
5+
render_error,
6+
style::{ComponentStyle, RawComponentStyle, Size, Style},
7+
};
8+
9+
pub struct Image {
10+
image_pixmap: Pixmap,
11+
children: Vec<Box<dyn Component>>,
12+
}
13+
14+
impl Component for Image {
15+
fn children(&self) -> &Vec<Box<dyn Component>> {
16+
&self.children
17+
}
18+
19+
fn style(&self, _context: &ComponentContext) -> RawComponentStyle {
20+
RawComponentStyle::default().size(
21+
Size::Num(self.image_pixmap.width() as f32),
22+
Size::Num(self.image_pixmap.height() as f32),
23+
)
24+
}
25+
26+
fn draw_self(
27+
&self,
28+
pixmap: &mut Pixmap,
29+
context: &ComponentContext,
30+
render_params: &RenderParams,
31+
_style: &ComponentStyle,
32+
_parent_style: &Style<f32>,
33+
) -> render_error::Result<()> {
34+
let transform =
35+
tiny_skia::Transform::from_scale(context.scale_factor, context.scale_factor);
36+
let paint = PixmapPaint::default();
37+
38+
pixmap.draw_pixmap(
39+
render_params.x as i32,
40+
render_params.y as i32,
41+
self.image_pixmap.as_ref(),
42+
&paint,
43+
transform,
44+
None,
45+
);
46+
47+
Ok(())
48+
}
49+
}
50+
51+
impl Image {
52+
pub fn new(image_data: Vec<u8>) -> anyhow::Result<Self> {
53+
let image_pixmap = Pixmap::decode_png(&image_data)?;
54+
55+
Ok(Self {
56+
image_pixmap,
57+
children: vec![],
58+
})
59+
}
60+
}

core/src/components/interface/render_error.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,7 @@ pub enum RenderError {
99

1010
#[error("No such file {0}")]
1111
NoSuchFile(String),
12+
13+
#[error("Failed to decode image data")]
14+
FailedDecodeImageData,
1215
}

core/src/components/watermark.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,12 @@ impl Component for Watermark {
3131
let attrs = Attrs::new()
3232
.color(parse_hex_to_cosmic_color(&config.color))
3333
.family(Family::Name(&config.font_family));
34+
let font_size = (pixmap.width() as f32 * 0.11).clamp(20., 30.);
3435

3536
context.font_renderer.lock().unwrap().draw_line(
3637
0.,
3738
render_params.y,
38-
Metrics::new(20., 20.),
39+
Metrics::new(font_size, font_size),
3940
&config.content,
4041
attrs,
4142
Some(Align::Center),

core/src/config.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,7 @@ pub struct CommandOutputConfig {
248248
pub enum Content {
249249
Code(Code),
250250
CommandOutput(Vec<CommandLineContent>),
251+
Image(Vec<u8>),
251252
}
252253

253254
#[derive(Clone, Builder, Serialize, Deserialize, Debug, JsonSchema, Default)]
@@ -398,7 +399,7 @@ impl CodeSnap {
398399
Ok(self)
399400
}
400401

401-
pub fn map_code<F>(&mut self, f: F) -> anyhow::Result<&mut Self>
402+
pub fn map_content<F>(&mut self, f: F) -> anyhow::Result<&mut Self>
402403
where
403404
F: Fn(Code) -> anyhow::Result<Content>,
404405
{

0 commit comments

Comments
 (0)