1use educe::Educe;
2use either::Either;
3use euclid::Angle;
4use image::RgbaImage;
5use log::warn;
6use nipdf::{
7 file::{Page, Rectangle},
8 graphics::trans::{LogicDeviceToDeviceSpace, UserToUserSpace, logic_device_to_device},
9};
10use prescript::Result;
11use snafu::{OptionExt, ResultExt, whatever};
12use tiny_skia::{Color, Pixmap};
13
14mod render;
15mod shading;
16use render::{Render, State};
17mod into_skia;
18pub(crate) use into_skia::*;
19use num_traits::ToPrimitive;
20
21#[derive(Debug, Educe, Clone, Copy)]
22#[educe(Default)]
23pub struct PageDimension {
24 #[educe(Default = 1.0)]
25 zoom: f32,
26 width: u32,
27 height: u32,
28 transform: UserToUserSpace,
30 rotate: i32,
31}
32
33impl PageDimension {
34 pub fn update(&mut self, dimension: &Rectangle, rotate: i32) {
35 self.rotate = rotate % 360;
36
37 let mut transform = UserToUserSpace::identity();
38 if dimension.left_x != 0.0 || dimension.lower_y != 0.0 {
39 transform = transform.then_translate((-dimension.left_x, -dimension.lower_y).into());
40 }
41 self.transform = transform;
42
43 #[allow(clippy::unwrap_used)]
46 {
47 self.width = dimension.width().to_u32().unwrap();
48 self.height = dimension.height().to_u32().unwrap();
49 }
50 if self.swap_wh() {
51 std::mem::swap(&mut self.width, &mut self.height);
52 }
53 }
54
55 pub fn canvas_width(&self) -> u32 {
56 #[allow(clippy::unwrap_used)]
59 (self.width as f32 * self.zoom).to_u32().unwrap()
60 }
61
62 pub fn canvas_height(&self) -> u32 {
63 #[allow(clippy::unwrap_used)]
66 (self.height as f32 * self.zoom).to_u32().unwrap()
67 }
68
69 fn swap_wh(&self) -> bool {
70 self.rotate.abs() == 90 || self.rotate.abs() == 270
71 }
72
73 pub fn logic_device_to_device(&self) -> LogicDeviceToDeviceSpace {
74 if self.rotate != 0 {
75 let (w, h) = if self.swap_wh() {
76 (self.height, self.width)
77 } else {
78 (self.width, self.height)
79 };
80
81 let r = logic_device_to_device(h, self.zoom);
82 r.then_translate((w as f32 * self.zoom * -0.5, h as f32 * self.zoom * -0.5).into())
83 .then_rotate(Angle::degrees(self.rotate as f32))
84 .then_translate((h as f32 * self.zoom * 0.5, w as f32 * self.zoom * 0.5).into())
85 } else {
86 logic_device_to_device(self.height, self.zoom)
87 }
88 }
89}
90#[derive(Debug, Educe, Clone)]
92#[educe(Default)]
93pub struct RenderOption {
94 crop: Option<Rectangle>,
96 #[educe(Default(expression = Color::WHITE))]
97 background_color: Color,
98 state: Option<State>,
100 rotate: i32,
101 dimension: PageDimension,
102 #[educe(Default = false)]
105 fail_fast: bool,
106}
107
108impl RenderOption {
109 pub fn create_canvas(&self) -> Result<Pixmap> {
110 let (w, h) = (
111 self.dimension.canvas_width(),
112 self.dimension.canvas_height(),
113 );
114 if w * h > 1024 * 1024 * 100 {
115 whatever!("page size too large: {}x{}", w, h);
116 }
117
118 let mut r = Pixmap::new(w, h).whatever_context("Failed create canvas")?;
119 if self.background_color.is_opaque() {
120 r.fill(self.background_color);
121 }
122 Ok(r)
123 }
124
125 pub fn to_image(&self, canvas: Pixmap) -> Result<RgbaImage> {
127 RgbaImage::from_raw(canvas.width(), canvas.height(), canvas.take())
128 .whatever_context("Failed create canvas")
129 }
130}
131#[derive(Educe)]
132#[educe(Default(new))]
133pub struct RenderOptionBuilder(RenderOption);
134
135impl RenderOptionBuilder {
136 pub fn zoom(mut self, zoom: f32) -> Self {
138 if zoom <= 0.0 {
139 warn!("zoom less than 0, ignored");
140 return self;
141 }
142 self.0.dimension.zoom = zoom;
143 self
144 }
145
146 pub fn page_box(mut self, dimension: &Rectangle, rotate_degree: i32) -> Self {
147 self.0.dimension.update(dimension, rotate_degree);
148 self
149 }
150
151 fn dimension(mut self, dimension: PageDimension) -> Self {
152 self.0.dimension = dimension;
153 self
154 }
155
156 pub fn crop(mut self, rect: Option<Rectangle>) -> Self {
157 self.0.crop = rect;
158 self
159 }
160
161 pub fn background_color(mut self, color: Color) -> Self {
162 self.0.background_color = color;
163 self
164 }
165
166 pub fn rotate(mut self, rotate: i32) -> Self {
167 self.0.rotate = rotate;
168 self
169 }
170
171 pub fn fail_fast(mut self, fail_fast: bool) -> Self {
172 self.0.fail_fast = fail_fast;
173 self
174 }
175
176 fn state(mut self, state: State) -> Self {
177 self.0.state = Some(state);
178 self
179 }
180
181 pub fn build(self) -> RenderOption {
182 self.0
183 }
184}
185
186pub fn render_page(page: &Page<'_>, option: RenderOptionBuilder) -> Result<RgbaImage> {
187 render_steps(page, option, None, false)
188}
189
190pub fn render_steps(
191 page: &Page<'_>,
192 option: RenderOptionBuilder,
193 steps: Option<usize>,
194 no_crop: bool,
195) -> Result<RgbaImage> {
196 let media_box = page.media_box().whatever_context("get page media box")?;
197 let crop_box = page.crop_box().whatever_context("get page crop box")?;
198 let mut canvas_box = crop_box;
199 if canvas_box.width() == 0.0 || canvas_box.height() == 0.0 {
201 canvas_box = Rectangle::from_xywh(0.0, 0.0, 597.6, 842.4);
202 }
203 let option = option
204 .page_box(&canvas_box, page.rotate())
205 .crop((!no_crop && need_crop(crop_box, media_box)).then_some(crop_box))
206 .rotate(page.rotate())
207 .build();
208 let content = page.content().whatever_context("get page content")?;
209 let ops = content
210 .operations()
211 .whatever_context("get page operations")?;
212 let mut canvas = option.create_canvas()?;
213 if !ops.is_empty() {
214 let resource = page.resources().whatever_context("get page resources")?;
216 let mut renderer = Render::new(&mut canvas, option.clone(), &resource)?;
217
218 let iter = if let Some(steps) = steps {
219 Either::Left(ops.into_iter().take(steps))
220 } else {
221 Either::Right(ops.into_iter())
222 };
223
224 for op in iter {
225 match renderer.exec(op) {
226 Ok(_) => (),
227 Err(e) if option.fail_fast => return Err(e),
228 Err(e) => log::error!("Operation failed: {}", e),
229 }
230 }
231 }
232 option.to_image(canvas)
233}
234
235fn need_crop(crop: Rectangle, media: Rectangle) -> bool {
236 crop != media
237}
238
239#[cfg(test)]
240mod render_tests;