Skip to main content

pdfsink_rs/
display.rs

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}