Update rust_highlight/src/lib.rs
Browse files- rust_highlight/src/lib.rs +328 -67
rust_highlight/src/lib.rs
CHANGED
|
@@ -1,83 +1,344 @@
|
|
|
|
|
| 1 |
use pyo3::prelude::*;
|
| 2 |
-
use
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
use std::path::Path;
|
|
|
|
| 4 |
|
| 5 |
#[pyfunction]
|
| 6 |
-
fn
|
| 7 |
-
id: usize,
|
| 8 |
-
image_path: String,
|
| 9 |
-
audio_path: String,
|
| 10 |
-
duration: f64,
|
| 11 |
-
words: Vec<(String, (u32, u32, u32, u32))>,
|
| 12 |
-
output_dir: String,
|
| 13 |
-
) -> PyResult<String> {
|
| 14 |
-
// Validate paths
|
| 15 |
-
if !Path::new(&image_path).exists() {
|
| 16 |
-
return Err(pyo3::exceptions::PyFileNotFoundError::new_err(format!(
|
| 17 |
-
"Image not found: {}",
|
| 18 |
-
image_path
|
| 19 |
-
)));
|
| 20 |
-
}
|
| 21 |
if !Path::new(&audio_path).exists() {
|
| 22 |
-
return Err(pyo3::exceptions::PyFileNotFoundError::new_err(format!(
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
}
|
| 27 |
|
| 28 |
-
|
| 29 |
-
let
|
| 30 |
-
let
|
| 31 |
-
let
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
let
|
| 37 |
-
let
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
|
|
|
|
|
|
|
|
|
| 41 |
}
|
| 42 |
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
.
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 76 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 77 |
}
|
| 78 |
|
| 79 |
#[pymodule]
|
| 80 |
fn rust_highlight(_py: Python, m: &PyModule) -> PyResult<()> {
|
| 81 |
-
m.add_function(wrap_pyfunction!(
|
| 82 |
Ok(())
|
| 83 |
}
|
|
|
|
| 1 |
+
// rust_highlight/src/lib.rs
|
| 2 |
use pyo3::prelude::*;
|
| 3 |
+
use opencv::core::{Mat, Point, Scalar, CV_8UC3};
|
| 4 |
+
use opencv::imgproc::{circle, get_text_size, line, put_text, HersheyFonts, LineTypes};
|
| 5 |
+
use std::process::{Command, Stdio};
|
| 6 |
+
use std::io::Write;
|
| 7 |
+
use std::time::Instant;
|
| 8 |
use std::path::Path;
|
| 9 |
+
use std::f64::consts::PI;
|
| 10 |
|
| 11 |
#[pyfunction]
|
| 12 |
+
fn generate_video_clip(id: usize, text: String, audio_path: String, duration: f64, clips_dir: String) -> PyResult<Option<String>> {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
if !Path::new(&audio_path).exists() {
|
| 14 |
+
return Err(pyo3::exceptions::PyFileNotFoundError::new_err(format!("Audio not found: {}", audio_path)));
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
let skip_spaces = false;
|
| 18 |
+
let fps: f64 = 30.0;
|
| 19 |
+
let animation_frames_per_char: usize = 2;
|
| 20 |
+
let width: i32 = 1280;
|
| 21 |
+
let height: i32 = 720;
|
| 22 |
+
let margin_x: i32 = 40;
|
| 23 |
+
let margin_y: i32 = 60;
|
| 24 |
+
let line_spacing: i32 = 8;
|
| 25 |
+
let font = HersheyFonts::FONT_HERSHEY_SIMPLEX as i32;
|
| 26 |
+
let default_font_scale: f64 = 1.5;
|
| 27 |
+
let header_font_scale: f64 = 2.0;
|
| 28 |
+
let default_thickness: i32 = 2;
|
| 29 |
+
let header_thickness: i32 = 3;
|
| 30 |
+
let default_text_color = Scalar::new(0.0, 0.0, 0.0, 0.0); // BGR Black
|
| 31 |
+
let header_text_color = Scalar::new(255.0, 0.0, 0.0, 0.0); // BGR Blue
|
| 32 |
+
let bg_color = Scalar::new(255.0, 255.0, 255.0, 0.0); // BGR White
|
| 33 |
+
let ffmpeg_preset = "ultrafast";
|
| 34 |
+
let crf = "28";
|
| 35 |
+
// Pen settings
|
| 36 |
+
let pen_color = Scalar::new(0.0, 0.0, 255.0, 0.0); // BGR Red
|
| 37 |
+
let pen_tip_radius: i32 = 5;
|
| 38 |
+
let pen_length: i32 = 20;
|
| 39 |
+
let pen_thickness: i32 = 2;
|
| 40 |
+
let pen_base_angle: i32 = 45;
|
| 41 |
+
let pen_movement_amplitude: i32 = 10;
|
| 42 |
+
|
| 43 |
+
let silent_video_name = format!("silent_video{}.mp4", id);
|
| 44 |
+
let silent_video_path = format!("{}/{}", clips_dir, silent_video_name);
|
| 45 |
+
let final_video_name = format!("clip{}.mp4", id);
|
| 46 |
+
let final_video_path = format!("{}/{}", clips_dir, final_video_name);
|
| 47 |
+
|
| 48 |
+
// Wrap text
|
| 49 |
+
let text_area_width = width - 2 * margin_x;
|
| 50 |
+
let (wrapped_lines, line_styles) = wrap_text_cv(&text, font, default_font_scale, default_thickness, text_area_width);
|
| 51 |
+
|
| 52 |
+
let full_text = wrapped_lines.join("\n");
|
| 53 |
+
if full_text.is_empty() {
|
| 54 |
+
println!("No text to animate.");
|
| 55 |
+
return Ok(None);
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
let visible_indices: Vec<usize> = if skip_spaces {
|
| 59 |
+
full_text.char_indices().filter(|&(_, ch)| ch != ' ' && ch != '\n' && ch != '\t').map(|(i, _)| i).collect()
|
| 60 |
+
} else {
|
| 61 |
+
(0..full_text.len()).collect()
|
| 62 |
+
};
|
| 63 |
+
|
| 64 |
+
let total_glyphs = visible_indices.len();
|
| 65 |
+
println!("Wrapped lines: {} lines, total glyphs (counted): {}", wrapped_lines.len(), total_glyphs);
|
| 66 |
+
if total_glyphs == 0 {
|
| 67 |
+
println!("No text to animate.");
|
| 68 |
+
return Ok(None);
|
| 69 |
}
|
| 70 |
|
| 71 |
+
// Pre-calc line heights and y_positions
|
| 72 |
+
let mut line_heights: Vec<i32> = Vec::new();
|
| 73 |
+
let mut y_positions: Vec<i32> = Vec::new();
|
| 74 |
+
let mut y = margin_y;
|
| 75 |
+
for (i, line) in wrapped_lines.iter().enumerate() {
|
| 76 |
+
let is_header = line_styles[i];
|
| 77 |
+
let font_scale = if is_header { header_font_scale } else { default_font_scale };
|
| 78 |
+
let thickness = if is_header { header_thickness } else { default_thickness };
|
| 79 |
+
let line_for_size = if line.is_empty() { "Ay".to_string() } else { line.clone() };
|
| 80 |
+
let mut base_line = 0;
|
| 81 |
+
let size = get_text_size(&line_for_size, font, font_scale, thickness, &mut base_line).unwrap();
|
| 82 |
+
let h = size.height;
|
| 83 |
+
let lh = h + base_line + line_spacing;
|
| 84 |
+
line_heights.push(lh);
|
| 85 |
+
y_positions.push(y);
|
| 86 |
+
y += lh;
|
| 87 |
}
|
| 88 |
|
| 89 |
+
// FFmpeg for silent video
|
| 90 |
+
let mut child = Command::new("ffmpeg")
|
| 91 |
+
.arg("-y")
|
| 92 |
+
.arg("-f").arg("rawvideo")
|
| 93 |
+
.arg("-pix_fmt").arg("bgr24")
|
| 94 |
+
.arg("-s").arg(format!("{}x{}", width, height))
|
| 95 |
+
.arg("-r").arg(fps.to_string())
|
| 96 |
+
.arg("-i").arg("-")
|
| 97 |
+
.arg("-an")
|
| 98 |
+
.arg("-c:v").arg("libx264")
|
| 99 |
+
.arg("-preset").arg(ffmpeg_preset)
|
| 100 |
+
.arg("-crf").arg(crf)
|
| 101 |
+
.arg("-pix_fmt").arg("yuv420p")
|
| 102 |
+
.arg(&silent_video_path)
|
| 103 |
+
.stdin(Stdio::piped())
|
| 104 |
+
.spawn()
|
| 105 |
+
.map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("Failed to spawn FFmpeg: {}", e)))?;
|
| 106 |
+
|
| 107 |
+
let mut stdin = child.stdin.take().unwrap();
|
| 108 |
+
|
| 109 |
+
let t0 = Instant::now();
|
| 110 |
+
let mut frames_sent: usize = 0;
|
| 111 |
+
let mut prev_visible_sub = String::new();
|
| 112 |
+
let mut last_pen_x: i32 = 0;
|
| 113 |
+
let mut last_pen_y: i32 = 0;
|
| 114 |
+
|
| 115 |
+
for &idx_in_full in visible_indices.iter() {
|
| 116 |
+
let visible_sub = &full_text[0..=idx_in_full];
|
| 117 |
+
if visible_sub != prev_visible_sub {
|
| 118 |
+
let lines: Vec<&str> = visible_sub.split('\n').collect();
|
| 119 |
+
let last_line = lines.last().unwrap();
|
| 120 |
+
let line_idx = lines.len() - 1;
|
| 121 |
+
let is_header = line_styles[line_idx];
|
| 122 |
+
let font_scale = if is_header { header_font_scale } else { default_font_scale };
|
| 123 |
+
let thickness = if is_header { header_thickness } else { default_thickness };
|
| 124 |
+
let mut base_line = 0;
|
| 125 |
+
let size = get_text_size(last_line, font, font_scale, thickness, &mut base_line).unwrap();
|
| 126 |
+
let w = size.width;
|
| 127 |
+
let h = size.height;
|
| 128 |
+
let pen_x = margin_x + w + 5;
|
| 129 |
+
let pen_y = y_positions[line_idx] + h / 2;
|
| 130 |
+
last_pen_x = pen_x;
|
| 131 |
+
last_pen_y = pen_y;
|
| 132 |
+
|
| 133 |
+
for anim_step in 0..animation_frames_per_char {
|
| 134 |
+
let anim_offset = (anim_step as f64) / (animation_frames_per_char as f64);
|
| 135 |
+
let frame_img = render_frame(
|
| 136 |
+
visible_sub,
|
| 137 |
+
pen_x,
|
| 138 |
+
pen_y,
|
| 139 |
+
anim_offset,
|
| 140 |
+
width as i32,
|
| 141 |
+
height as i32,
|
| 142 |
+
&wrapped_lines,
|
| 143 |
+
&line_styles,
|
| 144 |
+
&y_positions,
|
| 145 |
+
margin_x,
|
| 146 |
+
font,
|
| 147 |
+
default_font_scale,
|
| 148 |
+
header_font_scale,
|
| 149 |
+
default_thickness,
|
| 150 |
+
header_thickness,
|
| 151 |
+
default_text_color,
|
| 152 |
+
header_text_color,
|
| 153 |
+
bg_color,
|
| 154 |
+
pen_color,
|
| 155 |
+
pen_tip_radius,
|
| 156 |
+
pen_length,
|
| 157 |
+
pen_thickness,
|
| 158 |
+
pen_base_angle,
|
| 159 |
+
pen_movement_amplitude,
|
| 160 |
+
)?;
|
| 161 |
+
stdin.write_all(frame_img.data_bytes().unwrap())
|
| 162 |
+
.map_err(|e| pyo3::exceptions::PyIOError::new_err(format!("Failed to write frame: {}", e)))?;
|
| 163 |
+
frames_sent += 1;
|
| 164 |
+
}
|
| 165 |
+
prev_visible_sub = visible_sub.to_string();
|
| 166 |
+
}
|
| 167 |
+
}
|
| 168 |
+
|
| 169 |
+
drop(stdin);
|
| 170 |
+
child.wait().map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("FFmpeg failed: {}", e)))?;
|
| 171 |
+
|
| 172 |
+
let elapsed = t0.elapsed().as_secs_f64();
|
| 173 |
+
println!("Frames sent: {}, elapsed time: {:.3} seconds", frames_sent, elapsed);
|
| 174 |
+
|
| 175 |
+
if !Path::new(&silent_video_path).exists() {
|
| 176 |
+
println!("Silent video generation failed.");
|
| 177 |
+
return Ok(None);
|
| 178 |
}
|
| 179 |
+
|
| 180 |
+
let rendered_duration = frames_sent as f64 / fps;
|
| 181 |
+
println!("Rendered video duration: {:.3}s, Audio duration: {:.3}s", rendered_duration, duration);
|
| 182 |
+
|
| 183 |
+
let multiplier = if rendered_duration > 0.0 && duration > 0.0 {
|
| 184 |
+
duration / rendered_duration
|
| 185 |
+
} else {
|
| 186 |
+
1.0
|
| 187 |
+
};
|
| 188 |
+
|
| 189 |
+
// Combine with audio
|
| 190 |
+
let mut combine_child = Command::new("ffmpeg")
|
| 191 |
+
.arg("-y")
|
| 192 |
+
.arg("-i").arg(&silent_video_path)
|
| 193 |
+
.arg("-i").arg(&audio_path)
|
| 194 |
+
.arg("-filter:v").arg(format!("setpts=PTS*{}", multiplier))
|
| 195 |
+
.arg("-c:v").arg("libx264")
|
| 196 |
+
.arg("-preset").arg("ultrafast")
|
| 197 |
+
.arg("-crf").arg("28")
|
| 198 |
+
.arg("-pix_fmt").arg("yuv420p")
|
| 199 |
+
.arg("-c:a").arg("aac")
|
| 200 |
+
.arg("-shortest")
|
| 201 |
+
.arg("-map").arg("0:v:0")
|
| 202 |
+
.arg("-map").arg("1:a:0")
|
| 203 |
+
.arg(&final_video_path)
|
| 204 |
+
.spawn()
|
| 205 |
+
.map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("Failed to spawn FFmpeg for combine: {}", e)))?;
|
| 206 |
+
|
| 207 |
+
combine_child.wait().map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("FFmpeg combine failed: {}", e)))?;
|
| 208 |
+
|
| 209 |
+
// Clean up
|
| 210 |
+
std::fs::remove_file(&silent_video_path)
|
| 211 |
+
.map_err(|e| pyo3::exceptions::PyIOError::new_err(format!("Failed to remove silent video: {}", e)))?;
|
| 212 |
+
|
| 213 |
+
Ok(Some(final_video_path))
|
| 214 |
+
}
|
| 215 |
+
|
| 216 |
+
fn wrap_text_cv(text: &str, font: i32, default_font_scale: f64, default_thickness: i32, max_width: i32) -> (Vec<String>, Vec<bool>) {
|
| 217 |
+
let mut wrapped_lines: Vec<String> = Vec::new();
|
| 218 |
+
let mut styles: Vec<bool> = Vec::new();
|
| 219 |
+
for para in text.lines() {
|
| 220 |
+
let trimmed = para.trim_start();
|
| 221 |
+
let mut is_header = trimmed.starts_with("###");
|
| 222 |
+
let mut para_str = para.to_string();
|
| 223 |
+
if is_header {
|
| 224 |
+
para_str = trimmed[3..].trim().to_string();
|
| 225 |
+
}
|
| 226 |
+
let font_scale = if is_header { header_font_scale } else { default_font_scale }; // Use globals or pass
|
| 227 |
+
let thickness = if is_header { header_thickness } else { default_thickness };
|
| 228 |
+
if para_str.is_empty() {
|
| 229 |
+
wrapped_lines.push("".to_string());
|
| 230 |
+
styles.push(false);
|
| 231 |
+
continue;
|
| 232 |
+
}
|
| 233 |
+
let words: Vec<&str> = para_str.split_whitespace().collect();
|
| 234 |
+
let mut cur = String::new();
|
| 235 |
+
for &w in &words {
|
| 236 |
+
let candidate = if cur.is_empty() { w.to_string() } else { format!("{} {}", cur, w) };
|
| 237 |
+
let mut base_line = 0;
|
| 238 |
+
let size = get_text_size(&candidate, font, font_scale, thickness, &mut base_line).unwrap();
|
| 239 |
+
if size.width <= max_width {
|
| 240 |
+
cur = candidate;
|
| 241 |
+
} else {
|
| 242 |
+
if !cur.is_empty() {
|
| 243 |
+
wrapped_lines.push(cur);
|
| 244 |
+
styles.push(is_header);
|
| 245 |
+
cur = String::new();
|
| 246 |
+
}
|
| 247 |
+
let mut base_line_single = 0;
|
| 248 |
+
let size_single = get_text_size(w, font, font_scale, thickness, &mut base_line_single).unwrap();
|
| 249 |
+
if size_single.width > max_width {
|
| 250 |
+
let mut chunk = String::new();
|
| 251 |
+
for ch in w.chars() {
|
| 252 |
+
let cand2 = format!("{}{}", chunk, ch);
|
| 253 |
+
let mut base_line_ch = 0;
|
| 254 |
+
let size_ch = get_text_size(&cand2, font, font_scale, thickness, &mut base_line_ch).unwrap();
|
| 255 |
+
if size_ch.width <= max_width {
|
| 256 |
+
chunk = cand2;
|
| 257 |
+
} else {
|
| 258 |
+
wrapped_lines.push(chunk);
|
| 259 |
+
styles.push(is_header);
|
| 260 |
+
chunk = ch.to_string();
|
| 261 |
+
}
|
| 262 |
+
}
|
| 263 |
+
if !chunk.is_empty() {
|
| 264 |
+
cur = chunk;
|
| 265 |
+
}
|
| 266 |
+
} else {
|
| 267 |
+
cur = w.to_string();
|
| 268 |
+
}
|
| 269 |
+
}
|
| 270 |
+
}
|
| 271 |
+
if !cur.is_empty() {
|
| 272 |
+
wrapped_lines.push(cur);
|
| 273 |
+
styles.push(is_header);
|
| 274 |
+
}
|
| 275 |
+
}
|
| 276 |
+
(wrapped_lines, styles)
|
| 277 |
+
}
|
| 278 |
+
|
| 279 |
+
fn render_frame(
|
| 280 |
+
visible_text: &str,
|
| 281 |
+
pen_x: i32,
|
| 282 |
+
pen_y: i32,
|
| 283 |
+
anim_offset: f64,
|
| 284 |
+
width: i32,
|
| 285 |
+
height: i32,
|
| 286 |
+
line_styles: &Vec<bool>,
|
| 287 |
+
y_positions: &Vec<i32>,
|
| 288 |
+
margin_x: i32,
|
| 289 |
+
font: i32,
|
| 290 |
+
default_font_scale: f64,
|
| 291 |
+
header_font_scale: f64,
|
| 292 |
+
default_thickness: i32,
|
| 293 |
+
header_thickness: i32,
|
| 294 |
+
default_text_color: Scalar,
|
| 295 |
+
header_text_color: Scalar,
|
| 296 |
+
bg_color: Scalar,
|
| 297 |
+
pen_color: Scalar,
|
| 298 |
+
pen_tip_radius: i32,
|
| 299 |
+
pen_length: i32,
|
| 300 |
+
pen_thickness: i32,
|
| 301 |
+
pen_base_angle: i32,
|
| 302 |
+
pen_movement_amplitude: i32,
|
| 303 |
+
) -> PyResult<Mat> {
|
| 304 |
+
let mut img = Mat::new_rows_cols_with_default(height, width, CV_8UC3, bg_color)
|
| 305 |
+
.map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("Failed to create Mat: {}", e)))?;
|
| 306 |
+
|
| 307 |
+
let lines: Vec<&str> = visible_text.split('\n').collect();
|
| 308 |
+
for (idx, &line) in lines.iter().enumerate() {
|
| 309 |
+
let is_header = line_styles[idx];
|
| 310 |
+
let font_scale = if is_header { header_font_scale } else { default_font_scale };
|
| 311 |
+
let thickness = if is_header { header_thickness } else { default_thickness };
|
| 312 |
+
let color = if is_header { header_text_color } else { default_text_color };
|
| 313 |
+
let x = margin_x;
|
| 314 |
+
let y = y_positions[idx];
|
| 315 |
+
let mut base_line = 0;
|
| 316 |
+
let size = get_text_size(line, font, font_scale, thickness, &mut base_line).unwrap();
|
| 317 |
+
let h = size.height;
|
| 318 |
+
let y_draw = y + h;
|
| 319 |
+
if !line.is_empty() {
|
| 320 |
+
put_text(&mut img, line, Point::new(x, y_draw), font, font_scale, color, thickness, LineTypes::LINE_AA as i32, false)
|
| 321 |
+
.map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("Failed to put text: {}", e)))?;
|
| 322 |
+
}
|
| 323 |
+
}
|
| 324 |
+
|
| 325 |
+
if pen_x > 0 {
|
| 326 |
+
let offset_y = (pen_movement_amplitude as f64 * (anim_offset * PI).sin()) as i32;
|
| 327 |
+
let pen_tip_y = pen_y + offset_y;
|
| 328 |
+
let angle_rad = (pen_base_angle as f64).to_radians();
|
| 329 |
+
let pen_end_x = pen_x + (pen_length as f64 * angle_rad.cos()) as i32;
|
| 330 |
+
let pen_end_y = pen_tip_y - (pen_length as f64 * angle_rad.sin()) as i32;
|
| 331 |
+
line(&mut img, Point::new(pen_x, pen_tip_y), Point::new(pen_end_x, pen_end_y), pen_color, pen_thickness, LineTypes::LINE_8 as i32, 0)
|
| 332 |
+
.map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("Failed to draw line: {}", e)))?;
|
| 333 |
+
circle(&mut img, Point::new(pen_x, pen_tip_y), pen_tip_radius, pen_color, -1, LineTypes::LINE_8 as i32, 0)
|
| 334 |
+
.map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("Failed to draw circle: {}", e)))?;
|
| 335 |
+
}
|
| 336 |
+
|
| 337 |
+
Ok(img)
|
| 338 |
}
|
| 339 |
|
| 340 |
#[pymodule]
|
| 341 |
fn rust_highlight(_py: Python, m: &PyModule) -> PyResult<()> {
|
| 342 |
+
m.add_function(wrap_pyfunction!(generate_video_clip, m)?)?;
|
| 343 |
Ok(())
|
| 344 |
}
|