1 | // Copyright 2006 The Android Open Source Project |
2 | // Copyright 2020 Yevhenii Reizner |
3 | // |
4 | // Use of this source code is governed by a BSD-style license that can be |
5 | // found in the LICENSE file. |
6 | |
7 | use crate::*; |
8 | |
9 | use tiny_skia_path::{PathStroker, Scalar, SCALAR_MAX}; |
10 | |
11 | use crate::geom::ScreenIntRect; |
12 | use crate::mask::SubMaskRef; |
13 | use crate::pipeline::{RasterPipelineBlitter, RasterPipelineBuilder}; |
14 | use crate::pixmap::SubPixmapMut; |
15 | use crate::scan; |
16 | |
17 | use crate::geom::IntSizeExt; |
18 | #[cfg (all(not(feature = "std" ), feature = "no-std-float" ))] |
19 | use tiny_skia_path::NoStdFloat; |
20 | |
21 | /// A path filling rule. |
22 | #[derive (Copy, Clone, PartialEq, Debug)] |
23 | pub enum FillRule { |
24 | /// Specifies that "inside" is computed by a non-zero sum of signed edge crossings. |
25 | Winding, |
26 | /// Specifies that "inside" is computed by an odd number of edge crossings. |
27 | EvenOdd, |
28 | } |
29 | |
30 | impl Default for FillRule { |
31 | fn default() -> Self { |
32 | FillRule::Winding |
33 | } |
34 | } |
35 | |
36 | /// Controls how a shape should be painted. |
37 | #[derive (Clone, PartialEq, Debug)] |
38 | pub struct Paint<'a> { |
39 | /// A paint shader. |
40 | /// |
41 | /// Default: black color |
42 | pub shader: Shader<'a>, |
43 | |
44 | /// Paint blending mode. |
45 | /// |
46 | /// Default: SourceOver |
47 | pub blend_mode: BlendMode, |
48 | |
49 | /// Enables anti-aliased painting. |
50 | /// |
51 | /// Default: true |
52 | pub anti_alias: bool, |
53 | |
54 | /// Forces the high quality/precision rendering pipeline. |
55 | /// |
56 | /// `tiny-skia`, just like Skia, has two rendering pipelines: |
57 | /// one uses `f32` and another one uses `u16`. `u16` one is usually way faster, |
58 | /// but less precise. Which can lead to slight differences. |
59 | /// |
60 | /// By default, `tiny-skia` will choose the pipeline automatically, |
61 | /// depending on a blending mode and other parameters. |
62 | /// But you can force the high quality one using this flag. |
63 | /// |
64 | /// This feature is especially useful during testing. |
65 | /// |
66 | /// Unlike high quality pipeline, the low quality one doesn't support all |
67 | /// rendering stages, therefore we cannot force it like hq one. |
68 | /// |
69 | /// Default: false |
70 | pub force_hq_pipeline: bool, |
71 | } |
72 | |
73 | impl Default for Paint<'_> { |
74 | fn default() -> Self { |
75 | Paint { |
76 | shader: Shader::SolidColor(Color::BLACK), |
77 | blend_mode: BlendMode::default(), |
78 | anti_alias: true, |
79 | force_hq_pipeline: false, |
80 | } |
81 | } |
82 | } |
83 | |
84 | impl<'a> Paint<'a> { |
85 | /// Sets a paint source to a solid color. |
86 | pub fn set_color(&mut self, color: Color) { |
87 | self.shader = Shader::SolidColor(color); |
88 | } |
89 | |
90 | /// Sets a paint source to a solid color. |
91 | /// |
92 | /// `self.shader = Shader::SolidColor(Color::from_rgba8(50, 127, 150, 200));` shorthand. |
93 | pub fn set_color_rgba8(&mut self, r: u8, g: u8, b: u8, a: u8) { |
94 | self.set_color(Color::from_rgba8(r, g, b, a)) |
95 | } |
96 | |
97 | /// Checks that the paint source is a solid color. |
98 | pub fn is_solid_color(&self) -> bool { |
99 | matches!(self.shader, Shader::SolidColor(_)) |
100 | } |
101 | } |
102 | |
103 | impl Pixmap { |
104 | /// Draws a filled rectangle onto the pixmap. |
105 | /// |
106 | /// See [`PixmapMut::fill_rect`](struct.PixmapMut.html#method.fill_rect) for details. |
107 | pub fn fill_rect( |
108 | &mut self, |
109 | rect: Rect, |
110 | paint: &Paint, |
111 | transform: Transform, |
112 | mask: Option<&Mask>, |
113 | ) { |
114 | self.as_mut().fill_rect(rect, paint, transform, mask); |
115 | } |
116 | |
117 | /// Draws a filled path onto the pixmap. |
118 | /// |
119 | /// See [`PixmapMut::fill_path`](struct.PixmapMut.html#method.fill_path) for details. |
120 | pub fn fill_path( |
121 | &mut self, |
122 | path: &Path, |
123 | paint: &Paint, |
124 | fill_rule: FillRule, |
125 | transform: Transform, |
126 | mask: Option<&Mask>, |
127 | ) { |
128 | self.as_mut() |
129 | .fill_path(path, paint, fill_rule, transform, mask); |
130 | } |
131 | |
132 | /// Strokes a path. |
133 | /// |
134 | /// See [`PixmapMut::stroke_path`](struct.PixmapMut.html#method.stroke_path) for details. |
135 | pub fn stroke_path( |
136 | &mut self, |
137 | path: &Path, |
138 | paint: &Paint, |
139 | stroke: &Stroke, |
140 | transform: Transform, |
141 | mask: Option<&Mask>, |
142 | ) { |
143 | self.as_mut() |
144 | .stroke_path(path, paint, stroke, transform, mask); |
145 | } |
146 | |
147 | /// Draws a `Pixmap` on top of the current `Pixmap`. |
148 | /// |
149 | /// See [`PixmapMut::draw_pixmap`](struct.PixmapMut.html#method.draw_pixmap) for details. |
150 | pub fn draw_pixmap( |
151 | &mut self, |
152 | x: i32, |
153 | y: i32, |
154 | pixmap: PixmapRef, |
155 | paint: &PixmapPaint, |
156 | transform: Transform, |
157 | mask: Option<&Mask>, |
158 | ) { |
159 | self.as_mut() |
160 | .draw_pixmap(x, y, pixmap, paint, transform, mask); |
161 | } |
162 | |
163 | /// Applies a masks. |
164 | /// |
165 | /// See [`PixmapMut::apply_mask`](struct.PixmapMut.html#method.apply_mask) for details. |
166 | pub fn apply_mask(&mut self, mask: &Mask) { |
167 | self.as_mut().apply_mask(mask); |
168 | } |
169 | } |
170 | |
171 | impl PixmapMut<'_> { |
172 | // TODO: accept NonZeroRect? |
173 | /// Draws a filled rectangle onto the pixmap. |
174 | /// |
175 | /// This function is usually slower than filling a rectangular path, |
176 | /// but it produces better results. Mainly it doesn't suffer from weird |
177 | /// clipping of horizontal/vertical edges. |
178 | /// |
179 | /// Used mainly to render a pixmap onto a pixmap. |
180 | /// |
181 | /// Returns `None` when there is nothing to fill or in case of a numeric overflow. |
182 | pub fn fill_rect( |
183 | &mut self, |
184 | rect: Rect, |
185 | paint: &Paint, |
186 | transform: Transform, |
187 | mask: Option<&Mask>, |
188 | ) { |
189 | // TODO: we probably can use tiler for rect too |
190 | if transform.is_identity() && !DrawTiler::required(self.width(), self.height()) { |
191 | // TODO: ignore rects outside the pixmap |
192 | |
193 | let clip = self.size().to_screen_int_rect(0, 0); |
194 | |
195 | let mask = mask.map(|mask| mask.as_submask()); |
196 | let mut subpix = self.as_subpixmap(); |
197 | let mut blitter = match RasterPipelineBlitter::new(paint, mask, &mut subpix) { |
198 | Some(v) => v, |
199 | None => return, // nothing to do, all good |
200 | }; |
201 | |
202 | if paint.anti_alias { |
203 | scan::fill_rect_aa(&rect, &clip, &mut blitter); |
204 | } else { |
205 | scan::fill_rect(&rect, &clip, &mut blitter); |
206 | } |
207 | } else { |
208 | let path = PathBuilder::from_rect(rect); |
209 | self.fill_path(&path, paint, FillRule::Winding, transform, mask); |
210 | } |
211 | } |
212 | |
213 | /// Draws a filled path onto the pixmap. |
214 | pub fn fill_path( |
215 | &mut self, |
216 | path: &Path, |
217 | paint: &Paint, |
218 | fill_rule: FillRule, |
219 | transform: Transform, |
220 | mask: Option<&Mask>, |
221 | ) { |
222 | if transform.is_identity() { |
223 | // This is sort of similar to SkDraw::drawPath |
224 | |
225 | // Skip empty paths and horizontal/vertical lines. |
226 | let path_bounds = path.bounds(); |
227 | if path_bounds.width().is_nearly_zero() || path_bounds.height().is_nearly_zero() { |
228 | log::warn!("empty paths and horizontal/vertical lines cannot be filled" ); |
229 | return; |
230 | } |
231 | |
232 | if is_too_big_for_math(path) { |
233 | log::warn!("path coordinates are too big" ); |
234 | return; |
235 | } |
236 | |
237 | // TODO: ignore paths outside the pixmap |
238 | |
239 | if let Some(tiler) = DrawTiler::new(self.width(), self.height()) { |
240 | let mut path = path.clone(); // TODO: avoid cloning |
241 | let mut paint = paint.clone(); |
242 | |
243 | for tile in tiler { |
244 | let ts = Transform::from_translate(-(tile.x() as f32), -(tile.y() as f32)); |
245 | path = match path.transform(ts) { |
246 | Some(v) => v, |
247 | None => { |
248 | log::warn!("path transformation failed" ); |
249 | return; |
250 | } |
251 | }; |
252 | paint.shader.transform(ts); |
253 | |
254 | let clip_rect = tile.size().to_screen_int_rect(0, 0); |
255 | let mut subpix = match self.subpixmap(tile.to_int_rect()) { |
256 | Some(v) => v, |
257 | None => continue, // technically unreachable |
258 | }; |
259 | |
260 | let submask = mask.and_then(|mask| mask.submask(tile.to_int_rect())); |
261 | let mut blitter = match RasterPipelineBlitter::new(&paint, submask, &mut subpix) |
262 | { |
263 | Some(v) => v, |
264 | None => continue, // nothing to do, all good |
265 | }; |
266 | |
267 | // We're ignoring "errors" here, because `fill_path` will return `None` |
268 | // when rendering a tile that doesn't have a path on it. |
269 | // Which is not an error in this case. |
270 | if paint.anti_alias { |
271 | scan::path_aa::fill_path(&path, fill_rule, &clip_rect, &mut blitter); |
272 | } else { |
273 | scan::path::fill_path(&path, fill_rule, &clip_rect, &mut blitter); |
274 | } |
275 | |
276 | let ts = Transform::from_translate(tile.x() as f32, tile.y() as f32); |
277 | path = match path.transform(ts) { |
278 | Some(v) => v, |
279 | None => return, // technically unreachable |
280 | }; |
281 | paint.shader.transform(ts); |
282 | } |
283 | } else { |
284 | let clip_rect = self.size().to_screen_int_rect(0, 0); |
285 | let submask = mask.map(|mask| mask.as_submask()); |
286 | let mut subpix = self.as_subpixmap(); |
287 | let mut blitter = match RasterPipelineBlitter::new(paint, submask, &mut subpix) { |
288 | Some(v) => v, |
289 | None => return, // nothing to do, all good |
290 | }; |
291 | |
292 | if paint.anti_alias { |
293 | scan::path_aa::fill_path(path, fill_rule, &clip_rect, &mut blitter); |
294 | } else { |
295 | scan::path::fill_path(path, fill_rule, &clip_rect, &mut blitter); |
296 | } |
297 | } |
298 | } else { |
299 | let path = match path.clone().transform(transform) { |
300 | Some(v) => v, |
301 | None => { |
302 | log::warn!("path transformation failed" ); |
303 | return; |
304 | } |
305 | }; |
306 | |
307 | let mut paint = paint.clone(); |
308 | paint.shader.transform(transform); |
309 | |
310 | self.fill_path(&path, &paint, fill_rule, Transform::identity(), mask) |
311 | } |
312 | } |
313 | |
314 | /// Strokes a path. |
315 | /// |
316 | /// Stroking is implemented using two separate algorithms: |
317 | /// |
318 | /// 1. If a stroke width is wider than 1px (after applying the transformation), |
319 | /// a path will be converted into a stroked path and then filled using `fill_path`. |
320 | /// Which means that we have to allocate a separate `Path`, that can be 2-3x larger |
321 | /// then the original path. |
322 | /// 2. If a stroke width is thinner than 1px (after applying the transformation), |
323 | /// we will use hairline stroking, which doesn't involve a separate path allocation. |
324 | /// |
325 | /// Also, if a `stroke` has a dash array, then path will be converted into |
326 | /// a dashed path first and then stroked. Which means a yet another allocation. |
327 | pub fn stroke_path( |
328 | &mut self, |
329 | path: &Path, |
330 | paint: &Paint, |
331 | stroke: &Stroke, |
332 | transform: Transform, |
333 | mask: Option<&Mask>, |
334 | ) { |
335 | if stroke.width < 0.0 { |
336 | log::warn!("negative stroke width isn't allowed" ); |
337 | return; |
338 | } |
339 | |
340 | let res_scale = PathStroker::compute_resolution_scale(&transform); |
341 | |
342 | let dash_path; |
343 | let path = if let Some(ref dash) = stroke.dash { |
344 | dash_path = match path.dash(dash, res_scale) { |
345 | Some(v) => v, |
346 | None => { |
347 | log::warn!("path dashing failed" ); |
348 | return; |
349 | } |
350 | }; |
351 | &dash_path |
352 | } else { |
353 | path |
354 | }; |
355 | |
356 | if let Some(coverage) = treat_as_hairline(paint, stroke, transform) { |
357 | let mut paint = paint.clone(); |
358 | if coverage == 1.0 { |
359 | // No changes to the `paint`. |
360 | } else if paint.blend_mode.should_pre_scale_coverage() { |
361 | // This is the old technique, which we preserve for now so |
362 | // we don't change previous results (testing) |
363 | // the new way seems fine, its just (a tiny bit) different. |
364 | let scale = (coverage * 256.0) as i32; |
365 | let new_alpha = (255 * scale) >> 8; |
366 | paint.shader.apply_opacity(new_alpha as f32 / 255.0); |
367 | } |
368 | |
369 | if let Some(tiler) = DrawTiler::new(self.width(), self.height()) { |
370 | let mut path = path.clone(); // TODO: avoid cloning |
371 | let mut paint = paint.clone(); |
372 | |
373 | if !transform.is_identity() { |
374 | paint.shader.transform(transform); |
375 | path = match path.transform(transform) { |
376 | Some(v) => v, |
377 | None => { |
378 | log::warn!("path transformation failed" ); |
379 | return; |
380 | } |
381 | }; |
382 | } |
383 | |
384 | for tile in tiler { |
385 | let ts = Transform::from_translate(-(tile.x() as f32), -(tile.y() as f32)); |
386 | path = match path.transform(ts) { |
387 | Some(v) => v, |
388 | None => { |
389 | log::warn!("path transformation failed" ); |
390 | return; |
391 | } |
392 | }; |
393 | paint.shader.transform(ts); |
394 | |
395 | let mut subpix = match self.subpixmap(tile.to_int_rect()) { |
396 | Some(v) => v, |
397 | None => continue, // technically unreachable |
398 | }; |
399 | let submask = mask.and_then(|mask| mask.submask(tile.to_int_rect())); |
400 | |
401 | // We're ignoring "errors" here, because `stroke_hairline` will return `None` |
402 | // when rendering a tile that doesn't have a path on it. |
403 | // Which is not an error in this case. |
404 | Self::stroke_hairline(&path, &paint, stroke.line_cap, submask, &mut subpix); |
405 | |
406 | let ts = Transform::from_translate(tile.x() as f32, tile.y() as f32); |
407 | path = match path.transform(ts) { |
408 | Some(v) => v, |
409 | None => return, |
410 | }; |
411 | paint.shader.transform(ts); |
412 | } |
413 | } else { |
414 | let subpix = &mut self.as_subpixmap(); |
415 | let submask = mask.map(|mask| mask.as_submask()); |
416 | if !transform.is_identity() { |
417 | paint.shader.transform(transform); |
418 | |
419 | // TODO: avoid clone |
420 | let path = match path.clone().transform(transform) { |
421 | Some(v) => v, |
422 | None => { |
423 | log::warn!("path transformation failed" ); |
424 | return; |
425 | } |
426 | }; |
427 | |
428 | Self::stroke_hairline(&path, &paint, stroke.line_cap, submask, subpix); |
429 | } else { |
430 | Self::stroke_hairline(path, &paint, stroke.line_cap, submask, subpix); |
431 | } |
432 | } |
433 | } else { |
434 | let path = match path.stroke(stroke, res_scale) { |
435 | Some(v) => v, |
436 | None => { |
437 | log::warn!("path stroking failed" ); |
438 | return; |
439 | } |
440 | }; |
441 | |
442 | self.fill_path(&path, paint, FillRule::Winding, transform, mask); |
443 | } |
444 | } |
445 | |
446 | /// A stroking for paths with subpixel/hairline width. |
447 | fn stroke_hairline( |
448 | path: &Path, |
449 | paint: &Paint, |
450 | line_cap: LineCap, |
451 | mask: Option<SubMaskRef>, |
452 | pixmap: &mut SubPixmapMut, |
453 | ) { |
454 | let clip = pixmap.size.to_screen_int_rect(0, 0); |
455 | let mut blitter = match RasterPipelineBlitter::new(paint, mask, pixmap) { |
456 | Some(v) => v, |
457 | None => return, // nothing to do, all good |
458 | }; |
459 | if paint.anti_alias { |
460 | scan::hairline_aa::stroke_path(path, line_cap, &clip, &mut blitter); |
461 | } else { |
462 | scan::hairline::stroke_path(path, line_cap, &clip, &mut blitter); |
463 | } |
464 | } |
465 | |
466 | /// Draws a `Pixmap` on top of the current `Pixmap`. |
467 | /// |
468 | /// The same as filling a rectangle with a `pixmap` pattern. |
469 | pub fn draw_pixmap( |
470 | &mut self, |
471 | x: i32, |
472 | y: i32, |
473 | pixmap: PixmapRef, |
474 | paint: &PixmapPaint, |
475 | transform: Transform, |
476 | mask: Option<&Mask>, |
477 | ) { |
478 | let rect = pixmap.size().to_int_rect(x, y).to_rect(); |
479 | |
480 | // TODO: SkSpriteBlitter |
481 | // TODO: partially clipped |
482 | // TODO: clipped out |
483 | |
484 | // Translate pattern as well as bounds. |
485 | let patt_transform = Transform::from_translate(x as f32, y as f32); |
486 | |
487 | let paint = Paint { |
488 | shader: Pattern::new( |
489 | pixmap, |
490 | SpreadMode::Pad, // Pad, otherwise we will get weird borders overlap. |
491 | paint.quality, |
492 | paint.opacity, |
493 | patt_transform, |
494 | ), |
495 | blend_mode: paint.blend_mode, |
496 | anti_alias: false, // Skia doesn't use it too. |
497 | force_hq_pipeline: false, // Pattern will use hq anyway. |
498 | }; |
499 | |
500 | self.fill_rect(rect, &paint, transform, mask); |
501 | } |
502 | |
503 | /// Applies a masks. |
504 | /// |
505 | /// When a `Mask` is passed to drawing methods, it will be used to mask-out |
506 | /// content we're about to draw. |
507 | /// This method masks-out an already drawn content. |
508 | /// It's not as fast, but can be useful when a mask is not available during drawing. |
509 | /// |
510 | /// This method is similar to filling the whole pixmap with an another, |
511 | /// mask-like pixmap using the `DestinationOut` blend mode. |
512 | /// |
513 | /// `Mask` must have the same size as `Pixmap`. No transform or offset are allowed. |
514 | pub fn apply_mask(&mut self, mask: &Mask) { |
515 | if self.size() != mask.size() { |
516 | log::warn!("Pixmap and Mask are expected to have the same size" ); |
517 | return; |
518 | } |
519 | |
520 | // Just a dummy. |
521 | let pixmap_src = PixmapRef::from_bytes(&[0, 0, 0, 0], 1, 1).unwrap(); |
522 | |
523 | let mut p = RasterPipelineBuilder::new(); |
524 | p.push(pipeline::Stage::LoadMaskU8); |
525 | p.push(pipeline::Stage::LoadDestination); |
526 | p.push(pipeline::Stage::DestinationIn); |
527 | p.push(pipeline::Stage::Store); |
528 | let mut p = p.compile(); |
529 | let rect = self.size().to_screen_int_rect(0, 0); |
530 | p.run( |
531 | &rect, |
532 | pipeline::AAMaskCtx::default(), |
533 | mask.as_submask().mask_ctx(), |
534 | pixmap_src, |
535 | &mut self.as_subpixmap(), |
536 | ); |
537 | } |
538 | } |
539 | |
540 | fn treat_as_hairline(paint: &Paint, stroke: &Stroke, mut ts: Transform) -> Option<f32> { |
541 | fn fast_len(p: Point) -> f32 { |
542 | let mut x = p.x.abs(); |
543 | let mut y = p.y.abs(); |
544 | if x < y { |
545 | core::mem::swap(&mut x, &mut y); |
546 | } |
547 | |
548 | x + y.half() |
549 | } |
550 | |
551 | debug_assert!(stroke.width >= 0.0); |
552 | |
553 | if stroke.width == 0.0 { |
554 | return Some(1.0); |
555 | } |
556 | |
557 | if !paint.anti_alias { |
558 | return None; |
559 | } |
560 | |
561 | // We don't care about translate. |
562 | ts.tx = 0.0; |
563 | ts.ty = 0.0; |
564 | |
565 | // We need to try to fake a thick-stroke with a modulated hairline. |
566 | let mut points = [ |
567 | Point::from_xy(stroke.width, 0.0), |
568 | Point::from_xy(0.0, stroke.width), |
569 | ]; |
570 | ts.map_points(&mut points); |
571 | |
572 | let len0 = fast_len(points[0]); |
573 | let len1 = fast_len(points[1]); |
574 | |
575 | if len0 <= 1.0 && len1 <= 1.0 { |
576 | return Some(len0.ave(len1)); |
577 | } |
578 | |
579 | None |
580 | } |
581 | |
582 | /// Sometimes in the drawing pipeline, we have to perform math on path coordinates, even after |
583 | /// the path is in device-coordinates. Tessellation and clipping are two examples. Usually this |
584 | /// is pretty modest, but it can involve subtracting/adding coordinates, or multiplying by |
585 | /// small constants (e.g. 2,3,4). To try to preflight issues where these optionations could turn |
586 | /// finite path values into infinities (or NaNs), we allow the upper drawing code to reject |
587 | /// the path if its bounds (in device coordinates) is too close to max float. |
588 | pub(crate) fn is_too_big_for_math(path: &Path) -> bool { |
589 | // This value is just a guess. smaller is safer, but we don't want to reject largish paths |
590 | // that we don't have to. |
591 | const SCALE_DOWN_TO_ALLOW_FOR_SMALL_MULTIPLIES: f32 = 0.25; |
592 | const MAX: f32 = SCALAR_MAX * SCALE_DOWN_TO_ALLOW_FOR_SMALL_MULTIPLIES; |
593 | |
594 | let b: Rect = path.bounds(); |
595 | |
596 | // use ! expression so we return true if bounds contains NaN |
597 | !(b.left() >= -MAX && b.top() >= -MAX && b.right() <= MAX && b.bottom() <= MAX) |
598 | } |
599 | |
600 | /// Splits the target pixmap into a list of tiles. |
601 | /// |
602 | /// Skia/tiny-skia uses a lot of fixed-point math during path rendering. |
603 | /// Probably more for precision than performance. |
604 | /// And our fixed-point types are limited by 8192 and 32768. |
605 | /// Which means that we cannot render a path larger than 8192 onto a pixmap. |
606 | /// When pixmap is smaller than 8192, the path will be automatically clipped anyway, |
607 | /// but for large pixmaps we have to render in tiles. |
608 | pub(crate) struct DrawTiler { |
609 | image_width: u32, |
610 | image_height: u32, |
611 | x_offset: u32, |
612 | y_offset: u32, |
613 | finished: bool, |
614 | } |
615 | |
616 | impl DrawTiler { |
617 | // 8K is 1 too big, since 8K << supersample == 32768 which is too big for Fixed. |
618 | const MAX_DIMENSIONS: u32 = 8192 - 1; |
619 | |
620 | fn required(image_width: u32, image_height: u32) -> bool { |
621 | image_width > Self::MAX_DIMENSIONS || image_height > Self::MAX_DIMENSIONS |
622 | } |
623 | |
624 | pub(crate) fn new(image_width: u32, image_height: u32) -> Option<Self> { |
625 | if Self::required(image_width, image_height) { |
626 | Some(DrawTiler { |
627 | image_width, |
628 | image_height, |
629 | x_offset: 0, |
630 | y_offset: 0, |
631 | finished: false, |
632 | }) |
633 | } else { |
634 | None |
635 | } |
636 | } |
637 | } |
638 | |
639 | impl Iterator for DrawTiler { |
640 | type Item = ScreenIntRect; |
641 | |
642 | fn next(&mut self) -> Option<Self::Item> { |
643 | if self.finished { |
644 | return None; |
645 | } |
646 | |
647 | // TODO: iterate only over tiles that actually affected by the shape |
648 | |
649 | if self.x_offset < self.image_width && self.y_offset < self.image_height { |
650 | let h = if self.y_offset < self.image_height { |
651 | (self.image_height - self.y_offset).min(Self::MAX_DIMENSIONS) |
652 | } else { |
653 | self.image_height |
654 | }; |
655 | |
656 | let r = ScreenIntRect::from_xywh( |
657 | self.x_offset, |
658 | self.y_offset, |
659 | (self.image_width - self.x_offset).min(Self::MAX_DIMENSIONS), |
660 | h, |
661 | ); |
662 | |
663 | self.x_offset += Self::MAX_DIMENSIONS; |
664 | if self.x_offset >= self.image_width { |
665 | self.x_offset = 0; |
666 | self.y_offset += Self::MAX_DIMENSIONS; |
667 | } |
668 | |
669 | return r; |
670 | } |
671 | |
672 | None |
673 | } |
674 | } |
675 | |
676 | #[cfg (test)] |
677 | mod tests { |
678 | use super::*; |
679 | const MAX_DIM: u32 = DrawTiler::MAX_DIMENSIONS; |
680 | |
681 | #[test ] |
682 | fn skip() { |
683 | assert!(DrawTiler::new(100, 500).is_none()); |
684 | } |
685 | |
686 | #[test ] |
687 | fn horizontal() { |
688 | let mut iter = DrawTiler::new(10000, 500).unwrap(); |
689 | assert_eq!(iter.next(), ScreenIntRect::from_xywh(0, 0, MAX_DIM, 500)); |
690 | assert_eq!( |
691 | iter.next(), |
692 | ScreenIntRect::from_xywh(MAX_DIM, 0, 10000 - MAX_DIM, 500) |
693 | ); |
694 | assert_eq!(iter.next(), None); |
695 | } |
696 | |
697 | #[test ] |
698 | fn vertical() { |
699 | let mut iter = DrawTiler::new(500, 10000).unwrap(); |
700 | assert_eq!(iter.next(), ScreenIntRect::from_xywh(0, 0, 500, MAX_DIM)); |
701 | assert_eq!( |
702 | iter.next(), |
703 | ScreenIntRect::from_xywh(0, MAX_DIM, 500, 10000 - MAX_DIM) |
704 | ); |
705 | assert_eq!(iter.next(), None); |
706 | } |
707 | |
708 | #[test ] |
709 | fn rect() { |
710 | let mut iter = DrawTiler::new(10000, 10000).unwrap(); |
711 | // Row 1 |
712 | assert_eq!( |
713 | iter.next(), |
714 | ScreenIntRect::from_xywh(0, 0, MAX_DIM, MAX_DIM) |
715 | ); |
716 | assert_eq!( |
717 | iter.next(), |
718 | ScreenIntRect::from_xywh(MAX_DIM, 0, 10000 - MAX_DIM, MAX_DIM) |
719 | ); |
720 | // Row 2 |
721 | assert_eq!( |
722 | iter.next(), |
723 | ScreenIntRect::from_xywh(0, MAX_DIM, MAX_DIM, 10000 - MAX_DIM) |
724 | ); |
725 | assert_eq!( |
726 | iter.next(), |
727 | ScreenIntRect::from_xywh(MAX_DIM, MAX_DIM, 10000 - MAX_DIM, 10000 - MAX_DIM) |
728 | ); |
729 | assert_eq!(iter.next(), None); |
730 | } |
731 | } |
732 | |