1use std::collections::HashSet;
4use std::error::Error;
5use std::fmt;
6use std::io::Cursor;
7
8use jpeg_encoder::{ColorType as JpegColorType, Encoder as JpegEncoder};
9use oxml_layout::{
10 Effect, LayoutResult, LineCap, LineJoin, PageFrame, Paint as LayoutPaint, Path as LayoutPath,
11 PathCommand, PositionedElement, Stroke as LayoutStroke,
12};
13use tiny_skia::{
14 FillRule, Mask, Paint, PathBuilder, Pixmap, PixmapPaint, Stroke, StrokeDash, Transform,
15};
16use zune_jpeg::JpegDecoder;
17use zune_jpeg::zune_core::bytestream::ZCursor;
18use zune_jpeg::zune_core::colorspace::ColorSpace;
19use zune_jpeg::zune_core::options::DecoderOptions;
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub enum RasterFormat {
24 Png {
26 transparent_background: bool,
28 },
29 Jpeg {
31 quality: u8,
33 },
34 Tiff,
36}
37
38#[derive(Debug, Clone, Copy, PartialEq)]
40pub struct RasterOptions {
41 pub dpi: f64,
43 pub format: RasterFormat,
45}
46
47#[derive(Debug, Clone, PartialEq, Eq)]
49pub enum RasterOutput {
50 SeparatePages(Vec<Vec<u8>>),
52 MultiPageTiff(Vec<u8>),
54}
55
56#[derive(Debug, Clone, PartialEq, Eq)]
58#[non_exhaustive]
59pub enum RasterError {
60 InvalidDpi,
62 EmptyPageSelection,
64 PageOutOfRange {
66 page_index: usize,
67 page_count: usize,
68 },
69 DuplicatePage { page_index: usize },
71 InvalidJpegQuality { quality: u8 },
73 InvalidDimensions,
75 EncodeFailed { format: &'static str },
77}
78
79impl fmt::Display for RasterError {
80 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
81 match self {
82 Self::InvalidDpi => formatter.write_str("raster DPI must be finite and positive"),
83 Self::EmptyPageSelection => formatter.write_str("at least one raster page is required"),
84 Self::PageOutOfRange {
85 page_index,
86 page_count,
87 } => write!(
88 formatter,
89 "raster page index {page_index} is out of range for {page_count} pages"
90 ),
91 Self::DuplicatePage { page_index } => {
92 write!(
93 formatter,
94 "raster page index {page_index} was selected more than once"
95 )
96 }
97 Self::InvalidJpegQuality { quality } => {
98 write!(formatter, "JPEG quality {quality} is outside 1 through 100")
99 }
100 Self::InvalidDimensions => {
101 formatter.write_str("raster dimensions are invalid for the requested format")
102 }
103 Self::EncodeFailed { format } => write!(formatter, "{format} raster encoding failed"),
104 }
105 }
106}
107
108impl Error for RasterError {}
109
110pub fn render_page_to_png(layout: &LayoutResult, page_index: usize, dpi: f64) -> Option<Vec<u8>> {
117 let page = layout.pages.get(page_index)?;
118 let pixmap = render_page_to_pixmap_with_background(page, &layout.fonts, dpi, false)?;
119 pixmap.encode_png().ok()
120}
121
122pub fn render_all_pages(layout: &LayoutResult, dpi: f64) -> Vec<Vec<u8>> {
124 layout
125 .pages
126 .iter()
127 .filter_map(|page| {
128 let pixmap = render_page_to_pixmap_with_background(page, &layout.fonts, dpi, false)?;
129 pixmap.encode_png().ok()
130 })
131 .collect()
132}
133
134pub fn render_pages(
136 layout: &LayoutResult,
137 page_indices: &[usize],
138 options: RasterOptions,
139) -> Result<RasterOutput, RasterError> {
140 validate_request(layout, page_indices, options)?;
141
142 match options.format {
143 RasterFormat::Png {
144 transparent_background,
145 } => page_indices
146 .iter()
147 .map(|&page_index| {
148 let pixmap = render_selected_pixmap(
149 layout,
150 page_index,
151 options.dpi,
152 transparent_background,
153 )?;
154 pixmap
155 .encode_png()
156 .map_err(|_| RasterError::EncodeFailed { format: "PNG" })
157 })
158 .collect::<Result<Vec<_>, _>>()
159 .map(RasterOutput::SeparatePages),
160 RasterFormat::Jpeg { quality } => page_indices
161 .iter()
162 .map(|&page_index| {
163 let pixmap = render_selected_pixmap(layout, page_index, options.dpi, false)?;
164 encode_jpeg(&pixmap, quality)
165 })
166 .collect::<Result<Vec<_>, _>>()
167 .map(RasterOutput::SeparatePages),
168 RasterFormat::Tiff => {
169 encode_tiff(layout, page_indices, options.dpi).map(RasterOutput::MultiPageTiff)
170 }
171 }
172}
173
174fn validate_request(
175 layout: &LayoutResult,
176 page_indices: &[usize],
177 options: RasterOptions,
178) -> Result<(), RasterError> {
179 if !options.dpi.is_finite() || options.dpi <= 0.0 {
180 return Err(RasterError::InvalidDpi);
181 }
182 if page_indices.is_empty() {
183 return Err(RasterError::EmptyPageSelection);
184 }
185 if let RasterFormat::Jpeg { quality } = options.format
186 && !(1..=100).contains(&quality)
187 {
188 return Err(RasterError::InvalidJpegQuality { quality });
189 }
190 let page_count = layout.pages.len();
191 let mut seen = HashSet::with_capacity(page_indices.len());
192 for &page_index in page_indices {
193 if page_index >= page_count {
194 return Err(RasterError::PageOutOfRange {
195 page_index,
196 page_count,
197 });
198 }
199 if !seen.insert(page_index) {
200 return Err(RasterError::DuplicatePage { page_index });
201 }
202 }
203 Ok(())
204}
205
206fn render_selected_pixmap(
207 layout: &LayoutResult,
208 page_index: usize,
209 dpi: f64,
210 transparent_background: bool,
211) -> Result<Pixmap, RasterError> {
212 let page = layout
213 .pages
214 .get(page_index)
215 .ok_or(RasterError::InvalidDimensions)?;
216 render_page_to_pixmap_with_background(page, &layout.fonts, dpi, transparent_background)
217 .ok_or(RasterError::InvalidDimensions)
218}
219
220fn encode_jpeg(pixmap: &Pixmap, quality: u8) -> Result<Vec<u8>, RasterError> {
221 let width = u16::try_from(pixmap.width()).map_err(|_| RasterError::InvalidDimensions)?;
222 let height = u16::try_from(pixmap.height()).map_err(|_| RasterError::InvalidDimensions)?;
223 let rgb = opaque_rgb(pixmap);
224 let mut output = Vec::new();
225 JpegEncoder::new(&mut output, quality)
226 .encode(&rgb, width, height, JpegColorType::Rgb)
227 .map_err(|_| RasterError::EncodeFailed { format: "JPEG" })?;
228 Ok(output)
229}
230
231fn encode_tiff(
232 layout: &LayoutResult,
233 page_indices: &[usize],
234 dpi: f64,
235) -> Result<Vec<u8>, RasterError> {
236 let mut cursor = Cursor::new(Vec::new());
237 {
238 let mut encoder = tiff::encoder::TiffEncoder::new(&mut cursor)
239 .map_err(|_| RasterError::EncodeFailed { format: "TIFF" })?;
240 for &page_index in page_indices {
241 let pixmap = render_selected_pixmap(layout, page_index, dpi, false)?;
242 let rgb = opaque_rgb(&pixmap);
243 encoder
244 .write_image::<tiff::encoder::colortype::RGB8>(
245 pixmap.width(),
246 pixmap.height(),
247 &rgb,
248 )
249 .map_err(|_| RasterError::EncodeFailed { format: "TIFF" })?;
250 }
251 }
252 Ok(cursor.into_inner())
253}
254
255fn opaque_rgb(pixmap: &Pixmap) -> Vec<u8> {
256 let mut rgb = Vec::with_capacity(pixmap.width() as usize * pixmap.height() as usize * 3);
257 for pixel in pixmap.pixels() {
258 let alpha = pixel.alpha();
259 rgb.push(pixel.red().saturating_add(255 - alpha));
260 rgb.push(pixel.green().saturating_add(255 - alpha));
261 rgb.push(pixel.blue().saturating_add(255 - alpha));
262 }
263 rgb
264}
265
266#[cfg(test)]
268fn render_page_to_pixmap(
269 page: &PageFrame,
270 fonts: &[oxml_layout::FontData],
271 dpi: f64,
272) -> Option<Pixmap> {
273 render_page_to_pixmap_with_background(page, fonts, dpi, false)
274}
275
276fn render_page_to_pixmap_with_background(
277 page: &PageFrame,
278 fonts: &[oxml_layout::FontData],
279 dpi: f64,
280 transparent_background: bool,
281) -> Option<Pixmap> {
282 let scale = dpi / 72.0; let width = (page.width * scale).ceil() as u32;
284 let height = (page.height * scale).ceil() as u32;
285
286 let mut pixmap = Pixmap::new(width, height)?;
287
288 if !transparent_background {
289 pixmap.fill(tiny_skia::Color::WHITE);
290 }
291
292 let transform = Transform::from_scale(scale as f32, scale as f32);
293
294 if let Some(background) = &page.background {
295 let path = LayoutPath::rect(oxml_layout::Rect {
296 x: 0.0,
297 y: 0.0,
298 width: page.width,
299 height: page.height,
300 });
301 render_path(&mut pixmap, &path, Some(background), None, transform, None);
302 }
303 render_elements(&mut pixmap, fonts, &page.elements, transform, None);
304
305 Some(pixmap)
306}
307
308fn render_elements(
309 pixmap: &mut Pixmap,
310 fonts: &[oxml_layout::FontData],
311 elements: &[PositionedElement],
312 transform: Transform,
313 mask: Option<&Mask>,
314) {
315 for element in elements {
316 match element {
317 PositionedElement::FilledRect { rect, color } => {
318 let Some(rect) = tiny_skia::Rect::from_xywh(
319 rect.x as f32,
320 rect.y as f32,
321 rect.width as f32,
322 rect.height as f32,
323 ) else {
324 continue;
325 };
326 let mut paint = solid_paint(*color);
327 paint.anti_alias = false;
328 pixmap.fill_rect(rect, &paint, transform, mask);
329 }
330 PositionedElement::Line {
331 start,
332 end,
333 width,
334 color,
335 dash_pattern,
336 } => {
337 let mut builder = PathBuilder::new();
338 builder.move_to(start.x as f32, start.y as f32);
339 builder.line_to(end.x as f32, end.y as f32);
340 let Some(path) = builder.finish() else {
341 continue;
342 };
343 let dash = dash_pattern
344 .and_then(|(on, off)| StrokeDash::new(vec![on as f32, off as f32], 0.0));
345 let stroke = Stroke {
346 width: *width as f32,
347 dash,
348 ..Stroke::default()
349 };
350 pixmap.stroke_path(&path, &solid_paint(*color), &stroke, transform, mask);
351 }
352 PositionedElement::Text(glyph_run) => {
353 if let Some(font_data) = fonts.iter().find(|font| font.id == glyph_run.font_id) {
354 render_glyph_run(pixmap, glyph_run, font_data, transform, mask);
355 }
356 }
357 PositionedElement::MultilingualText(glyph_run) => {
358 if glyph_run.is_valid()
359 && let Some(font_data) = fonts.iter().find(|font| font.id == glyph_run.font_id)
360 {
361 render_positioned_glyph_run(
362 pixmap,
363 &glyph_run.legacy_projection(),
364 font_data,
365 transform,
366 mask,
367 &glyph_run.x_offsets,
368 &glyph_run.y_offsets,
369 &glyph_run.y_advances,
370 );
371 }
372 }
373 PositionedElement::Image {
374 rect,
375 data,
376 content_type,
377 ..
378 } => {
379 if !data.is_empty() {
380 render_image(pixmap, rect, data, content_type, transform, mask);
381 }
382 }
383 PositionedElement::LinkAnnotation { .. } => {}
384 PositionedElement::Path(element) => render_path(
385 pixmap,
386 &element.path,
387 element.fill.as_ref(),
388 element.stroke.as_ref(),
389 transform,
390 mask,
391 ),
392 PositionedElement::Group(group) => {
393 let child_transform = transform.pre_concat(layout_transform(group.transform));
394 let child_mask = match group.clip.as_ref() {
395 Some(clip) => {
396 let Some(path) = build_path(clip) else {
397 continue;
398 };
399 let mut child_mask = match mask {
400 Some(parent) => parent.clone(),
401 None => {
402 let Some(mask) = Mask::new(pixmap.width(), pixmap.height()) else {
403 continue;
404 };
405 mask
406 }
407 };
408 if mask.is_some() {
409 child_mask.intersect_path(
410 &path,
411 fill_rule(clip.fill_rule),
412 true,
413 child_transform,
414 );
415 } else {
416 child_mask.fill_path(
417 &path,
418 fill_rule(clip.fill_rule),
419 true,
420 child_transform,
421 );
422 }
423 Some(child_mask)
424 }
425 None => None,
426 };
427 let child_mask_ref = child_mask.as_ref().or(mask);
428 let opacity = if group.opacity.is_finite() {
429 group.opacity.clamp(0.0, 1.0) as f32
430 } else {
431 1.0
432 };
433 if opacity == 0.0 {
434 continue;
435 }
436 if opacity == 1.0 && group.effects.is_empty() {
437 render_elements(
438 pixmap,
439 fonts,
440 &group.children,
441 child_transform,
442 child_mask_ref,
443 );
444 } else if let Some(mut layer) = Pixmap::new(pixmap.width(), pixmap.height()) {
445 render_elements(
446 &mut layer,
447 fonts,
448 &group.children,
449 child_transform,
450 child_mask_ref,
451 );
452 if group.effects.is_empty() {
453 pixmap.draw_pixmap(
454 0,
455 0,
456 layer.as_ref(),
457 &PixmapPaint {
458 opacity,
459 ..PixmapPaint::default()
460 },
461 Transform::identity(),
462 None,
463 );
464 } else if let Some(mut composited) =
465 Pixmap::new(pixmap.width(), pixmap.height())
466 {
467 render_group_effects(
468 &mut composited,
469 &layer,
470 &group.effects,
471 child_transform,
472 mask,
473 );
474 composited.draw_pixmap(
475 0,
476 0,
477 layer.as_ref(),
478 &PixmapPaint::default(),
479 Transform::identity(),
480 None,
481 );
482 pixmap.draw_pixmap(
483 0,
484 0,
485 composited.as_ref(),
486 &PixmapPaint {
487 opacity,
488 ..PixmapPaint::default()
489 },
490 Transform::identity(),
491 None,
492 );
493 }
494 }
495 }
496 PositionedElement::MarkedContent { children, .. } => {
497 render_elements(pixmap, fonts, children, transform, mask);
498 }
499 _ => {}
500 }
501 }
502}
503
504fn render_group_effects(
505 pixmap: &mut Pixmap,
506 layer: &Pixmap,
507 effects: &[Effect],
508 transform: Transform,
509 mask: Option<&Mask>,
510) {
511 for effect in effects {
512 if let Effect::OuterShadow {
513 dx,
514 dy,
515 blur,
516 color,
517 } = effect
518 {
519 let Some((left, top, right, bottom)) = alpha_bounds(layer) else {
520 continue;
521 };
522 let (scale_x, scale_y) = transform.get_scale();
523 let scale = ((scale_x + scale_y) * 0.5).max(0.0);
524 let blur_radius = ((*blur as f32 * scale).max(0.0).ceil() as usize)
525 .min(layer.width().max(layer.height()) as usize);
526 let source_width = right - left + 1;
527 let source_height = bottom - top + 1;
528 let width = source_width.saturating_add(blur_radius.saturating_mul(2));
529 let height = source_height.saturating_add(blur_radius.saturating_mul(2));
530 let Some(pixel_count) = width.checked_mul(height) else {
531 continue;
532 };
533 let mut alpha = vec![0_u8; pixel_count];
534 for y in 0..source_height {
535 let source_start = ((top + y) * layer.width() as usize + left) * 4;
536 let source = &layer.data()[source_start..source_start + source_width * 4];
537 let target_start = (y + blur_radius) * width + blur_radius;
538 for (target, pixel) in alpha[target_start..target_start + source_width]
539 .iter_mut()
540 .zip(source.chunks_exact(4))
541 {
542 *target = pixel[3];
543 }
544 }
545 box_blur_alpha(&mut alpha, width, height, blur_radius);
546
547 let (Ok(shadow_width), Ok(shadow_height)) =
548 (u32::try_from(width), u32::try_from(height))
549 else {
550 continue;
551 };
552 let Some(mut shadow) = Pixmap::new(shadow_width, shadow_height) else {
553 continue;
554 };
555 let color_alpha = color.a.clamp(0.0, 1.0);
556 for (pixel, source_alpha) in shadow.data_mut().chunks_exact_mut(4).zip(alpha) {
557 let alpha = (f64::from(source_alpha) * color_alpha).round() as u8;
558 pixel[0] = (color.r.clamp(0.0, 1.0) * f64::from(alpha)).round() as u8;
559 pixel[1] = (color.g.clamp(0.0, 1.0) * f64::from(alpha)).round() as u8;
560 pixel[2] = (color.b.clamp(0.0, 1.0) * f64::from(alpha)).round() as u8;
561 pixel[3] = alpha;
562 }
563
564 let mut origin = tiny_skia::Point::from_xy(0.0, 0.0);
565 let mut offset = tiny_skia::Point::from_xy(*dx as f32, *dy as f32);
566 transform.map_point(&mut origin);
567 transform.map_point(&mut offset);
568 pixmap.draw_pixmap(
569 left as i32 - blur_radius as i32 + (offset.x - origin.x).round() as i32,
570 top as i32 - blur_radius as i32 + (offset.y - origin.y).round() as i32,
571 shadow.as_ref(),
572 &PixmapPaint::default(),
573 Transform::identity(),
574 mask,
575 );
576 }
577 }
578}
579
580fn alpha_bounds(layer: &Pixmap) -> Option<(usize, usize, usize, usize)> {
581 let width = layer.width() as usize;
582 let mut left = width;
583 let mut top = layer.height() as usize;
584 let mut right = 0;
585 let mut bottom = 0;
586 let mut found = false;
587 for (index, pixel) in layer.data().chunks_exact(4).enumerate() {
588 if pixel[3] == 0 {
589 continue;
590 }
591 let x = index % width;
592 let y = index / width;
593 left = left.min(x);
594 top = top.min(y);
595 right = right.max(x);
596 bottom = bottom.max(y);
597 found = true;
598 }
599 found.then_some((left, top, right, bottom))
600}
601
602fn box_blur_alpha(alpha: &mut [u8], width: usize, height: usize, radius: usize) {
603 if radius == 0 || width == 0 || height == 0 {
604 return;
605 }
606 let mut horizontal = vec![0_u8; alpha.len()];
607 let mut prefix = vec![0_u64; width.max(height) + 1];
608 let diameter = radius.saturating_mul(2).saturating_add(1) as u64;
609
610 for y in 0..height {
611 let row = &alpha[y * width..(y + 1) * width];
612 let target = &mut horizontal[y * width..(y + 1) * width];
613 prefix[0] = 0;
614 for (index, value) in row.iter().enumerate() {
615 prefix[index + 1] = prefix[index] + u64::from(*value);
616 }
617 for (x, target) in target.iter_mut().enumerate() {
618 let left = x.saturating_sub(radius);
619 let right = (x + radius + 1).min(width);
620 *target = ((prefix[right] - prefix[left]) / diameter) as u8;
621 }
622 }
623
624 for x in 0..width {
625 prefix[0] = 0;
626 for y in 0..height {
627 prefix[y + 1] = prefix[y] + u64::from(horizontal[y * width + x]);
628 }
629 for y in 0..height {
630 let top = y.saturating_sub(radius);
631 let bottom = (y + radius + 1).min(height);
632 alpha[y * width + x] = ((prefix[bottom] - prefix[top]) / diameter) as u8;
633 }
634 }
635}
636
637fn render_path(
638 pixmap: &mut Pixmap,
639 path: &LayoutPath,
640 fill: Option<&LayoutPaint>,
641 stroke: Option<&LayoutStroke>,
642 transform: Transform,
643 mask: Option<&Mask>,
644) {
645 let Some(path_geometry) = build_path(path) else {
646 return;
647 };
648 if let Some(paint) = fill.and_then(raster_paint) {
649 let paint_mask =
650 fill.and_then(|paint| gradient_domain_mask(pixmap, paint, transform, mask));
651 pixmap.fill_path(
652 &path_geometry,
653 &paint,
654 fill_rule(path.fill_rule),
655 transform,
656 paint_mask.as_ref().or(mask),
657 );
658 }
659 if let Some(stroke) = stroke
660 && let Some(paint) = raster_paint(&stroke.paint)
661 {
662 let paint_mask = gradient_domain_mask(pixmap, &stroke.paint, transform, mask);
663 let stroke = raster_stroke(stroke);
664 pixmap.stroke_path(
665 &path_geometry,
666 &paint,
667 &stroke,
668 transform,
669 paint_mask.as_ref().or(mask),
670 );
671 }
672}
673
674fn build_path(path: &LayoutPath) -> Option<tiny_skia::Path> {
675 let mut builder = PathBuilder::new();
676 for command in &path.commands {
677 match command {
678 PathCommand::MoveTo(point) => builder.move_to(point.x as f32, point.y as f32),
679 PathCommand::LineTo(point) => builder.line_to(point.x as f32, point.y as f32),
680 PathCommand::CurveTo { c1, c2, to } => builder.cubic_to(
681 c1.x as f32,
682 c1.y as f32,
683 c2.x as f32,
684 c2.y as f32,
685 to.x as f32,
686 to.y as f32,
687 ),
688 PathCommand::Close => builder.close(),
689 }
690 }
691 builder.finish()
692}
693
694fn fill_rule(rule: oxml_layout::FillRule) -> FillRule {
695 match rule {
696 oxml_layout::FillRule::NonZero => FillRule::Winding,
697 oxml_layout::FillRule::EvenOdd => FillRule::EvenOdd,
698 }
699}
700
701fn raster_paint(paint: &LayoutPaint) -> Option<Paint<'static>> {
702 match paint {
703 LayoutPaint::Solid(color) => Some(solid_paint(*color)),
704 LayoutPaint::Linear {
705 start, end, stops, ..
706 } => {
707 let shader = tiny_skia::LinearGradient::new(
708 tiny_skia::Point::from_xy(start.x as f32, start.y as f32),
709 tiny_skia::Point::from_xy(end.x as f32, end.y as f32),
710 gradient_stops(stops),
711 tiny_skia::SpreadMode::Pad,
712 Transform::identity(),
713 )?;
714 Some(Paint {
715 shader,
716 ..Paint::default()
717 })
718 }
719 LayoutPaint::Radial {
720 center,
721 radius,
722 focal,
723 stops,
724 ..
725 } => {
726 let shader = tiny_skia::RadialGradient::new(
727 tiny_skia::Point::from_xy(focal.x as f32, focal.y as f32),
728 0.0,
729 tiny_skia::Point::from_xy(center.x as f32, center.y as f32),
730 *radius as f32,
731 gradient_stops(stops),
732 tiny_skia::SpreadMode::Pad,
733 Transform::identity(),
734 )?;
735 Some(Paint {
736 shader,
737 ..Paint::default()
738 })
739 }
740 LayoutPaint::Tile { .. } => None,
741 }
742}
743
744fn gradient_domain_mask(
745 pixmap: &Pixmap,
746 paint: &LayoutPaint,
747 transform: Transform,
748 parent: Option<&Mask>,
749) -> Option<Mask> {
750 let path = match paint {
751 LayoutPaint::Linear {
752 start, end, extend, ..
753 } if *extend != (true, true) => linear_domain_path(
754 pixmap.width(),
755 pixmap.height(),
756 *start,
757 *end,
758 *extend,
759 transform,
760 )?,
761 LayoutPaint::Radial {
762 center,
763 radius,
764 extend,
765 ..
766 } if !extend.1 => build_path(&LayoutPath::ellipse(oxml_layout::Rect {
767 x: center.x - radius,
768 y: center.y - radius,
769 width: radius * 2.0,
770 height: radius * 2.0,
771 }))?,
772 _ => return None,
773 };
774 let mut domain = match parent {
775 Some(parent) => parent.clone(),
776 None => Mask::new(pixmap.width(), pixmap.height())?,
777 };
778 if parent.is_some() {
779 domain.intersect_path(&path, FillRule::Winding, true, transform);
780 } else {
781 domain.fill_path(&path, FillRule::Winding, true, transform);
782 }
783 Some(domain)
784}
785
786fn linear_domain_path(
787 width: u32,
788 height: u32,
789 start: oxml_layout::Point,
790 end: oxml_layout::Point,
791 extend: (bool, bool),
792 transform: Transform,
793) -> Option<tiny_skia::Path> {
794 let inverse = transform.invert()?;
795 let mut corners = [
796 tiny_skia::Point::from_xy(0.0, 0.0),
797 tiny_skia::Point::from_xy(width as f32, 0.0),
798 tiny_skia::Point::from_xy(width as f32, height as f32),
799 tiny_skia::Point::from_xy(0.0, height as f32),
800 ];
801 inverse.map_points(&mut corners);
802
803 let dx = (end.x - start.x) as f32;
804 let dy = (end.y - start.y) as f32;
805 let length_squared = dx * dx + dy * dy;
806 if !length_squared.is_finite() || length_squared <= f32::EPSILON {
807 return None;
808 }
809 let length = length_squared.sqrt();
810 let normal_x = -dy / length;
811 let normal_y = dx / length;
812 let mut min_t = f32::INFINITY;
813 let mut max_t = f32::NEG_INFINITY;
814 let mut min_normal = f32::INFINITY;
815 let mut max_normal = f32::NEG_INFINITY;
816 for corner in corners {
817 let x = corner.x - start.x as f32;
818 let y = corner.y - start.y as f32;
819 let t = (x * dx + y * dy) / length_squared;
820 let normal = x * normal_x + y * normal_y;
821 min_t = min_t.min(t);
822 max_t = max_t.max(t);
823 min_normal = min_normal.min(normal);
824 max_normal = max_normal.max(normal);
825 }
826 let min_t = if extend.0 { min_t - 1.0 } else { 0.0 };
827 let max_t = if extend.1 { max_t + 1.0 } else { 1.0 };
828 let min_normal = min_normal - 1.0;
829 let max_normal = max_normal + 1.0;
830 let point = |t: f32, normal: f32| {
831 tiny_skia::Point::from_xy(
832 start.x as f32 + dx * t + normal_x * normal,
833 start.y as f32 + dy * t + normal_y * normal,
834 )
835 };
836 let mut builder = PathBuilder::new();
837 let first = point(min_t, min_normal);
838 builder.move_to(first.x, first.y);
839 for corner in [
840 point(max_t, min_normal),
841 point(max_t, max_normal),
842 point(min_t, max_normal),
843 ] {
844 builder.line_to(corner.x, corner.y);
845 }
846 builder.close();
847 builder.finish()
848}
849
850fn gradient_stops(stops: &[oxml_layout::GradientStop]) -> Vec<tiny_skia::GradientStop> {
851 let mut normalized: Vec<(f64, oxml_layout::Color)> = stops
852 .iter()
853 .map(|stop| {
854 let offset = if stop.offset.is_nan() {
855 0.0
856 } else {
857 stop.offset.clamp(0.0, 1.0)
858 };
859 (offset, stop.color)
860 })
861 .collect();
862 normalized.sort_by(|left, right| left.0.total_cmp(&right.0));
863
864 let mut deduplicated: Vec<(f64, oxml_layout::Color)> = Vec::with_capacity(normalized.len());
865 for stop in normalized {
866 if let Some(previous) = deduplicated.last_mut()
867 && previous.0 == stop.0
868 {
869 *previous = stop;
870 } else {
871 deduplicated.push(stop);
872 }
873 }
874
875 deduplicated
876 .into_iter()
877 .map(|(offset, color)| tiny_skia::GradientStop::new(offset as f32, skia_color(color)))
878 .collect()
879}
880
881fn solid_paint(color: oxml_layout::Color) -> Paint<'static> {
882 let mut paint = Paint::default();
883 paint.set_color_rgba8(
884 (color.r.clamp(0.0, 1.0) * 255.0) as u8,
885 (color.g.clamp(0.0, 1.0) * 255.0) as u8,
886 (color.b.clamp(0.0, 1.0) * 255.0) as u8,
887 (color.a.clamp(0.0, 1.0) * 255.0) as u8,
888 );
889 paint
890}
891
892fn skia_color(color: oxml_layout::Color) -> tiny_skia::Color {
893 tiny_skia::Color::from_rgba(
894 color.r.clamp(0.0, 1.0) as f32,
895 color.g.clamp(0.0, 1.0) as f32,
896 color.b.clamp(0.0, 1.0) as f32,
897 color.a.clamp(0.0, 1.0) as f32,
898 )
899 .unwrap_or(tiny_skia::Color::TRANSPARENT)
900}
901
902fn raster_stroke(stroke: &LayoutStroke) -> Stroke {
903 Stroke {
904 width: stroke.width as f32,
905 line_cap: match stroke.cap {
906 LineCap::Butt => tiny_skia::LineCap::Butt,
907 LineCap::Round => tiny_skia::LineCap::Round,
908 LineCap::Square => tiny_skia::LineCap::Square,
909 },
910 line_join: match stroke.join {
911 LineJoin::Miter => tiny_skia::LineJoin::Miter,
912 LineJoin::Round => tiny_skia::LineJoin::Round,
913 LineJoin::Bevel => tiny_skia::LineJoin::Bevel,
914 },
915 dash: stroke.dash.as_ref().and_then(|dash| {
916 StrokeDash::new(dash.iter().map(|value| *value as f32).collect(), 0.0)
917 }),
918 ..Stroke::default()
919 }
920}
921
922fn layout_transform(transform: oxml_layout::Transform) -> Transform {
923 Transform::from_row(
924 transform.a as f32,
925 transform.b as f32,
926 transform.c as f32,
927 transform.d as f32,
928 transform.e as f32,
929 transform.f as f32,
930 )
931}
932
933fn render_glyph_run(
935 pixmap: &mut Pixmap,
936 glyph_run: &oxml_layout::GlyphRun,
937 font_data: &oxml_layout::FontData,
938 transform: Transform,
939 mask: Option<&Mask>,
940) {
941 render_positioned_glyph_run(pixmap, glyph_run, font_data, transform, mask, &[], &[], &[]);
942}
943
944#[allow(clippy::too_many_arguments)]
945fn render_positioned_glyph_run(
946 pixmap: &mut Pixmap,
947 glyph_run: &oxml_layout::GlyphRun,
948 font_data: &oxml_layout::FontData,
949 transform: Transform,
950 mask: Option<&Mask>,
951 x_offsets: &[f64],
952 y_offsets: &[f64],
953 y_advances: &[f64],
954) {
955 let Ok(face) = ttf_parser::Face::parse(&font_data.data, font_data.face_index) else {
956 return;
957 };
958
959 let upem = face.units_per_em() as f64;
960 let scale = glyph_run.font_size / upem;
961
962 let mut paint = Paint::default();
963 paint.set_color_rgba8(
964 (glyph_run.color.r * 255.0) as u8,
965 (glyph_run.color.g * 255.0) as u8,
966 (glyph_run.color.b * 255.0) as u8,
967 (glyph_run.color.a * 255.0) as u8,
968 );
969 paint.anti_alias = true;
970
971 let mut x = glyph_run.origin.x;
972 let mut y = glyph_run.origin.y;
973
974 for (i, &glyph_id) in glyph_run.glyph_ids.iter().enumerate() {
975 let gid = ttf_parser::GlyphId(glyph_id);
976
977 let mut builder = GlyphPathBuilder::new();
979 if face.outline_glyph(gid, &mut builder).is_some()
980 && let Some(path) = builder.finish()
981 {
982 let glyph_transform = transform
985 .pre_translate(
986 (x + x_offsets.get(i).copied().unwrap_or(0.0)) as f32,
987 (y - y_offsets.get(i).copied().unwrap_or(0.0)) as f32,
988 )
989 .pre_scale(scale as f32, -(scale as f32));
990
991 pixmap.fill_path(&path, &paint, FillRule::Winding, glyph_transform, mask);
992 }
993
994 if i < glyph_run.advances.len() {
995 x += glyph_run.advances[i];
996 }
997 y -= y_advances.get(i).copied().unwrap_or(0.0);
998 }
999}
1000
1001struct GlyphPathBuilder {
1003 pb: PathBuilder,
1004}
1005
1006impl GlyphPathBuilder {
1007 fn new() -> Self {
1008 GlyphPathBuilder {
1009 pb: PathBuilder::new(),
1010 }
1011 }
1012
1013 fn finish(self) -> Option<tiny_skia::Path> {
1014 self.pb.finish()
1015 }
1016}
1017
1018impl ttf_parser::OutlineBuilder for GlyphPathBuilder {
1019 fn move_to(&mut self, x: f32, y: f32) {
1020 self.pb.move_to(x, y);
1021 }
1022
1023 fn line_to(&mut self, x: f32, y: f32) {
1024 self.pb.line_to(x, y);
1025 }
1026
1027 fn quad_to(&mut self, x1: f32, y1: f32, x: f32, y: f32) {
1028 self.pb.quad_to(x1, y1, x, y);
1029 }
1030
1031 fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x: f32, y: f32) {
1032 self.pb.cubic_to(x1, y1, x2, y2, x, y);
1033 }
1034
1035 fn close(&mut self) {
1036 self.pb.close();
1037 }
1038}
1039
1040fn render_image(
1042 pixmap: &mut Pixmap,
1043 rect: &oxml_layout::Rect,
1044 data: &[u8],
1045 content_type: &str,
1046 transform: Transform,
1047 mask: Option<&Mask>,
1048) {
1049 let decoded = crate::image::decode_image(data, content_type);
1051 let Some(decoded) = decoded else {
1052 return;
1053 };
1054
1055 let img_pixmap = if decoded.is_jpeg {
1057 decode_jpeg_pixmap(data, decoded.width, decoded.height)
1058 } else if decoded.color_space == "DeviceRGB" {
1059 let mut rgba = Vec::with_capacity(decoded.width as usize * decoded.height as usize * 4);
1061 if let Some(alpha) = &decoded.alpha {
1062 for (rgb, &a) in decoded.data.chunks_exact(3).zip(alpha.iter()) {
1063 rgba.extend_from_slice(&[rgb[0], rgb[1], rgb[2], a]);
1064 }
1065 } else {
1066 for rgb in decoded.data.chunks_exact(3) {
1067 rgba.extend_from_slice(&[rgb[0], rgb[1], rgb[2], 255]);
1068 }
1069 }
1070 let size = tiny_skia::IntSize::from_wh(decoded.width, decoded.height);
1071 size.and_then(|s| Pixmap::from_vec(rgba, s))
1072 } else if decoded.color_space == "DeviceGray" {
1073 let mut rgba = Vec::with_capacity(decoded.width as usize * decoded.height as usize * 4);
1075 for &gray in &decoded.data {
1076 rgba.push(gray);
1077 rgba.push(gray);
1078 rgba.push(gray);
1079 rgba.push(255);
1080 }
1081 let size = tiny_skia::IntSize::from_wh(decoded.width, decoded.height);
1082 size.and_then(|s| Pixmap::from_vec(rgba, s))
1083 } else {
1084 return;
1085 };
1086
1087 let Some(img_pixmap) = img_pixmap else {
1088 return;
1089 };
1090
1091 if decoded.width == 0 || decoded.height == 0 {
1094 return;
1095 }
1096 let sx = rect.width as f32 / decoded.width as f32;
1097 let sy = rect.height as f32 / decoded.height as f32;
1098
1099 let img_transform = transform
1100 .pre_translate(rect.x as f32, rect.y as f32)
1101 .pre_scale(sx, sy);
1102
1103 let pattern = tiny_skia::Pattern::new(
1104 img_pixmap.as_ref(),
1105 tiny_skia::SpreadMode::Pad,
1106 tiny_skia::FilterQuality::Bilinear,
1107 1.0,
1108 Transform::identity(),
1109 );
1110
1111 let paint = Paint {
1112 shader: pattern,
1113 ..Paint::default()
1114 };
1115
1116 let fill_rect =
1117 tiny_skia::Rect::from_xywh(0.0, 0.0, decoded.width as f32, decoded.height as f32);
1118 if let Some(fill_rect) = fill_rect {
1119 pixmap.fill_rect(fill_rect, &paint, img_transform, mask);
1120 }
1121}
1122
1123fn decode_jpeg_pixmap(data: &[u8], width: u32, height: u32) -> Option<Pixmap> {
1124 let options = DecoderOptions::default().jpeg_set_out_colorspace(ColorSpace::RGBA);
1125 let mut decoder = JpegDecoder::new_with_options(ZCursor::new(data), options);
1126 let rgba = decoder.decode().ok()?;
1127 let (decoded_width, decoded_height) = decoder.dimensions()?;
1128 if decoded_width != width as usize || decoded_height != height as usize {
1129 return None;
1130 }
1131 let size = tiny_skia::IntSize::from_wh(width, height)?;
1132 Pixmap::from_vec(rgba, size)
1133}
1134
1135#[cfg(test)]
1136mod tests {
1137 use std::io::Cursor;
1138
1139 use super::{
1140 RasterFormat, RasterOptions, RasterOutput, decode_jpeg_pixmap, render_page_to_pixmap,
1141 render_pages,
1142 };
1143 use oxml_layout::{
1144 Color, Effect, FillRule, GradientStop, GroupElement, LayoutResult, MediaId, PageFrame,
1145 Paint, Path, PathCommand, PathElement, Point, PositionedElement, Rect, Stroke, Transform,
1146 };
1147
1148 fn rgb(pixel: tiny_skia::PremultipliedColorU8) -> (u8, u8, u8) {
1149 (pixel.red(), pixel.green(), pixel.blue())
1150 }
1151
1152 fn color(r: f32, g: f32, b: f32) -> Color {
1153 Color {
1154 r: r.into(),
1155 g: g.into(),
1156 b: b.into(),
1157 a: 1.0,
1158 }
1159 }
1160
1161 fn solid_path(path: Path, color: Color) -> PositionedElement {
1162 PositionedElement::Path(PathElement {
1163 path,
1164 fill: Some(Paint::Solid(color)),
1165 stroke: None,
1166 })
1167 }
1168
1169 fn page(elements: Vec<PositionedElement>) -> PageFrame {
1170 PageFrame::new(1, 32.0, 32.0, elements)
1171 }
1172
1173 fn layout(pages: Vec<PageFrame>) -> LayoutResult {
1174 LayoutResult::new(
1175 pages.into_iter().map(Into::into).collect(),
1176 Vec::new(),
1177 None,
1178 Vec::new(),
1179 )
1180 }
1181
1182 fn assert_near_rgb(actual: (u8, u8, u8), expected: (u8, u8, u8), tolerance: u8) {
1183 for (actual, expected) in [actual.0, actual.1, actual.2]
1184 .into_iter()
1185 .zip([expected.0, expected.1, expected.2])
1186 {
1187 let delta = actual.abs_diff(expected);
1188 assert!(
1189 delta <= tolerance,
1190 "channel {actual} differed from {expected} by {delta}, tolerance {tolerance}"
1191 );
1192 }
1193 }
1194
1195 fn decode_tiff_pages(bytes: &[u8]) -> Vec<(u32, u32, Vec<u8>)> {
1196 let mut decoder =
1197 tiff::decoder::Decoder::new(Cursor::new(bytes)).expect("decode TIFF header");
1198 let mut pages = Vec::new();
1199 loop {
1200 let (width, height) = decoder.dimensions().expect("TIFF dimensions");
1201 let tiff::decoder::DecodingResult::U8(data) =
1202 decoder.read_image().expect("read TIFF image")
1203 else {
1204 panic!("TIFF image should decode as RGB8");
1205 };
1206 pages.push((width, height, data));
1207 if !decoder.more_images() {
1208 break;
1209 }
1210 decoder.next_image().expect("advance TIFF image");
1211 }
1212 pages
1213 }
1214
1215 #[test]
1216 fn image_export_options_produce_the_declared_formats_and_exact_pages() {
1217 let layout = layout(vec![
1218 page(vec![PositionedElement::FilledRect {
1219 rect: Rect {
1220 x: 0.0,
1221 y: 0.0,
1222 width: 32.0,
1223 height: 32.0,
1224 },
1225 color: color(1.0, 0.0, 0.0),
1226 }]),
1227 page(vec![PositionedElement::FilledRect {
1228 rect: Rect {
1229 x: 0.0,
1230 y: 0.0,
1231 width: 32.0,
1232 height: 32.0,
1233 },
1234 color: color(0.0, 1.0, 0.0),
1235 }]),
1236 page(vec![PositionedElement::FilledRect {
1237 rect: Rect {
1238 x: 0.0,
1239 y: 0.0,
1240 width: 32.0,
1241 height: 32.0,
1242 },
1243 color: color(0.0, 0.0, 1.0),
1244 }]),
1245 ]);
1246
1247 let png = render_pages(
1248 &layout,
1249 &[2, 0],
1250 RasterOptions {
1251 dpi: 72.0,
1252 format: RasterFormat::Png {
1253 transparent_background: false,
1254 },
1255 },
1256 )
1257 .expect("selected PNG pages render");
1258 let RasterOutput::SeparatePages(png_pages) = png else {
1259 panic!("PNG renders as separate pages");
1260 };
1261 assert_eq!(png_pages.len(), 2);
1262 let png_page_2 = tiny_skia::Pixmap::decode_png(&png_pages[0]).expect("decode page 2 PNG");
1263 let png_page_0 = tiny_skia::Pixmap::decode_png(&png_pages[1]).expect("decode page 0 PNG");
1264 assert_eq!((png_page_2.width(), png_page_2.height()), (32, 32));
1265 assert_eq!((png_page_0.width(), png_page_0.height()), (32, 32));
1266 assert_eq!(rgb(png_page_2.pixel(16, 16).unwrap()), (0, 0, 255));
1267 assert_eq!(rgb(png_page_0.pixel(16, 16).unwrap()), (255, 0, 0));
1268
1269 let jpeg = render_pages(
1270 &layout,
1271 &[2, 0],
1272 RasterOptions {
1273 dpi: 72.0,
1274 format: RasterFormat::Jpeg { quality: 80 },
1275 },
1276 )
1277 .expect("selected JPEG pages render");
1278 let RasterOutput::SeparatePages(jpeg_pages) = jpeg else {
1279 panic!("JPEG renders as separate pages");
1280 };
1281 assert_eq!(jpeg_pages.len(), 2);
1282 let jpeg_page_2 = decode_jpeg_pixmap(&jpeg_pages[0], 32, 32).expect("decode page 2 JPEG");
1283 let jpeg_page_0 = decode_jpeg_pixmap(&jpeg_pages[1], 32, 32).expect("decode page 0 JPEG");
1284 assert_ne!(jpeg_pages[0], jpeg_pages[1]);
1285 assert_near_rgb(rgb(jpeg_page_2.pixel(16, 16).unwrap()), (0, 0, 255), 8);
1286 assert_near_rgb(rgb(jpeg_page_0.pixel(16, 16).unwrap()), (255, 0, 0), 8);
1287
1288 let tiff = render_pages(
1289 &layout,
1290 &[2, 0],
1291 RasterOptions {
1292 dpi: 72.0,
1293 format: RasterFormat::Tiff,
1294 },
1295 )
1296 .expect("selected TIFF pages render");
1297 let RasterOutput::MultiPageTiff(tiff) = tiff else {
1298 panic!("TIFF renders as one multi-page stream");
1299 };
1300 assert!(tiff.starts_with(b"II*\0") || tiff.starts_with(b"MM\0*"));
1301 let tiff_pages = decode_tiff_pages(&tiff);
1302 assert_eq!(tiff_pages.len(), 2);
1303 assert_eq!((tiff_pages[0].0, tiff_pages[0].1), (32, 32));
1304 assert_eq!((tiff_pages[1].0, tiff_pages[1].1), (32, 32));
1305 let first_center = 3 * (16 * 32 + 16);
1306 let second_center = 3 * (16 * 32 + 16);
1307 assert_eq!(
1308 &tiff_pages[0].2[first_center..first_center + 3],
1309 &[0, 0, 255]
1310 );
1311 assert_eq!(
1312 &tiff_pages[1].2[second_center..second_center + 3],
1313 &[255, 0, 0]
1314 );
1315 }
1316
1317 #[test]
1318 fn transparent_png_keeps_clear_pixels_but_authored_background_paints() {
1319 let blank = layout(vec![PageFrame::new(1, 1.0, 1.0, Vec::new())]);
1320 let transparent = render_pages(
1321 &blank,
1322 &[0],
1323 RasterOptions {
1324 dpi: 72.0,
1325 format: RasterFormat::Png {
1326 transparent_background: true,
1327 },
1328 },
1329 )
1330 .expect("transparent PNG renders");
1331 let RasterOutput::SeparatePages(pages) = transparent else {
1332 panic!("PNG renders as separate pages");
1333 };
1334 let clear = tiny_skia::Pixmap::decode_png(&pages[0]).expect("decode transparent PNG");
1335 assert_eq!(clear.pixel(0, 0).unwrap().alpha(), 0);
1336
1337 let opaque = render_pages(
1338 &blank,
1339 &[0],
1340 RasterOptions {
1341 dpi: 72.0,
1342 format: RasterFormat::Png {
1343 transparent_background: false,
1344 },
1345 },
1346 )
1347 .expect("opaque PNG renders");
1348 let RasterOutput::SeparatePages(pages) = opaque else {
1349 panic!("PNG renders as separate pages");
1350 };
1351 let white = tiny_skia::Pixmap::decode_png(&pages[0]).expect("decode opaque PNG");
1352 assert_eq!(white.pixel(0, 0).unwrap().alpha(), 255);
1353
1354 let mut painted = PageFrame::new(1, 1.0, 1.0, Vec::new());
1355 painted.background = Some(Paint::Solid(color(0.0, 0.0, 1.0)));
1356 let painted = render_pages(
1357 &layout(vec![painted]),
1358 &[0],
1359 RasterOptions {
1360 dpi: 72.0,
1361 format: RasterFormat::Png {
1362 transparent_background: true,
1363 },
1364 },
1365 )
1366 .expect("painted transparent PNG renders");
1367 let RasterOutput::SeparatePages(pages) = painted else {
1368 panic!("PNG renders as separate pages");
1369 };
1370 let blue = tiny_skia::Pixmap::decode_png(&pages[0]).expect("decode painted PNG");
1371 assert_eq!(blue.pixel(0, 0).unwrap().alpha(), 255);
1372 }
1373
1374 #[test]
1375 fn jpeg_quality_is_validated_and_changes_the_encoded_result() {
1376 let layout = layout(vec![page(vec![PositionedElement::FilledRect {
1377 rect: Rect {
1378 x: 0.0,
1379 y: 0.0,
1380 width: 32.0,
1381 height: 32.0,
1382 },
1383 color: color(1.0, 0.0, 0.0),
1384 }])]);
1385
1386 for quality in [0, 101] {
1387 assert!(
1388 render_pages(
1389 &layout,
1390 &[0],
1391 RasterOptions {
1392 dpi: 72.0,
1393 format: RasterFormat::Jpeg { quality },
1394 },
1395 )
1396 .is_err()
1397 );
1398 }
1399
1400 let low = render_pages(
1401 &layout,
1402 &[0],
1403 RasterOptions {
1404 dpi: 72.0,
1405 format: RasterFormat::Jpeg { quality: 30 },
1406 },
1407 )
1408 .expect("low-quality JPEG renders");
1409 let high = render_pages(
1410 &layout,
1411 &[0],
1412 RasterOptions {
1413 dpi: 72.0,
1414 format: RasterFormat::Jpeg { quality: 90 },
1415 },
1416 )
1417 .expect("high-quality JPEG renders");
1418 assert_ne!(low, high);
1419 }
1420
1421 #[test]
1422 fn existing_png_entry_points_equal_opaque_option_defaults() {
1423 let layout = layout(vec![page(vec![PositionedElement::FilledRect {
1424 rect: Rect {
1425 x: 0.0,
1426 y: 0.0,
1427 width: 32.0,
1428 height: 32.0,
1429 },
1430 color: Color::BLACK,
1431 }])]);
1432 let old_page = super::render_page_to_png(&layout, 0, 72.0).expect("old page API renders");
1433 let new_page = render_pages(
1434 &layout,
1435 &[0],
1436 RasterOptions {
1437 dpi: 72.0,
1438 format: RasterFormat::Png {
1439 transparent_background: false,
1440 },
1441 },
1442 )
1443 .expect("new page API renders");
1444 let RasterOutput::SeparatePages(pages) = new_page else {
1445 panic!("PNG renders as separate pages");
1446 };
1447 assert_eq!(old_page, pages[0]);
1448 assert_eq!(super::render_all_pages(&layout, 72.0), pages);
1449 }
1450
1451 #[test]
1452 fn half_alpha_over_white_rasterises_to_midpoint() {
1453 let page = PageFrame::new(
1454 1,
1455 1.0,
1456 1.0,
1457 vec![PositionedElement::FilledRect {
1458 rect: Rect {
1459 x: 0.0,
1460 y: 0.0,
1461 width: 1.0,
1462 height: 1.0,
1463 },
1464 color: Color {
1465 r: 0.0,
1466 g: 0.0,
1467 b: 0.0,
1468 a: 0.5,
1469 },
1470 }],
1471 );
1472
1473 let pixmap = render_page_to_pixmap(&page, &[], 72.0).expect("one-pixel page");
1474 let pixel = pixmap.pixel(0, 0).expect("one-pixel page has one pixel");
1475 assert_eq!((pixel.red(), pixel.green(), pixel.blue()), (128, 128, 128));
1476 }
1477
1478 #[test]
1479 fn jpeg_is_decoded_before_raster_composition() {
1480 let jpeg = vec![
1481 0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46, 0x00, 0x01, 0x01, 0x01,
1482 0x00, 0x60, 0x00, 0x60, 0x00, 0x00, 0xff, 0xdb, 0x00, 0x43, 0x00, 0x08, 0x06, 0x06,
1483 0x07, 0x06, 0x05, 0x08, 0x07, 0x07, 0x07, 0x09, 0x09, 0x08, 0x0a, 0x0c, 0x14, 0x0d,
1484 0x0c, 0x0b, 0x0b, 0x0c, 0x19, 0x12, 0x13, 0x0f, 0x14, 0x1d, 0x1a, 0x1f, 0x1e, 0x1d,
1485 0x1a, 0x1c, 0x1c, 0x20, 0x24, 0x2e, 0x27, 0x20, 0x22, 0x2c, 0x23, 0x1c, 0x1c, 0x28,
1486 0x37, 0x29, 0x2c, 0x30, 0x31, 0x34, 0x34, 0x34, 0x1f, 0x27, 0x39, 0x3d, 0x38, 0x32,
1487 0x3c, 0x2e, 0x33, 0x34, 0x32, 0xff, 0xdb, 0x00, 0x43, 0x01, 0x09, 0x09, 0x09, 0x0c,
1488 0x0b, 0x0c, 0x18, 0x0d, 0x0d, 0x18, 0x32, 0x21, 0x1c, 0x21, 0x32, 0x32, 0x32, 0x32,
1489 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32,
1490 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32,
1491 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32,
1492 0x32, 0x32, 0x32, 0x32, 0xff, 0xc0, 0x00, 0x11, 0x08, 0x00, 0x19, 0x00, 0x06, 0x03,
1493 0x01, 0x22, 0x00, 0x02, 0x11, 0x01, 0x03, 0x11, 0x01, 0xff, 0xc4, 0x00, 0x1f, 0x00,
1494 0x00, 0x01, 0x05, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00,
1495 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b,
1496 0xff, 0xc4, 0x00, 0xb5, 0x10, 0x00, 0x02, 0x01, 0x03, 0x03, 0x02, 0x04, 0x03, 0x05,
1497 0x05, 0x04, 0x04, 0x00, 0x00, 0x01, 0x7d, 0x01, 0x02, 0x03, 0x00, 0x04, 0x11, 0x05,
1498 0x12, 0x21, 0x31, 0x41, 0x06, 0x13, 0x51, 0x61, 0x07, 0x22, 0x71, 0x14, 0x32, 0x81,
1499 0x91, 0xa1, 0x08, 0x23, 0x42, 0xb1, 0xc1, 0x15, 0x52, 0xd1, 0xf0, 0x24, 0x33, 0x62,
1500 0x72, 0x82, 0x09, 0x0a, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x25, 0x26, 0x27, 0x28, 0x29,
1501 0x2a, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48,
1502 0x49, 0x4a, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5a, 0x63, 0x64, 0x65, 0x66,
1503 0x67, 0x68, 0x69, 0x6a, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, 0x83, 0x84,
1504 0x85, 0x86, 0x87, 0x88, 0x89, 0x8a, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99,
1505 0x9a, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, 0xa9, 0xaa, 0xb2, 0xb3, 0xb4, 0xb5,
1506 0xb6, 0xb7, 0xb8, 0xb9, 0xba, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, 0xc7, 0xc8, 0xc9, 0xca,
1507 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7, 0xd8, 0xd9, 0xda, 0xe1, 0xe2, 0xe3, 0xe4, 0xe5,
1508 0xe6, 0xe7, 0xe8, 0xe9, 0xea, 0xf1, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9,
1509 0xfa, 0xff, 0xc4, 0x00, 0x1f, 0x01, 0x00, 0x03, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,
1510 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05,
1511 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0xff, 0xc4, 0x00, 0xb5, 0x11, 0x00, 0x02, 0x01,
1512 0x02, 0x04, 0x04, 0x03, 0x04, 0x07, 0x05, 0x04, 0x04, 0x00, 0x01, 0x02, 0x77, 0x00,
1513 0x01, 0x02, 0x03, 0x11, 0x04, 0x05, 0x21, 0x31, 0x06, 0x12, 0x41, 0x51, 0x07, 0x61,
1514 0x71, 0x13, 0x22, 0x32, 0x81, 0x08, 0x14, 0x42, 0x91, 0xa1, 0xb1, 0xc1, 0x09, 0x23,
1515 0x33, 0x52, 0xf0, 0x15, 0x62, 0x72, 0xd1, 0x0a, 0x16, 0x24, 0x34, 0xe1, 0x25, 0xf1,
1516 0x17, 0x18, 0x19, 0x1a, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x35, 0x36, 0x37, 0x38, 0x39,
1517 0x3a, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x53, 0x54, 0x55, 0x56, 0x57,
1518 0x58, 0x59, 0x5a, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6a, 0x73, 0x74, 0x75,
1519 0x76, 0x77, 0x78, 0x79, 0x7a, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8a,
1520 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9a, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6,
1521 0xa7, 0xa8, 0xa9, 0xaa, 0xb2, 0xb3, 0xb4, 0xb5, 0xb6, 0xb7, 0xb8, 0xb9, 0xba, 0xc2,
1522 0xc3, 0xc4, 0xc5, 0xc6, 0xc7, 0xc8, 0xc9, 0xca, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7,
1523 0xd8, 0xd9, 0xda, 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea, 0xf2, 0xf3,
1524 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa, 0xff, 0xda, 0x00, 0x0c, 0x03, 0x01, 0x00,
1525 0x02, 0x11, 0x03, 0x11, 0x00, 0x3f, 0x00, 0xaf, 0xf0, 0x53, 0xfe, 0x44, 0xdb, 0xcf,
1526 0xfb, 0x08, 0x3f, 0xfe, 0x8b, 0x8e, 0x8a, 0x3e, 0x0a, 0x7f, 0xc8, 0x9b, 0x79, 0xff,
1527 0x00, 0x61, 0x07, 0xff, 0x00, 0xd1, 0x71, 0xd1, 0x40, 0x07, 0xc1, 0x4f, 0xf9, 0x13,
1528 0x6f, 0x3f, 0xec, 0x20, 0xff, 0x00, 0xfa, 0x2e, 0x3a, 0x28, 0xf8, 0x29, 0xff, 0x00,
1529 0x22, 0x6d, 0xe7, 0xfd, 0x84, 0x1f, 0xff, 0x00, 0x45, 0xc7, 0x45, 0x00, 0x7f, 0xff,
1530 0xd9,
1531 ];
1532 let image = PositionedElement::Image {
1533 rect: Rect {
1534 x: 0.0,
1535 y: 0.0,
1536 width: 6.0,
1537 height: 25.0,
1538 },
1539 data: jpeg.clone(),
1540 content_type: "image/jpeg".to_owned(),
1541 media_id: MediaId(1),
1542 };
1543 let decoded =
1544 decode_jpeg_pixmap(&jpeg, 6, 25).expect("valid JPEG fixture must decode to a pixmap");
1545 assert_eq!(rgb(decoded.pixel(0, 0).unwrap()), (1, 1, 1));
1546 assert_eq!(rgb(decoded.pixel(5, 0).unwrap()), (192, 192, 192));
1547 let pixmap = render_page_to_pixmap(&PageFrame::new(1, 6.0, 25.0, vec![image]), &[], 72.0)
1548 .expect("JPEG page must rasterise");
1549
1550 assert_eq!(rgb(pixmap.pixel(0, 0).unwrap()), (1, 1, 1));
1551 assert_eq!(rgb(pixmap.pixel(5, 0).unwrap()), (192, 192, 192));
1552 }
1553
1554 #[test]
1555 fn rotated_rectangle_has_a_filled_interior_and_empty_corner() {
1556 let element = PositionedElement::Group(GroupElement {
1557 transform: Transform::rotate_about(45.0, 16.0, 16.0),
1558 clip: None,
1559 opacity: 1.0,
1560 effects: Vec::new(),
1561 children: vec![solid_path(
1562 Path::rect(Rect {
1563 x: 10.0,
1564 y: 12.0,
1565 width: 12.0,
1566 height: 8.0,
1567 }),
1568 Color::BLACK,
1569 )],
1570 });
1571 let pixmap = render_page_to_pixmap(&page(vec![element]), &[], 72.0).unwrap();
1572
1573 assert_eq!(rgb(pixmap.pixel(16, 16).unwrap()), (0, 0, 0));
1574 assert_eq!(rgb(pixmap.pixel(9, 9).unwrap()), (255, 255, 255));
1575 }
1576
1577 #[test]
1578 fn dashed_line_contains_deterministic_gaps() {
1579 let element = PositionedElement::Line {
1580 start: Point { x: 2.0, y: 4.5 },
1581 end: Point { x: 30.0, y: 4.5 },
1582 width: 1.0,
1583 color: Color::BLACK,
1584 dash_pattern: Some((4.0, 4.0)),
1585 };
1586 let pixmap = render_page_to_pixmap(&page(vec![element]), &[], 72.0).unwrap();
1587
1588 assert_eq!(rgb(pixmap.pixel(3, 4).unwrap()), (0, 0, 0));
1589 assert_eq!(rgb(pixmap.pixel(7, 4).unwrap()), (255, 255, 255));
1590 assert_eq!(rgb(pixmap.pixel(11, 4).unwrap()), (0, 0, 0));
1591 }
1592
1593 #[test]
1594 fn nested_group_transforms_apply_child_before_parent() {
1595 let translated = |transform, children| {
1596 PositionedElement::Group(GroupElement {
1597 transform,
1598 clip: None,
1599 opacity: 1.0,
1600 effects: Vec::new(),
1601 children,
1602 })
1603 };
1604 let leaf = solid_path(
1605 Path::rect(Rect {
1606 x: 0.0,
1607 y: 0.0,
1608 width: 2.0,
1609 height: 2.0,
1610 }),
1611 Color::BLACK,
1612 );
1613 let element = translated(
1614 Transform {
1615 e: 4.0,
1616 ..Transform::IDENTITY
1617 },
1618 vec![translated(
1619 Transform {
1620 e: 3.0,
1621 ..Transform::IDENTITY
1622 },
1623 vec![translated(
1624 Transform {
1625 f: 5.0,
1626 ..Transform::IDENTITY
1627 },
1628 vec![leaf],
1629 )],
1630 )],
1631 );
1632 let pixmap = render_page_to_pixmap(&page(vec![element]), &[], 72.0).unwrap();
1633
1634 assert_eq!(rgb(pixmap.pixel(7, 5).unwrap()), (0, 0, 0));
1635 assert_eq!(rgb(pixmap.pixel(1, 1).unwrap()), (255, 255, 255));
1636 }
1637
1638 #[test]
1639 fn group_clip_masks_children_and_preserves_outside_pixels() {
1640 let element = PositionedElement::Group(GroupElement {
1641 transform: Transform::IDENTITY,
1642 clip: Some(Path::rect(Rect {
1643 x: 4.0,
1644 y: 4.0,
1645 width: 8.0,
1646 height: 12.0,
1647 })),
1648 opacity: 1.0,
1649 effects: Vec::new(),
1650 children: vec![PositionedElement::Group(GroupElement {
1651 transform: Transform::IDENTITY,
1652 clip: Some(Path::rect(Rect {
1653 x: 8.0,
1654 y: 4.0,
1655 width: 8.0,
1656 height: 12.0,
1657 })),
1658 opacity: 1.0,
1659 effects: Vec::new(),
1660 children: vec![solid_path(
1661 Path::rect(Rect {
1662 x: 0.0,
1663 y: 0.0,
1664 width: 24.0,
1665 height: 24.0,
1666 }),
1667 Color::BLACK,
1668 )],
1669 })],
1670 });
1671 let pixmap = render_page_to_pixmap(&page(vec![element]), &[], 72.0).unwrap();
1672
1673 assert_eq!(rgb(pixmap.pixel(10, 10).unwrap()), (0, 0, 0));
1674 assert_eq!(rgb(pixmap.pixel(6, 10).unwrap()), (255, 255, 255));
1675 assert_eq!(rgb(pixmap.pixel(14, 10).unwrap()), (255, 255, 255));
1676 }
1677
1678 #[test]
1679 fn group_opacity_is_applied_once_to_the_composited_subtree() {
1680 let element = PositionedElement::Group(GroupElement {
1681 transform: Transform::IDENTITY,
1682 clip: None,
1683 opacity: 0.5,
1684 effects: Vec::new(),
1685 children: vec![
1686 solid_path(
1687 Path::rect(Rect {
1688 x: 2.0,
1689 y: 2.0,
1690 width: 12.0,
1691 height: 12.0,
1692 }),
1693 Color::BLACK,
1694 ),
1695 solid_path(
1696 Path::rect(Rect {
1697 x: 8.0,
1698 y: 2.0,
1699 width: 12.0,
1700 height: 12.0,
1701 }),
1702 Color::BLACK,
1703 ),
1704 ],
1705 });
1706 let pixmap = render_page_to_pixmap(&page(vec![element]), &[], 72.0).unwrap();
1707
1708 assert_eq!(rgb(pixmap.pixel(4, 4).unwrap()), (128, 128, 128));
1709 assert_eq!(rgb(pixmap.pixel(10, 4).unwrap()), (128, 128, 128));
1710 }
1711
1712 #[test]
1713 fn outer_shadow_is_coloured_offset_and_behind_group_content() {
1714 let element = PositionedElement::Group(GroupElement {
1715 transform: Transform::IDENTITY,
1716 clip: None,
1717 opacity: 1.0,
1718 effects: vec![Effect::OuterShadow {
1719 dx: 4.0,
1720 dy: 0.0,
1721 blur: 0.0,
1722 color: Color {
1723 r: 1.0,
1724 g: 0.0,
1725 b: 0.0,
1726 a: 1.0,
1727 },
1728 }],
1729 children: vec![solid_path(
1730 Path::rect(Rect {
1731 x: 4.0,
1732 y: 4.0,
1733 width: 8.0,
1734 height: 8.0,
1735 }),
1736 Color::BLACK,
1737 )],
1738 });
1739 let pixmap = render_page_to_pixmap(&page(vec![element]), &[], 72.0).unwrap();
1740
1741 assert_eq!(rgb(pixmap.pixel(6, 6).unwrap()), (0, 0, 0));
1742 assert_eq!(rgb(pixmap.pixel(14, 6).unwrap()), (255, 0, 0));
1743 assert_eq!(rgb(pixmap.pixel(2, 6).unwrap()), (255, 255, 255));
1744 }
1745
1746 #[test]
1747 fn outer_shadow_blur_extends_alpha_and_obeys_group_opacity() {
1748 let element = PositionedElement::Group(GroupElement {
1749 transform: Transform::IDENTITY,
1750 clip: None,
1751 opacity: 0.5,
1752 effects: vec![Effect::OuterShadow {
1753 dx: 0.0,
1754 dy: 0.0,
1755 blur: 2.0,
1756 color: Color::BLACK,
1757 }],
1758 children: vec![solid_path(
1759 Path::rect(Rect {
1760 x: 12.0,
1761 y: 12.0,
1762 width: 4.0,
1763 height: 4.0,
1764 }),
1765 Color::BLACK,
1766 )],
1767 });
1768 let pixmap = render_page_to_pixmap(&page(vec![element]), &[], 72.0).unwrap();
1769
1770 assert_eq!(rgb(pixmap.pixel(13, 13).unwrap()), (128, 128, 128));
1771 let blurred = rgb(pixmap.pixel(10, 13).unwrap());
1772 assert!(blurred.0 < 255 && blurred.0 > 128, "{blurred:?}");
1773 assert_eq!(blurred.0, blurred.1);
1774 assert_eq!(blurred.1, blurred.2);
1775 assert_eq!(rgb(pixmap.pixel(8, 13).unwrap()), (255, 255, 255));
1776 }
1777
1778 #[test]
1779 fn path_fill_rule_selects_the_tiny_skia_rule() {
1780 let nested = |fill_rule| Path {
1781 commands: vec![
1782 PathCommand::MoveTo(Point { x: 2.0, y: 2.0 }),
1783 PathCommand::LineTo(Point { x: 14.0, y: 2.0 }),
1784 PathCommand::LineTo(Point { x: 14.0, y: 14.0 }),
1785 PathCommand::LineTo(Point { x: 2.0, y: 14.0 }),
1786 PathCommand::Close,
1787 PathCommand::MoveTo(Point { x: 5.0, y: 5.0 }),
1788 PathCommand::LineTo(Point { x: 11.0, y: 5.0 }),
1789 PathCommand::LineTo(Point { x: 11.0, y: 11.0 }),
1790 PathCommand::LineTo(Point { x: 5.0, y: 11.0 }),
1791 PathCommand::Close,
1792 ],
1793 fill_rule,
1794 };
1795 let nonzero = render_page_to_pixmap(
1796 &page(vec![solid_path(nested(FillRule::NonZero), Color::BLACK)]),
1797 &[],
1798 72.0,
1799 )
1800 .unwrap();
1801 let evenodd = render_page_to_pixmap(
1802 &page(vec![solid_path(nested(FillRule::EvenOdd), Color::BLACK)]),
1803 &[],
1804 72.0,
1805 )
1806 .unwrap();
1807
1808 assert_eq!(rgb(nonzero.pixel(8, 8).unwrap()), (0, 0, 0));
1809 assert_eq!(rgb(evenodd.pixel(8, 8).unwrap()), (255, 255, 255));
1810 }
1811
1812 #[test]
1813 fn linear_and_radial_gradients_sample_expected_colours() {
1814 let stops = vec![
1815 GradientStop {
1816 offset: 0.0,
1817 color: Color {
1818 r: 1.0,
1819 g: 0.0,
1820 b: 0.0,
1821 a: 1.0,
1822 },
1823 },
1824 GradientStop {
1825 offset: 1.0,
1826 color: Color {
1827 r: 0.0,
1828 g: 0.0,
1829 b: 1.0,
1830 a: 1.0,
1831 },
1832 },
1833 ];
1834 let linear = PositionedElement::Path(PathElement {
1835 path: Path::rect(Rect {
1836 x: 0.0,
1837 y: 0.0,
1838 width: 16.0,
1839 height: 8.0,
1840 }),
1841 fill: Some(Paint::linear(
1842 Point { x: 0.0, y: 0.0 },
1843 Point { x: 16.0, y: 0.0 },
1844 stops.clone(),
1845 (true, true),
1846 )),
1847 stroke: None,
1848 });
1849 let radial = PositionedElement::Path(PathElement {
1850 path: Path::rect(Rect {
1851 x: 0.0,
1852 y: 16.0,
1853 width: 16.0,
1854 height: 16.0,
1855 }),
1856 fill: Some(Paint::radial(
1857 Point { x: 8.0, y: 24.0 },
1858 8.0,
1859 Point { x: 8.0, y: 24.0 },
1860 stops,
1861 (true, true),
1862 )),
1863 stroke: None,
1864 });
1865 let pixmap = render_page_to_pixmap(&page(vec![linear, radial]), &[], 72.0).unwrap();
1866
1867 let linear_start = rgb(pixmap.pixel(1, 4).unwrap());
1868 let linear_end = rgb(pixmap.pixel(14, 4).unwrap());
1869 assert!(linear_start.0 > linear_start.2);
1870 assert!(linear_end.2 > linear_end.0);
1871 let radial_center = rgb(pixmap.pixel(8, 24).unwrap());
1872 let radial_edge = rgb(pixmap.pixel(1, 24).unwrap());
1873 assert!(radial_center.0 > radial_center.2);
1874 assert!(radial_edge.2 > radial_edge.0);
1875 }
1876
1877 #[test]
1878 fn translated_group_gradient_uses_local_coordinates_exactly_once() {
1879 let red = Color {
1880 r: 1.0,
1881 g: 0.0,
1882 b: 0.0,
1883 a: 1.0,
1884 };
1885 let blue = Color {
1886 r: 0.0,
1887 g: 0.0,
1888 b: 1.0,
1889 a: 1.0,
1890 };
1891 let element = PositionedElement::Group(GroupElement {
1892 transform: Transform {
1893 e: 12.0,
1894 ..Transform::IDENTITY
1895 },
1896 clip: None,
1897 opacity: 1.0,
1898 effects: Vec::new(),
1899 children: vec![PositionedElement::Path(PathElement {
1900 path: Path::rect(Rect {
1901 x: 0.0,
1902 y: 2.0,
1903 width: 8.0,
1904 height: 8.0,
1905 }),
1906 fill: Some(Paint::linear(
1907 Point { x: 0.0, y: 0.0 },
1908 Point { x: 8.0, y: 0.0 },
1909 vec![
1910 GradientStop {
1911 offset: 0.0,
1912 color: red,
1913 },
1914 GradientStop {
1915 offset: 1.0,
1916 color: blue,
1917 },
1918 ],
1919 (true, true),
1920 )),
1921 stroke: None,
1922 })],
1923 });
1924 let pixmap = render_page_to_pixmap(&page(vec![element]), &[], 72.0).unwrap();
1925
1926 let start = rgb(pixmap.pixel(13, 6).unwrap());
1927 let end = rgb(pixmap.pixel(18, 6).unwrap());
1928 assert!(
1929 start.0 > start.2,
1930 "expected red-dominant start, got {start:?}"
1931 );
1932 assert!(end.2 > end.0, "expected blue-dominant end, got {end:?}");
1933 assert_eq!(rgb(pixmap.pixel(9, 6).unwrap()), (255, 255, 255));
1934 }
1935
1936 #[test]
1937 fn gradient_extend_flags_leave_outside_regions_unpainted() {
1938 let stops = vec![
1939 GradientStop {
1940 offset: 0.0,
1941 color: Color::BLACK,
1942 },
1943 GradientStop {
1944 offset: 1.0,
1945 color: Color::WHITE,
1946 },
1947 ];
1948 let linear = PositionedElement::Path(PathElement {
1949 path: Path::rect(Rect {
1950 x: 0.0,
1951 y: 0.0,
1952 width: 32.0,
1953 height: 12.0,
1954 }),
1955 fill: Some(Paint::linear(
1956 Point { x: 8.0, y: 0.0 },
1957 Point { x: 24.0, y: 0.0 },
1958 stops.clone(),
1959 (false, false),
1960 )),
1961 stroke: None,
1962 });
1963 let radial = PositionedElement::Path(PathElement {
1964 path: Path::rect(Rect {
1965 x: 0.0,
1966 y: 16.0,
1967 width: 32.0,
1968 height: 16.0,
1969 }),
1970 fill: Some(Paint::radial(
1971 Point { x: 16.0, y: 24.0 },
1972 6.0,
1973 Point { x: 16.0, y: 24.0 },
1974 stops,
1975 (false, false),
1976 )),
1977 stroke: None,
1978 });
1979 let pixmap = render_page_to_pixmap(&page(vec![linear, radial]), &[], 72.0).unwrap();
1980
1981 assert_eq!(rgb(pixmap.pixel(4, 6).unwrap()), (255, 255, 255));
1982 assert_ne!(rgb(pixmap.pixel(12, 6).unwrap()), (255, 255, 255));
1983 assert_ne!(rgb(pixmap.pixel(16, 24).unwrap()), (255, 255, 255));
1984 assert_eq!(rgb(pixmap.pixel(24, 24).unwrap()), (255, 255, 255));
1985 }
1986
1987 #[test]
1988 fn gradient_stops_are_sorted_clamped_and_last_repeated_offset_wins() {
1989 let element = PositionedElement::Path(PathElement {
1990 path: Path::rect(Rect {
1991 x: 0.0,
1992 y: 0.0,
1993 width: 16.0,
1994 height: 8.0,
1995 }),
1996 fill: Some(Paint::linear(
1997 Point { x: 0.0, y: 0.0 },
1998 Point { x: 16.0, y: 0.0 },
1999 vec![
2000 GradientStop {
2001 offset: 2.0,
2002 color: Color {
2003 r: 0.0,
2004 g: 0.0,
2005 b: 1.0,
2006 a: 1.0,
2007 },
2008 },
2009 GradientStop {
2010 offset: -1.0,
2011 color: Color {
2012 r: 1.0,
2013 g: 0.0,
2014 b: 0.0,
2015 a: 1.0,
2016 },
2017 },
2018 GradientStop {
2019 offset: f64::NAN,
2020 color: Color {
2021 r: 0.0,
2022 g: 1.0,
2023 b: 0.0,
2024 a: 1.0,
2025 },
2026 },
2027 ],
2028 (true, true),
2029 )),
2030 stroke: None,
2031 });
2032 let pixmap = render_page_to_pixmap(&page(vec![element]), &[], 72.0).unwrap();
2033
2034 let start = rgb(pixmap.pixel(1, 4).unwrap());
2035 let end = rgb(pixmap.pixel(14, 4).unwrap());
2036 assert!(start.1 > start.0 && start.1 > start.2);
2037 assert!(end.2 > end.0 && end.2 > end.1);
2038 }
2039
2040 #[test]
2041 fn page_background_replaces_the_white_default() {
2042 let mut painted = page(Vec::new());
2043 painted.background = Some(Paint::Solid(Color {
2044 r: 0.0,
2045 g: 1.0,
2046 b: 0.0,
2047 a: 1.0,
2048 }));
2049 let painted = render_page_to_pixmap(&painted, &[], 72.0).unwrap();
2050 let defaulted = render_page_to_pixmap(&page(Vec::new()), &[], 72.0).unwrap();
2051 let mut gradient = page(Vec::new());
2052 gradient.background = Some(Paint::linear(
2053 Point { x: 0.0, y: 0.0 },
2054 Point { x: 32.0, y: 0.0 },
2055 vec![
2056 GradientStop {
2057 offset: 0.0,
2058 color: Color::BLACK,
2059 },
2060 GradientStop {
2061 offset: 1.0,
2062 color: Color::WHITE,
2063 },
2064 ],
2065 (true, true),
2066 ));
2067 let gradient = render_page_to_pixmap(&gradient, &[], 72.0).unwrap();
2068
2069 assert_eq!(rgb(painted.pixel(4, 4).unwrap()), (0, 255, 0));
2070 assert_eq!(rgb(defaulted.pixel(4, 4).unwrap()), (255, 255, 255));
2071 assert!(gradient.pixel(4, 4).unwrap().red() < gradient.pixel(28, 4).unwrap().red());
2072 }
2073
2074 #[test]
2075 fn path_dash_array_contains_deterministic_gaps() {
2076 let mut stroke = Stroke::new(Paint::Solid(Color::BLACK), 1.0);
2077 stroke.dash = Some(vec![4.0, 4.0]);
2078 let element = PositionedElement::Path(PathElement {
2079 path: Path {
2080 commands: vec![
2081 PathCommand::MoveTo(Point { x: 2.0, y: 8.5 }),
2082 PathCommand::LineTo(Point { x: 30.0, y: 8.5 }),
2083 ],
2084 fill_rule: FillRule::NonZero,
2085 },
2086 fill: None,
2087 stroke: Some(stroke),
2088 });
2089 let pixmap = render_page_to_pixmap(&page(vec![element]), &[], 72.0).unwrap();
2090
2091 assert_eq!(rgb(pixmap.pixel(3, 8).unwrap()), (0, 0, 0));
2092 assert_eq!(rgb(pixmap.pixel(7, 8).unwrap()), (255, 255, 255));
2093 }
2094}