1use crate::types::{BBox, Bounded, Char, Curve, Edge, Line, Page, Point, Word};
2use crate::table::{Table, TableFinder, TableSettings};
3use crate::Result;
4use font8x8::UnicodeFonts;
5use image::{ImageBuffer, ImageFormat, Rgba, RgbaImage};
6use imageproc::drawing::{
7 draw_filled_circle_mut, draw_filled_rect_mut, draw_hollow_circle_mut, draw_hollow_rect_mut,
8 draw_line_segment_mut,
9};
10use imageproc::rect::Rect as ImageRect;
11use serde::{Deserialize, Serialize};
12use std::path::{Path, PathBuf};
13use std::process::Command;
14use std::time::{SystemTime, UNIX_EPOCH};
15
16const DEFAULT_RESOLUTION: f64 = 72.0;
17
18#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
19pub struct RgbaColor {
20 pub r: u8,
21 pub g: u8,
22 pub b: u8,
23 pub a: u8,
24}
25
26impl RgbaColor {
27 pub const fn new(r: u8, g: u8, b: u8, a: u8) -> Self {
28 Self { r, g, b, a }
29 }
30
31 pub fn to_rgba(self) -> Rgba<u8> {
32 Rgba([self.r, self.g, self.b, self.a])
33 }
34}
35
36impl Default for RgbaColor {
37 fn default() -> Self {
38 Self::new(0, 0, 0, 255)
39 }
40}
41
42#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
43pub struct RenderOptions {
44 pub resolution: Option<f64>,
45 pub width: Option<f64>,
46 pub height: Option<f64>,
47 pub antialias: bool,
48 pub force_mediabox: bool,
49}
50
51impl Default for RenderOptions {
52 fn default() -> Self {
53 Self {
54 resolution: Some(DEFAULT_RESOLUTION),
55 width: None,
56 height: None,
57 antialias: false,
58 force_mediabox: false,
59 }
60 }
61}
62
63pub trait HasBBox {
64 fn bbox(&self) -> BBox;
65}
66
67impl<T> HasBBox for T
68where
69 T: Bounded,
70{
71 fn bbox(&self) -> BBox {
72 Bounded::bbox(self)
73 }
74}
75
76impl HasBBox for BBox {
77 fn bbox(&self) -> BBox {
78 *self
79 }
80}
81
82impl HasBBox for (f64, f64, f64, f64) {
83 fn bbox(&self) -> BBox {
84 BBox::new(self.0, self.1, self.2, self.3)
85 }
86}
87
88impl HasBBox for Table {
89 fn bbox(&self) -> BBox {
90 self.bbox
91 }
92}
93
94pub trait HasCenter {
95 fn center(&self) -> Point;
96}
97
98impl<T> HasCenter for T
99where
100 T: HasBBox,
101{
102 fn center(&self) -> Point {
103 self.bbox().center()
104 }
105}
106
107pub trait HasLineSegments {
108 fn line_segments(&self) -> Vec<(Point, Point)>;
109}
110
111impl HasLineSegments for Line {
112 fn line_segments(&self) -> Vec<(Point, Point)> {
113 self.pts
114 .windows(2)
115 .map(|pair| (pair[0], pair[1]))
116 .collect::<Vec<_>>()
117 }
118}
119
120impl HasLineSegments for Edge {
121 fn line_segments(&self) -> Vec<(Point, Point)> {
122 vec![(Point::new(self.x0, self.top), Point::new(self.x1, self.bottom))]
123 }
124}
125
126impl HasLineSegments for Curve {
127 fn line_segments(&self) -> Vec<(Point, Point)> {
128 self.pts
129 .windows(2)
130 .map(|pair| (pair[0], pair[1]))
131 .collect::<Vec<_>>()
132 }
133}
134
135impl HasLineSegments for (Point, Point) {
136 fn line_segments(&self) -> Vec<(Point, Point)> {
137 vec![*self]
138 }
139}
140
141impl HasLineSegments for ((f64, f64), (f64, f64)) {
142 fn line_segments(&self) -> Vec<(Point, Point)> {
143 vec![(Point::new((self.0).0, (self.0).1), Point::new((self.1).0, (self.1).1))]
144 }
145}
146
147#[derive(Debug, Clone)]
148pub struct PageImage {
149 pub page: Page,
150 pub resolution: f64,
151 pub antialias: bool,
152 pub force_mediabox: bool,
153 pub bbox: BBox,
154 pub original: RgbaImage,
155 pub annotated: RgbaImage,
156}
157
158impl PageImage {
159 pub fn new(page: &Page, options: RenderOptions) -> Result<Self> {
160 let set_count = [options.resolution.is_some(), options.width.is_some(), options.height.is_some()]
161 .into_iter()
162 .filter(|item| *item)
163 .count();
164 if set_count > 1 {
165 return Err(crate::Error::Message(
166 "pass at most one of resolution, width, or height".to_string(),
167 ));
168 }
169
170 let bbox = if page.bbox != page.mediabox {
171 page.bbox
172 } else if options.force_mediabox {
173 page.mediabox
174 } else {
175 page.cropbox
176 };
177
178 let resolution = if let Some(resolution) = options.resolution {
179 resolution
180 } else if let Some(width) = options.width {
181 DEFAULT_RESOLUTION * (width / bbox.width())
182 } else if let Some(height) = options.height {
183 DEFAULT_RESOLUTION * (height / bbox.height())
184 } else {
185 DEFAULT_RESOLUTION
186 };
187
188 let scale = resolution / DEFAULT_RESOLUTION;
189 let width_px = ((bbox.width() * scale).round() as i64).max(1) as u32;
190 let height_px = ((bbox.height() * scale).round() as i64).max(1) as u32;
191 let mut image = ImageBuffer::from_pixel(width_px, height_px, Rgba([255, 255, 255, 255]));
192
193 let mut page_image = Self {
194 page: page.clone(),
195 resolution,
196 antialias: options.antialias,
197 force_mediabox: options.force_mediabox,
198 bbox,
199 original: image.clone(),
200 annotated: image.clone(),
201 };
202 page_image.render_page_content(&mut image);
203 page_image.original = image.clone();
204 page_image.annotated = image;
205 Ok(page_image)
206 }
207
208 pub fn width(&self) -> u32 {
209 self.annotated.width()
210 }
211
212 pub fn height(&self) -> u32 {
213 self.annotated.height()
214 }
215
216 pub fn reset(&mut self) -> &mut Self {
217 self.annotated = self.original.clone();
218 self
219 }
220
221 pub fn copy(&self) -> Self {
222 self.clone()
223 }
224
225 pub fn save<P: AsRef<Path>>(
226 &self,
227 dest: P,
228 format: Option<ImageFormat>,
229 _quantize: bool,
230 _colors: u16,
231 _bits: u8,
232 ) -> Result<()> {
233 if let Some(format) = format {
234 self.annotated.save_with_format(dest, format)?;
235 } else {
236 self.annotated.save(dest)?;
237 }
238 Ok(())
239 }
240
241 pub fn show(&self) -> Result<PathBuf> {
242 let millis = SystemTime::now()
243 .duration_since(UNIX_EPOCH)
244 .map_err(|err| crate::Error::Message(err.to_string()))?
245 .as_millis();
246 let path = std::env::temp_dir().join(format!("pdfsink-rs-page-{millis}.png"));
247 self.save(&path, Some(ImageFormat::Png), false, 256, 8)?;
248
249 #[cfg(target_os = "macos")]
250 let _ = Command::new("open").arg(&path).spawn();
251 #[cfg(target_os = "linux")]
252 let _ = Command::new("xdg-open").arg(&path).spawn();
253 #[cfg(target_os = "windows")]
254 let _ = Command::new("cmd").args(["/C", "start", path.to_string_lossy().as_ref()]).spawn();
255
256 Ok(path)
257 }
258
259 pub fn draw_line<T: HasLineSegments>(&mut self, item: &T, stroke: RgbaColor, stroke_width: u32) -> &mut Self {
260 let color = stroke.to_rgba();
261 for (start, end) in item.line_segments() {
262 let (x0, y0) = self.project_point(start);
263 let (x1, y1) = self.project_point(end);
264 let offset = (stroke_width.max(1) as i32 - 1) / 2;
265 for dx in -offset..=offset {
266 for dy in -offset..=offset {
267 draw_line_segment_mut(
268 &mut self.annotated,
269 (x0 + dx as f32, y0 + dy as f32),
270 (x1 + dx as f32, y1 + dy as f32),
271 color,
272 );
273 }
274 }
275 }
276 self
277 }
278
279 pub fn draw_lines<T: HasLineSegments>(&mut self, items: &[T], stroke: RgbaColor, stroke_width: u32) -> &mut Self {
280 for item in items {
281 self.draw_line(item, stroke, stroke_width);
282 }
283 self
284 }
285
286 pub fn draw_vline(&mut self, location: f64, stroke: RgbaColor, stroke_width: u32) -> &mut Self {
287 self.draw_line(
288 &(
289 Point::new(location, self.bbox.top),
290 Point::new(location, self.bbox.bottom),
291 ),
292 stroke,
293 stroke_width,
294 )
295 }
296
297 pub fn draw_vlines(&mut self, locations: &[f64], stroke: RgbaColor, stroke_width: u32) -> &mut Self {
298 for location in locations {
299 self.draw_vline(*location, stroke, stroke_width);
300 }
301 self
302 }
303
304 pub fn draw_hline(&mut self, location: f64, stroke: RgbaColor, stroke_width: u32) -> &mut Self {
305 self.draw_line(
306 &(
307 Point::new(self.bbox.x0, location),
308 Point::new(self.bbox.x1, location),
309 ),
310 stroke,
311 stroke_width,
312 )
313 }
314
315 pub fn draw_hlines(&mut self, locations: &[f64], stroke: RgbaColor, stroke_width: u32) -> &mut Self {
316 for location in locations {
317 self.draw_hline(*location, stroke, stroke_width);
318 }
319 self
320 }
321
322 pub fn draw_rect<T: HasBBox>(
323 &mut self,
324 item: &T,
325 fill: Option<RgbaColor>,
326 stroke: Option<RgbaColor>,
327 stroke_width: u32,
328 ) -> &mut Self {
329 let bbox = item.bbox();
330 let rect = self.project_rect(bbox);
331 if rect.width() == 0 || rect.height() == 0 {
332 return self;
333 }
334 if let Some(fill) = fill {
335 draw_filled_rect_mut(&mut self.annotated, rect, fill.to_rgba());
336 }
337 if let Some(stroke) = stroke {
338 for inset in 0..stroke_width.max(1) {
339 let x = rect.left() + inset as i32;
340 let y = rect.top() + inset as i32;
341 let w = rect.width().saturating_sub(inset.saturating_mul(2));
342 let h = rect.height().saturating_sub(inset.saturating_mul(2));
343 if w == 0 || h == 0 {
344 continue;
345 }
346 let inset_rect = ImageRect::at(x, y).of_size(w, h);
347 draw_hollow_rect_mut(&mut self.annotated, inset_rect, stroke.to_rgba());
348 }
349 }
350 self
351 }
352
353 pub fn draw_rects<T: HasBBox>(
354 &mut self,
355 items: &[T],
356 fill: Option<RgbaColor>,
357 stroke: Option<RgbaColor>,
358 stroke_width: u32,
359 ) -> &mut Self {
360 for item in items {
361 self.draw_rect(item, fill, stroke, stroke_width);
362 }
363 self
364 }
365
366 pub fn draw_circle<T: HasCenter>(
367 &mut self,
368 item: &T,
369 radius: i32,
370 fill: Option<RgbaColor>,
371 stroke: Option<RgbaColor>,
372 ) -> &mut Self {
373 let center = item.center();
374 let (x, y) = self.project_point(center);
375 let center = (x.round() as i32, y.round() as i32);
376 if let Some(fill) = fill {
377 draw_filled_circle_mut(&mut self.annotated, center, radius.max(1), fill.to_rgba());
378 }
379 if let Some(stroke) = stroke {
380 draw_hollow_circle_mut(&mut self.annotated, center, radius.max(1), stroke.to_rgba());
381 }
382 self
383 }
384
385 pub fn draw_circles<T: HasCenter>(
386 &mut self,
387 items: &[T],
388 radius: i32,
389 fill: Option<RgbaColor>,
390 stroke: Option<RgbaColor>,
391 ) -> &mut Self {
392 for item in items {
393 self.draw_circle(item, radius, fill, stroke);
394 }
395 self
396 }
397
398 pub fn outline_words(&mut self, words: &[Word], stroke: RgbaColor, stroke_width: u32) -> &mut Self {
399 self.draw_rects(words, None, Some(stroke), stroke_width)
400 }
401
402 pub fn outline_chars(&mut self, chars: &[Char], stroke: RgbaColor, stroke_width: u32) -> &mut Self {
403 self.draw_rects(chars, None, Some(stroke), stroke_width)
404 }
405
406 pub fn outline_edges(&mut self, edges: &[Edge], stroke: RgbaColor, stroke_width: u32) -> &mut Self {
407 self.draw_lines(edges, stroke, stroke_width)
408 }
409
410 pub fn outline_tables(&mut self, tables: &[Table], fill: Option<RgbaColor>, stroke: Option<RgbaColor>, stroke_width: u32) -> &mut Self {
411 for table in tables {
412 for cell in &table.cells {
413 self.draw_rect(cell, fill, stroke, stroke_width);
414 }
415 }
416 self
417 }
418
419 pub fn debug_tablefinder(&mut self, table_settings: Option<TableSettings>) -> Result<&mut Self> {
420 let finder = if let Some(settings) = table_settings {
421 self.page.debug_tablefinder(settings)?
422 } else {
423 self.page.debug_tablefinder(TableSettings::default())?
424 };
425 self.overlay_tablefinder(&finder);
426 Ok(self)
427 }
428
429 pub fn overlay_tablefinder(&mut self, finder: &TableFinder) -> &mut Self {
430 let red = RgbaColor::new(255, 0, 0, 255);
431 let light_blue = RgbaColor::new(173, 216, 230, 96);
432 for edge in &finder.edges {
433 self.draw_line(edge, red, 1);
434 }
435 for intersection in finder.intersections.keys() {
436 let point = Point::new(intersection.0.into_inner(), intersection.1.into_inner());
437 self.draw_circle(&point, 4, Some(red), Some(red));
438 }
439 for cell in &finder.cells {
440 self.draw_rect(cell, Some(light_blue), Some(red), 1);
441 }
442 self
443 }
444
445 fn render_page_content(&mut self, image: &mut RgbaImage) {
446 for rect in &self.page.rects {
447 let fill = if rect.fill {
448 Some(RgbaColor::new(235, 235, 235, 255).to_rgba())
449 } else {
450 None
451 };
452 let stroke = if rect.stroke {
453 Some(RgbaColor::default().to_rgba())
454 } else {
455 None
456 };
457 let projected = self.project_rect(Bounded::bbox(rect));
458 if let Some(fill) = fill {
459 draw_filled_rect_mut(image, projected, fill);
460 }
461 if let Some(stroke) = stroke {
462 draw_hollow_rect_mut(image, projected, stroke);
463 }
464 }
465
466 for line in &self.page.lines {
467 self.render_segment(image, line);
468 }
469 for curve in &self.page.curves {
470 self.render_segment(image, curve);
471 }
472 for image_obj in &self.page.images {
473 let rect = self.project_rect(Bounded::bbox(image_obj));
474 draw_hollow_rect_mut(image, rect, RgbaColor::new(120, 120, 120, 255).to_rgba());
475 }
476 for ch in &self.page.chars {
477 self.render_char(image, ch);
478 }
479 }
480
481 fn render_segment<T: HasLineSegments>(&self, image: &mut RgbaImage, item: &T) {
482 for (start, end) in item.line_segments() {
483 let (x0, y0) = self.project_point(start);
484 let (x1, y1) = self.project_point(end);
485 draw_line_segment_mut(image, (x0, y0), (x1, y1), RgbaColor::default().to_rgba());
486 }
487 }
488
489 fn render_char(&self, image: &mut RgbaImage, ch: &Char) {
490 let Some(letter) = ch.text.chars().next() else {
491 return;
492 };
493 if letter.is_whitespace() {
494 return;
495 }
496 let bbox = Bounded::bbox(ch);
497 let left = ((bbox.x0 - self.bbox.x0) * self.scale()).floor().max(0.0) as u32;
498 let top = ((bbox.top - self.bbox.top) * self.scale()).floor().max(0.0) as u32;
499 let width = ((bbox.width() * self.scale()).ceil().max(1.0)) as u32;
500 let height = ((bbox.height() * self.scale()).ceil().max(1.0)) as u32;
501
502 if let Some(glyph) = font8x8::BASIC_FONTS.get(letter) {
503 for (row_idx, row) in glyph.iter().enumerate() {
504 for col_idx in 0..8u32 {
505 if ((*row >> col_idx) & 1) == 1 {
506 let x_start = left + (col_idx * width / 8);
507 let x_end = left + (((col_idx + 1) * width + 7) / 8).max(1);
508 let y_start = top + (row_idx as u32 * height / 8);
509 let y_end = top + ((((row_idx as u32) + 1) * height + 7) / 8).max(1);
510 for y in y_start..y_end.min(image.height()) {
511 for x in x_start..x_end.min(image.width()) {
512 image.put_pixel(x, y, RgbaColor::default().to_rgba());
513 }
514 }
515 }
516 }
517 }
518 } else {
519 let rect = self.project_rect(bbox);
520 draw_hollow_rect_mut(image, rect, RgbaColor::default().to_rgba());
521 }
522 }
523
524 fn scale(&self) -> f64 {
525 self.resolution / DEFAULT_RESOLUTION
526 }
527
528 fn project_point(&self, point: Point) -> (f32, f32) {
529 (
530 ((point.x - self.bbox.x0) * self.scale()) as f32,
531 ((point.y - self.bbox.top) * self.scale()) as f32,
532 )
533 }
534
535 fn project_rect(&self, bbox: BBox) -> ImageRect {
536 let x = ((bbox.x0 - self.bbox.x0) * self.scale()).floor() as i32;
537 let y = ((bbox.top - self.bbox.top) * self.scale()).floor() as i32;
538 let width = ((bbox.width() * self.scale()).ceil() as i64).max(1) as u32;
539 let height = ((bbox.height() * self.scale()).ceil() as i64).max(1) as u32;
540 ImageRect::at(x, y).of_size(width, height)
541 }
542}
543
544impl HasCenter for Point {
545 fn center(&self) -> Point {
546 *self
547 }
548}
549
550impl Page {
551 pub fn to_image(
552 &self,
553 resolution: Option<f64>,
554 width: Option<f64>,
555 height: Option<f64>,
556 antialias: bool,
557 force_mediabox: bool,
558 ) -> Result<PageImage> {
559 PageImage::new(
560 self,
561 RenderOptions {
562 resolution,
563 width,
564 height,
565 antialias,
566 force_mediabox,
567 },
568 )
569 }
570}