From 2f86d5fd7e452e89ee6cf5b0386f5ee0dcdcd063 Mon Sep 17 00:00:00 2001 From: "Chrome Release Bot (LUCI)" Date: Thu, 21 Mar 2024 17:19:53 +0000 Subject: [PATCH] Publish DEPS for 114.0.5735.358 git-subtree-dir: skia git-subtree-split: 1759c6ae9316996b9f150c0ce9d0ca78a3d15c02 --- BUILD.gn | 985 ++++++++++++++++++ DIR_METADATA | 11 + OWNERS | 15 + config/SkUserConfig.h | 259 +++++ config/sk_ref_cnt_ext_debug.h | 65 ++ config/sk_ref_cnt_ext_release.h | 26 + ext/.gitignore | 1 + ext/SkDiscardableMemory_chrome.cc | 47 + ext/SkDiscardableMemory_chrome.h | 45 + ext/SkMemory_new_handler.cpp | 135 +++ ext/benchmarking_canvas.cc | 585 +++++++++++ ext/benchmarking_canvas.h | 80 ++ ext/cicp.cc | 241 +++++ ext/cicp.h | 52 + ext/convolver.cc | 719 +++++++++++++ ext/convolver.h | 243 +++++ ext/convolver_SSE2.cc | 458 ++++++++ ext/convolver_SSE2.h | 27 + ext/convolver_mips_dspr2.cc | 478 +++++++++ ext/convolver_mips_dspr2.h | 25 + ext/convolver_neon.cc | 339 ++++++ ext/convolver_neon.h | 30 + ext/convolver_unittest.cc | 531 ++++++++++ ext/data/test_fonts/ChromiumAATTest.ttf | Bin 0 -> 3020 bytes .../fuchsia_test_fonts_manifest.json | 205 ++++ .../basicdrawing/00_pc_clean.png | Bin 0 -> 289 bytes .../basicdrawing/00_vc_clean.png | Bin 0 -> 289 bytes .../basicdrawing/01_pc_drawargb.png | Bin 0 -> 289 bytes .../basicdrawing/01_vc_drawargb.png | Bin 0 -> 289 bytes .../basicdrawing/02_pc_drawline_black.png | Bin 0 -> 537 bytes .../basicdrawing/02_vc_drawline_black.png | Bin 0 -> 537 bytes .../basicdrawing/03_pc_drawrect_green.png | Bin 0 -> 417 bytes .../basicdrawing/03_vc_drawrect_green.png | Bin 0 -> 417 bytes .../basicdrawing/04_pc_drawrect_noop.png | Bin 0 -> 417 bytes .../basicdrawing/04_vc_drawrect_noop.png | Bin 0 -> 417 bytes .../basicdrawing/05_pc_drawrect_noop.png | Bin 0 -> 433 bytes .../basicdrawing/05_vc_drawrect_noop.png | Bin 0 -> 433 bytes .../basicdrawing/06_pc_drawpaint_black.png | Bin 0 -> 109 bytes .../basicdrawing/06_vc_drawpaint_black.png | Bin 0 -> 109 bytes .../07_pc_drawline_left_to_right.png | Bin 0 -> 126 bytes .../07_vc_drawline_left_to_right.png | Bin 0 -> 126 bytes .../basicdrawing/08_pc_drawline_red.png | Bin 0 -> 271 bytes .../basicdrawing/08_vc_drawline_red.png | Bin 0 -> 271 bytes .../vectorcanvastest/bitmaps/00_pc_opaque.png | Bin 0 -> 5140 bytes .../vectorcanvastest/bitmaps/00_vc_opaque.png | Bin 0 -> 5140 bytes .../vectorcanvastest/bitmaps/01_pc_alpha.png | Bin 0 -> 2699 bytes .../vectorcanvastest/bitmaps/01_vc_alpha.png | Bin 0 -> 2699 bytes .../vectorcanvastest/bitmaps/bitmap_alpha.png | Bin 0 -> 422 bytes .../bitmaps/bitmap_opaque.png | Bin 0 -> 3287 bytes .../circles/00_pc_circle_stroke.png | Bin 0 -> 383 bytes .../circles/00_vc_circle_stroke.png | Bin 0 -> 410 bytes .../circles/01_pc_circle_fill.png | Bin 0 -> 463 bytes .../circles/01_vc_circle_fill.png | Bin 0 -> 481 bytes .../circles/02_pc_circle_over_strike.png | Bin 0 -> 477 bytes .../circles/02_vc_circle_over_strike.png | Bin 0 -> 513 bytes .../circles/03_pc_circle_stroke_and_fill.png | Bin 0 -> 566 bytes .../circles/03_vc_circle_stroke_and_fill.png | Bin 0 -> 602 bytes .../circles/04_pc_mixed_stroke.png | Bin 0 -> 1180 bytes .../circles/04_vc_mixed_stroke.png | Bin 0 -> 1228 bytes .../clippingclean/00_pc_clipped.png | Bin 0 -> 1354 bytes .../clippingclean/00_vc_clipped.png | Bin 0 -> 1354 bytes .../clippingclean/01_pc_unclipped.png | Bin 0 -> 4683 bytes .../clippingclean/01_vc_unclipped.png | Bin 0 -> 4683 bytes .../clippingcombined/00_pc_combined.png | Bin 0 -> 1354 bytes .../clippingcombined/00_vc_combined.png | Bin 0 -> 1354 bytes .../clippingintersect/00_pc_intersect.png | Bin 0 -> 1211 bytes .../clippingintersect/00_vc_intersect.png | Bin 0 -> 1211 bytes .../clippingpath/00_pc_path.png | Bin 0 -> 1132 bytes .../clippingpath/00_vc_path.png | Bin 0 -> 1132 bytes .../clippingrect/00_pc_rect.png | Bin 0 -> 1459 bytes .../clippingrect/00_vc_rect.png | Bin 0 -> 1459 bytes .../diagonallines/00_pc_nw-se.png | Bin 0 -> 536 bytes .../diagonallines/00_vc_nw-se.png | Bin 0 -> 536 bytes .../diagonallines/01_pc_sw-ne.png | Bin 0 -> 735 bytes .../diagonallines/01_vc_sw-ne.png | Bin 0 -> 737 bytes .../diagonallines/02_pc_ne-sw.png | Bin 0 -> 756 bytes .../diagonallines/02_vc_ne-sw.png | Bin 0 -> 760 bytes .../diagonallines/03_pc_se-nw.png | Bin 0 -> 765 bytes .../diagonallines/03_vc_se-nw.png | Bin 0 -> 781 bytes .../lineorientation/00_pc_horizontal.png | Bin 0 -> 313 bytes .../lineorientation/00_vc_horizontal.png | Bin 0 -> 319 bytes .../lineorientation/01_pc_vertical.png | Bin 0 -> 328 bytes .../lineorientation/01_vc_vertical.png | Bin 0 -> 344 bytes .../lineorientation/02_pc_horizontal_180.png | Bin 0 -> 333 bytes .../lineorientation/02_vc_horizontal_180.png | Bin 0 -> 348 bytes .../lineorientation/03_pc_vertical_180.png | Bin 0 -> 332 bytes .../lineorientation/03_vc_vertical_180.png | Bin 0 -> 351 bytes .../matrix/00_pc_translate1.png | Bin 0 -> 5139 bytes .../matrix/00_vc_translate1.png | Bin 0 -> 5139 bytes .../matrix/01_pc_translate2.png | Bin 0 -> 4645 bytes .../matrix/01_vc_translate2.png | Bin 0 -> 4645 bytes .../vectorcanvastest/matrix/02_pc_scale.png | Bin 0 -> 6566 bytes .../vectorcanvastest/matrix/02_vc_scale.png | Bin 0 -> 12292 bytes .../vectorcanvastest/matrix/03_pc_rotate.png | Bin 0 -> 9749 bytes .../vectorcanvastest/matrix/03_vc_rotate.png | Bin 0 -> 13795 bytes .../patheffects/00_pc_dash_line.png | Bin 0 -> 299 bytes .../patheffects/00_vc_dash_line.png | Bin 0 -> 299 bytes .../patheffects/01_pc_dash_path.png | Bin 0 -> 348 bytes .../patheffects/01_vc_dash_path.png | Bin 0 -> 343 bytes .../patheffects/02_pc_dash_rect.png | Bin 0 -> 387 bytes .../patheffects/02_vc_dash_rect.png | Bin 0 -> 395 bytes .../patheffects/03_pc_circle.png | Bin 0 -> 497 bytes .../patheffects/03_vc_circle.png | Bin 0 -> 519 bytes .../pathorientation/00_pc_drawpath_ltr.png | Bin 0 -> 307 bytes .../pathorientation/00_vc_drawpath_ltr.png | Bin 0 -> 307 bytes .../pathorientation/01_pc_drawpath_rtl.png | Bin 0 -> 313 bytes .../pathorientation/01_vc_drawpath_rtl.png | Bin 0 -> 319 bytes ext/event_tracer_impl.cc | 89 ++ ext/event_tracer_impl.h | 12 + ext/fontmgr_default.cc | 34 + ext/fontmgr_default.h | 26 + ext/fontmgr_default_android.cc | 16 + ext/fontmgr_default_fuchsia.cc | 22 + ext/fontmgr_default_linux.cc | 18 + ext/fontmgr_default_win.cc | 16 + ext/fontmgr_fuchsia_unittest.cc | 63 ++ ext/google_logging.cc | 46 + ext/image_operations.cc | 417 ++++++++ ext/image_operations.h | 122 +++ ext/image_operations_bench.cc | 291 ++++++ ext/image_operations_unittest.cc | 581 +++++++++++ ext/legacy_display_globals.cc | 44 + ext/legacy_display_globals.h | 27 + ext/opacity_filter_canvas.cc | 70 ++ ext/opacity_filter_canvas.h | 57 + ext/platform_canvas.cc | 71 ++ ext/platform_canvas.h | 114 ++ ext/platform_canvas_unittest.cc | 369 +++++++ ext/raster_handle_allocator_win.cc | 164 +++ ext/recursive_gaussian_convolution.cc | 271 +++++ ext/recursive_gaussian_convolution.h | 71 ++ ...recursive_gaussian_convolution_unittest.cc | 394 +++++++ ext/rgba_to_yuva.cc | 119 +++ ext/rgba_to_yuva.h | 30 + ext/skcolorspace_primaries.cc | 71 ++ ext/skcolorspace_primaries.h | 129 +++ ext/skcolorspace_primaries_unittest.cc | 59 ++ ext/skcolorspace_trfn.h | 102 ++ ext/skia_histogram.cc | 46 + ext/skia_histogram.h | 49 + ext/skia_memory_dump_provider.cc | 52 + ext/skia_memory_dump_provider.h | 36 + ext/skia_memory_dump_provider_unittest.cc | 26 + ext/skia_trace_memory_dump_impl.cc | 91 ++ ext/skia_trace_memory_dump_impl.h | 77 ++ ext/skia_utils_base.cc | 122 +++ ext/skia_utils_base.h | 64 ++ ext/skia_utils_base_unittest.cc | 97 ++ ext/skia_utils_ios.h | 54 + ext/skia_utils_ios.mm | 136 +++ ext/skia_utils_ios_unittest.mm | 780 ++++++++++++++ ext/skia_utils_mac.h | 98 ++ ext/skia_utils_mac.mm | 257 +++++ ext/skia_utils_mac_unittest.mm | 214 ++++ ext/skia_utils_win.cc | 393 +++++++ ext/skia_utils_win.h | 127 +++ ext/skottie_unittest.cc | 56 + ext/test_fonts.h | 16 + ext/test_fonts_fuchsia.cc | 20 + ext/test_fonts_fuchsia.h | 18 + ext/test_fonts_fuchsia_cfv2.cc | 23 + ext/test_fonts_mac.mm | 48 + features.gni | 42 + public/mojom/BUILD.gn | 162 +++ public/mojom/OWNERS | 5 + public/mojom/bitmap.mojom | 60 ++ public/mojom/bitmap_skbitmap_mojom_traits.cc | 200 ++++ public/mojom/bitmap_skbitmap_mojom_traits.h | 76 ++ public/mojom/image_info.mojom | 79 ++ public/mojom/image_info_mojom_traits.cc | 244 +++++ public/mojom/image_info_mojom_traits.h | 83 ++ public/mojom/skcolor.mojom | 11 + public/mojom/skcolor4f.mojom | 14 + public/mojom/skcolor4f_mojom_traits.h | 30 + public/mojom/skcolor_mojom_traits.h | 24 + public/mojom/skcolorspace_primaries.mojom | 16 + .../skcolorspace_primaries_mojom_traits.h | 58 ++ public/mojom/surface_origin.mojom | 11 + public/mojom/surface_origin_mojom_traits.h | 47 + public/mojom/test/OWNERS | 4 + public/mojom/test/mojom_traits_unittest.cc | 491 +++++++++ public/mojom/tile_mode.mojom | 13 + public/mojom/tile_mode_mojom_traits.h | 52 + skia_Prefix.pch | 12 + skia_resources.grd | 53 + tools/filter_fuzz_stub/filter_fuzz_stub.cc | 96 ++ tools/fuzzers/BUILD.gn | 136 +++ tools/fuzzers/OWNERS | 2 + tools/fuzzers/fuzzer_environment.cc | 19 + 189 files changed, 14457 insertions(+) create mode 100644 BUILD.gn create mode 100644 DIR_METADATA create mode 100644 OWNERS create mode 100644 config/SkUserConfig.h create mode 100644 config/sk_ref_cnt_ext_debug.h create mode 100644 config/sk_ref_cnt_ext_release.h create mode 100644 ext/.gitignore create mode 100644 ext/SkDiscardableMemory_chrome.cc create mode 100644 ext/SkDiscardableMemory_chrome.h create mode 100644 ext/SkMemory_new_handler.cpp create mode 100644 ext/benchmarking_canvas.cc create mode 100644 ext/benchmarking_canvas.h create mode 100644 ext/cicp.cc create mode 100644 ext/cicp.h create mode 100644 ext/convolver.cc create mode 100644 ext/convolver.h create mode 100644 ext/convolver_SSE2.cc create mode 100644 ext/convolver_SSE2.h create mode 100644 ext/convolver_mips_dspr2.cc create mode 100644 ext/convolver_mips_dspr2.h create mode 100644 ext/convolver_neon.cc create mode 100644 ext/convolver_neon.h create mode 100644 ext/convolver_unittest.cc create mode 100644 ext/data/test_fonts/ChromiumAATTest.ttf create mode 100644 ext/data/test_fonts/fuchsia_test_fonts_manifest.json create mode 100644 ext/data/vectorcanvastest/basicdrawing/00_pc_clean.png create mode 100644 ext/data/vectorcanvastest/basicdrawing/00_vc_clean.png create mode 100644 ext/data/vectorcanvastest/basicdrawing/01_pc_drawargb.png create mode 100644 ext/data/vectorcanvastest/basicdrawing/01_vc_drawargb.png create mode 100644 ext/data/vectorcanvastest/basicdrawing/02_pc_drawline_black.png create mode 100644 ext/data/vectorcanvastest/basicdrawing/02_vc_drawline_black.png create mode 100644 ext/data/vectorcanvastest/basicdrawing/03_pc_drawrect_green.png create mode 100644 ext/data/vectorcanvastest/basicdrawing/03_vc_drawrect_green.png create mode 100644 ext/data/vectorcanvastest/basicdrawing/04_pc_drawrect_noop.png create mode 100644 ext/data/vectorcanvastest/basicdrawing/04_vc_drawrect_noop.png create mode 100644 ext/data/vectorcanvastest/basicdrawing/05_pc_drawrect_noop.png create mode 100644 ext/data/vectorcanvastest/basicdrawing/05_vc_drawrect_noop.png create mode 100644 ext/data/vectorcanvastest/basicdrawing/06_pc_drawpaint_black.png create mode 100644 ext/data/vectorcanvastest/basicdrawing/06_vc_drawpaint_black.png create mode 100644 ext/data/vectorcanvastest/basicdrawing/07_pc_drawline_left_to_right.png create mode 100644 ext/data/vectorcanvastest/basicdrawing/07_vc_drawline_left_to_right.png create mode 100644 ext/data/vectorcanvastest/basicdrawing/08_pc_drawline_red.png create mode 100644 ext/data/vectorcanvastest/basicdrawing/08_vc_drawline_red.png create mode 100644 ext/data/vectorcanvastest/bitmaps/00_pc_opaque.png create mode 100644 ext/data/vectorcanvastest/bitmaps/00_vc_opaque.png create mode 100644 ext/data/vectorcanvastest/bitmaps/01_pc_alpha.png create mode 100644 ext/data/vectorcanvastest/bitmaps/01_vc_alpha.png create mode 100644 ext/data/vectorcanvastest/bitmaps/bitmap_alpha.png create mode 100644 ext/data/vectorcanvastest/bitmaps/bitmap_opaque.png create mode 100644 ext/data/vectorcanvastest/circles/00_pc_circle_stroke.png create mode 100644 ext/data/vectorcanvastest/circles/00_vc_circle_stroke.png create mode 100644 ext/data/vectorcanvastest/circles/01_pc_circle_fill.png create mode 100644 ext/data/vectorcanvastest/circles/01_vc_circle_fill.png create mode 100644 ext/data/vectorcanvastest/circles/02_pc_circle_over_strike.png create mode 100644 ext/data/vectorcanvastest/circles/02_vc_circle_over_strike.png create mode 100644 ext/data/vectorcanvastest/circles/03_pc_circle_stroke_and_fill.png create mode 100644 ext/data/vectorcanvastest/circles/03_vc_circle_stroke_and_fill.png create mode 100644 ext/data/vectorcanvastest/circles/04_pc_mixed_stroke.png create mode 100644 ext/data/vectorcanvastest/circles/04_vc_mixed_stroke.png create mode 100644 ext/data/vectorcanvastest/clippingclean/00_pc_clipped.png create mode 100644 ext/data/vectorcanvastest/clippingclean/00_vc_clipped.png create mode 100644 ext/data/vectorcanvastest/clippingclean/01_pc_unclipped.png create mode 100644 ext/data/vectorcanvastest/clippingclean/01_vc_unclipped.png create mode 100644 ext/data/vectorcanvastest/clippingcombined/00_pc_combined.png create mode 100644 ext/data/vectorcanvastest/clippingcombined/00_vc_combined.png create mode 100644 ext/data/vectorcanvastest/clippingintersect/00_pc_intersect.png create mode 100644 ext/data/vectorcanvastest/clippingintersect/00_vc_intersect.png create mode 100644 ext/data/vectorcanvastest/clippingpath/00_pc_path.png create mode 100644 ext/data/vectorcanvastest/clippingpath/00_vc_path.png create mode 100644 ext/data/vectorcanvastest/clippingrect/00_pc_rect.png create mode 100644 ext/data/vectorcanvastest/clippingrect/00_vc_rect.png create mode 100644 ext/data/vectorcanvastest/diagonallines/00_pc_nw-se.png create mode 100644 ext/data/vectorcanvastest/diagonallines/00_vc_nw-se.png create mode 100644 ext/data/vectorcanvastest/diagonallines/01_pc_sw-ne.png create mode 100644 ext/data/vectorcanvastest/diagonallines/01_vc_sw-ne.png create mode 100644 ext/data/vectorcanvastest/diagonallines/02_pc_ne-sw.png create mode 100644 ext/data/vectorcanvastest/diagonallines/02_vc_ne-sw.png create mode 100644 ext/data/vectorcanvastest/diagonallines/03_pc_se-nw.png create mode 100644 ext/data/vectorcanvastest/diagonallines/03_vc_se-nw.png create mode 100644 ext/data/vectorcanvastest/lineorientation/00_pc_horizontal.png create mode 100644 ext/data/vectorcanvastest/lineorientation/00_vc_horizontal.png create mode 100644 ext/data/vectorcanvastest/lineorientation/01_pc_vertical.png create mode 100644 ext/data/vectorcanvastest/lineorientation/01_vc_vertical.png create mode 100644 ext/data/vectorcanvastest/lineorientation/02_pc_horizontal_180.png create mode 100644 ext/data/vectorcanvastest/lineorientation/02_vc_horizontal_180.png create mode 100644 ext/data/vectorcanvastest/lineorientation/03_pc_vertical_180.png create mode 100644 ext/data/vectorcanvastest/lineorientation/03_vc_vertical_180.png create mode 100644 ext/data/vectorcanvastest/matrix/00_pc_translate1.png create mode 100644 ext/data/vectorcanvastest/matrix/00_vc_translate1.png create mode 100644 ext/data/vectorcanvastest/matrix/01_pc_translate2.png create mode 100644 ext/data/vectorcanvastest/matrix/01_vc_translate2.png create mode 100644 ext/data/vectorcanvastest/matrix/02_pc_scale.png create mode 100644 ext/data/vectorcanvastest/matrix/02_vc_scale.png create mode 100644 ext/data/vectorcanvastest/matrix/03_pc_rotate.png create mode 100644 ext/data/vectorcanvastest/matrix/03_vc_rotate.png create mode 100644 ext/data/vectorcanvastest/patheffects/00_pc_dash_line.png create mode 100644 ext/data/vectorcanvastest/patheffects/00_vc_dash_line.png create mode 100644 ext/data/vectorcanvastest/patheffects/01_pc_dash_path.png create mode 100644 ext/data/vectorcanvastest/patheffects/01_vc_dash_path.png create mode 100644 ext/data/vectorcanvastest/patheffects/02_pc_dash_rect.png create mode 100644 ext/data/vectorcanvastest/patheffects/02_vc_dash_rect.png create mode 100644 ext/data/vectorcanvastest/patheffects/03_pc_circle.png create mode 100644 ext/data/vectorcanvastest/patheffects/03_vc_circle.png create mode 100644 ext/data/vectorcanvastest/pathorientation/00_pc_drawpath_ltr.png create mode 100644 ext/data/vectorcanvastest/pathorientation/00_vc_drawpath_ltr.png create mode 100644 ext/data/vectorcanvastest/pathorientation/01_pc_drawpath_rtl.png create mode 100644 ext/data/vectorcanvastest/pathorientation/01_vc_drawpath_rtl.png create mode 100644 ext/event_tracer_impl.cc create mode 100644 ext/event_tracer_impl.h create mode 100644 ext/fontmgr_default.cc create mode 100644 ext/fontmgr_default.h create mode 100644 ext/fontmgr_default_android.cc create mode 100644 ext/fontmgr_default_fuchsia.cc create mode 100644 ext/fontmgr_default_linux.cc create mode 100644 ext/fontmgr_default_win.cc create mode 100644 ext/fontmgr_fuchsia_unittest.cc create mode 100644 ext/google_logging.cc create mode 100644 ext/image_operations.cc create mode 100644 ext/image_operations.h create mode 100644 ext/image_operations_bench.cc create mode 100644 ext/image_operations_unittest.cc create mode 100644 ext/legacy_display_globals.cc create mode 100644 ext/legacy_display_globals.h create mode 100644 ext/opacity_filter_canvas.cc create mode 100644 ext/opacity_filter_canvas.h create mode 100644 ext/platform_canvas.cc create mode 100644 ext/platform_canvas.h create mode 100644 ext/platform_canvas_unittest.cc create mode 100644 ext/raster_handle_allocator_win.cc create mode 100644 ext/recursive_gaussian_convolution.cc create mode 100644 ext/recursive_gaussian_convolution.h create mode 100644 ext/recursive_gaussian_convolution_unittest.cc create mode 100644 ext/rgba_to_yuva.cc create mode 100644 ext/rgba_to_yuva.h create mode 100644 ext/skcolorspace_primaries.cc create mode 100644 ext/skcolorspace_primaries.h create mode 100644 ext/skcolorspace_primaries_unittest.cc create mode 100644 ext/skcolorspace_trfn.h create mode 100644 ext/skia_histogram.cc create mode 100644 ext/skia_histogram.h create mode 100644 ext/skia_memory_dump_provider.cc create mode 100644 ext/skia_memory_dump_provider.h create mode 100644 ext/skia_memory_dump_provider_unittest.cc create mode 100644 ext/skia_trace_memory_dump_impl.cc create mode 100644 ext/skia_trace_memory_dump_impl.h create mode 100644 ext/skia_utils_base.cc create mode 100644 ext/skia_utils_base.h create mode 100644 ext/skia_utils_base_unittest.cc create mode 100644 ext/skia_utils_ios.h create mode 100644 ext/skia_utils_ios.mm create mode 100644 ext/skia_utils_ios_unittest.mm create mode 100644 ext/skia_utils_mac.h create mode 100644 ext/skia_utils_mac.mm create mode 100644 ext/skia_utils_mac_unittest.mm create mode 100644 ext/skia_utils_win.cc create mode 100644 ext/skia_utils_win.h create mode 100644 ext/skottie_unittest.cc create mode 100644 ext/test_fonts.h create mode 100644 ext/test_fonts_fuchsia.cc create mode 100644 ext/test_fonts_fuchsia.h create mode 100644 ext/test_fonts_fuchsia_cfv2.cc create mode 100644 ext/test_fonts_mac.mm create mode 100644 features.gni create mode 100644 public/mojom/BUILD.gn create mode 100644 public/mojom/OWNERS create mode 100644 public/mojom/bitmap.mojom create mode 100644 public/mojom/bitmap_skbitmap_mojom_traits.cc create mode 100644 public/mojom/bitmap_skbitmap_mojom_traits.h create mode 100644 public/mojom/image_info.mojom create mode 100644 public/mojom/image_info_mojom_traits.cc create mode 100644 public/mojom/image_info_mojom_traits.h create mode 100644 public/mojom/skcolor.mojom create mode 100644 public/mojom/skcolor4f.mojom create mode 100644 public/mojom/skcolor4f_mojom_traits.h create mode 100644 public/mojom/skcolor_mojom_traits.h create mode 100644 public/mojom/skcolorspace_primaries.mojom create mode 100644 public/mojom/skcolorspace_primaries_mojom_traits.h create mode 100644 public/mojom/surface_origin.mojom create mode 100644 public/mojom/surface_origin_mojom_traits.h create mode 100644 public/mojom/test/OWNERS create mode 100644 public/mojom/test/mojom_traits_unittest.cc create mode 100644 public/mojom/tile_mode.mojom create mode 100644 public/mojom/tile_mode_mojom_traits.h create mode 100644 skia_Prefix.pch create mode 100644 skia_resources.grd create mode 100644 tools/filter_fuzz_stub/filter_fuzz_stub.cc create mode 100644 tools/fuzzers/BUILD.gn create mode 100644 tools/fuzzers/OWNERS create mode 100644 tools/fuzzers/fuzzer_environment.cc diff --git a/BUILD.gn b/BUILD.gn new file mode 100644 index 000000000000..ea7cfd9e3179 --- /dev/null +++ b/BUILD.gn @@ -0,0 +1,985 @@ +# Copyright 2013 The Chromium Authors +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +import("//build/buildflag_header.gni") +import("//build/config/compiler/compiler.gni") +import("//build/config/features.gni") +import("//build/config/freetype/freetype.gni") +import("//build/config/sanitizers/sanitizers.gni") +import("//gpu/vulkan/features.gni") +import("//testing/test.gni") +import("//third_party/skia/gn/shared_sources.gni") +import("//third_party/skia/modules/skcms/skcms.gni") +import("//tools/grit/grit_rule.gni") +import("features.gni") + +if (current_cpu == "arm") { + import("//build/config/arm.gni") +} +if (current_cpu == "mipsel" || current_cpu == "mips64el") { + import("//build/config/mips.gni") +} + +buildflag_header("buildflags") { + header = "buildflags.h" + flags = [ + "SKIA_SUPPORT_SKOTTIE=$skia_support_skottie", + "ENABLE_SKIA_GRAPHITE=$enable_skia_graphite", + "SKIA_USE_DAWN=$skia_use_dawn", + "SKIA_USE_METAL=$skia_use_metal", + ] +} + +# External-facing config for dependent code. +config("skia_config") { + defines = [ + "SK_CODEC_DECODES_PNG", + "SK_CODEC_DECODES_WEBP", + "SK_ENCODE_PNG", + "SK_ENCODE_WEBP", + "SK_ENABLE_SKSL", + "SK_UNTIL_CRBUG_1187654_IS_FIXED", + "SK_USER_CONFIG_HEADER=\"../../skia/config/SkUserConfig.h\"", + "SK_WIN_FONTMGR_NO_SIMULATIONS", + "SK_DISABLE_LEGACY_IMAGE_FACTORIES", + "SK_DISABLE_LEGACY_MAKE_TEXTURE_IMAGE", + "SK_DISABLE_LEGACY_IMAGE_FLUSH", + "SK_DISABLE_LEGACY_GET_BACKEND_TEXTURE", + "SK_DISABLE_LEGACY_IMAGE_ENCODE_METHODS", + "SK_DISABLE_LEGACY_IMAGE_ENCODER", + ] + + include_dirs = [ "//third_party/skia" ] + + if (use_blink) { + defines += [ + "SK_CODEC_DECODES_JPEG", + "SK_ENCODE_JPEG", + "SK_HAS_WUFFS_LIBRARY", + ] + include_dirs += [ "//third_party/wuffs/src/release/c" ] + } + + if (is_component_build) { + defines += [ "SKIA_DLL" ] + if (is_win) { + defines += [ "SKCMS_API=__declspec(dllexport)" ] + } else { + defines += [ "SKCMS_API=__attribute__((visibility(\"default\")))" ] + } + } + + if (is_android) { + defines += [ + "SK_BUILD_FOR_ANDROID", + "USE_CHROMIUM_SKIA", + ] + } + + if (is_mac) { + defines += [ "SK_BUILD_FOR_MAC" ] + } + if (is_ios) { + defines += [ "SK_BUILD_FOR_IOS" ] + } + + if (is_win) { + defines += [ "GR_GL_FUNCTION_TYPE=__stdcall" ] + } + + if (skia_support_gpu) { + workaround_header = "gpu/config/gpu_driver_bug_workaround_autogen.h" + configs = [] + defines += [ + "SK_GANESH", + "SK_GPU_WORKAROUNDS_HEADER=\"$workaround_header\"", + ] + if (skia_use_gl) { + defines += [ "SK_GL" ] + } + if (enable_vulkan) { + defines += [ "SK_VULKAN=1" ] + include_dirs += [ "//third_party/vulkan/include" ] + configs += [ + "//third_party/vulkan-deps/vulkan-headers/src:vulkan_headers_config", + ] + } + if (enable_skia_graphite) { + defines += [ "SK_GRAPHITE" ] + if (skia_use_dawn) { + defines += [ "SK_DAWN" ] + configs += [ "//third_party/dawn/include/dawn:public" ] + } + if (skia_use_metal) { + defines += [ "SK_METAL" ] + } + } + } +} + +# Internal-facing config for Skia library code. +config("skia_library_config") { + # Turn on SK_API to export Skia's public API + defines = [ + "IS_SKIA_IMPL=1", + "SKIA_IMPLEMENTATION=1", + ] + + if (use_blink && !use_system_freetype) { + defines += [ "SK_FREETYPE_MINIMUM_RUNTIME_VERSION=(((FREETYPE_MAJOR) * 0x01000000) | ((FREETYPE_MINOR) * 0x00010000) | ((FREETYPE_PATCH) * 0x00000100))" ] + } + if (use_blink) { + defines += [ "SK_TYPEFACE_FACTORY_FREETYPE" ] + } + if (is_win) { + defines += [ "SK_TYPEFACE_FACTORY_DIRECTWRITE" ] + } + if (is_apple) { + defines += [ "SK_TYPEFACE_FACTORY_CORETEXT" ] + } + + if (current_cpu == "arm") { + if (arm_use_neon) { + defines += [ "SK_ARM_HAS_NEON" ] + } else if (arm_optionally_use_neon) { + defines += [ "SK_ARM_HAS_OPTIONAL_NEON" ] + } + } + + # Settings for text blitting, chosen to approximate the system browser. + if (is_linux || is_chromeos) { + defines += [ + "SK_GAMMA_EXPONENT=1.2", + "SK_GAMMA_CONTRAST=0.2", + ] + } else if (is_android) { + defines += [ + "SK_GAMMA_APPLY_TO_A8", + "SK_GAMMA_EXPONENT=1.4", + "SK_GAMMA_CONTRAST=0.0", + ] + } else if (is_win) { + defines += [ + "SK_GAMMA_SRGB", + "SK_GAMMA_CONTRAST=0.5", + ] + } else if (is_mac) { + defines += [ + "SK_GAMMA_SRGB", + "SK_GAMMA_CONTRAST=0.0", + ] + } + + if (is_android) { + defines += [ + # Android devices are typically more memory constrained, so default to a + # smaller glyph cache (it may be overriden at runtime when the renderer + # starts up, depending on the actual device memory). + "SK_DEFAULT_FONT_CACHE_LIMIT=1048576", # 1024 * 1024 + ] + } else { + defines += [ "SK_DEFAULT_FONT_CACHE_LIMIT=20971520" ] # 20 * 1024 * 1024 + } + + if (is_win) { + defines += [ + # On windows, GDI handles are a scarse system-wide resource so we have to + # keep the glyph cache, which holds up to 4 GDI handles per entry, to a + # fairly small size. http://crbug.com/314387 + "SK_DEFAULT_FONT_CACHE_COUNT_LIMIT=256", + ] + } +} + +source_set("skcms") { + cflags = [] + if (!is_win || is_clang) { + cflags += [ + "-w", + "-std=c11", + ] + } + + # LLVM automatically sets the equivalent of GCC's -mfp16-format=ieee on ARM + # builds by default, while GCC itself does not. We need it to enable support + # for half-precision floating point data types used by SKCMS on ARM. + if ((is_linux || is_chromeos) && !is_clang && current_cpu == "arm") { + cflags += [ "-mfp16-format=ieee" ] + } + + public = [ "//third_party/skia/modules/skcms/skcms.h" ] + include_dirs = [ "//third_party/skia/modules/skcms" ] + sources = rebase_path(skcms_sources, ".", "//third_party/skia/modules/skcms") +} + +component("skia") { + deps = [] + public = [ + "ext/benchmarking_canvas.h", + "ext/cicp.h", + "ext/convolver.h", + "ext/event_tracer_impl.h", + "ext/image_operations.h", + "ext/legacy_display_globals.h", + "ext/opacity_filter_canvas.h", + "ext/recursive_gaussian_convolution.h", + "ext/rgba_to_yuva.h", + "ext/skcolorspace_primaries.h", + "ext/skcolorspace_trfn.h", + "ext/skia_memory_dump_provider.h", + "ext/skia_trace_memory_dump_impl.h", + "ext/skia_utils_base.h", + ] + sources = [ + # Chrome sources. + "config/SkUserConfig.h", + "config/sk_ref_cnt_ext_debug.h", + "config/sk_ref_cnt_ext_release.h", + "ext/SkDiscardableMemory_chrome.cc", + "ext/SkDiscardableMemory_chrome.h", + "ext/SkMemory_new_handler.cpp", + "ext/benchmarking_canvas.cc", + "ext/cicp.cc", + "ext/convolver.cc", + "ext/event_tracer_impl.cc", + "ext/google_logging.cc", + "ext/image_operations.cc", + "ext/legacy_display_globals.cc", + "ext/opacity_filter_canvas.cc", + "ext/recursive_gaussian_convolution.cc", + "ext/rgba_to_yuva.cc", + "ext/skcolorspace_primaries.cc", + "ext/skia_histogram.cc", + "ext/skia_histogram.h", + "ext/skia_memory_dump_provider.cc", + "ext/skia_trace_memory_dump_impl.cc", + "ext/skia_utils_base.cc", + ] + if (!is_apple) { + public += [ "ext/fontmgr_default.h" ] + sources += [ "ext/fontmgr_default.cc" ] + } + if (is_android) { + sources += [ "ext/fontmgr_default_android.cc" ] + } + if (is_linux || is_chromeos) { + sources += [ "ext/fontmgr_default_linux.cc" ] + } + if (is_ios) { + public += [ "ext/skia_utils_ios.h" ] + sources += [ "ext/skia_utils_ios.mm" ] + } + if (is_mac) { + public += [ "ext/skia_utils_mac.h" ] + sources += [ "ext/skia_utils_mac.mm" ] + } + if (is_win) { + public += [ "ext/skia_utils_win.h" ] + sources += [ + "ext/fontmgr_default_win.cc", + "ext/skia_utils_win.cc", + ] + } + + if (use_blink) { + public += [ "ext/platform_canvas.h" ] + sources += [ "ext/platform_canvas.cc" ] + } + if (!is_ios && (current_cpu == "x86" || current_cpu == "x64")) { + sources += [ + "ext/convolver_SSE2.cc", + "ext/convolver_SSE2.h", + ] + } else if (current_cpu == "mipsel" && mips_dsp_rev >= 2) { + sources += [ + "ext/convolver_mips_dspr2.cc", + "ext/convolver_mips_dspr2.h", + ] + } else if (current_cpu == "arm" || current_cpu == "arm64") { + if (arm_use_neon) { + sources += [ + "ext/convolver_neon.cc", + "ext/convolver_neon.h", + ] + } + } + + if (is_win) { + sources += [ + # Select the right BitmapPlatformDevice. + "ext/raster_handle_allocator_win.cc", + ] + } + + public += skia_core_public + public += skia_utils_public + public += skia_effects_public + public += skia_effects_imagefilter_public + public += skia_utils_chromium + public += skia_discardable_memory_chromium + public += skia_encode_public + public += skia_encode_png_public + public += skia_encode_webp_public + + # The imported Skia gni source paths are made absolute by gn. + defines = [] + sources += skia_sksl_sources + sources += skia_utils_private + sources += skia_xps_sources + sources += skia_encode_srcs + sources += skia_encode_png_srcs + sources += skia_encode_webp_srcs + sources += [ + "//third_party/skia/src/fonts/SkFontMgr_indirect.cpp", + "//third_party/skia/src/fonts/SkRemotableFontMgr.cpp", + "//third_party/skia/src/ports/SkGlobalInitialization_default.cpp", + "//third_party/skia/src/ports/SkImageGenerator_none.cpp", + "//third_party/skia/src/ports/SkOSFile_stdio.cpp", + "//third_party/skia/src/sfnt/SkOTTable_name.cpp", + "//third_party/skia/src/sfnt/SkOTUtils.cpp", + ] + if (use_blink) { + sources -= [ "//third_party/skia/src/ports/SkImageGenerator_none.cpp" ] + sources += skia_codec_core + sources += skia_codec_decode_bmp + sources += skia_encode_jpeg_srcs + public += skia_encode_jpeg_public + sources += [ + "//third_party/skia/src/codec/SkEncodedInfo.cpp", + "//third_party/skia/src/codec/SkIcoCodec.cpp", + "//third_party/skia/src/codec/SkJpegCodec.cpp", + "//third_party/skia/src/codec/SkJpegDecoderMgr.cpp", + "//third_party/skia/src/codec/SkJpegMultiPicture.cpp", + "//third_party/skia/src/codec/SkJpegSegmentScan.cpp", + "//third_party/skia/src/codec/SkJpegSourceMgr.cpp", + "//third_party/skia/src/codec/SkJpegUtility.cpp", + "//third_party/skia/src/codec/SkParseEncodedOrigin.cpp", + "//third_party/skia/src/codec/SkPngCodec.cpp", + "//third_party/skia/src/codec/SkWebpCodec.cpp", + "//third_party/skia/src/codec/SkWuffsCodec.cpp", + "//third_party/skia/src/ports/SkImageGenerator_skia.cpp", + ] + deps += [ "//third_party/wuffs" ] + } else { + sources += skia_no_encode_jpeg_srcs + } + + if (skia_support_xmp) { + sources += skia_xml_sources + sources += [ "//third_party/skia/src/codec/SkJpegXmp.cpp" ] + deps += [ "//third_party/expat" ] + defines += [ "SK_CODEC_DECODES_JPEG_GAINMAPS" ] + } + + # Remove unused util sources. + sources -= [ "//third_party/skia/src/utils/SkParsePath.cpp" ] + + if (is_win) { + libs = [ "fontsub.lib" ] + } + + # need separate win section to handle chromes auto gn filter + # (build/config/BUILDCONFIG.gn) + if (is_win) { + sources -= [ + #windows + "//third_party/skia/src/utils/win/SkWGL_win.cpp", + ] + } + + # Select Skia ports. + + # FreeType is needed everywhere where blink is used, on Linux and Android as main + # font backend, on Windows and Mac as fallback backend for Variations. + if (use_blink) { + # See SK_TYPEFACE_FACTORY_FREETYPE + sources += [ + "//third_party/skia/src/ports/SkFontHost_FreeType.cpp", + "//third_party/skia/src/ports/SkFontHost_FreeType_common.cpp", + ] + } + + if (is_win) { + # See SK_TYPEFACE_FACTORY_DIRECTWRITE + sources += [ + "//third_party/skia/src/ports/SkFontHost_win.cpp", + "//third_party/skia/src/ports/SkFontMgr_win_dw.cpp", + "//third_party/skia/src/ports/SkOSFile_win.cpp", + "//third_party/skia/src/ports/SkRemotableFontMgr_win_dw.cpp", + "//third_party/skia/src/ports/SkScalerContext_win_dw.cpp", + "//third_party/skia/src/ports/SkTypeface_win_dw.cpp", + ] + } else { + sources += [ "//third_party/skia/src/ports/SkOSFile_posix.cpp" ] + } + + if (is_apple) { + # See SK_TYPEFACE_FACTORY_CORETEXT + sources += [ + "//third_party/skia/include/ports/SkFontMgr_mac_ct.h", + "//third_party/skia/src/ports/SkFontMgr_mac_ct.cpp", + "//third_party/skia/src/ports/SkFontMgr_mac_ct_factory.cpp", + "//third_party/skia/src/ports/SkScalerContext_mac_ct.cpp", + "//third_party/skia/src/ports/SkScalerContext_mac_ct.h", + "//third_party/skia/src/ports/SkTypeface_mac_ct.cpp", + "//third_party/skia/src/ports/SkTypeface_mac_ct.h", + ] + } + + if (is_linux || is_chromeos) { + sources += [ + "//third_party/skia/src/ports/SkFontConfigInterface.cpp", + "//third_party/skia/src/ports/SkFontConfigInterface_direct.cpp", + "//third_party/skia/src/ports/SkFontConfigInterface_direct_factory.cpp", + "//third_party/skia/src/ports/SkFontMgr_FontConfigInterface.cpp", + ] + } + + if (is_linux || is_chromeos || is_android) { + sources += [ + # Retain the files for the SkFontMgr_Android on linux to emulate android + # fonts. See content/zygote/zygote_main_linux.cc + # Note that this requires expat. + "//third_party/skia/src/ports/SkFontMgr_android.cpp", + "//third_party/skia/src/ports/SkFontMgr_android_parser.cpp", + ] + } + + if (is_win || (is_apple && use_blink)) { + sources += [ + # Add the FreeType custom font manager as a fallback backend for variable fonts. + "//third_party/skia/src/ports/SkFontMgr_custom.cpp", + "//third_party/skia/src/ports/SkFontMgr_custom_empty.cpp", + ] + } + + if (is_fuchsia) { + sources += [ + "//third_party/skia/src/ports/SkFontMgr_custom.cpp", + "//third_party/skia/src/ports/SkFontMgr_fuchsia.cpp", + "ext/fontmgr_default_fuchsia.cc", + ] + deps += [ + "//third_party/fuchsia-sdk/sdk/fidl/fuchsia.fonts:fuchsia.fonts_hlcpp", + "//third_party/fuchsia-sdk/sdk/fidl/fuchsia.io:fuchsia.io_hlcpp", + "//third_party/fuchsia-sdk/sdk/pkg/sys_cpp", + "//third_party/fuchsia-sdk/sdk/pkg/zx", + "//third_party/icu:icuuc", + ] + } + + if (is_clang && !is_nacl) { + # Skia won't compile with some of the more strict clang warnings. + # e.g. it does: + # SkASSERT(!"sk_out_of_memory"); + configs -= [ "//build/config/clang:extra_warnings" ] + } + + configs -= [ "//build/config/compiler:chromium_code" ] + configs += [ + ":skia_config", + ":skia_library_config", + "//build/config/compiler:no_chromium_code", + ] + public_configs = [ ":skia_config" ] + + deps += [ + ":skcms", + ":skia_opts", + "//base", + "//base/third_party/dynamic_annotations", + "//third_party/libpng", + "//third_party/libwebp", + "//third_party/libwebp:libwebp_webp", + ] + public_deps = [ + ":buildflags", + ":skia_core_and_effects", + ] + + if (use_blink) { + deps += [ + "//build/config/freetype", + "//third_party:jpeg", + ] + } + + if (is_linux || is_chromeos) { + deps += [ + "//third_party/expat", + "//third_party/fontconfig", + "//third_party/icu:icuuc", + ] + } + + if (is_android) { + deps += [ + "//third_party/android_ndk:cpu_features", + "//third_party/expat", + ] + } + + if (!is_debug) { + configs -= [ "//build/config/compiler:default_optimization" ] + configs += [ "//build/config/compiler:optimize_max" ] + } + + frameworks = [] + if (is_ios) { + frameworks += [ "ImageIO.framework" ] + } + if (is_apple && skia_support_gpu) { + frameworks += [ + "CoreFoundation.framework", + "CoreGraphics.framework", + "CoreText.framework", + "Foundation.framework", + ] + if (is_mac) { + frameworks += [ "AppKit.framework" ] + } + if (is_ios) { + frameworks += [ "IOSurface.framework" ] + } + } + + if (is_fuchsia) { + deps += [ "//third_party/expat" ] + } + + if (skia_support_gpu) { + public += skia_gpu_public + public += skia_gpu_chromium_public + sources += skia_gpu_private + sources += skia_null_gpu_sources + sources += skia_sksl_gpu_sources + sources += skia_shared_gpu_sources + if (skia_use_gl) { + public += skia_gpu_gl_public + sources += skia_gpu_gl_private + } + if (enable_vulkan) { + public += skia_gpu_vk_public + public += skia_gpu_vk_chromium_public + sources += skia_gpu_vk_private + sources += skia_shared_vk_sources + sources += skia_gpu_vk_chromium_private + } + if (enable_skia_graphite) { + deps += [ ":skia_graphite" ] + } + deps += [ "//gpu/config:workaround_list" ] + } + + if (skia_support_pdf) { + deps += [ + "//third_party:freetype_harfbuzz", + "//third_party/zlib", + ] + public += skia_pdf_public + sources += skia_pdf_sources + } else { + sources += [ "//third_party/skia/src/pdf/SkDocument_PDF_None.cpp" ] + } + + if (skia_support_skottie) { + import("//third_party/skia/modules/skottie/skottie.gni") + import("//third_party/skia/modules/skresources/skresources.gni") + import("//third_party/skia/modules/sksg/sksg.gni") + import("//third_party/skia/modules/skshaper/skshaper.gni") + public += skia_skottie_public + sources += skia_skottie_sources + sources += skia_skresources_sources + sources += skia_sksg_sources + sources += skia_shaper_primitive_sources + } +} + +# Template for things that are logically part of :skia, but need to be split out +# so custom compile flags can be applied. +# +# These are all opted out of check_includes, due to (logically) being part of +# skia. +template("skia_source_set") { + source_set(target_name) { + forward_variables_from(invoker, "*") + + check_includes = false + + if (!is_debug) { + configs -= [ "//build/config/compiler:default_optimization" ] + configs += [ "//build/config/compiler:optimize_max" ] + } + + configs -= [ "//build/config/compiler:chromium_code" ] + configs += [ + ":skia_config", + ":skia_library_config", + "//build/config/compiler:no_chromium_code", + ] + public_configs = [ ":skia_config" ] + + # Android, the only user of mismatched sample files (for now), gets a small + # binary size decrease by using AFDO to optimize for size in these + # source_sets. However, that also comes at a relatively big performance + # cost. + if (using_mismatched_sample_profile) { + configs -= [ "//build/config/compiler:afdo_optimize_size" ] + } + if (is_win) { + cflags_cc = [ + "/wd5041", # out-of-line definition for constexpr static data member is + # not needed and is deprecated in C++17 + ] + } + } +} + +# Split out for targeted removal of the afdo_optimize_size config on Android. +skia_source_set("skia_core_and_effects") { + defines = [] + sources = skia_core_sources + sources += skia_effects_sources + sources += skia_effects_imagefilter_sources + if (skia_support_gpu) { + deps = [ "//gpu/config:workaround_list" ] + } + visibility = [ ":skia" ] +} + +# Bits that involve special vector-y hardware. +if (current_cpu == "arm64") { + skia_source_set("skia_opts_crc32") { + sources = skia_opts.crc32_sources + cflags = [ "-march=armv8-a+crc" ] + visibility = [ ":skia_opts" ] + } +} +if (current_cpu == "x86" || current_cpu == "x64") { + skia_source_set("skia_opts_sse3") { + sources = skia_opts.ssse3_sources + if (!is_win || is_clang) { + cflags = [ "-mssse3" ] + } + if (is_win) { + defines = [ "SK_CPU_SSE_LEVEL=31" ] + } + visibility = [ ":skia_opts" ] + } + skia_source_set("skia_opts_sse42") { + sources = skia_opts.sse42_sources + if (!is_win || is_clang) { + cflags = [ "-msse4.2" ] + } + if (is_win) { + defines = [ "SK_CPU_SSE_LEVEL=42" ] + } + visibility = [ ":skia_opts" ] + } + skia_source_set("skia_opts_avx") { + sources = skia_opts.avx_sources + if (!is_win) { + cflags = [ "-mavx" ] + } + if (is_win) { + cflags = [ "/arch:AVX" ] + } + visibility = [ ":skia_opts" ] + } + skia_source_set("skia_opts_hsw") { + sources = skia_opts.hsw_sources + if (!is_win) { + cflags = [ + "-mavx2", + "-mbmi", + "-mbmi2", + "-mf16c", + "-mfma", + ] + } + if (is_win) { + cflags = [ "/arch:AVX2" ] + } + visibility = [ ":skia_opts" ] + } + skia_source_set("skia_opts_skx") { + sources = skia_opts.skx_sources + if (!is_win) { + cflags = [ "-march=skylake-avx512" ] + } + if (is_win) { + cflags = [ "/arch:AVX512" ] + } + visibility = [ ":skia_opts" ] + } +} + +skia_source_set("skia_opts") { + cflags = [] + defines = [] + + deps = [ "//base" ] + + if (current_cpu == "x86" || current_cpu == "x64") { + deps += [ + ":skia_opts_avx", + ":skia_opts_hsw", + ":skia_opts_skx", + ":skia_opts_sse3", + ":skia_opts_sse42", + ] + } else if (current_cpu == "arm") { + # The assembly uses the frame pointer register (r7 in Thumb/r11 in + # ARM), the compiler doesn't like that. + if (!is_ios) { + cflags += [ "-fomit-frame-pointer" ] + } + + if (arm_version >= 7) { + if (arm_use_neon || arm_optionally_use_neon) { + # Root build config sets -mfpu=$arm_fpu, which we expect to be neon + # when running this. + if (!arm_use_neon) { + configs -= [ "//build/config/compiler:compiler_arm_fpu" ] + cflags += [ "-mfpu=neon" ] + } + } + } + } else if (current_cpu == "arm64") { + deps += [ ":skia_opts_crc32" ] + } else if (current_cpu == "mipsel") { + cflags += [ "-fomit-frame-pointer" ] + } else if (current_cpu == "mips64el") { + cflags += [ "-fomit-frame-pointer" ] + } else if (current_cpu == "ppc64") { + # Conditional and empty body needed to avoid assert() below. + } else if (current_cpu == "s390x") { + # Conditional and empty body needed to avoid assert() below. + } else if (current_cpu == "riscv64") { + # Conditional and empty body needed to avoid assert() below. + } else { + assert(false, "Unknown cpu target") + } + + visibility = [ ":skia" ] +} + +# Split out Graphite sources because of conflicting file names with Ganesh. +if (enable_skia_graphite) { + skia_source_set("skia_graphite") { + public = skia_graphite_public + sources = skia_graphite_sources + deps = [] + if (skia_use_dawn) { + sources += skia_shared_dawn_sources + sources += skia_graphite_dawn_sources + deps += [ + ":skia_ganesh_dawn", + "//third_party/dawn/src/dawn:cpp", + "//third_party/dawn/src/dawn:proc", + ] + } + if (skia_use_metal) { + sources += skia_shared_mtl_sources + sources += skia_graphite_mtl_sources + deps += [ ":skia_ganesh_metal" ] + frameworks = [ "Metal.framework" ] + } + } + + # Even though we only use Dawn and Metal with Graphite, we need to build the + # Ganesh sources, since both Ganesh and Graphite use the same defines for + # enabling backends, or else we'll get undefined symbol errors in some cases. + if (skia_use_dawn) { + skia_source_set("skia_ganesh_dawn") { + public = skia_gpu_dawn_public + sources = skia_gpu_dawn_private + + # Ganesh Dawn backend has SkASSERT(!"umimplemented") calls which cause + # -Wstring-conversion to fail - exempt this code since it's unused anyway. + cflags_cc = [ "-Wno-string-conversion" ] + } + } + if (skia_use_metal) { + # Split out Ganesh Metal sources, because they require ARC. + skia_source_set("skia_ganesh_metal") { + public = skia_gpu_metal_public + sources = skia_gpu_metal_private + cflags_objcc = [ + "-Wno-unguarded-availability", + "-fobjc-arc", + ] + } + } +} + +# Font copies. +if (is_fuchsia) { + copy("copy_fuchsia_fonts_manifest") { + sources = [ "ext/data/test_fonts/fuchsia_test_fonts_manifest.json" ] + outputs = [ "$root_out_dir/test_fonts/all.font_manifest.json" ] + } +} +if (is_apple) { + bundle_data("test_fonts_bundle_data") { + public_deps = [ "//third_party/test_fonts" ] + + sources = [ + "$root_out_dir/test_fonts/Ahem.ttf", + "ext/data/test_fonts/ChromiumAATTest.ttf", + ] + + outputs = [ "{{bundle_resources_dir}}/{{source_file_part}}" ] + } +} + +group("test_fonts_resources") { + testonly = true + deps = [] + data_deps = [] + + if (is_apple) { + deps += [ ":test_fonts_bundle_data" ] + data_deps += [ ":test_fonts_bundle_data" ] + } else { + deps += [ "//third_party/test_fonts" ] + data_deps += [ "//third_party/test_fonts" ] + } + + if (is_fuchsia) { + deps += [ ":copy_fuchsia_fonts_manifest" ] + data_deps += [ ":copy_fuchsia_fonts_manifest" ] + } +} + +# Fuchsia components that use the test fonts must include +# "//build/config/fuchsia/test/test_fonts.shard.test-cml" in their +# `additional_manifest_fragments`. +source_set("test_fonts") { + testonly = true + + deps = [ ":test_fonts_resources" ] + + if (is_fuchsia) { + public = [ + "ext/test_fonts.h", + "ext/test_fonts_fuchsia.h", + ] + sources = [ + "ext/test_fonts_fuchsia.cc", + "ext/test_fonts_fuchsia_cfv2.cc", + ] + public_deps = [ + "//third_party/fuchsia-sdk/sdk/fidl/fuchsia.fonts:fuchsia.fonts_hlcpp", + ] + deps += [ + "//base", + "//skia", + "//third_party/fuchsia-sdk/sdk/pkg/sys_cpp", + ] + } + if (is_mac) { + public = [ "ext/test_fonts.h" ] + sources = [ "ext/test_fonts_mac.mm" ] + deps += [ "//base" ] + } +} + +test("skia_unittests") { + sources = [ + "ext/convolver_unittest.cc", + "ext/image_operations_unittest.cc", + "ext/platform_canvas_unittest.cc", + "ext/recursive_gaussian_convolution_unittest.cc", + "ext/skcolorspace_primaries_unittest.cc", + "ext/skia_memory_dump_provider_unittest.cc", + "ext/skia_utils_base_unittest.cc", + ] + if (is_ios) { + sources += [ "ext/skia_utils_ios_unittest.mm" ] + } + if (is_mac) { + sources += [ "ext/skia_utils_mac_unittest.mm" ] + } + + if (!is_win) { + sources -= [ "ext/platform_canvas_unittest.cc" ] + } + + deps = [ + ":skia", + ":test_fonts", + "//base", + "//base/test:test_support", + "//mojo/core/test:run_all_unittests", + "//testing/gtest", + "//ui/gfx", + "//ui/gfx/geometry", + ] + + data_deps = [ "//testing/buildbot/filters:skia_unittests_filters" ] + + if (!is_ios) { + sources += [ "public/mojom/test/mojom_traits_unittest.cc" ] + deps += [ + "//mojo/public/cpp/bindings", + "//mojo/public/cpp/test_support:test_utils", + "//skia/public/mojom", + ] + } + + if (is_fuchsia) { + sources += [ "ext/fontmgr_fuchsia_unittest.cc" ] + deps += [ + "//third_party/fuchsia-sdk/sdk/fidl/fuchsia.fonts:fuchsia.fonts_hlcpp", + ] + additional_manifest_fragments = + [ "//build/config/fuchsia/test/test_fonts.shard.test-cml" ] + } + + if (skia_support_skottie) { + sources += [ "ext/skottie_unittest.cc" ] + } +} + +if (!is_ios) { + executable("image_operations_bench") { + sources = [ "ext/image_operations_bench.cc" ] + + deps = [ + ":skia", + "//base", + "//build/win:default_exe_manifest", + ] + } + + executable("filter_fuzz_stub") { + testonly = true + sources = [ "tools/filter_fuzz_stub/filter_fuzz_stub.cc" ] + + deps = [ + ":skia", + "//base", + "//base/test:test_support", + "//build/win:default_exe_manifest", + ] + } +} + +group("fuzzers") { + deps = [ "//skia/tools/fuzzers" ] +} + +grit("skia_resources") { + source = "skia_resources.grd" + + outputs = [ + "grit/skia_resources.h", + "grit/skia_resources_map.cc", + "grit/skia_resources_map.h", + "skia_resources.pak", + ] + + deps = [ "//skia/public/mojom:mojom_js__generator" ] +} diff --git a/DIR_METADATA b/DIR_METADATA new file mode 100644 index 000000000000..c3717e62dd0c --- /dev/null +++ b/DIR_METADATA @@ -0,0 +1,11 @@ +# Metadata information for this directory. +# +# For more information on DIR_METADATA files, see: +# https://source.chromium.org/chromium/infra/infra/+/main:go/src/infra/tools/dirmd/README.md +# +# For the schema of this file, see Metadata message: +# https://source.chromium.org/chromium/infra/infra/+/main:go/src/infra/tools/dirmd/proto/dir_metadata.proto + +monorail { + component: "Internals>Skia" +} \ No newline at end of file diff --git a/OWNERS b/OWNERS new file mode 100644 index 000000000000..1c5983b532e6 --- /dev/null +++ b/OWNERS @@ -0,0 +1,15 @@ +set noparent +borenet@google.com +brianosman@google.com +bungeman@google.com +egdaniel@google.com +fmalita@chromium.org +kjlubick@chromium.org +michaelludwig@google.com +robertphillips@google.com +scroggo@google.com +senorblanco@chromium.org +thakis@chromium.org + +# For Fuchsia-specific changes: +per-file ..._fuchsia*=file://build/fuchsia/OWNERS diff --git a/config/SkUserConfig.h b/config/SkUserConfig.h new file mode 100644 index 000000000000..f81410af517e --- /dev/null +++ b/config/SkUserConfig.h @@ -0,0 +1,259 @@ + +/* + * Copyright 2006 The Android Open Source Project + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + + +#ifndef SKIA_CONFIG_SKUSERCONFIG_H_ +#define SKIA_CONFIG_SKUSERCONFIG_H_ + +/* SkTypes.h, the root of the public header files, does the following trick: + + #include "include/config/SkUserConfig.h" + #include "include/core/SkPostConfig.h" + #include "include/core/SkPreConfig.h" + + SkPreConfig.h runs first, and it is responsible for initializing certain + skia defines. + + SkPostConfig.h runs last, and its job is to just check that the final + defines are consistent (i.e. that we don't have mutually conflicting + defines). + + SkUserConfig.h (this file) runs in the middle. It gets to change or augment + the list of flags initially set in preconfig, and then postconfig checks + that everything still makes sense. + + Below are optional defines that add, subtract, or change default behavior + in Skia. Your port can locally edit this file to enable/disable flags as + you choose, or these can be delared on your command line (i.e. -Dfoo). + + By default, this include file will always default to having all of the flags + commented out, so including it will have no effect. +*/ + +/////////////////////////////////////////////////////////////////////////////// + +/* Skia has lots of debug-only code. Often this is just null checks or other + parameter checking, but sometimes it can be quite intrusive (e.g. check that + each 32bit pixel is in premultiplied form). This code can be very useful + during development, but will slow things down in a shipping product. + + By default, these mutually exclusive flags are defined in SkPreConfig.h, + based on the presence or absence of NDEBUG, but that decision can be changed + here. + */ +//#define SK_DEBUG +//#define SK_RELEASE + +/* Skia has certain debug-only code that is extremely intensive even for debug + builds. This code is useful for diagnosing specific issues, but is not + generally applicable, therefore it must be explicitly enabled to avoid + the performance impact. By default these flags are undefined, but can be + enabled by uncommenting them below. + */ +//#define SK_DEBUG_GLYPH_CACHE +//#define SK_DEBUG_PATH + +/* preconfig will have attempted to determine the endianness of the system, + but you can change these mutually exclusive flags here. + */ +//#define SK_CPU_BENDIAN +//#define SK_CPU_LENDIAN + +/* Most compilers use the same bit endianness for bit flags in a byte as the + system byte endianness, and this is the default. If for some reason this + needs to be overridden, specify which of the mutually exclusive flags to + use. For example, some atom processors in certain configurations have big + endian byte order but little endian bit orders. +*/ +//#define SK_UINT8_BITFIELD_BENDIAN +//#define SK_UINT8_BITFIELD_LENDIAN + + +/* To write debug messages to a console, skia will call SkDebugf(...) following + printf conventions (e.g. const char* format, ...). If you want to redirect + this to something other than printf, define yours here + */ +//#define SkDebugf(...) MyFunction(__VA_ARGS__) + +/* + * To specify a different default font cache limit, define this. If this is + * undefined, skia will use a built-in value. + */ +//#define SK_DEFAULT_FONT_CACHE_LIMIT (1024 * 1024) + +/* + * To specify the default size of the image cache, undefine this and set it to + * the desired value (in bytes). SkGraphics.h as a runtime API to set this + * value as well. If this is undefined, a built-in value will be used. + */ +//#define SK_DEFAULT_IMAGE_CACHE_LIMIT (1024 * 1024) + +/* Define this to set the upper limit for text to support LCD. Values that + are very large increase the cost in the font cache and draw slower, without + improving readability. If this is undefined, Skia will use its default + value (e.g. 48) + */ +//#define SK_MAX_SIZE_FOR_LCDTEXT 48 + +/* Change the kN32_SkColorType ordering to BGRA to work in X windows. + */ +//#define SK_R32_SHIFT 16 + + +/* Determines whether to build code that supports the Ganesh GPU backend. Some classes + that are not GPU-specific, such as SkShader subclasses, have optional code + that is used allows them to interact with this GPU backend. If you'd like to + include this code, include -DSK_GANESH in your cflags or uncomment below. + Defaults to not set (No Ganesh GPU backend). + This define affects the ABI of Skia, so make sure it matches the client which uses + the compiled version of Skia. +*/ +// #define SK_GANESH + +/* Skia makes use of histogram logging macros to trace the frequency of + * events. By default, Skia provides no-op versions of these macros. + * Skia consumers can provide their own definitions of these macros to + * integrate with their histogram collection backend. + */ +//#define SK_HISTOGRAM_BOOLEAN(name, sample) +//#define SK_HISTOGRAM_EXACT_LINEAR(name, sample, value_max) +//#define SK_HISTOGRAM_MEMORY_KB(name, sample) +#include "base/component_export.h" +#include "skia/ext/skia_histogram.h" + +// ===== Begin Chrome-specific definitions ===== + +#ifdef DCHECK_ALWAYS_ON + #undef SK_RELEASE + #define SK_DEBUG +#endif + +/* Define this to provide font subsetter for font subsetting when generating + PDF documents. + */ +#define SK_PDF_USE_HARFBUZZ_SUBSET + +// Handle exporting using base/component_export.h +#define SK_API COMPONENT_EXPORT(SKIA) + +// Chromium does not use these fonts. This define causes type1 fonts to be +// converted to type3 when producing PDFs, and reduces build size. +#define SK_PDF_DO_NOT_SUPPORT_TYPE_1_FONTS + +#ifdef SK_DEBUG +#define SK_REF_CNT_MIXIN_INCLUDE "skia/config/sk_ref_cnt_ext_debug.h" +#else +#define SK_REF_CNT_MIXIN_INCLUDE "skia/config/sk_ref_cnt_ext_release.h" +#endif + +// Log the file and line number for assertions. +#define SkDebugf(...) SkDebugf_FileLine(__FILE__, __LINE__, __VA_ARGS__) +SK_API void SkDebugf_FileLine(const char* file, + int line, + const char* format, + ...); + +#define SK_ABORT(format, ...) SkAbort_FileLine(__FILE__, __LINE__, \ + format,##__VA_ARGS__) +[[noreturn]] SK_API void SkAbort_FileLine(const char* file, + int line, + const char* format, + ...); + +#if !defined(ANDROID) // On Android, we use the skia default settings. +#define SK_A32_SHIFT 24 +#define SK_R32_SHIFT 16 +#define SK_G32_SHIFT 8 +#define SK_B32_SHIFT 0 +#endif + +#if defined(SK_BUILD_FOR_MAC) + +#define SK_CPU_LENDIAN +#undef SK_CPU_BENDIAN + +#elif defined(SK_BUILD_FOR_UNIX) || defined(SK_BUILD_FOR_ANDROID) + +// Prefer FreeType's emboldening algorithm to Skia's +// TODO: skia used to just use hairline, but has improved since then, so +// we should revisit this choice... +#define SK_USE_FREETYPE_EMBOLDEN + +#if defined(SK_BUILD_FOR_UNIX) && defined(SK_CPU_BENDIAN) +// Above we set the order for ARGB channels in registers. I suspect that, on +// big endian machines, you can keep this the same and everything will work. +// The in-memory order will be different, of course, but as long as everything +// is reading memory as words rather than bytes, it will all work. However, if +// you find that colours are messed up I thought that I would leave a helpful +// locator for you. Also see the comments in +// base/gfx/bitmap_platform_device_linux.h +#error Read the comment at this location +#endif + +#endif + +#if defined(__has_attribute) +#if __has_attribute(trivial_abi) +#define SK_TRIVIAL_ABI [[clang::trivial_abi]] +#else +#define SK_TRIVIAL_ABI +#endif +#else +#define SK_TRIVIAL_ABI +#endif + +// These flags are no longer defined in Skia, but we have them (temporarily) +// until we update our call-sites (typically these are for API changes). +// +// Remove these as we update our sites. + +#define SK_LEGACY_LAYER_BOUNDS_EXPANSION // skbug.com/12083, skbug.com/12303 + +// Workaround for poor anisotropic mipmap quality, +// pending Skia ripmap support. +// (https://bugs.chromium.org/p/skia/issues/detail?id=4863) +#define SK_SUPPORT_LEGACY_ANISOTROPIC_MIPMAP_SCALE + +// Temporarily insulate Chrome pixel tests from Skia LOD bias change on GPU. +#define SK_USE_LEGACY_MIPMAP_LOD_BIAS + +// Max. verb count for paths rendered by the edge-AA tessellating path renderer. +#define GR_AA_TESSELLATOR_MAX_VERB_COUNT 100 + +#define SK_FORCE_AAA + +#define SK_SUPPORT_LEGACY_DRAWLOOPER + +#define SK_USE_LEGACY_MIPMAP_BUILDER + +#define SK_SUPPORT_LEGACY_CONIC_CHOP + +// To be replaced with SK_ENABLE_SKSL_IN_RASTER_PIPELINE (go/sksl-rp): +#define SK_ENABLE_SKVM + +// Use the original std::vector based serializer +// Remove when new streaming support operations has been verified. +// #define SK_SUPPORT_LEGACY_STRIKE_SERIALIZATION + +///////////////////////// Imported from BUILD.gn and skia_common.gypi + +/* In some places Skia can use static initializers for global initialization, + * or fall back to lazy runtime initialization. Chrome always wants the latter. + */ +#define SK_ALLOW_STATIC_GLOBAL_INITIALIZERS 0 + +/* Restrict formats for Skia font matching to SFNT type fonts. */ +#define SK_FONT_CONFIG_INTERFACE_ONLY_ALLOW_SFNT_FONTS + +#define SK_IGNORE_BLURRED_RRECT_OPT +#define SK_USE_DISCARDABLE_SCALEDIMAGECACHE + +#define SK_ATTR_DEPRECATED SK_NOTHING_ARG1 +#define GR_GL_CUSTOM_SETUP_HEADER "GrGLConfig_chrome.h" + +#endif // SKIA_CONFIG_SKUSERCONFIG_H_ diff --git a/config/sk_ref_cnt_ext_debug.h b/config/sk_ref_cnt_ext_debug.h new file mode 100644 index 000000000000..9aa22fb9eec6 --- /dev/null +++ b/config/sk_ref_cnt_ext_debug.h @@ -0,0 +1,65 @@ +// Copyright 2013 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef SKIA_CONFIG_SK_REF_CNT_EXT_DEBUG_H_ +#define SKIA_CONFIG_SK_REF_CNT_EXT_DEBUG_H_ + +#ifdef SKIA_CONFIG_SK_REF_CNT_EXT_RELEASE_H_ +#error Only one SkRefCnt should be used. +#endif + +#include + +class SkRefCnt; + +namespace WTF { + void adopted(const SkRefCnt*); + void requireAdoption(const SkRefCnt*); +} + +// Alternate implementation of SkRefCnt for Chromium debug builds +class SK_API SkRefCnt : public SkRefCntBase { +public: + SkRefCnt(); + ~SkRefCnt() override; + void ref() const { SkASSERT(flags_.load() != AdoptionRequired_Flag); SkRefCntBase::ref(); } + void deref() const { SkRefCntBase::unref(); } +private: + void adopted() const { flags_ |= Adopted_Flag; } + void requireAdoption() const { flags_ |= AdoptionRequired_Flag; } + + enum { + Adopted_Flag = 0x1, + AdoptionRequired_Flag = 0x2, + }; + + mutable std::atomic flags_; + + friend void WTF::adopted(const SkRefCnt*); + friend void WTF::requireAdoption(const SkRefCnt*); +}; + +inline SkRefCnt::SkRefCnt() : flags_(0) { } + +inline SkRefCnt::~SkRefCnt() { } + +// Bootstrap for Blink's WTF::RefPtr + +namespace WTF { +inline void adopted(const SkRefCnt* object) { + if (!object) + return; + object->adopted(); +} +inline void requireAdoption(const SkRefCnt* object) { + if (!object) + return; + object->requireAdoption(); +} +} // namespace WTF + +using WTF::adopted; +using WTF::requireAdoption; + +#endif // SKIA_CONFIG_SK_REF_CNT_EXT_DEBUG_H_ diff --git a/config/sk_ref_cnt_ext_release.h b/config/sk_ref_cnt_ext_release.h new file mode 100644 index 000000000000..2ff35afdbb11 --- /dev/null +++ b/config/sk_ref_cnt_ext_release.h @@ -0,0 +1,26 @@ +// Copyright 2013 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef SKIA_CONFIG_SK_REF_CNT_EXT_RELEASE_H_ +#define SKIA_CONFIG_SK_REF_CNT_EXT_RELEASE_H_ + +#ifdef SKIA_CONFIG_SK_REF_CNT_EXT_DEBUG_H_ +#error Only one SkRefCnt should be used. +#endif + +// Alternate implementation of SkRefCnt for Chromium release builds +class SK_API SkRefCnt : public SkRefCntBase { +public: + void deref() const { SkRefCntBase::unref(); } +}; + +namespace WTF { +inline void Adopted(const SkRefCnt* object) {} +inline void RequireAdoption(const SkRefCnt* object) {} +} // namespace WTF + +using WTF::Adopted; +using WTF::RequireAdoption; + +#endif // SKIA_CONFIG_SK_REF_CNT_EXT_RELEASE_H_ diff --git a/ext/.gitignore b/ext/.gitignore new file mode 100644 index 000000000000..375d0df8a163 --- /dev/null +++ b/ext/.gitignore @@ -0,0 +1 @@ +skia_commit_hash.h diff --git a/ext/SkDiscardableMemory_chrome.cc b/ext/SkDiscardableMemory_chrome.cc new file mode 100644 index 000000000000..9962e1391f9b --- /dev/null +++ b/ext/SkDiscardableMemory_chrome.cc @@ -0,0 +1,47 @@ +// Copyright 2013 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "skia/ext/SkDiscardableMemory_chrome.h" + +#include + +#include + +#include "base/functional/callback_helpers.h" +#include "base/memory/discardable_memory.h" +#include "base/memory/discardable_memory_allocator.h" + +SkDiscardableMemoryChrome::~SkDiscardableMemoryChrome() = default; + +bool SkDiscardableMemoryChrome::lock() { + return discardable_->Lock(); +} + +void* SkDiscardableMemoryChrome::data() { + return discardable_->data(); +} + +void SkDiscardableMemoryChrome::unlock() { + discardable_->Unlock(); +} + +SkDiscardableMemoryChrome::SkDiscardableMemoryChrome( + std::unique_ptr memory) + : discardable_(std::move(memory)) {} + +base::trace_event::MemoryAllocatorDump* +SkDiscardableMemoryChrome::CreateMemoryAllocatorDump( + const char* name, + base::trace_event::ProcessMemoryDump* pmd) const { + return discardable_->CreateMemoryAllocatorDump(name, pmd); +} + +SkDiscardableMemory* SkDiscardableMemory::Create(size_t bytes) { + // TODO(crbug.com/1034271): Make the caller handle a nullptr return value, + // and do not die when the allocation fails. + auto discardable = base::DiscardableMemoryAllocator::GetInstance() + ->AllocateLockedDiscardableMemoryWithRetryOrDie( + bytes, base::DoNothing()); + return new SkDiscardableMemoryChrome(std::move(discardable)); +} diff --git a/ext/SkDiscardableMemory_chrome.h b/ext/SkDiscardableMemory_chrome.h new file mode 100644 index 000000000000..63440ee9fb6b --- /dev/null +++ b/ext/SkDiscardableMemory_chrome.h @@ -0,0 +1,45 @@ +// Copyright 2013 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef SKIA_EXT_SKDISCARDABLEMEMORY_CHROME_H_ +#define SKIA_EXT_SKDISCARDABLEMEMORY_CHROME_H_ + +#include + +#include "third_party/skia/include/private/chromium/SkDiscardableMemory.h" + +namespace base { +class DiscardableMemory; + +namespace trace_event { +class MemoryAllocatorDump; +class ProcessMemoryDump; +} + +} // namespace base + +// This class implements the SkDiscardableMemory interface using +// base::DiscardableMemory. +class SK_API SkDiscardableMemoryChrome : public SkDiscardableMemory { +public: + ~SkDiscardableMemoryChrome() override; + + // SkDiscardableMemory: + bool lock() override; + void* data() override; + void unlock() override; + + base::trace_event::MemoryAllocatorDump* CreateMemoryAllocatorDump( + const char* name, + base::trace_event::ProcessMemoryDump* pmd) const; + +private: + friend class SkDiscardableMemory; + + SkDiscardableMemoryChrome(std::unique_ptr memory); + + std::unique_ptr discardable_; +}; + +#endif // SKIA_EXT_SKDISCARDABLEMEMORY_CHROME_H_ diff --git a/ext/SkMemory_new_handler.cpp b/ext/SkMemory_new_handler.cpp new file mode 100644 index 000000000000..886feba39dfb --- /dev/null +++ b/ext/SkMemory_new_handler.cpp @@ -0,0 +1,135 @@ +// Copyright 2012 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include +#include + +#include + +#include "base/debug/alias.h" +#include "base/process/memory.h" +#include "build/build_config.h" +#include "third_party/skia/include/core/SkTypes.h" +#include "third_party/skia/include/private/base/SkMalloc.h" + +#if BUILDFLAG(IS_WIN) +#include +#endif + +// This implementation of sk_malloc_flags() and friends is similar to +// SkMemory_malloc.cpp, except it uses base::UncheckedMalloc and friends +// for non-SK_MALLOC_THROW calls. +// +// The name of this file is historic: a previous implementation tried to +// use std::set_new_handler() for the same effect, but it didn't actually work. + +static inline void* throw_on_failure(size_t size, void* p) { + if (size > 0 && p == NULL) { + // If we've got a NULL here, the only reason we should have failed is running out of RAM. + sk_out_of_memory(); + } + return p; +} + +void sk_abort_no_print() { + // Linker's ICF feature may merge this function with other functions with + // the same definition (e.g. any function whose sole job is to call abort()) + // and it may confuse the crash report processing system. + // http://crbug.com/860850 + static int static_variable_to_make_this_function_unique = 0x736b; // "sk" + base::debug::Alias(&static_variable_to_make_this_function_unique); + + abort(); +} + +void sk_out_of_memory(void) { + SkASSERT(!"sk_out_of_memory"); + base::TerminateBecauseOutOfMemory(0); + // Extra safety abort(). + abort(); +} + +void* sk_realloc_throw(void* addr, size_t size) { + // This is the "normal" behavior of realloc(), but per man malloc(3), POSIX + // compliance doesn't require it. Skia does though, starting with + // https://skia-review.googlesource.com/c/skia/+/647456. + if (size == 0) { + sk_free(addr); + return nullptr; + } + return throw_on_failure(size, realloc(addr, size)); +} + +void sk_free(void* p) { + if (p) { + free(p); + } +} + +// We get lots of bugs filed on us that amount to overcommiting bitmap memory, +// then some time later failing to back that VM with physical memory. +// They're hard to track down, so in Debug mode we touch all memory right up front. +// +// For malloc, fill is an arbitrary byte and ideally not 0. For calloc, it's got to be 0. +static void* prevent_overcommit(int fill, size_t size, void* p) { + // We probably only need to touch one byte per page, but memset makes things easy. + SkDEBUGCODE(memset(p, fill, size)); + return p; +} + +static void* malloc_throw(size_t size) { + return prevent_overcommit(0x42, size, throw_on_failure(size, malloc(size))); +} + +static void* malloc_nothrow(size_t size) { + // TODO(b.kelemen): we should always use UncheckedMalloc but currently it + // doesn't work as intended everywhere. + void* result; +#if BUILDFLAG(IS_IOS) + result = malloc(size); +#else + // It's the responsibility of the caller to check the return value. + std::ignore = base::UncheckedMalloc(size, &result); +#endif + if (result) { + prevent_overcommit(0x47, size, result); + } + return result; +} + +static void* calloc_throw(size_t size) { + return prevent_overcommit(0, size, throw_on_failure(size, calloc(size, 1))); +} + +static void* calloc_nothrow(size_t size) { + // TODO(b.kelemen): we should always use UncheckedCalloc but currently it + // doesn't work as intended everywhere. + void* result; +#if BUILDFLAG(IS_IOS) + result = calloc(1, size); +#else + // It's the responsibility of the caller to check the return value. + std::ignore = base::UncheckedCalloc(size, 1, &result); +#endif + if (result) { + prevent_overcommit(0, size, result); + } + return result; +} + +void* sk_malloc_flags(size_t size, unsigned flags) { + if (flags & SK_MALLOC_ZERO_INITIALIZE) { + if (flags & SK_MALLOC_THROW) { + return calloc_throw(size); + } else { + return calloc_nothrow(size); + } + } else { + if (flags & SK_MALLOC_THROW) { + return malloc_throw(size); + } else { + return malloc_nothrow(size); + } + } +} diff --git a/ext/benchmarking_canvas.cc b/ext/benchmarking_canvas.cc new file mode 100644 index 000000000000..b58434740169 --- /dev/null +++ b/ext/benchmarking_canvas.cc @@ -0,0 +1,585 @@ +// Copyright 2013 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "skia/ext/benchmarking_canvas.h" + +#include +#include +#include +#include + +#include "base/check_op.h" +#include "base/memory/ptr_util.h" +#include "base/memory/raw_ptr.h" +#include "base/time/time.h" +#include "base/values.h" +#include "third_party/skia/include/core/SkBitmap.h" +#include "third_party/skia/include/core/SkColorFilter.h" +#include "third_party/skia/include/core/SkImage.h" +#include "third_party/skia/include/core/SkImageFilter.h" +#include "third_party/skia/include/core/SkPaint.h" +#include "third_party/skia/include/core/SkPath.h" +#include "third_party/skia/include/core/SkPicture.h" +#include "third_party/skia/include/core/SkRRect.h" +#include "third_party/skia/include/core/SkRegion.h" +#include "third_party/skia/include/core/SkString.h" +#include "third_party/skia/include/core/SkTextBlob.h" + +namespace { + +class FlagsBuilder { +public: + FlagsBuilder(char separator) + : separator_(separator) {} + + void addFlag(bool flag_val, const char flag_name[]) { + if (!flag_val) + return; + if (!oss_.str().empty()) + oss_ << separator_; + + oss_ << flag_name; + } + + std::string str() const { + return oss_.str(); + } + +private: + char separator_; + std::ostringstream oss_; +}; + +base::Value AsValue(bool b) { + return base::Value(b); +} + +base::Value AsValue(SkScalar scalar) { + return base::Value(scalar); +} + +base::Value AsValue(const SkSize& size) { + base::Value::Dict val; + val.Set("width", AsValue(size.width())); + val.Set("height", AsValue(size.height())); + + return base::Value(std::move(val)); +} + +base::Value AsValue(const SkPoint& point) { + base::Value::Dict val; + val.Set("x", AsValue(point.x())); + val.Set("y", AsValue(point.y())); + + return base::Value(std::move(val)); +} + +base::Value AsValue(const SkRect& rect) { + base::Value::Dict val; + val.Set("left", AsValue(rect.fLeft)); + val.Set("top", AsValue(rect.fTop)); + val.Set("right", AsValue(rect.fRight)); + val.Set("bottom", AsValue(rect.fBottom)); + + return base::Value(std::move(val)); +} + +base::Value AsValue(const SkRRect& rrect) { + base::Value::Dict radii_val; + radii_val.Set("upper-left", AsValue(rrect.radii(SkRRect::kUpperLeft_Corner))); + radii_val.Set("upper-right", + AsValue(rrect.radii(SkRRect::kUpperRight_Corner))); + radii_val.Set("lower-right", + AsValue(rrect.radii(SkRRect::kLowerRight_Corner))); + radii_val.Set("lower-left", AsValue(rrect.radii(SkRRect::kLowerLeft_Corner))); + + base::Value::Dict val; + val.Set("rect", AsValue(rrect.rect())); + val.Set("radii", std::move(radii_val)); + + return base::Value(std::move(val)); +} + +base::Value AsValue(const SkMatrix& matrix) { + base::Value::List val; + for (int i = 0; i < 9; ++i) + val.Append(AsValue(matrix[i])); + + return base::Value(std::move(val)); +} + +base::Value AsValue(SkColor color) { + base::Value::Dict val; + val.Set("a", int{SkColorGetA(color)}); + val.Set("r", int{SkColorGetR(color)}); + val.Set("g", int{SkColorGetG(color)}); + val.Set("b", int{SkColorGetB(color)}); + + return base::Value(std::move(val)); +} + +base::Value AsValue(SkBlendMode mode) { + return base::Value(SkBlendMode_Name(mode)); +} + +base::Value AsValue(SkCanvas::PointMode mode) { + static const char* gModeStrings[] = { "Points", "Lines", "Polygon" }; + DCHECK_LT(static_cast(mode), std::size(gModeStrings)); + + return base::Value(gModeStrings[mode]); +} + +base::Value AsValue(const SkColorFilter& filter) { + base::Value::Dict val; + + if (filter.isAlphaUnchanged()) { + FlagsBuilder builder('|'); + builder.addFlag(true, "kAlphaUnchanged_Flag"); + + val.Set("flags", builder.str()); + } + + SkScalar color_matrix[20]; + if (filter.asAColorMatrix(color_matrix)) { + base::Value::List color_matrix_val; + for (unsigned i = 0; i < 20; ++i) { + color_matrix_val.Append(AsValue(color_matrix[i])); + } + + val.Set("color_matrix", std::move(color_matrix_val)); + } + + return base::Value(std::move(val)); +} + +base::Value AsValue(const SkImageFilter& filter) { + base::Value::Dict val; + val.Set("inputs", filter.countInputs()); + + SkColorFilter* color_filter; + if (filter.asColorFilter(&color_filter)) { + val.Set("color_filter", AsValue(*color_filter)); + SkSafeUnref(color_filter); // ref'd in asColorFilter + } + + return base::Value(std::move(val)); +} + +base::Value AsValue(const SkPaint& paint) { + base::Value::Dict val; + SkPaint default_paint; + + if (paint.getColor() != default_paint.getColor()) + val.Set("Color", AsValue(paint.getColor())); + + if (paint.getStyle() != default_paint.getStyle()) { + static const char* gStyleStrings[] = { "Fill", "Stroke", "StrokeFill" }; + DCHECK_LT(static_cast(paint.getStyle()), + std::size(gStyleStrings)); + val.Set("Style", gStyleStrings[paint.getStyle()]); + } + + if (paint.asBlendMode() != default_paint.asBlendMode()) { + val.Set("Xfermode", AsValue(paint.getBlendMode_or(SkBlendMode::kSrcOver))); + } + + if (paint.isAntiAlias() || paint.isDither()) { + FlagsBuilder builder('|'); + builder.addFlag(paint.isAntiAlias(), "AntiAlias"); + builder.addFlag(paint.isDither(), "Dither"); + + val.Set("Flags", builder.str()); + } + + if (paint.getColorFilter()) + val.Set("ColorFilter", AsValue(*paint.getColorFilter())); + + if (paint.getImageFilter()) + val.Set("ImageFilter", AsValue(*paint.getImageFilter())); + + return base::Value(std::move(val)); +} + +base::Value SaveLayerFlagsAsValue(SkCanvas::SaveLayerFlags flags) { + return base::Value(int{flags}); +} + +base::Value AsValue(SkClipOp op) { + static const char* gOpStrings[] = { "Difference", + "Intersect", + "Union", + "XOR", + "ReverseDifference", + "Replace" + }; + size_t index = static_cast(op); + DCHECK_LT(index, std::size(gOpStrings)); + return base::Value(gOpStrings[index]); +} + +base::Value AsValue(const SkRegion& region) { + base::Value::Dict val; + val.Set("bounds", AsValue(SkRect::Make(region.getBounds()))); + + return base::Value(std::move(val)); +} + +base::Value AsValue(const SkImage& image) { + base::Value::Dict val; + val.Set("size", AsValue(SkSize::Make(image.width(), image.height()))); + + return base::Value(std::move(val)); +} + +base::Value AsValue(const SkTextBlob& blob) { + base::Value::Dict val; + val.Set("bounds", AsValue(blob.bounds())); + + return base::Value(std::move(val)); +} + +base::Value AsValue(const SkPath& path) { + base::Value::Dict val; + + static const char* gFillStrings[] = + { "winding", "even-odd", "inverse-winding", "inverse-even-odd" }; + size_t index = static_cast(path.getFillType()); + DCHECK_LT(index, std::size(gFillStrings)); + val.Set("fill-type", gFillStrings[index]); + val.Set("convex", path.isConvex()); + val.Set("is-rect", path.isRect(nullptr)); + val.Set("bounds", AsValue(path.getBounds())); + + static const char* gVerbStrings[] = + { "move", "line", "quad", "conic", "cubic", "close", "done" }; + static const int gPtsPerVerb[] = { 1, 1, 2, 2, 3, 0, 0 }; + static const int gPtOffsetPerVerb[] = { 0, 1, 1, 1, 1, 0, 0 }; + static_assert( + std::size(gVerbStrings) == static_cast(SkPath::kDone_Verb + 1), + "gVerbStrings size mismatch"); + static_assert( + std::size(gVerbStrings) == std::size(gPtsPerVerb), + "gPtsPerVerb size mismatch"); + static_assert( + std::size(gVerbStrings) == std::size(gPtOffsetPerVerb), + "gPtOffsetPerVerb size mismatch"); + + base::Value::List verbs_val; + SkPath::RawIter iter(const_cast(path)); + SkPoint points[4]; + + for (SkPath::Verb verb = iter.next(points); verb != SkPath::kDone_Verb; + verb = iter.next(points)) { + DCHECK_LT(static_cast(verb), std::size(gVerbStrings)); + + base::Value::Dict verb_val; + base::Value::List pts_val; + + for (int i = 0; i < gPtsPerVerb[verb]; ++i) + pts_val.Append(AsValue(points[i + gPtOffsetPerVerb[verb]])); + + verb_val.Set(gVerbStrings[verb], std::move(pts_val)); + + if (SkPath::kConic_Verb == verb) + verb_val.Set("weight", AsValue(iter.conicWeight())); + + verbs_val.Append(std::move(verb_val)); + } + val.Set("verbs", std::move(verbs_val)); + + return base::Value(std::move(val)); +} + +template +base::Value AsListValue(const T array[], size_t count) { + base::Value::List val; + + for (size_t i = 0; i < count; ++i) + val.Append(AsValue(array[i])); + + return base::Value(std::move(val)); +} + +} // namespace + +namespace skia { + +class BenchmarkingCanvas::AutoOp { +public: + // AutoOp objects are always scoped within draw call frames, + // so the paint is guaranteed to be valid for their lifetime. + AutoOp(BenchmarkingCanvas* canvas, + const char op_name[], + const SkPaint* paint = nullptr) + : canvas_(canvas) { + DCHECK(canvas); + DCHECK(op_name); + + op_record_.Set("cmd_string", op_name); + base::Value* op_params = op_record_.Set("info", base::Value::List()); + DCHECK(op_params); + DCHECK(op_params->is_list()); + op_params_ = &op_params->GetList(); + + if (paint) { + this->addParam("paint", AsValue(*paint)); + filtered_paint_ = *paint; + } + + start_ticks_ = base::TimeTicks::Now(); + } + + ~AutoOp() { + base::TimeDelta ticks = base::TimeTicks::Now() - start_ticks_; + op_record_.Set("cmd_time", ticks.InMillisecondsF()); + + canvas_->op_records_.Append(std::move(op_record_)); + } + + void addParam(const char name[], base::Value value) { + base::Value::Dict param; + param.Set(name, std::move(value)); + + op_params_->Append(std::move(param)); + } + + const SkPaint* paint() const { return &filtered_paint_; } + +private: + raw_ptr canvas_; + base::Value::Dict op_record_; + raw_ptr op_params_; + base::TimeTicks start_ticks_; + + SkPaint filtered_paint_; +}; + +BenchmarkingCanvas::BenchmarkingCanvas(SkCanvas* canvas) + : INHERITED(canvas->imageInfo().width(), + canvas->imageInfo().height()) { + addCanvas(canvas); +} + +BenchmarkingCanvas::~BenchmarkingCanvas() = default; + +size_t BenchmarkingCanvas::CommandCount() const { + return op_records_.size(); +} + +const base::Value::List& BenchmarkingCanvas::Commands() const { + return op_records_; +} + +double BenchmarkingCanvas::GetTime(size_t index) { + const base::Value& op = op_records_[index]; + if (!op.is_dict()) + return 0; + return op.GetDict().FindDouble("cmd_time").value_or(0); +} + +void BenchmarkingCanvas::willSave() { + AutoOp op(this, "Save"); + + INHERITED::willSave(); +} + +SkCanvas::SaveLayerStrategy BenchmarkingCanvas::getSaveLayerStrategy( + const SaveLayerRec& rec) { + AutoOp op(this, "SaveLayer", rec.fPaint); + if (rec.fBounds) + op.addParam("bounds", AsValue(*rec.fBounds)); + if (rec.fSaveLayerFlags) + op.addParam("flags", SaveLayerFlagsAsValue(rec.fSaveLayerFlags)); + + return INHERITED::getSaveLayerStrategy(rec); +} + +void BenchmarkingCanvas::willRestore() { + AutoOp op(this, "Restore"); + + INHERITED::willRestore(); +} + +void BenchmarkingCanvas::didConcat44(const SkM44& m) { + SkScalar values[16]; + m.getColMajor(values); + AutoOp op(this, "Concat"); + op.addParam("matrix", AsListValue(values, 16)); + + INHERITED::didConcat44(m); +} + +void BenchmarkingCanvas::didScale(SkScalar x, SkScalar y) { + AutoOp op(this, "Scale"); + op.addParam("scale-x", AsValue(x)); + op.addParam("scale-y", AsValue(y)); + + INHERITED::didScale(x, y); +} + +void BenchmarkingCanvas::didTranslate(SkScalar x, SkScalar y) { + AutoOp op(this, "Translate"); + op.addParam("translate-x", AsValue(x)); + op.addParam("translate-y", AsValue(y)); + + INHERITED::didTranslate(x, y); +} + +void BenchmarkingCanvas::didSetM44(const SkM44& m) { + SkScalar values[16]; + m.getColMajor(values); + AutoOp op(this, "SetMatrix"); + op.addParam("matrix", AsListValue(values, 16)); + + INHERITED::didSetM44(m); +} + +void BenchmarkingCanvas::onClipRect(const SkRect& rect, + SkClipOp region_op, + SkCanvas::ClipEdgeStyle style) { + AutoOp op(this, "ClipRect"); + op.addParam("rect", AsValue(rect)); + op.addParam("op", AsValue(region_op)); + op.addParam("anti-alias", AsValue(style == kSoft_ClipEdgeStyle)); + + INHERITED::onClipRect(rect, region_op, style); +} + +void BenchmarkingCanvas::onClipRRect(const SkRRect& rrect, + SkClipOp region_op, + SkCanvas::ClipEdgeStyle style) { + AutoOp op(this, "ClipRRect"); + op.addParam("rrect", AsValue(rrect)); + op.addParam("op", AsValue(region_op)); + op.addParam("anti-alias", AsValue(style == kSoft_ClipEdgeStyle)); + + INHERITED::onClipRRect(rrect, region_op, style); +} + +void BenchmarkingCanvas::onClipPath(const SkPath& path, + SkClipOp region_op, + SkCanvas::ClipEdgeStyle style) { + AutoOp op(this, "ClipPath"); + op.addParam("path", AsValue(path)); + op.addParam("op", AsValue(region_op)); + op.addParam("anti-alias", AsValue(style == kSoft_ClipEdgeStyle)); + + INHERITED::onClipPath(path, region_op, style); +} + +void BenchmarkingCanvas::onClipRegion(const SkRegion& region, + SkClipOp region_op) { + AutoOp op(this, "ClipRegion"); + op.addParam("region", AsValue(region)); + op.addParam("op", AsValue(region_op)); + + INHERITED::onClipRegion(region, region_op); +} + +void BenchmarkingCanvas::onDrawPaint(const SkPaint& paint) { + AutoOp op(this, "DrawPaint", &paint); + + INHERITED::onDrawPaint(*op.paint()); +} + +void BenchmarkingCanvas::onDrawPoints(PointMode mode, size_t count, + const SkPoint pts[], const SkPaint& paint) { + AutoOp op(this, "DrawPoints", &paint); + op.addParam("mode", AsValue(mode)); + op.addParam("points", AsListValue(pts, count)); + + INHERITED::onDrawPoints(mode, count, pts, *op.paint()); +} + +void BenchmarkingCanvas::onDrawRect(const SkRect& rect, const SkPaint& paint) { + AutoOp op(this, "DrawRect", &paint); + op.addParam("rect", AsValue(rect)); + + INHERITED::onDrawRect(rect, *op.paint()); +} + +void BenchmarkingCanvas::onDrawOval(const SkRect& rect, const SkPaint& paint) { + AutoOp op(this, "DrawOval", &paint); + op.addParam("rect", AsValue(rect)); + + INHERITED::onDrawOval(rect, *op.paint()); +} + +void BenchmarkingCanvas::onDrawRRect(const SkRRect& rrect, const SkPaint& paint) { + AutoOp op(this, "DrawRRect", &paint); + op.addParam("rrect", AsValue(rrect)); + + INHERITED::onDrawRRect(rrect, *op.paint()); +} + +void BenchmarkingCanvas::onDrawDRRect(const SkRRect& outer, const SkRRect& inner, + const SkPaint& paint) { + AutoOp op(this, "DrawDRRect", &paint); + op.addParam("outer", AsValue(outer)); + op.addParam("inner", AsValue(inner)); + + INHERITED::onDrawDRRect(outer, inner, *op.paint()); +} + +void BenchmarkingCanvas::onDrawPath(const SkPath& path, const SkPaint& paint) { + AutoOp op(this, "DrawPath", &paint); + op.addParam("path", AsValue(path)); + + INHERITED::onDrawPath(path, *op.paint()); +} + +void BenchmarkingCanvas::onDrawPicture(const SkPicture* picture, + const SkMatrix* matrix, + const SkPaint* paint) { + DCHECK(picture); + AutoOp op(this, "DrawPicture", paint); + op.addParam("picture", AsValue(picture)); + if (matrix) + op.addParam("matrix", AsValue(*matrix)); + + INHERITED::onDrawPicture(picture, matrix, op.paint()); +} + +void BenchmarkingCanvas::onDrawImage2(const SkImage* image, + SkScalar left, + SkScalar top, + const SkSamplingOptions& sampling, + const SkPaint* paint) { + DCHECK(image); + AutoOp op(this, "DrawImage", paint); + op.addParam("image", AsValue(*image)); + op.addParam("left", AsValue(left)); + op.addParam("top", AsValue(top)); + + INHERITED::onDrawImage2(image, left, top, sampling, op.paint()); +} + +void BenchmarkingCanvas::onDrawImageRect2(const SkImage* image, + const SkRect& src, + const SkRect& dst, + const SkSamplingOptions& sampling, + const SkPaint* paint, + SrcRectConstraint constraint) { + DCHECK(image); + AutoOp op(this, "DrawImageRect", paint); + op.addParam("image", AsValue(*image)); + op.addParam("src", AsValue(src)); + op.addParam("dst", AsValue(dst)); + + INHERITED::onDrawImageRect2(image, src, dst, sampling, op.paint(), + constraint); +} + +void BenchmarkingCanvas::onDrawTextBlob(const SkTextBlob* blob, SkScalar x, SkScalar y, + const SkPaint& paint) { + DCHECK(blob); + AutoOp op(this, "DrawTextBlob", &paint); + op.addParam("blob", AsValue(*blob)); + op.addParam("x", AsValue(x)); + op.addParam("y", AsValue(y)); + + INHERITED::onDrawTextBlob(blob, x, y, *op.paint()); +} + +} // namespace skia diff --git a/ext/benchmarking_canvas.h b/ext/benchmarking_canvas.h new file mode 100644 index 000000000000..5aa183e973d9 --- /dev/null +++ b/ext/benchmarking_canvas.h @@ -0,0 +1,80 @@ +// Copyright 2013 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef SKIA_EXT_BENCHMARKING_CANVAS_H_ +#define SKIA_EXT_BENCHMARKING_CANVAS_H_ + +#include + +#include "base/values.h" +#include "third_party/skia/include/utils/SkNWayCanvas.h" + +namespace skia { + +class SK_API BenchmarkingCanvas : public SkNWayCanvas { +public: + BenchmarkingCanvas(SkCanvas* canvas); + ~BenchmarkingCanvas() override; + + // Returns the number of draw commands executed on this canvas. + size_t CommandCount() const; + + // Returns the list of executed draw commands. + const base::Value::List& Commands() const; + + // Return the recorded render time (milliseconds) for a draw command index. + double GetTime(size_t index); + +protected: + // SkCanvas overrides + void willSave() override; + SaveLayerStrategy getSaveLayerStrategy(const SaveLayerRec&) override; + void willRestore() override; + + void didConcat44(const SkM44&) override; + void didScale(SkScalar, SkScalar) override; + void didTranslate(SkScalar, SkScalar) override; + void didSetM44(const SkM44&) override; + + void onClipRect(const SkRect&, SkClipOp, ClipEdgeStyle) override; + void onClipRRect(const SkRRect&, SkClipOp, ClipEdgeStyle) override; + void onClipPath(const SkPath&, SkClipOp, ClipEdgeStyle) override; + void onClipRegion(const SkRegion&, SkClipOp) override; + + void onDrawPaint(const SkPaint&) override; + void onDrawPoints(PointMode, size_t count, const SkPoint pts[], + const SkPaint&) override; + void onDrawRect(const SkRect&, const SkPaint&) override; + void onDrawOval(const SkRect&, const SkPaint&) override; + void onDrawRRect(const SkRRect&, const SkPaint&) override; + void onDrawDRRect(const SkRRect&, const SkRRect&, const SkPaint&) override; + void onDrawPath(const SkPath&, const SkPaint&) override; + + void onDrawPicture(const SkPicture*, const SkMatrix*, const SkPaint*) override; + + void onDrawImage2(const SkImage*, + SkScalar left, + SkScalar top, + const SkSamplingOptions&, + const SkPaint*) override; + void onDrawImageRect2(const SkImage*, + const SkRect& src, + const SkRect& dst, + const SkSamplingOptions&, + const SkPaint*, + SrcRectConstraint) override; + + void onDrawTextBlob(const SkTextBlob* blob, SkScalar x, SkScalar y, + const SkPaint& paint) override; + +private: + typedef SkNWayCanvas INHERITED; + + class AutoOp; + + base::Value::List op_records_; +}; + +} +#endif // SKIA_EXT_BENCHMARKING_CANVAS_H_ diff --git a/ext/cicp.cc b/ext/cicp.cc new file mode 100644 index 000000000000..c15cd8da0c2f --- /dev/null +++ b/ext/cicp.cc @@ -0,0 +1,241 @@ +// Copyright 2022 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "skia/ext/cicp.h" +#include "skia/ext/skcolorspace_primaries.h" +#include "skia/ext/skcolorspace_trfn.h" + +namespace skia { + +bool CICPGetPrimaries(uint8_t primaries, SkColorSpacePrimaries& sk_primaries) { + // Rec. ITU-T H.273, Table 2. + switch (primaries) { + case 0: + // Reserved. + break; + case 1: + sk_primaries = SkNamedPrimariesExt::kRec709; + return true; + case 2: + // Unspecified. + break; + case 3: + // Reserved. + break; + case 4: + sk_primaries = SkNamedPrimariesExt::kRec470SystemM; + return true; + case 5: + sk_primaries = SkNamedPrimariesExt::kRec470SystemBG; + return true; + case 6: + sk_primaries = SkNamedPrimariesExt::kRec601; + return true; + case 7: + sk_primaries = SkNamedPrimariesExt::kSMPTE_ST_240; + return true; + case 8: + sk_primaries = SkNamedPrimariesExt::kGenericFilm; + return true; + case 9: + sk_primaries = SkNamedPrimariesExt::kRec2020; + return true; + case 10: + sk_primaries = SkNamedPrimariesExt::kSMPTE_ST_428_1; + return true; + case 11: + sk_primaries = SkNamedPrimariesExt::kSMPTE_RP_431_2; + return true; + case 12: + sk_primaries = SkNamedPrimariesExt::kSMPTE_EG_432_1; + return true; + case 22: + sk_primaries = SkNamedPrimariesExt::kITU_T_H273_Value22; + return true; + default: + // Reserved. + break; + } + sk_primaries = SkNamedPrimariesExt::kInvalid; + return false; +} + +bool CICPGetTransferFn(uint8_t transfer_characteristics, + bool prefer_srgb_trfn, + skcms_TransferFunction& trfn) { + // Rec. ITU-T H.273, Table 3. + switch (transfer_characteristics) { + case 0: + // Reserved. + break; + case 1: + trfn = prefer_srgb_trfn ? SkNamedTransferFnExt::kSRGB + : SkNamedTransferFnExt::kRec709; + return true; + case 2: + // Unspecified. + break; + case 3: + // Reserved. + break; + case 4: + trfn = SkNamedTransferFnExt::kRec470SystemM; + return true; + case 5: + trfn = SkNamedTransferFnExt::kRec470SystemBG; + return true; + case 6: + trfn = prefer_srgb_trfn ? SkNamedTransferFnExt::kSRGB + : SkNamedTransferFnExt::kRec601; + return true; + case 7: + trfn = SkNamedTransferFnExt::kSMPTE_ST_240; + return true; + case 8: + trfn = SkNamedTransferFn::kLinear; + return true; + case 9: + // Logarithmic transfer characteristic (100:1 range). + break; + case 10: + // Logarithmic transfer characteristic (100 * Sqrt( 10 ) : 1 range). + break; + case 11: + trfn = prefer_srgb_trfn ? SkNamedTransferFnExt::kSRGB + : SkNamedTransferFnExt::kIEC61966_2_4; + break; + case 12: + // Rec. ITU-R BT.1361-0 extended colour gamut system (historical). + // Same as kRec709 on positive values, differs on negative values. + break; + case 13: + // IEC 61966-2-1. + trfn = SkNamedTransferFnExt::kSRGB; + return true; + case 14: + trfn = SkNamedTransferFnExt::kRec2020_10bit; + return true; + case 15: + trfn = SkNamedTransferFnExt::kRec2020_12bit; + return true; + case 16: + trfn = SkNamedTransferFn::kPQ; + return true; + case 17: + trfn = SkNamedTransferFnExt::kSMPTE_ST_428_1; + return true; + case 18: + trfn = SkNamedTransferFn::kHLG; + return true; + default: + // 19-255 Reserved. + break; + } + + trfn = SkNamedTransferFnExt::kInvalid; + return false; +} + +sk_sp CICPGetSkColorSpace(uint8_t color_primaries, + uint8_t transfer_characteristics, + uint8_t matrix_coefficients, + uint8_t full_range_flag, + bool prefer_srgb_trfn) { + if (matrix_coefficients != 0) + return nullptr; + + if (full_range_flag != 1) + return nullptr; + + skcms_TransferFunction trfn; + if (!CICPGetTransferFn(transfer_characteristics, prefer_srgb_trfn, trfn)) + return nullptr; + + SkColorSpacePrimaries primaries; + if (!CICPGetPrimaries(color_primaries, primaries)) + return nullptr; + + skcms_Matrix3x3 primaries_matrix; + if (!primaries.toXYZD50(&primaries_matrix)) + return nullptr; + + return SkColorSpace::MakeRGB(trfn, primaries_matrix); +} + +bool CICPGetSkYUVColorSpace(uint8_t matrix_coefficients, + uint8_t full_range_flag, + uint8_t bits_per_color, + SkYUVColorSpace& yuv_color_space) { + // Rec. ITU-T H.273, Table 4. + switch (matrix_coefficients) { + case 0: + // The identity matrix. + if (full_range_flag) { + yuv_color_space = kIdentity_SkYUVColorSpace; + return true; + } + break; + case 1: + // Rec. ITU-R BT.709-6. + yuv_color_space = full_range_flag ? kRec709_Full_SkYUVColorSpace + : kRec709_Limited_SkYUVColorSpace; + return true; + case 5: + // Rec. ITU-R BT.470-6 System B, G (historical). + case 6: + // Rec. ITU-R BT.601-7. + yuv_color_space = full_range_flag ? kJPEG_SkYUVColorSpace + : kRec601_Limited_SkYUVColorSpace; + return true; + case 9: + // Rec. ITU-R BT.2020-2 (non-constant luminance) + case 10: + // Rec. ITU-R BT.2020-2 (constant luminance) + switch (bits_per_color) { + case 8: + yuv_color_space = full_range_flag + ? kBT2020_8bit_Full_SkYUVColorSpace + : kBT2020_8bit_Limited_SkYUVColorSpace; + return true; + case 10: + yuv_color_space = full_range_flag + ? kBT2020_10bit_Full_SkYUVColorSpace + : kBT2020_10bit_Limited_SkYUVColorSpace; + return true; + case 12: + yuv_color_space = full_range_flag + ? kBT2020_12bit_Full_SkYUVColorSpace + : kBT2020_12bit_Limited_SkYUVColorSpace; + return true; + default: + break; + } + break; + case 2: + // Unspecified. + case 3: + // Reserved. + case 4: + // United States Federal Communications Commission. + case 7: + // SMPTE ST 240. + case 8: + // YCgCo + case 11: + // YDZDX + case 12: + // Chromaticity-derived non-constant luminance system. + case 13: + // Chromaticity-derived constant luminance system. + case 14: + // ICTCP + default: + // Reserved. + break; + } + yuv_color_space = kIdentity_SkYUVColorSpace; + return false; +} + +} // namespace skia diff --git a/ext/cicp.h b/ext/cicp.h new file mode 100644 index 000000000000..063bd5cea40e --- /dev/null +++ b/ext/cicp.h @@ -0,0 +1,52 @@ +// Copyright 2022 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef SKIA_EXT_CICP_H_ +#define SKIA_EXT_CICP_H_ + +#include + +#include "third_party/skia/include/core/SkColorSpace.h" +#include "third_party/skia/include/core/SkImageInfo.h" + +namespace skia { + +// Convert from a CICP primary value listed in Rec. ITU-T H.273, Table 2 to an +// SkColorSpacePrimaries. Return true if `primaries` is valid. All valid values +// are supported. +SK_API bool CICPGetPrimaries(uint8_t primaries, + SkColorSpacePrimaries& sk_primaries); + +// Convert from a CICP transfer value listed in Rec. ITU-T H.273, Table 3 to an +// skcms_TransferFunction. Return true if `transfer_characteristics` is valid +// and can be represented using an skcms_TransferFunction (several valid values +// cannot). If `prefer_srgb_trfn` is set to true, then use the sRGB transfer +// function for all Rec709-like content. +SK_API bool CICPGetTransferFn(uint8_t transfer_characteristics, + bool prefer_srgb_trfn, + skcms_TransferFunction& sk_trfn); + +// Return the SkColorSpace resulting from the CICPGetPrimaries and +// CICPGetTransferFn. This function does not populate an SkYUVColorSpace, so +// return nullptr if `matrix_coefficients` is not the identity or +// `full_range_flag` is not full range. +SK_API sk_sp CICPGetSkColorSpace(uint8_t color_primaries, + uint8_t transfer_characteristics, + uint8_t matrix_coefficients, + uint8_t full_range_flag, + bool prefer_srgb_trfn); + +// Convert from a CICP matrix value listed in Rec. ITU-T H.273, Table 4 to an +// SkYUVColorSpace. The result depends on full or limited range as well as +// the number of bits per color. Return true if the combination of +// `matrix_coefficients`, `full_range_flag`, and `bits_per_color` is valid and +// supported (several valid combinations are not supported). +SK_API bool CICPGetSkYUVColorSpace(uint8_t matrix_coefficients, + uint8_t full_range_flag, + uint8_t bits_per_color, + SkYUVColorSpace& yuv_color_space); + +} // namespace skia + +#endif // SKIA_EXT_CICP_H_ diff --git a/ext/convolver.cc b/ext/convolver.cc new file mode 100644 index 000000000000..8b3bd070d168 --- /dev/null +++ b/ext/convolver.cc @@ -0,0 +1,719 @@ +// Copyright 2011 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include + +#include "base/check_op.h" +#include "base/notreached.h" +#include "skia/ext/convolver.h" +#include "skia/ext/convolver_SSE2.h" +#include "skia/ext/convolver_mips_dspr2.h" +#include "skia/ext/convolver_neon.h" +#include "third_party/skia/include/core/SkSize.h" +#include "third_party/skia/include/core/SkTypes.h" + +namespace skia { + +namespace { + +// Converts the argument to an 8-bit unsigned value by clamping to the range +// 0-255. +inline unsigned char ClampTo8(int a) { + if (static_cast(a) < 256) + return a; // Avoid the extra check in the common case. + if (a < 0) + return 0; + return 255; +} + +// Takes the value produced by accumulating element-wise product of image with +// a kernel and brings it back into range. +// All of the filter scaling factors are in fixed point with kShiftBits bits of +// fractional part. +inline unsigned char BringBackTo8(int a, bool take_absolute) { + a >>= ConvolutionFilter1D::kShiftBits; + if (take_absolute) + a = std::abs(a); + return ClampTo8(a); +} + +// Stores a list of rows in a circular buffer. The usage is you write into it +// by calling AdvanceRow. It will keep track of which row in the buffer it +// should use next, and the total number of rows added. +class CircularRowBuffer { + public: + // The number of pixels in each row is given in |source_row_pixel_width|. + // The maximum number of rows needed in the buffer is |max_y_filter_size| + // (we only need to store enough rows for the biggest filter). + // + // We use the |first_input_row| to compute the coordinates of all of the + // following rows returned by Advance(). + CircularRowBuffer(int dest_row_pixel_width, int max_y_filter_size, + int first_input_row) + : row_byte_width_(dest_row_pixel_width * 4), + num_rows_(max_y_filter_size), + next_row_(0), + next_row_coordinate_(first_input_row) { + buffer_.resize(row_byte_width_ * max_y_filter_size); + row_addresses_.resize(num_rows_); + } + + // Moves to the next row in the buffer, returning a pointer to the beginning + // of it. + unsigned char* AdvanceRow() { + unsigned char* row = &buffer_[next_row_ * row_byte_width_]; + next_row_coordinate_++; + + // Set the pointer to the next row to use, wrapping around if necessary. + next_row_++; + if (next_row_ == num_rows_) + next_row_ = 0; + return row; + } + + // Returns a pointer to an "unrolled" array of rows. These rows will start + // at the y coordinate placed into |*first_row_index| and will continue in + // order for the maximum number of rows in this circular buffer. + // + // The |first_row_index_| may be negative. This means the circular buffer + // starts before the top of the image (it hasn't been filled yet). + unsigned char* const* GetRowAddresses(int* first_row_index) { + // Example for a 4-element circular buffer holding coords 6-9. + // Row 0 Coord 8 + // Row 1 Coord 9 + // Row 2 Coord 6 <- next_row_ = 2, next_row_coordinate_ = 10. + // Row 3 Coord 7 + // + // The "next" row is also the first (lowest) coordinate. This computation + // may yield a negative value, but that's OK, the math will work out + // since the user of this buffer will compute the offset relative + // to the first_row_index and the negative rows will never be used. + *first_row_index = next_row_coordinate_ - num_rows_; + + int cur_row = next_row_; + for (int i = 0; i < num_rows_; i++) { + row_addresses_[i] = &buffer_[cur_row * row_byte_width_]; + + // Advance to the next row, wrapping if necessary. + cur_row++; + if (cur_row == num_rows_) + cur_row = 0; + } + return &row_addresses_[0]; + } + + private: + // The buffer storing the rows. They are packed, each one row_byte_width_. + std::vector buffer_; + + // Number of bytes per row in the |buffer_|. + int row_byte_width_; + + // The number of rows available in the buffer. + int num_rows_; + + // The next row index we should write into. This wraps around as the + // circular buffer is used. + int next_row_; + + // The y coordinate of the |next_row_|. This is incremented each time a + // new row is appended and does not wrap. + int next_row_coordinate_; + + // Buffer used by GetRowAddresses(). + std::vector row_addresses_; +}; + +// Convolves horizontally along a single row. The row data is given in +// |src_data| and continues for the num_values() of the filter. +template +void ConvolveHorizontally(const unsigned char* src_data, + const ConvolutionFilter1D& filter, + unsigned char* out_row) { + // Loop over each pixel on this row in the output image. + int num_values = filter.num_values(); + for (int out_x = 0; out_x < num_values; out_x++) { + // Get the filter that determines the current output pixel. + int filter_offset, filter_length; + const ConvolutionFilter1D::Fixed* filter_values = + filter.FilterForValue(out_x, &filter_offset, &filter_length); + + // Compute the first pixel in this row that the filter affects. It will + // touch |filter_length| pixels (4 bytes each) after this. + const unsigned char* row_to_filter = &src_data[filter_offset * 4]; + + // Apply the filter to the row to get the destination pixel in |accum|. + int accum[4] = {0}; + for (int filter_x = 0; filter_x < filter_length; filter_x++) { + ConvolutionFilter1D::Fixed cur_filter = filter_values[filter_x]; + accum[0] += cur_filter * row_to_filter[filter_x * 4 + 0]; + accum[1] += cur_filter * row_to_filter[filter_x * 4 + 1]; + accum[2] += cur_filter * row_to_filter[filter_x * 4 + 2]; + if (has_alpha) + accum[3] += cur_filter * row_to_filter[filter_x * 4 + 3]; + } + + // Bring this value back in range. All of the filter scaling factors + // are in fixed point with kShiftBits bits of fractional part. + accum[0] >>= ConvolutionFilter1D::kShiftBits; + accum[1] >>= ConvolutionFilter1D::kShiftBits; + accum[2] >>= ConvolutionFilter1D::kShiftBits; + if (has_alpha) + accum[3] >>= ConvolutionFilter1D::kShiftBits; + + // Store the new pixel. + out_row[out_x * 4 + 0] = ClampTo8(accum[0]); + out_row[out_x * 4 + 1] = ClampTo8(accum[1]); + out_row[out_x * 4 + 2] = ClampTo8(accum[2]); + if (has_alpha) + out_row[out_x * 4 + 3] = ClampTo8(accum[3]); + } +} + +// Does vertical convolution to produce one output row. The filter values and +// length are given in the first two parameters. These are applied to each +// of the rows pointed to in the |source_data_rows| array, with each row +// being |pixel_width| wide. +// +// The output must have room for |pixel_width * 4| bytes. +template +void ConvolveVertically(const ConvolutionFilter1D::Fixed* filter_values, + int filter_length, + unsigned char* const* source_data_rows, + int pixel_width, + unsigned char* out_row) { + // We go through each column in the output and do a vertical convolution, + // generating one output pixel each time. + for (int out_x = 0; out_x < pixel_width; out_x++) { + // Compute the number of bytes over in each row that the current column + // we're convolving starts at. The pixel will cover the next 4 bytes. + int byte_offset = out_x * 4; + + // Apply the filter to one column of pixels. + int accum[4] = {0}; + for (int filter_y = 0; filter_y < filter_length; filter_y++) { + ConvolutionFilter1D::Fixed cur_filter = filter_values[filter_y]; + accum[0] += cur_filter * source_data_rows[filter_y][byte_offset + 0]; + accum[1] += cur_filter * source_data_rows[filter_y][byte_offset + 1]; + accum[2] += cur_filter * source_data_rows[filter_y][byte_offset + 2]; + if (has_alpha) + accum[3] += cur_filter * source_data_rows[filter_y][byte_offset + 3]; + } + + // Bring this value back in range. All of the filter scaling factors + // are in fixed point with kShiftBits bits of precision. + accum[0] >>= ConvolutionFilter1D::kShiftBits; + accum[1] >>= ConvolutionFilter1D::kShiftBits; + accum[2] >>= ConvolutionFilter1D::kShiftBits; + if (has_alpha) + accum[3] >>= ConvolutionFilter1D::kShiftBits; + + // Store the new pixel. + out_row[byte_offset + 0] = ClampTo8(accum[0]); + out_row[byte_offset + 1] = ClampTo8(accum[1]); + out_row[byte_offset + 2] = ClampTo8(accum[2]); + if (has_alpha) { + unsigned char alpha = ClampTo8(accum[3]); + + // Make sure the alpha channel doesn't come out smaller than any of the + // color channels. We use premultipled alpha channels, so this should + // never happen, but rounding errors will cause this from time to time. + // These "impossible" colors will cause overflows (and hence random pixel + // values) when the resulting bitmap is drawn to the screen. + // + // We only need to do this when generating the final output row (here). + int max_color_channel = std::max(out_row[byte_offset + 0], + std::max(out_row[byte_offset + 1], out_row[byte_offset + 2])); + if (alpha < max_color_channel) + out_row[byte_offset + 3] = max_color_channel; + else + out_row[byte_offset + 3] = alpha; + } else { + // No alpha channel, the image is opaque. + out_row[byte_offset + 3] = 0xff; + } + } +} + +void ConvolveVertically(const ConvolutionFilter1D::Fixed* filter_values, + int filter_length, + unsigned char* const* source_data_rows, + int pixel_width, + unsigned char* out_row, + bool source_has_alpha) { + if (source_has_alpha) { + ConvolveVertically(filter_values, filter_length, + source_data_rows, + pixel_width, + out_row); + } else { + ConvolveVertically(filter_values, filter_length, + source_data_rows, + pixel_width, + out_row); + } +} + +} // namespace + +// ConvolutionFilter1D --------------------------------------------------------- + +ConvolutionFilter1D::ConvolutionFilter1D() + : max_filter_(0) { +} + +ConvolutionFilter1D::~ConvolutionFilter1D() = default; + +void ConvolutionFilter1D::AddFilter(int filter_offset, + const float* filter_values, + int filter_length) { + SkASSERT(filter_length > 0); + + std::vector fixed_values; + fixed_values.reserve(filter_length); + + for (int i = 0; i < filter_length; ++i) + fixed_values.push_back(FloatToFixed(filter_values[i])); + + AddFilter(filter_offset, &fixed_values[0], filter_length); +} + +void ConvolutionFilter1D::AddFilter(int filter_offset, + const Fixed* filter_values, + int filter_length) { + // It is common for leading/trailing filter values to be zeros. In such + // cases it is beneficial to only store the central factors. + // For a scaling to 1/4th in each dimension using a Lanczos-2 filter on + // a 1080p image this optimization gives a ~10% speed improvement. + int filter_size = filter_length; + int first_non_zero = 0; + while (first_non_zero < filter_length && filter_values[first_non_zero] == 0) + first_non_zero++; + + if (first_non_zero < filter_length) { + // Here we have at least one non-zero factor. + int last_non_zero = filter_length - 1; + while (last_non_zero >= 0 && filter_values[last_non_zero] == 0) + last_non_zero--; + + filter_offset += first_non_zero; + filter_length = last_non_zero + 1 - first_non_zero; + SkASSERT(filter_length > 0); + + for (int i = first_non_zero; i <= last_non_zero; i++) + filter_values_.push_back(filter_values[i]); + } else { + // Here all the factors were zeroes. + filter_length = 0; + } + + FilterInstance instance; + + // We pushed filter_length elements onto filter_values_ + instance.data_location = (static_cast(filter_values_.size()) - + filter_length); + instance.offset = filter_offset; + instance.trimmed_length = filter_length; + instance.length = filter_size; + filters_.push_back(instance); + + max_filter_ = std::max(max_filter_, filter_length); +} + +const ConvolutionFilter1D::Fixed* ConvolutionFilter1D::GetSingleFilter( + int* specified_filter_length, + int* filter_offset, + int* filter_length) const { + const FilterInstance& filter = filters_[0]; + *filter_offset = filter.offset; + *filter_length = filter.trimmed_length; + *specified_filter_length = filter.length; + if (filter.trimmed_length == 0) + return NULL; + + return &filter_values_[filter.data_location]; +} + +typedef void (*ConvolveVertically_pointer)( + const ConvolutionFilter1D::Fixed* filter_values, + int filter_length, + unsigned char* const* source_data_rows, + int pixel_width, + unsigned char* out_row, + bool has_alpha); +typedef void (*Convolve4RowsHorizontally_pointer)( + const unsigned char* src_data[4], + const ConvolutionFilter1D& filter, + unsigned char* out_row[4]); +typedef void (*ConvolveHorizontally_pointer)( + const unsigned char* src_data, + const ConvolutionFilter1D& filter, + unsigned char* out_row, + bool has_alpha); + +struct ConvolveProcs { + // This is how many extra pixels may be read by the + // conolve*horizontally functions. + int extra_horizontal_reads; + ConvolveVertically_pointer convolve_vertically; + Convolve4RowsHorizontally_pointer convolve_4rows_horizontally; + ConvolveHorizontally_pointer convolve_horizontally; +}; + +void SetupSIMD(ConvolveProcs *procs) { +#ifdef SIMD_SSE2 + procs->extra_horizontal_reads = 3; + procs->convolve_vertically = &ConvolveVertically_SSE2; + procs->convolve_4rows_horizontally = &Convolve4RowsHorizontally_SSE2; + procs->convolve_horizontally = &ConvolveHorizontally_SSE2; +#elif defined SIMD_MIPS_DSPR2 + procs->extra_horizontal_reads = 3; + procs->convolve_vertically = &ConvolveVertically_mips_dspr2; + procs->convolve_horizontally = &ConvolveHorizontally_mips_dspr2; +#elif defined SIMD_NEON + procs->extra_horizontal_reads = 3; + procs->convolve_vertically = &ConvolveVertically_Neon; + procs->convolve_4rows_horizontally = &Convolve4RowsHorizontally_Neon; + procs->convolve_horizontally = &ConvolveHorizontally_Neon; +#endif +} + +void BGRAConvolve2D(const unsigned char* source_data, + int source_byte_row_stride, + bool source_has_alpha, + const ConvolutionFilter1D& filter_x, + const ConvolutionFilter1D& filter_y, + int output_byte_row_stride, + unsigned char* output, + bool use_simd_if_possible) { + ConvolveProcs simd; + simd.extra_horizontal_reads = 0; + simd.convolve_vertically = NULL; + simd.convolve_4rows_horizontally = NULL; + simd.convolve_horizontally = NULL; + if (use_simd_if_possible) { + SetupSIMD(&simd); + } + + int max_y_filter_size = filter_y.max_filter(); + + // The next row in the input that we will generate a horizontally + // convolved row for. If the filter doesn't start at the beginning of the + // image (this is the case when we are only resizing a subset), then we + // don't want to generate any output rows before that. Compute the starting + // row for convolution as the first pixel for the first vertical filter. + int filter_offset, filter_length; + const ConvolutionFilter1D::Fixed* filter_values = + filter_y.FilterForValue(0, &filter_offset, &filter_length); + int next_x_row = filter_offset; + + // We loop over each row in the input doing a horizontal convolution. This + // will result in a horizontally convolved image. We write the results into + // a circular buffer of convolved rows and do vertical convolution as rows + // are available. This prevents us from having to store the entire + // intermediate image and helps cache coherency. + // We will need four extra rows to allow horizontal convolution could be done + // simultaneously. We also padding each row in row buffer to be aligned-up to + // 16 bytes. + // TODO(jiesun): We do not use aligned load from row buffer in vertical + // convolution pass yet. Somehow Windows does not like it. + int row_buffer_width = (filter_x.num_values() + 15) & ~0xF; + int row_buffer_height = max_y_filter_size + + (simd.convolve_4rows_horizontally ? 4 : 0); + CircularRowBuffer row_buffer(row_buffer_width, + row_buffer_height, + filter_offset); + + // Loop over every possible output row, processing just enough horizontal + // convolutions to run each subsequent vertical convolution. + SkASSERT(output_byte_row_stride >= filter_x.num_values() * 4); + int num_output_rows = filter_y.num_values(); + + // We need to check which is the last line to convolve before we advance 4 + // lines in one iteration. + int last_filter_offset, last_filter_length; + + // SSE2 can access up to 3 extra pixels past the end of the + // buffer. At the bottom of the image, we have to be careful + // not to access data past the end of the buffer. Normally + // we fall back to the C++ implementation for the last row. + // If the last row is less than 3 pixels wide, we may have to fall + // back to the C++ version for more rows. Compute how many + // rows we need to avoid the SSE implementation for here. + filter_x.FilterForValue(filter_x.num_values() - 1, &last_filter_offset, + &last_filter_length); + int avoid_simd_rows = 1 + simd.extra_horizontal_reads / + (last_filter_offset + last_filter_length); + + filter_y.FilterForValue(num_output_rows - 1, &last_filter_offset, + &last_filter_length); + + for (int out_y = 0; out_y < num_output_rows; out_y++) { + filter_values = filter_y.FilterForValue(out_y, + &filter_offset, &filter_length); + + // Generate output rows until we have enough to run the current filter. + while (next_x_row < filter_offset + filter_length) { + if (simd.convolve_4rows_horizontally && + next_x_row + 3 < last_filter_offset + last_filter_length - + avoid_simd_rows) { + const unsigned char* src[4]; + unsigned char* out_row[4]; + for (int i = 0; i < 4; ++i) { + src[i] = &source_data[(next_x_row + i) * source_byte_row_stride]; + out_row[i] = row_buffer.AdvanceRow(); + } + simd.convolve_4rows_horizontally(src, filter_x, out_row); + next_x_row += 4; + } else { + // Check if we need to avoid SSE2 for this row. + if (simd.convolve_horizontally && + next_x_row < last_filter_offset + last_filter_length - + avoid_simd_rows) { + simd.convolve_horizontally( + &source_data[next_x_row * source_byte_row_stride], + filter_x, row_buffer.AdvanceRow(), source_has_alpha); + } else { + if (source_has_alpha) { + ConvolveHorizontally( + &source_data[next_x_row * source_byte_row_stride], + filter_x, row_buffer.AdvanceRow()); + } else { + ConvolveHorizontally( + &source_data[next_x_row * source_byte_row_stride], + filter_x, row_buffer.AdvanceRow()); + } + } + next_x_row++; + } + } + + // Compute where in the output image this row of final data will go. + unsigned char* cur_output_row = &output[out_y * output_byte_row_stride]; + + // Get the list of rows that the circular buffer has, in order. + int first_row_in_circular_buffer; + unsigned char* const* rows_to_convolve = + row_buffer.GetRowAddresses(&first_row_in_circular_buffer); + + // Now compute the start of the subset of those rows that the filter + // needs. + unsigned char* const* first_row_for_filter = + &rows_to_convolve[filter_offset - first_row_in_circular_buffer]; + + if (simd.convolve_vertically) { + simd.convolve_vertically(filter_values, filter_length, + first_row_for_filter, + filter_x.num_values(), cur_output_row, + source_has_alpha); + } else { + ConvolveVertically(filter_values, filter_length, + first_row_for_filter, + filter_x.num_values(), cur_output_row, + source_has_alpha); + } + } +} + +void SingleChannelConvolveX1D(const unsigned char* source_data, + int source_byte_row_stride, + int input_channel_index, + int input_channel_count, + const ConvolutionFilter1D& filter, + const SkISize& image_size, + unsigned char* output, + int output_byte_row_stride, + int output_channel_index, + int output_channel_count, + bool absolute_values) { + int filter_offset, filter_length, filter_size; + // Very much unlike BGRAConvolve2D, here we expect to have the same filter + // for all pixels. + const ConvolutionFilter1D::Fixed* filter_values = + filter.GetSingleFilter(&filter_size, &filter_offset, &filter_length); + + if (filter_values == NULL || image_size.width() < filter_size) { + NOTREACHED(); + return; + } + + int centrepoint = filter_length / 2; + if (filter_size - filter_offset != 2 * filter_offset) { + // This means the original filter was not symmetrical AND + // got clipped from one side more than from the other. + centrepoint = filter_size / 2 - filter_offset; + } + + const unsigned char* source_data_row = source_data; + unsigned char* output_row = output; + + for (int r = 0; r < image_size.height(); ++r) { + unsigned char* target_byte = output_row + output_channel_index; + // Process the lead part, padding image to the left with the first pixel. + int c = 0; + for (; c < centrepoint; ++c, target_byte += output_channel_count) { + int accval = 0; + int i = 0; + int pixel_byte_index = input_channel_index; + for (; i < centrepoint - c; ++i) // Padding part. + accval += filter_values[i] * source_data_row[pixel_byte_index]; + + for (; i < filter_length; ++i, pixel_byte_index += input_channel_count) + accval += filter_values[i] * source_data_row[pixel_byte_index]; + + *target_byte = BringBackTo8(accval, absolute_values); + } + + // Now for the main event. + for (; c < image_size.width() - centrepoint; + ++c, target_byte += output_channel_count) { + int accval = 0; + int pixel_byte_index = (c - centrepoint) * input_channel_count + + input_channel_index; + + for (int i = 0; i < filter_length; + ++i, pixel_byte_index += input_channel_count) { + accval += filter_values[i] * source_data_row[pixel_byte_index]; + } + + *target_byte = BringBackTo8(accval, absolute_values); + } + + for (; c < image_size.width(); ++c, target_byte += output_channel_count) { + int accval = 0; + int overlap_taps = image_size.width() - c + centrepoint; + int pixel_byte_index = (c - centrepoint) * input_channel_count + + input_channel_index; + int i = 0; + for (; i < overlap_taps - 1; ++i, pixel_byte_index += input_channel_count) + accval += filter_values[i] * source_data_row[pixel_byte_index]; + + for (; i < filter_length; ++i) + accval += filter_values[i] * source_data_row[pixel_byte_index]; + + *target_byte = BringBackTo8(accval, absolute_values); + } + + source_data_row += source_byte_row_stride; + output_row += output_byte_row_stride; + } +} + +void SingleChannelConvolveY1D(const unsigned char* source_data, + int source_byte_row_stride, + int input_channel_index, + int input_channel_count, + const ConvolutionFilter1D& filter, + const SkISize& image_size, + unsigned char* output, + int output_byte_row_stride, + int output_channel_index, + int output_channel_count, + bool absolute_values) { + int filter_offset, filter_length, filter_size; + // Very much unlike BGRAConvolve2D, here we expect to have the same filter + // for all pixels. + const ConvolutionFilter1D::Fixed* filter_values = + filter.GetSingleFilter(&filter_size, &filter_offset, &filter_length); + + if (filter_values == NULL || image_size.height() < filter_size) { + NOTREACHED(); + return; + } + + int centrepoint = filter_length / 2; + if (filter_size - filter_offset != 2 * filter_offset) { + // This means the original filter was not symmetrical AND + // got clipped from one side more than from the other. + centrepoint = filter_size / 2 - filter_offset; + } + + for (int c = 0; c < image_size.width(); ++c) { + unsigned char* target_byte = output + c * output_channel_count + + output_channel_index; + int r = 0; + + for (; r < centrepoint; ++r, target_byte += output_byte_row_stride) { + int accval = 0; + int i = 0; + int pixel_byte_index = c * input_channel_count + input_channel_index; + + for (; i < centrepoint - r; ++i) // Padding part. + accval += filter_values[i] * source_data[pixel_byte_index]; + + for (; i < filter_length; ++i, pixel_byte_index += source_byte_row_stride) + accval += filter_values[i] * source_data[pixel_byte_index]; + + *target_byte = BringBackTo8(accval, absolute_values); + } + + for (; r < image_size.height() - centrepoint; + ++r, target_byte += output_byte_row_stride) { + int accval = 0; + int pixel_byte_index = (r - centrepoint) * source_byte_row_stride + + c * input_channel_count + input_channel_index; + for (int i = 0; i < filter_length; + ++i, pixel_byte_index += source_byte_row_stride) { + accval += filter_values[i] * source_data[pixel_byte_index]; + } + + *target_byte = BringBackTo8(accval, absolute_values); + } + + for (; r < image_size.height(); + ++r, target_byte += output_byte_row_stride) { + int accval = 0; + int overlap_taps = image_size.height() - r + centrepoint; + int pixel_byte_index = (r - centrepoint) * source_byte_row_stride + + c * input_channel_count + input_channel_index; + int i = 0; + for (; i < overlap_taps - 1; + ++i, pixel_byte_index += source_byte_row_stride) { + accval += filter_values[i] * source_data[pixel_byte_index]; + } + + for (; i < filter_length; ++i) + accval += filter_values[i] * source_data[pixel_byte_index]; + + *target_byte = BringBackTo8(accval, absolute_values); + } + } +} + +void SetUpGaussianConvolutionKernel(ConvolutionFilter1D* filter, + float kernel_sigma, + bool derivative) { + DCHECK(filter != NULL); + DCHECK_GT(kernel_sigma, 0.0); + const int tail_length = static_cast(4.0f * kernel_sigma + 0.5f); + const int kernel_size = tail_length * 2 + 1; + const float sigmasq = kernel_sigma * kernel_sigma; + std::vector kernel_weights(kernel_size, 0.0); + float kernel_sum = 1.0f; + + kernel_weights[tail_length] = 1.0f; + + for (int ii = 1; ii <= tail_length; ++ii) { + float v = std::exp(-0.5f * ii * ii / sigmasq); + kernel_weights[tail_length + ii] = v; + kernel_weights[tail_length - ii] = v; + kernel_sum += 2.0f * v; + } + + for (int i = 0; i < kernel_size; ++i) + kernel_weights[i] /= kernel_sum; + + if (derivative) { + kernel_weights[tail_length] = 0.0; + for (int ii = 1; ii <= tail_length; ++ii) { + float v = sigmasq * kernel_weights[tail_length + ii] / ii; + kernel_weights[tail_length + ii] = v; + kernel_weights[tail_length - ii] = -v; + } + } + + filter->AddFilter(0, &kernel_weights[0], kernel_weights.size()); +} + +} // namespace skia diff --git a/ext/convolver.h b/ext/convolver.h new file mode 100644 index 000000000000..2409ce4ec1dd --- /dev/null +++ b/ext/convolver.h @@ -0,0 +1,243 @@ +// Copyright 2012 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef SKIA_EXT_CONVOLVER_H_ +#define SKIA_EXT_CONVOLVER_H_ + +#include + +#include +#include + +#include "build/build_config.h" +#include "third_party/skia/include/core/SkSize.h" +#include "third_party/skia/include/core/SkTypes.h" + +// We can build SSE2 optimized versions for all x86 CPUs +// except when building for the IOS emulator. +#if defined(ARCH_CPU_X86_FAMILY) && !BUILDFLAG(IS_IOS) +#define SIMD_SSE2 1 +#define SIMD_PADDING 8 // 8 * int16_t +#endif + +#if defined (ARCH_CPU_MIPS_FAMILY) && \ + defined(__mips_dsp) && (__mips_dsp_rev >= 2) +#define SIMD_MIPS_DSPR2 1 +#endif + +#if defined(ARCH_CPU_ARM_FAMILY) && \ + (defined(__ARM_NEON__) || defined(__ARM_NEON)) +#define SIMD_NEON 1 +#endif + +// avoid confusion with Mac OS X's math library (Carbon) +#if defined(__APPLE__) +#undef FloatToFixed +#undef FixedToFloat +#endif + +namespace skia { + +// Represents a filter in one dimension. Each output pixel has one entry in this +// object for the filter values contributing to it. You build up the filter +// list by calling AddFilter for each output pixel (in order). +// +// We do 2-dimensional convolution by first convolving each row by one +// ConvolutionFilter1D, then convolving each column by another one. +// +// Entries are stored in fixed point, shifted left by kShiftBits. +class ConvolutionFilter1D { + public: + typedef short Fixed; + + // The number of bits that fixed point values are shifted by. + enum { kShiftBits = 14 }; + + SK_API ConvolutionFilter1D(); + SK_API ~ConvolutionFilter1D(); + + // Convert between floating point and our fixed point representation. + static Fixed FloatToFixed(float f) { + return static_cast(f * (1 << kShiftBits)); + } + static unsigned char FixedToChar(Fixed x) { + return static_cast(x >> kShiftBits); + } + static float FixedToFloat(Fixed x) { + // The cast relies on Fixed being a short, implying that on + // the platforms we care about all (16) bits will fit into + // the mantissa of a (32-bit) float. + static_assert(sizeof(Fixed) == 2, + "fixed type should fit in float mantissa"); + float raw = static_cast(x); + return ldexpf(raw, -kShiftBits); + } + + // Returns the maximum pixel span of a filter. + int max_filter() const { return max_filter_; } + + // Returns the number of filters in this filter. This is the dimension of the + // output image. + int num_values() const { return static_cast(filters_.size()); } + + // Appends the given list of scaling values for generating a given output + // pixel. |filter_offset| is the distance from the edge of the image to where + // the scaling factors start. The scaling factors apply to the source pixels + // starting from this position, and going for the next |filter_length| pixels. + // + // You will probably want to make sure your input is normalized (that is, + // all entries in |filter_values| sub to one) to prevent affecting the overall + // brighness of the image. + // + // The filter_length must be > 0. + // + // This version will automatically convert your input to fixed point. + SK_API void AddFilter(int filter_offset, + const float* filter_values, + int filter_length); + + // Same as the above version, but the input is already fixed point. + void AddFilter(int filter_offset, + const Fixed* filter_values, + int filter_length); + + // Retrieves a filter for the given |value_offset|, a position in the output + // image in the direction we're convolving. The offset and length of the + // filter values are put into the corresponding out arguments (see AddFilter + // above for what these mean), and a pointer to the first scaling factor is + // returned. There will be |filter_length| values in this array. + inline const Fixed* FilterForValue(int value_offset, + int* filter_offset, + int* filter_length) const { + const FilterInstance& filter = filters_[value_offset]; + *filter_offset = filter.offset; + *filter_length = filter.trimmed_length; + if (filter.trimmed_length == 0) { + return NULL; + } + return &filter_values_[filter.data_location]; + } + + // Retrieves the filter for the offset 0, presumed to be the one and only. + // The offset and length of the filter values are put into the corresponding + // out arguments (see AddFilter). Note that |filter_legth| and + // |specified_filter_length| may be different if leading/trailing zeros of the + // original floating point form were clipped. + // There will be |filter_length| values in the return array. + // Returns NULL if the filter is 0-length (for instance when all floating + // point values passed to AddFilter were clipped to 0). + SK_API const Fixed* GetSingleFilter(int* specified_filter_length, + int* filter_offset, + int* filter_length) const; + + inline void PaddingForSIMD() { + // Padding |padding_count| of more dummy coefficients after the coefficients + // of last filter to prevent SIMD instructions which load 8 or 16 bytes + // together to access invalid memory areas. We are not trying to align the + // coefficients right now due to the opaqueness of implementation. + // This has to be done after all |AddFilter| calls. +#ifdef SIMD_PADDING + for (int i = 0; i < SIMD_PADDING; ++i) + filter_values_.push_back(static_cast(0)); +#endif + } + + private: + struct FilterInstance { + // Offset within filter_values for this instance of the filter. + int data_location; + + // Distance from the left of the filter to the center. IN PIXELS + int offset; + + // Number of values in this filter instance. + int trimmed_length; + + // Filter length as specified. Note that this may be different from + // 'trimmed_length' if leading/trailing zeros of the original floating + // point form were clipped differently on each tail. + int length; + }; + + // Stores the information for each filter added to this class. + std::vector filters_; + + // We store all the filter values in this flat list, indexed by + // |FilterInstance.data_location| to avoid the mallocs required for storing + // each one separately. + std::vector filter_values_; + + // The maximum size of any filter we've added. + int max_filter_; +}; + +// Does a two-dimensional convolution on the given source image. +// +// It is assumed the source pixel offsets referenced in the input filters +// reference only valid pixels, so the source image size is not required. Each +// row of the source image starts |source_byte_row_stride| after the previous +// one (this allows you to have rows with some padding at the end). +// +// The result will be put into the given output buffer. The destination image +// size will be xfilter.num_values() * yfilter.num_values() pixels. It will be +// in rows of exactly xfilter.num_values() * 4 bytes. +// +// |source_has_alpha| is a hint that allows us to avoid doing computations on +// the alpha channel if the image is opaque. If you don't know, set this to +// true and it will work properly, but setting this to false will be a few +// percent faster if you know the image is opaque. +// +// The layout in memory is assumed to be 4-bytes per pixel in B-G-R-A order +// (this is ARGB when loaded into 32-bit words on a little-endian machine). +SK_API void BGRAConvolve2D(const unsigned char* source_data, + int source_byte_row_stride, + bool source_has_alpha, + const ConvolutionFilter1D& xfilter, + const ConvolutionFilter1D& yfilter, + int output_byte_row_stride, + unsigned char* output, + bool use_simd_if_possible); + +// Does a 1D convolution of the given source image along the X dimension on +// a single channel of the bitmap. +// +// The function uses the same convolution kernel for each pixel. That kernel +// must be added to |filter| at offset 0. This is a most straightforward +// implementation of convolution, intended chiefly for development purposes. +SK_API void SingleChannelConvolveX1D(const unsigned char* source_data, + int source_byte_row_stride, + int input_channel_index, + int input_channel_count, + const ConvolutionFilter1D& filter, + const SkISize& image_size, + unsigned char* output, + int output_byte_row_stride, + int output_channel_index, + int output_channel_count, + bool absolute_values); + +// Does a 1D convolution of the given source image along the Y dimension on +// a single channel of the bitmap. +SK_API void SingleChannelConvolveY1D(const unsigned char* source_data, + int source_byte_row_stride, + int input_channel_index, + int input_channel_count, + const ConvolutionFilter1D& filter, + const SkISize& image_size, + unsigned char* output, + int output_byte_row_stride, + int output_channel_index, + int output_channel_count, + bool absolute_values); + +// Set up the |filter| instance with a gaussian kernel. |kernel_sigma| is the +// parameter of gaussian. If |derivative| is true, the kernel will be that of +// the first derivative. Intended for use with the two routines above. +SK_API void SetUpGaussianConvolutionKernel(ConvolutionFilter1D* filter, + float kernel_sigma, + bool derivative); + +} // namespace skia + +#endif // SKIA_EXT_CONVOLVER_H_ diff --git a/ext/convolver_SSE2.cc b/ext/convolver_SSE2.cc new file mode 100644 index 000000000000..e66e59735599 --- /dev/null +++ b/ext/convolver_SSE2.cc @@ -0,0 +1,458 @@ +// Copyright 2011 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include + +#include "build/build_config.h" +#include "skia/ext/convolver.h" +#include "skia/ext/convolver_SSE2.h" +#include "third_party/skia/include/core/SkTypes.h" + +#include // ARCH_CPU_X86_FAMILY was defined in build/config.h + +namespace skia { + +// Convolves horizontally along a single row. The row data is given in +// |src_data| and continues for the num_values() of the filter. +void ConvolveHorizontally_SSE2(const unsigned char* src_data, + const ConvolutionFilter1D& filter, + unsigned char* out_row, + bool /*has_alpha*/) { + int num_values = filter.num_values(); + + int filter_offset, filter_length; + __m128i zero = _mm_setzero_si128(); + __m128i mask[4]; + // |mask| will be used to decimate all extra filter coefficients that are + // loaded by SIMD when |filter_length| is not divisible by 4. + // mask[0] is not used in following algorithm. + mask[1] = _mm_set_epi16(0, 0, 0, 0, 0, 0, 0, -1); + mask[2] = _mm_set_epi16(0, 0, 0, 0, 0, 0, -1, -1); + mask[3] = _mm_set_epi16(0, 0, 0, 0, 0, -1, -1, -1); + + // Output one pixel each iteration, calculating all channels (RGBA) together. + for (int out_x = 0; out_x < num_values; out_x++) { + const ConvolutionFilter1D::Fixed* filter_values = + filter.FilterForValue(out_x, &filter_offset, &filter_length); + + __m128i accum = _mm_setzero_si128(); + + // Compute the first pixel in this row that the filter affects. It will + // touch |filter_length| pixels (4 bytes each) after this. + const __m128i* row_to_filter = + reinterpret_cast(&src_data[filter_offset << 2]); + + // We will load and accumulate with four coefficients per iteration. + for (int filter_x = 0; filter_x < filter_length >> 2; filter_x++) { + + // Load 4 coefficients => duplicate 1st and 2nd of them for all channels. + __m128i coeff, coeff16; + // [16] xx xx xx xx c3 c2 c1 c0 + coeff = _mm_loadl_epi64(reinterpret_cast(filter_values)); + // [16] xx xx xx xx c1 c1 c0 c0 + coeff16 = _mm_shufflelo_epi16(coeff, _MM_SHUFFLE(1, 1, 0, 0)); + // [16] c1 c1 c1 c1 c0 c0 c0 c0 + coeff16 = _mm_unpacklo_epi16(coeff16, coeff16); + + // Load four pixels => unpack the first two pixels to 16 bits => + // multiply with coefficients => accumulate the convolution result. + // [8] a3 b3 g3 r3 a2 b2 g2 r2 a1 b1 g1 r1 a0 b0 g0 r0 + __m128i src8 = _mm_loadu_si128(row_to_filter); + // [16] a1 b1 g1 r1 a0 b0 g0 r0 + __m128i src16 = _mm_unpacklo_epi8(src8, zero); + __m128i mul_hi = _mm_mulhi_epi16(src16, coeff16); + __m128i mul_lo = _mm_mullo_epi16(src16, coeff16); + // [32] a0*c0 b0*c0 g0*c0 r0*c0 + __m128i t = _mm_unpacklo_epi16(mul_lo, mul_hi); + accum = _mm_add_epi32(accum, t); + // [32] a1*c1 b1*c1 g1*c1 r1*c1 + t = _mm_unpackhi_epi16(mul_lo, mul_hi); + accum = _mm_add_epi32(accum, t); + + // Duplicate 3rd and 4th coefficients for all channels => + // unpack the 3rd and 4th pixels to 16 bits => multiply with coefficients + // => accumulate the convolution results. + // [16] xx xx xx xx c3 c3 c2 c2 + coeff16 = _mm_shufflelo_epi16(coeff, _MM_SHUFFLE(3, 3, 2, 2)); + // [16] c3 c3 c3 c3 c2 c2 c2 c2 + coeff16 = _mm_unpacklo_epi16(coeff16, coeff16); + // [16] a3 g3 b3 r3 a2 g2 b2 r2 + src16 = _mm_unpackhi_epi8(src8, zero); + mul_hi = _mm_mulhi_epi16(src16, coeff16); + mul_lo = _mm_mullo_epi16(src16, coeff16); + // [32] a2*c2 b2*c2 g2*c2 r2*c2 + t = _mm_unpacklo_epi16(mul_lo, mul_hi); + accum = _mm_add_epi32(accum, t); + // [32] a3*c3 b3*c3 g3*c3 r3*c3 + t = _mm_unpackhi_epi16(mul_lo, mul_hi); + accum = _mm_add_epi32(accum, t); + + // Advance the pixel and coefficients pointers. + row_to_filter += 1; + filter_values += 4; + } + + // When |filter_length| is not divisible by 4, we need to decimate some of + // the filter coefficient that was loaded incorrectly to zero; Other than + // that the algorithm is same with above, exceot that the 4th pixel will be + // always absent. + int r = filter_length&3; + if (r) { + // Note: filter_values must be padded to align_up(filter_offset, 8). + __m128i coeff, coeff16; + coeff = _mm_loadl_epi64(reinterpret_cast(filter_values)); + // Mask out extra filter taps. + coeff = _mm_and_si128(coeff, mask[r]); + coeff16 = _mm_shufflelo_epi16(coeff, _MM_SHUFFLE(1, 1, 0, 0)); + coeff16 = _mm_unpacklo_epi16(coeff16, coeff16); + + // Note: line buffer must be padded to align_up(filter_offset, 16). + // We resolve this by use C-version for the last horizontal line. + __m128i src8 = _mm_loadu_si128(row_to_filter); + __m128i src16 = _mm_unpacklo_epi8(src8, zero); + __m128i mul_hi = _mm_mulhi_epi16(src16, coeff16); + __m128i mul_lo = _mm_mullo_epi16(src16, coeff16); + __m128i t = _mm_unpacklo_epi16(mul_lo, mul_hi); + accum = _mm_add_epi32(accum, t); + t = _mm_unpackhi_epi16(mul_lo, mul_hi); + accum = _mm_add_epi32(accum, t); + + src16 = _mm_unpackhi_epi8(src8, zero); + coeff16 = _mm_shufflelo_epi16(coeff, _MM_SHUFFLE(3, 3, 2, 2)); + coeff16 = _mm_unpacklo_epi16(coeff16, coeff16); + mul_hi = _mm_mulhi_epi16(src16, coeff16); + mul_lo = _mm_mullo_epi16(src16, coeff16); + t = _mm_unpacklo_epi16(mul_lo, mul_hi); + accum = _mm_add_epi32(accum, t); + } + + // Shift right for fixed point implementation. + accum = _mm_srai_epi32(accum, ConvolutionFilter1D::kShiftBits); + + // Packing 32 bits |accum| to 16 bits per channel (signed saturation). + accum = _mm_packs_epi32(accum, zero); + // Packing 16 bits |accum| to 8 bits per channel (unsigned saturation). + accum = _mm_packus_epi16(accum, zero); + + // Store the pixel value of 32 bits. + *(reinterpret_cast(out_row)) = _mm_cvtsi128_si32(accum); + out_row += 4; + } +} + +// Convolves horizontally along four rows. The row data is given in +// |src_data| and continues for the num_values() of the filter. +// The algorithm is almost same as |ConvolveHorizontally_SSE2|. Please +// refer to that function for detailed comments. +void Convolve4RowsHorizontally_SSE2(const unsigned char* src_data[4], + const ConvolutionFilter1D& filter, + unsigned char* out_row[4]) { + int num_values = filter.num_values(); + + int filter_offset, filter_length; + __m128i zero = _mm_setzero_si128(); + __m128i mask[4]; + // |mask| will be used to decimate all extra filter coefficients that are + // loaded by SIMD when |filter_length| is not divisible by 4. + // mask[0] is not used in following algorithm. + mask[1] = _mm_set_epi16(0, 0, 0, 0, 0, 0, 0, -1); + mask[2] = _mm_set_epi16(0, 0, 0, 0, 0, 0, -1, -1); + mask[3] = _mm_set_epi16(0, 0, 0, 0, 0, -1, -1, -1); + + // Output one pixel each iteration, calculating all channels (RGBA) together. + for (int out_x = 0; out_x < num_values; out_x++) { + const ConvolutionFilter1D::Fixed* filter_values = + filter.FilterForValue(out_x, &filter_offset, &filter_length); + + // four pixels in a column per iteration. + __m128i accum0 = _mm_setzero_si128(); + __m128i accum1 = _mm_setzero_si128(); + __m128i accum2 = _mm_setzero_si128(); + __m128i accum3 = _mm_setzero_si128(); + int start = (filter_offset<<2); + // We will load and accumulate with four coefficients per iteration. + for (int filter_x = 0; filter_x < (filter_length >> 2); filter_x++) { + __m128i coeff, coeff16lo, coeff16hi; + // [16] xx xx xx xx c3 c2 c1 c0 + coeff = _mm_loadl_epi64(reinterpret_cast(filter_values)); + // [16] xx xx xx xx c1 c1 c0 c0 + coeff16lo = _mm_shufflelo_epi16(coeff, _MM_SHUFFLE(1, 1, 0, 0)); + // [16] c1 c1 c1 c1 c0 c0 c0 c0 + coeff16lo = _mm_unpacklo_epi16(coeff16lo, coeff16lo); + // [16] xx xx xx xx c3 c3 c2 c2 + coeff16hi = _mm_shufflelo_epi16(coeff, _MM_SHUFFLE(3, 3, 2, 2)); + // [16] c3 c3 c3 c3 c2 c2 c2 c2 + coeff16hi = _mm_unpacklo_epi16(coeff16hi, coeff16hi); + + __m128i src8, src16, mul_hi, mul_lo, t; + +#define ITERATION(src, accum) \ + src8 = _mm_loadu_si128(reinterpret_cast(src)); \ + src16 = _mm_unpacklo_epi8(src8, zero); \ + mul_hi = _mm_mulhi_epi16(src16, coeff16lo); \ + mul_lo = _mm_mullo_epi16(src16, coeff16lo); \ + t = _mm_unpacklo_epi16(mul_lo, mul_hi); \ + accum = _mm_add_epi32(accum, t); \ + t = _mm_unpackhi_epi16(mul_lo, mul_hi); \ + accum = _mm_add_epi32(accum, t); \ + src16 = _mm_unpackhi_epi8(src8, zero); \ + mul_hi = _mm_mulhi_epi16(src16, coeff16hi); \ + mul_lo = _mm_mullo_epi16(src16, coeff16hi); \ + t = _mm_unpacklo_epi16(mul_lo, mul_hi); \ + accum = _mm_add_epi32(accum, t); \ + t = _mm_unpackhi_epi16(mul_lo, mul_hi); \ + accum = _mm_add_epi32(accum, t) + + ITERATION(src_data[0] + start, accum0); + ITERATION(src_data[1] + start, accum1); + ITERATION(src_data[2] + start, accum2); + ITERATION(src_data[3] + start, accum3); + + start += 16; + filter_values += 4; + } + + int r = filter_length & 3; + if (r) { + // Note: filter_values must be padded to align_up(filter_offset, 8); + __m128i coeff; + coeff = _mm_loadl_epi64(reinterpret_cast(filter_values)); + // Mask out extra filter taps. + coeff = _mm_and_si128(coeff, mask[r]); + + __m128i coeff16lo = _mm_shufflelo_epi16(coeff, _MM_SHUFFLE(1, 1, 0, 0)); + /* c1 c1 c1 c1 c0 c0 c0 c0 */ + coeff16lo = _mm_unpacklo_epi16(coeff16lo, coeff16lo); + __m128i coeff16hi = _mm_shufflelo_epi16(coeff, _MM_SHUFFLE(3, 3, 2, 2)); + coeff16hi = _mm_unpacklo_epi16(coeff16hi, coeff16hi); + + __m128i src8, src16, mul_hi, mul_lo, t; + + ITERATION(src_data[0] + start, accum0); + ITERATION(src_data[1] + start, accum1); + ITERATION(src_data[2] + start, accum2); + ITERATION(src_data[3] + start, accum3); + } + + accum0 = _mm_srai_epi32(accum0, ConvolutionFilter1D::kShiftBits); + accum0 = _mm_packs_epi32(accum0, zero); + accum0 = _mm_packus_epi16(accum0, zero); + accum1 = _mm_srai_epi32(accum1, ConvolutionFilter1D::kShiftBits); + accum1 = _mm_packs_epi32(accum1, zero); + accum1 = _mm_packus_epi16(accum1, zero); + accum2 = _mm_srai_epi32(accum2, ConvolutionFilter1D::kShiftBits); + accum2 = _mm_packs_epi32(accum2, zero); + accum2 = _mm_packus_epi16(accum2, zero); + accum3 = _mm_srai_epi32(accum3, ConvolutionFilter1D::kShiftBits); + accum3 = _mm_packs_epi32(accum3, zero); + accum3 = _mm_packus_epi16(accum3, zero); + + *(reinterpret_cast(out_row[0])) = _mm_cvtsi128_si32(accum0); + *(reinterpret_cast(out_row[1])) = _mm_cvtsi128_si32(accum1); + *(reinterpret_cast(out_row[2])) = _mm_cvtsi128_si32(accum2); + *(reinterpret_cast(out_row[3])) = _mm_cvtsi128_si32(accum3); + + out_row[0] += 4; + out_row[1] += 4; + out_row[2] += 4; + out_row[3] += 4; + } +} + +// Does vertical convolution to produce one output row. The filter values and +// length are given in the first two parameters. These are applied to each +// of the rows pointed to in the |source_data_rows| array, with each row +// being |pixel_width| wide. +// +// The output must have room for |pixel_width * 4| bytes. +template +void ConvolveVertically_SSE2(const ConvolutionFilter1D::Fixed* filter_values, + int filter_length, + unsigned char* const* source_data_rows, + int pixel_width, + unsigned char* out_row) { + int width = pixel_width & ~3; + + __m128i zero = _mm_setzero_si128(); + __m128i accum0, accum1, accum2, accum3, coeff16; + const __m128i* src; + // Output four pixels per iteration (16 bytes). + for (int out_x = 0; out_x < width; out_x += 4) { + + // Accumulated result for each pixel. 32 bits per RGBA channel. + accum0 = _mm_setzero_si128(); + accum1 = _mm_setzero_si128(); + accum2 = _mm_setzero_si128(); + accum3 = _mm_setzero_si128(); + + // Convolve with one filter coefficient per iteration. + for (int filter_y = 0; filter_y < filter_length; filter_y++) { + + // Duplicate the filter coefficient 8 times. + // [16] cj cj cj cj cj cj cj cj + coeff16 = _mm_set1_epi16(filter_values[filter_y]); + + // Load four pixels (16 bytes) together. + // [8] a3 b3 g3 r3 a2 b2 g2 r2 a1 b1 g1 r1 a0 b0 g0 r0 + src = reinterpret_cast( + &source_data_rows[filter_y][out_x << 2]); + __m128i src8 = _mm_loadu_si128(src); + + // Unpack 1st and 2nd pixels from 8 bits to 16 bits for each channels => + // multiply with current coefficient => accumulate the result. + // [16] a1 b1 g1 r1 a0 b0 g0 r0 + __m128i src16 = _mm_unpacklo_epi8(src8, zero); + __m128i mul_hi = _mm_mulhi_epi16(src16, coeff16); + __m128i mul_lo = _mm_mullo_epi16(src16, coeff16); + // [32] a0 b0 g0 r0 + __m128i t = _mm_unpacklo_epi16(mul_lo, mul_hi); + accum0 = _mm_add_epi32(accum0, t); + // [32] a1 b1 g1 r1 + t = _mm_unpackhi_epi16(mul_lo, mul_hi); + accum1 = _mm_add_epi32(accum1, t); + + // Unpack 3rd and 4th pixels from 8 bits to 16 bits for each channels => + // multiply with current coefficient => accumulate the result. + // [16] a3 b3 g3 r3 a2 b2 g2 r2 + src16 = _mm_unpackhi_epi8(src8, zero); + mul_hi = _mm_mulhi_epi16(src16, coeff16); + mul_lo = _mm_mullo_epi16(src16, coeff16); + // [32] a2 b2 g2 r2 + t = _mm_unpacklo_epi16(mul_lo, mul_hi); + accum2 = _mm_add_epi32(accum2, t); + // [32] a3 b3 g3 r3 + t = _mm_unpackhi_epi16(mul_lo, mul_hi); + accum3 = _mm_add_epi32(accum3, t); + } + + // Shift right for fixed point implementation. + accum0 = _mm_srai_epi32(accum0, ConvolutionFilter1D::kShiftBits); + accum1 = _mm_srai_epi32(accum1, ConvolutionFilter1D::kShiftBits); + accum2 = _mm_srai_epi32(accum2, ConvolutionFilter1D::kShiftBits); + accum3 = _mm_srai_epi32(accum3, ConvolutionFilter1D::kShiftBits); + + // Packing 32 bits |accum| to 16 bits per channel (signed saturation). + // [16] a1 b1 g1 r1 a0 b0 g0 r0 + accum0 = _mm_packs_epi32(accum0, accum1); + // [16] a3 b3 g3 r3 a2 b2 g2 r2 + accum2 = _mm_packs_epi32(accum2, accum3); + + // Packing 16 bits |accum| to 8 bits per channel (unsigned saturation). + // [8] a3 b3 g3 r3 a2 b2 g2 r2 a1 b1 g1 r1 a0 b0 g0 r0 + accum0 = _mm_packus_epi16(accum0, accum2); + + if (has_alpha) { + // Compute the max(ri, gi, bi) for each pixel. + // [8] xx a3 b3 g3 xx a2 b2 g2 xx a1 b1 g1 xx a0 b0 g0 + __m128i a = _mm_srli_epi32(accum0, 8); + // [8] xx xx xx max3 xx xx xx max2 xx xx xx max1 xx xx xx max0 + __m128i b = _mm_max_epu8(a, accum0); // Max of r and g. + // [8] xx xx a3 b3 xx xx a2 b2 xx xx a1 b1 xx xx a0 b0 + a = _mm_srli_epi32(accum0, 16); + // [8] xx xx xx max3 xx xx xx max2 xx xx xx max1 xx xx xx max0 + b = _mm_max_epu8(a, b); // Max of r and g and b. + // [8] max3 00 00 00 max2 00 00 00 max1 00 00 00 max0 00 00 00 + b = _mm_slli_epi32(b, 24); + + // Make sure the value of alpha channel is always larger than maximum + // value of color channels. + accum0 = _mm_max_epu8(b, accum0); + } else { + // Set value of alpha channels to 0xFF. + __m128i mask = _mm_set1_epi32(0xff000000); + accum0 = _mm_or_si128(accum0, mask); + } + + // Store the convolution result (16 bytes) and advance the pixel pointers. + _mm_storeu_si128(reinterpret_cast<__m128i*>(out_row), accum0); + out_row += 16; + } + + // When the width of the output is not divisible by 4, We need to save one + // pixel (4 bytes) each time. And also the fourth pixel is always absent. + if (pixel_width & 3) { + accum0 = _mm_setzero_si128(); + accum1 = _mm_setzero_si128(); + accum2 = _mm_setzero_si128(); + for (int filter_y = 0; filter_y < filter_length; ++filter_y) { + coeff16 = _mm_set1_epi16(filter_values[filter_y]); + // [8] a3 b3 g3 r3 a2 b2 g2 r2 a1 b1 g1 r1 a0 b0 g0 r0 + src = reinterpret_cast( + &source_data_rows[filter_y][width<<2]); + __m128i src8 = _mm_loadu_si128(src); + // [16] a1 b1 g1 r1 a0 b0 g0 r0 + __m128i src16 = _mm_unpacklo_epi8(src8, zero); + __m128i mul_hi = _mm_mulhi_epi16(src16, coeff16); + __m128i mul_lo = _mm_mullo_epi16(src16, coeff16); + // [32] a0 b0 g0 r0 + __m128i t = _mm_unpacklo_epi16(mul_lo, mul_hi); + accum0 = _mm_add_epi32(accum0, t); + // [32] a1 b1 g1 r1 + t = _mm_unpackhi_epi16(mul_lo, mul_hi); + accum1 = _mm_add_epi32(accum1, t); + // [16] a3 b3 g3 r3 a2 b2 g2 r2 + src16 = _mm_unpackhi_epi8(src8, zero); + mul_hi = _mm_mulhi_epi16(src16, coeff16); + mul_lo = _mm_mullo_epi16(src16, coeff16); + // [32] a2 b2 g2 r2 + t = _mm_unpacklo_epi16(mul_lo, mul_hi); + accum2 = _mm_add_epi32(accum2, t); + } + + accum0 = _mm_srai_epi32(accum0, ConvolutionFilter1D::kShiftBits); + accum1 = _mm_srai_epi32(accum1, ConvolutionFilter1D::kShiftBits); + accum2 = _mm_srai_epi32(accum2, ConvolutionFilter1D::kShiftBits); + // [16] a1 b1 g1 r1 a0 b0 g0 r0 + accum0 = _mm_packs_epi32(accum0, accum1); + // [16] a3 b3 g3 r3 a2 b2 g2 r2 + accum2 = _mm_packs_epi32(accum2, zero); + // [8] a3 b3 g3 r3 a2 b2 g2 r2 a1 b1 g1 r1 a0 b0 g0 r0 + accum0 = _mm_packus_epi16(accum0, accum2); + if (has_alpha) { + // [8] xx a3 b3 g3 xx a2 b2 g2 xx a1 b1 g1 xx a0 b0 g0 + __m128i a = _mm_srli_epi32(accum0, 8); + // [8] xx xx xx max3 xx xx xx max2 xx xx xx max1 xx xx xx max0 + __m128i b = _mm_max_epu8(a, accum0); // Max of r and g. + // [8] xx xx a3 b3 xx xx a2 b2 xx xx a1 b1 xx xx a0 b0 + a = _mm_srli_epi32(accum0, 16); + // [8] xx xx xx max3 xx xx xx max2 xx xx xx max1 xx xx xx max0 + b = _mm_max_epu8(a, b); // Max of r and g and b. + // [8] max3 00 00 00 max2 00 00 00 max1 00 00 00 max0 00 00 00 + b = _mm_slli_epi32(b, 24); + accum0 = _mm_max_epu8(b, accum0); + } else { + __m128i mask = _mm_set1_epi32(0xff000000); + accum0 = _mm_or_si128(accum0, mask); + } + + for (int out_x = width; out_x < pixel_width; out_x++) { + *(reinterpret_cast(out_row)) = _mm_cvtsi128_si32(accum0); + accum0 = _mm_srli_si128(accum0, 4); + out_row += 4; + } + } +} + +void ConvolveVertically_SSE2(const ConvolutionFilter1D::Fixed* filter_values, + int filter_length, + unsigned char* const* source_data_rows, + int pixel_width, + unsigned char* out_row, + bool has_alpha) { + if (has_alpha) { + ConvolveVertically_SSE2(filter_values, + filter_length, + source_data_rows, + pixel_width, + out_row); + } else { + ConvolveVertically_SSE2(filter_values, + filter_length, + source_data_rows, + pixel_width, + out_row); + } +} + +} // namespace skia diff --git a/ext/convolver_SSE2.h b/ext/convolver_SSE2.h new file mode 100644 index 000000000000..4b2ad759b7de --- /dev/null +++ b/ext/convolver_SSE2.h @@ -0,0 +1,27 @@ +// Copyright 2012 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef SKIA_EXT_CONVOLVER_SSE2_H_ +#define SKIA_EXT_CONVOLVER_SSE2_H_ + +#include "skia/ext/convolver.h" + +namespace skia { + +void ConvolveVertically_SSE2(const ConvolutionFilter1D::Fixed* filter_values, + int filter_length, + unsigned char* const* source_data_rows, + int pixel_width, + unsigned char* out_row, + bool has_alpha); +void Convolve4RowsHorizontally_SSE2(const unsigned char* src_data[4], + const ConvolutionFilter1D& filter, + unsigned char* out_row[4]); +void ConvolveHorizontally_SSE2(const unsigned char* src_data, + const ConvolutionFilter1D& filter, + unsigned char* out_row, + bool has_alpha); +} // namespace skia + +#endif // SKIA_EXT_CONVOLVER_SSE2_H_ diff --git a/ext/convolver_mips_dspr2.cc b/ext/convolver_mips_dspr2.cc new file mode 100644 index 000000000000..137db79a48ac --- /dev/null +++ b/ext/convolver_mips_dspr2.cc @@ -0,0 +1,478 @@ +// Copyright 2013 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include +#include "skia/ext/convolver.h" +#include "skia/ext/convolver_mips_dspr2.h" +#include "third_party/skia/include/core/SkTypes.h" + +namespace skia { +// Convolves horizontally along a single row. The row data is given in +// |src_data| and continues for the num_values() of the filter. +void ConvolveHorizontally_mips_dspr2(const unsigned char* src_data, + const ConvolutionFilter1D& filter, + unsigned char* out_row, + bool has_alpha) { +#if SIMD_MIPS_DSPR2 + int row_to_filter = 0; + int num_values = filter.num_values(); + if (has_alpha) { + for (int out_x = 0; out_x < num_values; out_x++) { + // Get the filter that determines the current output pixel. + int filter_offset, filter_length; + const ConvolutionFilter1D::Fixed* filter_values = + filter.FilterForValue(out_x, &filter_offset, &filter_length); + int filter_x = 0; + + __asm__ __volatile__ ( + ".set push \n" + ".set noreorder \n" + + "beqz %[filter_len], 3f \n" + " sll $t0, %[filter_offset], 2 \n" + "addu %[rtf], %[src_data], $t0 \n" + "mtlo $0, $ac0 \n" + "mtlo $0, $ac1 \n" + "mtlo $0, $ac2 \n" + "mtlo $0, $ac3 \n" + "srl $t7, %[filter_len], 2 \n" + "beqz $t7, 2f \n" + " li %[fx], 0 \n" + + "11: \n" + "addu $t4, %[filter_val], %[fx] \n" + "sll $t5, %[fx], 1 \n" + "ulw $t6, 0($t4) \n" // t6 = |cur[1]|cur[0]| + "ulw $t8, 4($t4) \n" // t8 = |cur[3]|cur[2]| + "addu $t0, %[rtf], $t5 \n" + "lw $t1, 0($t0) \n" // t1 = |a0|b0|g0|r0| + "lw $t2, 4($t0) \n" // t2 = |a1|b1|g1|r1| + "lw $t3, 8($t0) \n" // t3 = |a2|b2|g2|r2| + "lw $t4, 12($t0) \n" // t4 = |a3|b3|g3|r3| + "precrq.qb.ph $t0, $t2, $t1 \n" // t0 = |a1|g1|a0|g0| + "precr.qb.ph $t5, $t2, $t1 \n" // t5 = |b1|r1|b0|r0| + "preceu.ph.qbla $t1, $t0 \n" // t1 = |0|a1|0|a0| + "preceu.ph.qbra $t2, $t0 \n" // t2 = |0|g1|0|g0| + "preceu.ph.qbla $t0, $t5 \n" // t0 = |0|b1|0|b0| + "preceu.ph.qbra $t5, $t5 \n" // t5 = |0|r1|0|r0| + "dpa.w.ph $ac0, $t1, $t6 \n" // ac0+(cur*a1)+(cur*a0) + "dpa.w.ph $ac1, $t0, $t6 \n" // ac1+(cur*b1)+(cur*b0) + "dpa.w.ph $ac2, $t2, $t6 \n" // ac2+(cur*g1)+(cur*g0) + "dpa.w.ph $ac3, $t5, $t6 \n" // ac3+(cur*r1)+(cur*r0) + "precrq.qb.ph $t0, $t4, $t3 \n" // t0 = |a3|g3|a2|g2| + "precr.qb.ph $t5, $t4, $t3 \n" // t5 = |b3|r3|b2|r2| + "preceu.ph.qbla $t1, $t0 \n" // t1 = |0|a3|0|a2| + "preceu.ph.qbra $t2, $t0 \n" // t2 = |0|g3|0|g2| + "preceu.ph.qbla $t0, $t5 \n" // t0 = |0|b3|0|b2| + "preceu.ph.qbra $t5, $t5 \n" // t5 = |0|r3|0|r2| + "dpa.w.ph $ac0, $t1, $t8 \n" // ac0+(cur*a3)+(cur*a2) + "dpa.w.ph $ac1, $t0, $t8 \n" // ac1+(cur*b3)+(cur*b2) + "dpa.w.ph $ac2, $t2, $t8 \n" // ac2+(cur*g3)+(cur*g2) + "dpa.w.ph $ac3, $t5, $t8 \n" // ac3+(cur*r3)+(cur*r2) + "addiu $t7, $t7, -1 \n" + "bgtz $t7, 11b \n" + " addiu %[fx], %[fx], 8 \n" + + "2: \n" + "andi $t7, %[filter_len], 0x3 \n" // residual + "beqz $t7, 3f \n" + " nop \n" + + "21: \n" + "sll $t1, %[fx], 1 \n" + "addu $t2, %[filter_val], %[fx] \n" + "addu $t0, %[rtf], $t1 \n" + "lh $t6, 0($t2) \n" // t6 = filter_val[fx] + "lbu $t1, 0($t0) \n" // t1 = row[fx * 4 + 0] + "lbu $t2, 1($t0) \n" // t2 = row[fx * 4 + 1] + "lbu $t3, 2($t0) \n" // t3 = row[fx * 4 + 2] + "lbu $t4, 3($t0) \n" // t4 = row[fx * 4 + 2] + "maddu $ac3, $t6, $t1 \n" + "maddu $ac2, $t6, $t2 \n" + "maddu $ac1, $t6, $t3 \n" + "maddu $ac0, $t6, $t4 \n" + "addiu $t7, $t7, -1 \n" + "bgtz $t7, 21b \n" + " addiu %[fx], %[fx], 2 \n" + + "3: \n" + "extrv.w $t0, $ac0, %[kShiftBits] \n" // a >> kShiftBits + "extrv.w $t1, $ac1, %[kShiftBits] \n" // b >> kShiftBits + "extrv.w $t2, $ac2, %[kShiftBits] \n" // g >> kShiftBits + "extrv.w $t3, $ac3, %[kShiftBits] \n" // r >> kShiftBits + "sll $t5, %[out_x], 2 \n" + "repl.ph $t6, 128 \n" // t6 = | 128 | 128 | + "addu $t5, %[out_row], $t5 \n" + "append $t2, $t3, 16 \n" + "append $t0, $t1, 16 \n" + "subu.ph $t1, $t0, $t6 \n" + "shll_s.ph $t1, $t1, 8 \n" + "shra.ph $t1, $t1, 8 \n" + "addu.ph $t1, $t1, $t6 \n" + "subu.ph $t3, $t2, $t6 \n" + "shll_s.ph $t3, $t3, 8 \n" + "shra.ph $t3, $t3, 8 \n" + "addu.ph $t3, $t3, $t6 \n" + "precr.qb.ph $t0, $t1, $t3 \n" + "usw $t0, 0($t5) \n" + + ".set pop \n" + : [fx] "+r" (filter_x), [out_x] "+r" (out_x), [out_row] "+r" (out_row), + [rtf] "+r" (row_to_filter) + : [filter_val] "r" (filter_values), [filter_len] "r" (filter_length), + [kShiftBits] "r" (ConvolutionFilter1D::kShiftBits), + [filter_offset] "r" (filter_offset), [src_data] "r" (src_data) + : "lo", "hi", "$ac1lo", "$ac1hi", "$ac2lo", "$ac2hi", "$ac3lo", "$ac3hi", + "t0", "t1", "t2", "t3", "t4", "t5", "t6", "t7", "t8" + ); + } + } else { + for (int out_x = 0; out_x < num_values; out_x++) { + // Get the filter that determines the current output pixel. + int filter_offset, filter_length; + const ConvolutionFilter1D::Fixed* filter_values = + filter.FilterForValue(out_x, &filter_offset, &filter_length); + int filter_x = 0; + __asm__ __volatile__ ( + ".set push \n" + ".set noreorder \n" + + "beqz %[filter_len], 3f \n" + " sll $t0, %[filter_offset], 2 \n" + "addu %[rtf], %[src_data], $t0 \n" + "mtlo $0, $ac1 \n" + "mtlo $0, $ac2 \n" + "mtlo $0, $ac3 \n" + "srl $t7, %[filter_len], 2 \n" + "beqz $t7, 2f \n" + " li %[fx], 0 \n" + + "11: \n" + "addu $t4, %[filter_val], %[fx] \n" + "sll $t5, %[fx], 1 \n" + "ulw $t6, 0($t4) \n" // t6 = |cur[1]|cur[0]| + "ulw $t8, 4($t4) \n" // t8 = |cur[3]|cur[2]| + "addu $t0, %[rtf], $t5 \n" + "lw $t1, 0($t0) \n" // t1 = |a0|b0|g0|r0| + "lw $t2, 4($t0) \n" // t2 = |a1|b1|g1|r1| + "lw $t3, 8($t0) \n" // t3 = |a2|b2|g2|r2| + "lw $t4, 12($t0) \n" // t4 = |a3|b3|g3|r3| + "precrq.qb.ph $t0, $t2, $t1 \n" // t0 = |a1|g1|a0|g0| + "precr.qb.ph $t5, $t2, $t1 \n" // t5 = |b1|r1|b0|r0| + "preceu.ph.qbra $t2, $t0 \n" // t2 = |0|g1|0|g0| + "preceu.ph.qbla $t0, $t5 \n" // t0 = |0|b1|0|b0| + "preceu.ph.qbra $t5, $t5 \n" // t5 = |0|r1|0|r0| + "dpa.w.ph $ac1, $t0, $t6 \n" // ac1+(cur*b1)+(cur*b0) + "dpa.w.ph $ac2, $t2, $t6 \n" // ac2+(cur*g1)+(cur*g0) + "dpa.w.ph $ac3, $t5, $t6 \n" // ac3+(cur*r1)+(cur*r0) + "precrq.qb.ph $t0, $t4, $t3 \n" // t0 = |a3|g3|a2|g2| + "precr.qb.ph $t5, $t4, $t3 \n" // t5 = |b3|r3|b2|r2| + "preceu.ph.qbra $t2, $t0 \n" // t2 = |0|g3|0|g2| + "preceu.ph.qbla $t0, $t5 \n" // t0 = |0|b3|0|b2| + "preceu.ph.qbra $t5, $t5 \n" // t5 = |0|r3|0|r2| + "dpa.w.ph $ac1, $t0, $t8 \n" // ac1+(cur*b3)+(cur*b2) + "dpa.w.ph $ac2, $t2, $t8 \n" // ac2+(cur*g3)+(cur*g2) + "dpa.w.ph $ac3, $t5, $t8 \n" // ac3+(cur*r3)+(cur*r2) + "addiu $t7, $t7, -1 \n" + "bgtz $t7, 11b \n" + " addiu %[fx], %[fx], 8 \n" + + "2: \n" + "andi $t7, %[filter_len], 0x3 \n" // residual + "beqz $t7, 3f \n" + " nop \n" + + "21: \n" + "sll $t1, %[fx], 1 \n" + "addu $t2, %[filter_val], %[fx] \n" + "addu $t0, %[rtf], $t1 \n" + "lh $t6, 0($t2) \n" // t6 = filter_val[fx] + "lbu $t1, 0($t0) \n" // t1 = row[fx * 4 + 0] + "lbu $t2, 1($t0) \n" // t2 = row[fx * 4 + 1] + "lbu $t3, 2($t0) \n" // t3 = row[fx * 4 + 2] + "maddu $ac3, $t6, $t1 \n" + "maddu $ac2, $t6, $t2 \n" + "maddu $ac1, $t6, $t3 \n" + "addiu $t7, $t7, -1 \n" + "bgtz $t7, 21b \n" + " addiu %[fx], %[fx], 2 \n" + + "3: \n" + "extrv.w $t1, $ac1, %[kShiftBits] \n" // b >> kShiftBits + "extrv.w $t2, $ac2, %[kShiftBits] \n" // g >> kShiftBits + "extrv.w $t3, $ac3, %[kShiftBits] \n" // r >> kShiftBits + "repl.ph $t6, 128 \n" // t6 = | 128 | 128 | + "sll $t8, %[out_x], 2 \n" + "addu $t8, %[out_row], $t8 \n" + "append $t2, $t3, 16 \n" + "andi $t1, 0xFFFF \n" + "subu.ph $t5, $t1, $t6 \n" + "shll_s.ph $t5, $t5, 8 \n" + "shra.ph $t5, $t5, 8 \n" + "addu.ph $t5, $t5, $t6 \n" + "subu.ph $t4, $t2, $t6 \n" + "shll_s.ph $t4, $t4, 8 \n" + "shra.ph $t4, $t4, 8 \n" + "addu.ph $t4, $t4, $t6 \n" + "precr.qb.ph $t0, $t5, $t4 \n" + "usw $t0, 0($t8) \n" + + ".set pop \n" + : [fx] "+r" (filter_x), [out_x] "+r" (out_x), [out_row] "+r" (out_row), + [rtf] "+r" (row_to_filter) + : [filter_val] "r" (filter_values), [filter_len] "r" (filter_length), + [kShiftBits] "r" (ConvolutionFilter1D::kShiftBits), + [filter_offset] "r" (filter_offset), [src_data] "r" (src_data) + : "lo", "hi", "$ac1lo", "$ac1hi", "$ac2lo", "$ac2hi", "$ac3lo", "$ac3hi", + "t0", "t1", "t2", "t3", "t4", "t5", "t6", "t7", "t8" + ); + } + } +#endif +} +void ConvolveVertically_mips_dspr2(const ConvolutionFilter1D::Fixed* filter_val, + int filter_length, + unsigned char* const* source_data_rows, + int pixel_width, + unsigned char* out_row, + bool has_alpha) { +#if SIMD_MIPS_DSPR2 + // We go through each column in the output and do a vertical convolution, + // generating one output pixel each time. + int byte_offset; + int cnt; + int filter_y; + if (has_alpha) { + for (int out_x = 0; out_x < pixel_width; out_x++) { + __asm__ __volatile__ ( + ".set push \n" + ".set noreorder \n" + + "beqz %[filter_len], 3f \n" + " sll %[offset], %[out_x], 2 \n" + "mtlo $0, $ac0 \n" + "mtlo $0, $ac1 \n" + "mtlo $0, $ac2 \n" + "mtlo $0, $ac3 \n" + "srl %[cnt], %[filter_len], 2 \n" + "beqz %[cnt], 2f \n" + " li %[fy], 0 \n" + + "11: \n" + "sll $t1, %[fy], 1 \n" + "addu $t0, %[src_data_rows], $t1 \n" + "lw $t1, 0($t0) \n" + "lw $t2, 4($t0) \n" + "lw $t3, 8($t0) \n" + "lw $t4, 12($t0) \n" + "addu $t1, $t1, %[offset] \n" + "addu $t2, $t2, %[offset] \n" + "addu $t3, $t3, %[offset] \n" + "addu $t4, $t4, %[offset] \n" + "lw $t1, 0($t1) \n" // t1 = |a0|b0|g0|r0| + "lw $t2, 0($t2) \n" // t2 = |a1|b1|g1|r1| + "lw $t3, 0($t3) \n" // t3 = |a0|b0|g0|r0| + "lw $t4, 0($t4) \n" // t4 = |a1|b1|g1|r1| + "precrq.qb.ph $t5, $t2, $t1 \n" // t5 = |a1|g1|a0|g0| + "precr.qb.ph $t6, $t2, $t1 \n" // t6 = |b1|r1|b0|r0| + "preceu.ph.qbla $t0, $t5 \n" // t0 = |0|a1|0|a0| + "preceu.ph.qbra $t1, $t5 \n" // t1 = |0|g1|0|g0| + "preceu.ph.qbla $t2, $t6 \n" // t2 = |0|b1|0|b0| + "preceu.ph.qbra $t5, $t6 \n" // t5 = |0|r1|0|r0| + "addu $t6, %[filter_val], %[fy] \n" + "ulw $t7, 0($t6) \n" // t7 = |cur_1|cur_0| + "ulw $t6, 4($t6) \n" // t6 = |cur_3|cur_2| + "dpa.w.ph $ac0, $t5, $t7 \n" // (cur*r1)+(cur*r0) + "dpa.w.ph $ac1, $t1, $t7 \n" // (cur*g1)+(cur*g0) + "dpa.w.ph $ac2, $t2, $t7 \n" // (cur*b1)+(cur*b0) + "dpa.w.ph $ac3, $t0, $t7 \n" // (cur*a1)+(cur*a0) + "precrq.qb.ph $t5, $t4, $t3 \n" // t5 = |a3|g3|a2|g2| + "precr.qb.ph $t7, $t4, $t3 \n" // t7 = |b3|r3|b2|r2| + "preceu.ph.qbla $t0, $t5 \n" // t0 = |0|a3|0|a2| + "preceu.ph.qbra $t1, $t5 \n" // t1 = |0|g3|0|g2| + "preceu.ph.qbla $t2, $t7 \n" // t2 = |0|b3|0|b2| + "preceu.ph.qbra $t5, $t7 \n" // t5 = |0|r3|0|r2| + "dpa.w.ph $ac0, $t5, $t6 \n" // (cur*r3)+(cur*r2) + "dpa.w.ph $ac1, $t1, $t6 \n" // (cur*g3)+(cur*g2) + "dpa.w.ph $ac2, $t2, $t6 \n" // (cur*b3)+(cur*b2) + "dpa.w.ph $ac3, $t0, $t6 \n" // (cur*a3)+(cur*a2) + "addiu %[cnt], %[cnt], -1 \n" + "bgtz %[cnt], 11b \n" + " addiu %[fy], %[fy], 8 \n" + + "2: \n" + "andi %[cnt], %[filter_len], 0x3 \n" // residual + "beqz %[cnt], 3f \n" + " nop \n" + + "21: \n" + "addu $t0, %[filter_val], %[fy] \n" + "lh $t4, 0($t0) \n" // t4=filter_val[fx] + "sll $t1, %[fy], 1 \n" + "addu $t0, %[src_data_rows], $t1 \n" + "lw $t1, 0($t0) \n" + "addu $t0, $t1, %[offset] \n" + "lbu $t1, 0($t0) \n" // t1 = row[fx*4 + 0] + "lbu $t2, 1($t0) \n" // t2 = row[fx*4 + 1] + "lbu $t3, 2($t0) \n" // t3 = row[fx*4 + 2] + "lbu $t0, 3($t0) \n" // t4 = row[fx*4 + 2] + "maddu $ac0, $t4, $t1 \n" + "maddu $ac1, $t4, $t2 \n" + "maddu $ac2, $t4, $t3 \n" + "maddu $ac3, $t4, $t0 \n" + "addiu %[cnt], %[cnt], -1 \n" + "bgtz %[cnt], 21b \n" + " addiu %[fy], %[fy], 2 \n" + + "3: \n" + "extrv.w $t3, $ac0, %[kShiftBits] \n" // a >> kShiftBits + "extrv.w $t2, $ac1, %[kShiftBits] \n" // b >> kShiftBits + "extrv.w $t1, $ac2, %[kShiftBits] \n" // g >> kShiftBits + "extrv.w $t0, $ac3, %[kShiftBits] \n" // r >> kShiftBits + "repl.ph $t4, 128 \n" // t4 = | 128 | 128 | + "addu $t5, %[out_row], %[offset] \n" + "append $t2, $t3, 16 \n" // t2 = |0|g|0|r| + "append $t0, $t1, 16 \n" // t0 = |0|a|0|b| + "subu.ph $t1, $t0, $t4 \n" + "shll_s.ph $t1, $t1, 8 \n" + "shra.ph $t1, $t1, 8 \n" + "addu.ph $t1, $t1, $t4 \n" // Clamp(a)|Clamp(b) + "subu.ph $t2, $t2, $t4 \n" + "shll_s.ph $t2, $t2, 8 \n" + "shra.ph $t2, $t2, 8 \n" + "addu.ph $t2, $t2, $t4 \n" // Clamp(g)|Clamp(r) + "andi $t3, $t1, 0xFF \n" // t3 = ClampTo8(b) + "cmp.lt.ph $t3, $t2 \n" // cmp b, g, r + "pick.ph $t0, $t2, $t3 \n" + "andi $t3, $t0, 0xFF \n" + "srl $t4, $t0, 16 \n" + "cmp.lt.ph $t3, $t4 \n" + "pick.ph $t0, $t4, $t3 \n" // t0 = max_color_ch + "srl $t3, $t1, 16 \n" // t1 = ClampTo8(a) + "cmp.lt.ph $t3, $t0 \n" + "pick.ph $t0, $t0, $t3 \n" + "ins $t1, $t0, 16, 8 \n" + "precr.qb.ph $t0, $t1, $t2 \n" // t0 = |a|b|g|r| + "usw $t0, 0($t5) \n" + + ".set pop \n" + : [filter_val] "+r" (filter_val), [filter_len] "+r" (filter_length), + [offset] "+r" (byte_offset), [fy] "+r" (filter_y), [cnt] "+r" (cnt), + [out_x] "+r" (out_x), [pixel_width] "+r" (pixel_width) + : [src_data_rows] "r" (source_data_rows), [out_row] "r" (out_row), + [kShiftBits] "r" (ConvolutionFilter1D::kShiftBits) + : "lo", "hi", "$ac1lo", "$ac1hi", "$ac2lo", "$ac2hi", "$ac3lo", "$ac3hi", + "t0", "t1", "t2", "t3", "t4", "t5", "t6","t7", "memory" + ); + } + } else { + for (int out_x = 0; out_x < pixel_width; out_x++) { + __asm__ __volatile__ ( + ".set push \n" + ".set noreorder \n" + + "beqz %[filter_len], 3f \n" + " sll %[offset], %[out_x], 2 \n" + "mtlo $0, $ac0 \n" + "mtlo $0, $ac1 \n" + "mtlo $0, $ac2 \n" + "srl %[cnt], %[filter_len], 2 \n" + "beqz %[cnt], 2f \n" + " li %[fy], 0 \n" + + "11: \n" + "sll $t1, %[fy], 1 \n" + "addu $t0, %[src_data_rows], $t1 \n" + "lw $t1, 0($t0) \n" + "lw $t2, 4($t0) \n" + "lw $t3, 8($t0) \n" + "lw $t4, 12($t0) \n" + "addu $t1, $t1, %[offset] \n" + "addu $t2, $t2, %[offset] \n" + "addu $t3, $t3, %[offset] \n" + "addu $t4, $t4, %[offset] \n" + "lw $t1, 0($t1) \n" // t1 = |a0|b0|g0|r0| + "lw $t2, 0($t2) \n" // t2 = |a1|b1|g1|r1| + "lw $t3, 0($t3) \n" // t3 = |a0|b0|g0|r0| + "lw $t4, 0($t4) \n" // t4 = |a1|b1|g1|r1| + "precrq.qb.ph $t5, $t2, $t1 \n" // t5 = |a1|g1|a0|g0| + "precr.qb.ph $t6, $t2, $t1 \n" // t6 = |b1|r1|b0|r0| + "preceu.ph.qbra $t1, $t5 \n" // t1 = |0|g1|0|g0| + "preceu.ph.qbla $t2, $t6 \n" // t2 = |0|b1|0|b0| + "preceu.ph.qbra $t5, $t6 \n" // t5 = |0|r1|0|r0| + "addu $t6, %[filter_val], %[fy] \n" + "ulw $t0, 0($t6) \n" // t0 = |cur_1|cur_0| + "ulw $t6, 4($t6) \n" // t6 = |cur_1|cur_0| + "dpa.w.ph $ac0, $t5, $t0 \n" // (cur*r1)+(cur*r0) + "dpa.w.ph $ac1, $t1, $t0 \n" // (cur*g1)+(cur*g0) + "dpa.w.ph $ac2, $t2, $t0 \n" // (cur*b1)+(cur*b0) + "precrq.qb.ph $t5, $t4, $t3 \n" // t5 = |a3|g3|a2|g2| + "precr.qb.ph $t0, $t4, $t3 \n" // t0 = |b3|r3|b2|r2| + "preceu.ph.qbra $t1, $t5 \n" // t1 = |0|g3|0|g2| + "preceu.ph.qbla $t2, $t0 \n" // t2 = |0|b3|0|b2| + "preceu.ph.qbra $t5, $t0 \n" // t5 = |0|r3|0|r2| + "dpa.w.ph $ac0, $t5, $t6 \n" // (cur*r1)+(cur*r0) + "dpa.w.ph $ac1, $t1, $t6 \n" // (cur*g1)+(cur*g0) + "dpa.w.ph $ac2, $t2, $t6 \n" // (cur*b1)+(cur*b0) + "addiu %[cnt], %[cnt], -1 \n" + "bgtz %[cnt], 11b \n" + " addiu %[fy], %[fy], 8 \n" + + "2: \n" + "andi %[cnt], %[filter_len], 0x3 \n" // residual + "beqz %[cnt], 3f \n" + " nop \n" + + "21: \n" + "addu $t0, %[filter_val], %[fy] \n" + "lh $t4, 0($t0) \n" // filter_val[fx] + "sll $t1, %[fy], 1 \n" + "addu $t0, %[src_data_rows], $t1 \n" + "lw $t1, 0($t0) \n" + "addu $t0, $t1, %[offset] \n" + "lbu $t1, 0($t0) \n" // t1 = row[fx*4 + 0] + "lbu $t2, 1($t0) \n" // t2 = row[fx*4 + 1] + "lbu $t3, 2($t0) \n" // t3 = row[fx*4 + 2] + "maddu $ac0, $t4, $t1 \n" + "maddu $ac1, $t4, $t2 \n" + "maddu $ac2, $t4, $t3 \n" + "addiu %[cnt], %[cnt], -1 \n" + "bgtz %[cnt], 21b \n" + " addiu %[fy], %[fy], 2 \n" + + "3: \n" + "extrv.w $t3, $ac0, %[kShiftBits] \n" // r >> kShiftBits + "extrv.w $t2, $ac1, %[kShiftBits] \n" // g >> kShiftBits + "extrv.w $t1, $ac2, %[kShiftBits] \n" // b >> kShiftBits + "repl.ph $t6, 128 \n" // t6 = | 128 | 128 | + "addu $t5, %[out_row], %[offset] \n" + "append $t2, $t3, 16 \n" // t2 = |0|g|0|r| + "andi $t1, $t1, 0xFFFF \n" + "subu.ph $t1, $t1, $t6 \n" + "shll_s.ph $t1, $t1, 8 \n" + "shra.ph $t1, $t1, 8 \n" + "addu.ph $t1, $t1, $t6 \n" // Clamp(a)|Clamp(b) + "subu.ph $t2, $t2, $t6 \n" + "shll_s.ph $t2, $t2, 8 \n" + "shra.ph $t2, $t2, 8 \n" + "addu.ph $t2, $t2, $t6 \n" // Clamp(g)|Clamp(r) + "li $t0, 0xFF \n" + "ins $t1, $t0, 16, 8 \n" + "precr.qb.ph $t0, $t1, $t2 \n" // t0 = |a|b|g|r| + "usw $t0, 0($t5) \n" + + ".set pop \n" + : [filter_val] "+r" (filter_val), [filter_len] "+r" (filter_length), + [offset] "+r" (byte_offset), [fy] "+r" (filter_y), [cnt] "+r" (cnt), + [out_x] "+r" (out_x), [pixel_width] "+r" (pixel_width) + : [src_data_rows] "r" (source_data_rows), [out_row] "r" (out_row), + [kShiftBits] "r" (ConvolutionFilter1D::kShiftBits) + : "lo", "hi", "$ac1lo", "$ac1hi", "$ac2lo", "$ac2hi", "$ac3lo", "$ac3hi", + "t0", "t1", "t2", "t3", "t4", "t5", "t6", "memory" + ); + } + } +#endif +} +} // namespace skia diff --git a/ext/convolver_mips_dspr2.h b/ext/convolver_mips_dspr2.h new file mode 100644 index 000000000000..1fd8774046aa --- /dev/null +++ b/ext/convolver_mips_dspr2.h @@ -0,0 +1,25 @@ +// Copyright 2013 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef SKIA_EXT_CONVOLVER_MIPS_DSPR2_H_ +#define SKIA_EXT_CONVOLVER_MIPS_DSPR2_H_ + +#include "skia/ext/convolver.h" + +namespace skia { + +void ConvolveVertically_mips_dspr2(const ConvolutionFilter1D::Fixed* filter_val, + int filter_length, + unsigned char* const* source_data_rows, + int pixel_width, + unsigned char* out_row, + bool has_alpha); + +void ConvolveHorizontally_mips_dspr2(const unsigned char* src_data, + const ConvolutionFilter1D& filter, + unsigned char* out_row, + bool has_alpha); +} // namespace skia + +#endif // SKIA_EXT_CONVOLVER_MIPS_DSPR2_H_ diff --git a/ext/convolver_neon.cc b/ext/convolver_neon.cc new file mode 100644 index 000000000000..cae6bc2f833f --- /dev/null +++ b/ext/convolver_neon.cc @@ -0,0 +1,339 @@ +/* + * Copyright 2016 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#include "skia/ext/convolver_neon.h" + +#include + +namespace skia { + +static SK_ALWAYS_INLINE int32x4_t +AccumRemainder(const unsigned char* pixels_left, + const ConvolutionFilter1D::Fixed* filter_values, + int r) { + int remainder[4] = {0, 0, 0, 0}; + for (int i = 0; i < r; i++) { + ConvolutionFilter1D::Fixed coeff = filter_values[i]; + remainder[0] += coeff * pixels_left[i * 4 + 0]; + remainder[1] += coeff * pixels_left[i * 4 + 1]; + remainder[2] += coeff * pixels_left[i * 4 + 2]; + remainder[3] += coeff * pixels_left[i * 4 + 3]; + } + return vld1q_s32(remainder); +} + +// Convolves horizontally along a single row. The row data is given in +// |src_data| and continues for the num_values() of the filter. +void ConvolveHorizontally_Neon(const unsigned char* src_data, + const ConvolutionFilter1D& filter, + unsigned char* out_row, + bool /*has_alpha*/) { + // Loop over each pixel on this row in the output image. + int num_values = filter.num_values(); + for (int out_x = 0; out_x < num_values; out_x++) { + // Get the filter that determines the current output pixel. + int filter_offset, filter_length; + const ConvolutionFilter1D::Fixed* filter_values = + filter.FilterForValue(out_x, &filter_offset, &filter_length); + + // Compute the first pixel in this row that the filter affects. It will + // touch |filter_length| pixels (4 bytes each) after this. + const unsigned char* row_to_filter = &src_data[filter_offset * 4]; + + // Apply the filter to the row to get the destination pixel in |accum|. + int32x4_t accum = vdupq_n_s32(0); + for (int filter_x = 0; filter_x < (filter_length / 4); filter_x++) { + // Load 4 coefficients. + int16x4_t coeffs = vld1_s16(filter_values); + // Load 4 pixels into a q-register. + uint8x16_t pixels = vld1q_u8(row_to_filter); + + // Expand to 16-bit channels split across two q-registers. + int16x8_t p01_16 = vreinterpretq_s16_u16(vmovl_u8(vget_low_u8(pixels))); + int16x8_t p23_16 = vreinterpretq_s16_u16(vmovl_u8(vget_high_u8(pixels))); + + // Scale each pixel (each d-register) by its filter coefficients, + // accumulating into 32-bit. + accum = vmlal_lane_s16(accum, vget_low_s16(p01_16), coeffs, 0); + accum = vmlal_lane_s16(accum, vget_high_s16(p01_16), coeffs, 1); + accum = vmlal_lane_s16(accum, vget_low_s16(p23_16), coeffs, 2); + accum = vmlal_lane_s16(accum, vget_high_s16(p23_16), coeffs, 3); + + // Advance to next elements. + row_to_filter += 16; + filter_values += 4; + } + + int remainder = filter_length & 3; + if (remainder) { + int remainder_offset = (filter_offset + filter_length - remainder) * 4; + accum += + AccumRemainder(src_data + remainder_offset, filter_values, remainder); + } + + // Bring this value back in range. All of the filter scaling factors + // are in fixed point with kShiftBits bits of fractional part. + int16x4_t accum16 = vqshrn_n_s32(accum, ConvolutionFilter1D::kShiftBits); + + // Pack and store the new pixel. + uint8x8_t accum8 = vqmovun_s16(vcombine_s16(accum16, accum16)); + vst1_lane_u32(reinterpret_cast(out_row), + vreinterpret_u32_u8(accum8), 0); + out_row += 4; + } +} + +// Convolves horizontally along four rows. The row data is given in +// |src_data| and continues for the num_values() of the filter. +// The algorithm is almost same as |convolve_horizontally|. Please +// refer to that function for detailed comments. +void Convolve4RowsHorizontally_Neon(const unsigned char* src_data[4], + const ConvolutionFilter1D& filter, + unsigned char* out_row[4]) { + // Output one pixel each iteration, calculating all channels (RGBA) together. + int num_values = filter.num_values(); + for (int out_x = 0; out_x < num_values; out_x++) { + int filter_offset, filter_length; + const ConvolutionFilter1D::Fixed* filter_values = + filter.FilterForValue(out_x, &filter_offset, &filter_length); + + // Four pixels in a column per iteration. + int32x4_t accum0 = vdupq_n_s32(0); + int32x4_t accum1 = vdupq_n_s32(0); + int32x4_t accum2 = vdupq_n_s32(0); + int32x4_t accum3 = vdupq_n_s32(0); + + int start = filter_offset * 4; + + // Load and accumulate with four coefficients per iteration. + for (int filter_x = 0; filter_x < (filter_length / 4); filter_x++) { + // Load 4 coefficients. + int16x4_t coeffs = vld1_s16(filter_values); + + auto iteration = [=](const uint8_t* src) { + // c.f. ConvolveHorizontally_Neon() above. + uint8x16_t pixels = vld1q_u8(src); + int16x8_t p01_16 = vreinterpretq_s16_u16(vmovl_u8(vget_low_u8(pixels))); + int16x8_t p23_16 = + vreinterpretq_s16_u16(vmovl_u8(vget_high_u8(pixels))); + int32x4_t accum = vdupq_n_s32(0); + accum = vmlal_lane_s16(accum, vget_low_s16(p01_16), coeffs, 0); + accum = vmlal_lane_s16(accum, vget_high_s16(p01_16), coeffs, 1); + accum = vmlal_lane_s16(accum, vget_low_s16(p23_16), coeffs, 2); + accum = vmlal_lane_s16(accum, vget_high_s16(p23_16), coeffs, 3); + return accum; + }; + + accum0 += iteration(src_data[0] + start); + accum1 += iteration(src_data[1] + start); + accum2 += iteration(src_data[2] + start); + accum3 += iteration(src_data[3] + start); + + start += 16; + filter_values += 4; + } + + int remainder = filter_length & 3; + if (remainder) { + int remainder_offset = (filter_offset + filter_length - remainder) * 4; + accum0 += AccumRemainder(src_data[0] + remainder_offset, filter_values, + remainder); + accum1 += AccumRemainder(src_data[1] + remainder_offset, filter_values, + remainder); + accum2 += AccumRemainder(src_data[2] + remainder_offset, filter_values, + remainder); + accum3 += AccumRemainder(src_data[3] + remainder_offset, filter_values, + remainder); + } + + auto pack_result = [](int32x4_t accum) { + int16x4_t accum16 = vqshrn_n_s32(accum, ConvolutionFilter1D::kShiftBits); + return vqmovun_s16(vcombine_s16(accum16, accum16)); + }; + + uint8x8_t res0 = pack_result(accum0); + uint8x8_t res1 = pack_result(accum1); + uint8x8_t res2 = pack_result(accum2); + uint8x8_t res3 = pack_result(accum3); + + vst1_lane_u32(reinterpret_cast(out_row[0]), + vreinterpret_u32_u8(res0), 0); + vst1_lane_u32(reinterpret_cast(out_row[1]), + vreinterpret_u32_u8(res1), 0); + vst1_lane_u32(reinterpret_cast(out_row[2]), + vreinterpret_u32_u8(res2), 0); + vst1_lane_u32(reinterpret_cast(out_row[3]), + vreinterpret_u32_u8(res3), 0); + out_row[0] += 4; + out_row[1] += 4; + out_row[2] += 4; + out_row[3] += 4; + } +} + +// Does vertical convolution to produce one output row. The filter values and +// length are given in the first two parameters. These are applied to each +// of the rows pointed to in the |source_data_rows| array, with each row +// being |pixel_width| wide. +// +// The output must have room for |pixel_width * 4| bytes. +void ConvolveVertically_Neon(const ConvolutionFilter1D::Fixed* filter_values, + int filter_length, + unsigned char* const* source_data_rows, + int pixel_width, + unsigned char* out_row, + bool has_alpha) { + int width = pixel_width & ~3; + + // Output four pixels per iteration (16 bytes). + for (int out_x = 0; out_x < width; out_x += 4) { + // Accumulated result for each pixel. 32 bits per RGBA channel. + int32x4_t accum0 = vdupq_n_s32(0); + int32x4_t accum1 = vdupq_n_s32(0); + int32x4_t accum2 = vdupq_n_s32(0); + int32x4_t accum3 = vdupq_n_s32(0); + + // Convolve with one filter coefficient per iteration. + for (int filter_y = 0; filter_y < filter_length; filter_y++) { + // Load four pixels (16 bytes) together. + // [8] a3 b3 g3 r3 a2 b2 g2 r2 a1 b1 g1 r1 a0 b0 g0 r0 + uint8x16_t src8 = vld1q_u8(&source_data_rows[filter_y][out_x << 2]); + + int16x8_t src16_01 = vreinterpretq_s16_u16(vmovl_u8(vget_low_u8(src8))); + int16x8_t src16_23 = vreinterpretq_s16_u16(vmovl_u8(vget_high_u8(src8))); + + accum0 = + vmlal_n_s16(accum0, vget_low_s16(src16_01), filter_values[filter_y]); + accum1 = + vmlal_n_s16(accum1, vget_high_s16(src16_01), filter_values[filter_y]); + accum2 = + vmlal_n_s16(accum2, vget_low_s16(src16_23), filter_values[filter_y]); + accum3 = + vmlal_n_s16(accum3, vget_high_s16(src16_23), filter_values[filter_y]); + } + + // Shift right for fixed point implementation. + // Packing 32 bits |accum| to 16 bits per channel (unsigned saturation). + int16x4_t accum16_0 = vqshrn_n_s32(accum0, ConvolutionFilter1D::kShiftBits); + int16x4_t accum16_1 = vqshrn_n_s32(accum1, ConvolutionFilter1D::kShiftBits); + int16x4_t accum16_2 = vqshrn_n_s32(accum2, ConvolutionFilter1D::kShiftBits); + int16x4_t accum16_3 = vqshrn_n_s32(accum3, ConvolutionFilter1D::kShiftBits); + + // [16] a1 b1 g1 r1 a0 b0 g0 r0 + int16x8_t accum16_low = vcombine_s16(accum16_0, accum16_1); + // [16] a3 b3 g3 r3 a2 b2 g2 r2 + int16x8_t accum16_high = vcombine_s16(accum16_2, accum16_3); + + // Packing 16 bits |accum| to 8 bits per channel (unsigned saturation). + // [8] a3 b3 g3 r3 a2 b2 g2 r2 a1 b1 g1 r1 a0 b0 g0 r0 + uint8x16_t accum8 = + vcombine_u8(vqmovun_s16(accum16_low), vqmovun_s16(accum16_high)); + + if (has_alpha) { + // Compute the max(ri, gi, bi) for each pixel. + // [8] xx a3 b3 g3 xx a2 b2 g2 xx a1 b1 g1 xx a0 b0 g0 + uint8x16_t a = + vreinterpretq_u8_u32(vshrq_n_u32(vreinterpretq_u32_u8(accum8), 8)); + // [8] xx xx xx max3 xx xx xx max2 xx xx xx max1 xx xx xx max0 + uint8x16_t b = vmaxq_u8(a, accum8); // Max of r and g + // [8] xx xx a3 b3 xx xx a2 b2 xx xx a1 b1 xx xx a0 b0 + a = vreinterpretq_u8_u32(vshrq_n_u32(vreinterpretq_u32_u8(accum8), 16)); + // [8] xx xx xx max3 xx xx xx max2 xx xx xx max1 xx xx xx max0 + b = vmaxq_u8(a, b); // Max of r and g and b. + // [8] max3 00 00 00 max2 00 00 00 max1 00 00 00 max0 00 00 00 + b = vreinterpretq_u8_u32(vshlq_n_u32(vreinterpretq_u32_u8(b), 24)); + + // Make sure the value of alpha channel is always larger than maximum + // value of color channels. + accum8 = vmaxq_u8(b, accum8); + } else { + // Set value of alpha channels to 0xFF. + accum8 = vreinterpretq_u8_u32(vreinterpretq_u32_u8(accum8) | + vdupq_n_u32(0xFF000000)); + } + + // Store the convolution result (16 bytes) and advance the pixel pointers. + vst1q_u8(out_row, accum8); + out_row += 16; + } + + // Process the leftovers when the width of the output is not divisible + // by 4, that is at most 3 pixels. + int remainder = pixel_width & 3; + if (remainder) { + int32x4_t accum0 = vdupq_n_s32(0); + int32x4_t accum1 = vdupq_n_s32(0); + int32x4_t accum2 = vdupq_n_s32(0); + + for (int filter_y = 0; filter_y < filter_length; ++filter_y) { + // [8] a3 b3 g3 r3 a2 b2 g2 r2 a1 b1 g1 r1 a0 b0 g0 r0 + uint8x16_t src8 = vld1q_u8(&source_data_rows[filter_y][width * 4]); + + int16x8_t src16_01 = vreinterpretq_s16_u16(vmovl_u8(vget_low_u8(src8))); + int16x8_t src16_23 = vreinterpretq_s16_u16(vmovl_u8(vget_high_u8(src8))); + + accum0 = + vmlal_n_s16(accum0, vget_low_s16(src16_01), filter_values[filter_y]); + accum1 = + vmlal_n_s16(accum1, vget_high_s16(src16_01), filter_values[filter_y]); + accum2 = + vmlal_n_s16(accum2, vget_low_s16(src16_23), filter_values[filter_y]); + } + + int16x4_t accum16_0 = vqshrn_n_s32(accum0, ConvolutionFilter1D::kShiftBits); + int16x4_t accum16_1 = vqshrn_n_s32(accum1, ConvolutionFilter1D::kShiftBits); + int16x4_t accum16_2 = vqshrn_n_s32(accum2, ConvolutionFilter1D::kShiftBits); + + int16x8_t accum16_low = vcombine_s16(accum16_0, accum16_1); + int16x8_t accum16_high = vcombine_s16(accum16_2, accum16_2); + + uint8x16_t accum8 = + vcombine_u8(vqmovun_s16(accum16_low), vqmovun_s16(accum16_high)); + + if (has_alpha) { + // Compute the max(ri, gi, bi) for each pixel. + // [8] xx a3 b3 g3 xx a2 b2 g2 xx a1 b1 g1 xx a0 b0 g0 + uint8x16_t a = + vreinterpretq_u8_u32(vshrq_n_u32(vreinterpretq_u32_u8(accum8), 8)); + // [8] xx xx xx max3 xx xx xx max2 xx xx xx max1 xx xx xx max0 + uint8x16_t b = vmaxq_u8(a, accum8); // Max of r and g + // [8] xx xx a3 b3 xx xx a2 b2 xx xx a1 b1 xx xx a0 b0 + a = vreinterpretq_u8_u32(vshrq_n_u32(vreinterpretq_u32_u8(accum8), 16)); + // [8] xx xx xx max3 xx xx xx max2 xx xx xx max1 xx xx xx max0 + b = vmaxq_u8(a, b); // Max of r and g and b. + // [8] max3 00 00 00 max2 00 00 00 max1 00 00 00 max0 00 00 00 + b = vreinterpretq_u8_u32(vshlq_n_u32(vreinterpretq_u32_u8(b), 24)); + + // Make sure the value of alpha channel is always larger than maximum + // value of color channels. + accum8 = vmaxq_u8(b, accum8); + } else { + // Set value of alpha channels to 0xFF. + accum8 = vreinterpretq_u8_u32(vreinterpretq_u32_u8(accum8) | + vdupq_n_u32(0xFF000000)); + } + + switch (remainder) { + case 1: + vst1q_lane_u32(reinterpret_cast(out_row), + vreinterpretq_u32_u8(accum8), 0); + break; + case 2: + vst1_u32(reinterpret_cast(out_row), + vreinterpret_u32_u8(vget_low_u8(accum8))); + break; + case 3: + vst1_u32(reinterpret_cast(out_row), + vreinterpret_u32_u8(vget_low_u8(accum8))); + vst1q_lane_u32(reinterpret_cast(out_row + 8), + vreinterpretq_u32_u8(accum8), 2); + break; + } + } +} + +} // namespace skia diff --git a/ext/convolver_neon.h b/ext/convolver_neon.h new file mode 100644 index 000000000000..ae253638adcc --- /dev/null +++ b/ext/convolver_neon.h @@ -0,0 +1,30 @@ +// Copyright 2018 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef SKIA_EXT_CONVOLVER_NEON_H_ +#define SKIA_EXT_CONVOLVER_NEON_H_ + +#include "skia/ext/convolver.h" + +namespace skia { + +void ConvolveHorizontally_Neon(const unsigned char* src_data, + const ConvolutionFilter1D& filter, + unsigned char* out_row, + bool has_alpha); + +void Convolve4RowsHorizontally_Neon(const unsigned char* src_data[4], + const ConvolutionFilter1D& filter, + unsigned char* out_row[4]); + +void ConvolveVertically_Neon(const ConvolutionFilter1D::Fixed* filter_values, + int filter_length, + unsigned char* const* source_data_rows, + int pixel_width, + unsigned char* out_row, + bool has_alpha); + +} // namespace skia + +#endif // SKIA_EXT_CONVOLVER_NEON_H_ diff --git a/ext/convolver_unittest.cc b/ext/convolver_unittest.cc new file mode 100644 index 000000000000..45a7449be270 --- /dev/null +++ b/ext/convolver_unittest.cc @@ -0,0 +1,531 @@ +// Copyright 2012 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "skia/ext/convolver.h" + +#include +#include +#include + +#include +#include +#include + +#include "base/logging.h" +#include "base/time/time.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "third_party/skia/include/core/SkBitmap.h" +#include "third_party/skia/include/core/SkColorPriv.h" +#include "third_party/skia/include/core/SkRect.h" +#include "third_party/skia/include/core/SkTypes.h" + +namespace skia { + +namespace { + +// Fills the given filter with impulse functions for the range 0->num_entries. +void FillImpulseFilter(int num_entries, ConvolutionFilter1D* filter) { + float one = 1.0f; + for (int i = 0; i < num_entries; i++) + filter->AddFilter(i, &one, 1); +} + +// Filters the given input with the impulse function, and verifies that it +// does not change. +void TestImpulseConvolution(const unsigned char* data, int width, int height) { + int byte_count = width * height * 4; + + ConvolutionFilter1D filter_x; + FillImpulseFilter(width, &filter_x); + + ConvolutionFilter1D filter_y; + FillImpulseFilter(height, &filter_y); + + std::vector output; + output.resize(byte_count); + BGRAConvolve2D(data, width * 4, true, filter_x, filter_y, + filter_x.num_values() * 4, &output[0], false); + + // Output should exactly match input. + EXPECT_EQ(0, memcmp(data, &output[0], byte_count)); +} + +// Fills the destination filter with a box filter averaging every two pixels +// to produce the output. +void FillBoxFilter(int size, ConvolutionFilter1D* filter) { + const float box[2] = { 0.5, 0.5 }; + for (int i = 0; i < size; i++) + filter->AddFilter(i * 2, box, 2); +} + +} // namespace + +// Tests that each pixel, when set and run through the impulse filter, does +// not change. +TEST(Convolver, Impulse) { + // We pick an "odd" size that is not likely to fit on any boundaries so that + // we can see if all the widths and paddings are handled properly. + int width = 15; + int height = 31; + int byte_count = width * height * 4; + std::vector input; + input.resize(byte_count); + + unsigned char* input_ptr = &input[0]; + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + for (int channel = 0; channel < 3; channel++) { + memset(input_ptr, 0, byte_count); + input_ptr[(y * width + x) * 4 + channel] = 0xff; + // Always set the alpha channel or it will attempt to "fix" it for us. + input_ptr[(y * width + x) * 4 + 3] = 0xff; + TestImpulseConvolution(input_ptr, width, height); + } + } + } +} + +// Tests that using a box filter to halve an image results in every square of 4 +// pixels in the original get averaged to a pixel in the output. +TEST(Convolver, Halve) { + static const int kSize = 16; + + int src_width = kSize; + int src_height = kSize; + int src_row_stride = src_width * 4; + int src_byte_count = src_row_stride * src_height; + std::vector input; + input.resize(src_byte_count); + + int dest_width = src_width / 2; + int dest_height = src_height / 2; + int dest_byte_count = dest_width * dest_height * 4; + std::vector output; + output.resize(dest_byte_count); + + // First fill the array with a bunch of random data. + srand(static_cast(time(NULL))); + for (int i = 0; i < src_byte_count; i++) + input[i] = rand() * 255 / RAND_MAX; + + // Compute the filters. + ConvolutionFilter1D filter_x, filter_y; + FillBoxFilter(dest_width, &filter_x); + FillBoxFilter(dest_height, &filter_y); + + // Do the convolution. + BGRAConvolve2D(&input[0], src_width, true, filter_x, filter_y, + filter_x.num_values() * 4, &output[0], false); + + // Compute the expected results and check, allowing for a small difference + // to account for rounding errors. + for (int y = 0; y < dest_height; y++) { + for (int x = 0; x < dest_width; x++) { + for (int channel = 0; channel < 4; channel++) { + int src_offset = (y * 2 * src_row_stride + x * 2 * 4) + channel; + int value = input[src_offset] + // Top left source pixel. + input[src_offset + 4] + // Top right source pixel. + input[src_offset + src_row_stride] + // Lower left. + input[src_offset + src_row_stride + 4]; // Lower right. + value /= 4; // Average. + int difference = value - output[(y * dest_width + x) * 4 + channel]; + EXPECT_TRUE(difference >= -1 || difference <= 1); + } + } + } +} + +// Tests the optimization in Convolver1D::AddFilter that avoids storing +// leading/trailing zeroes. +TEST(Convolver, AddFilter) { + skia::ConvolutionFilter1D filter; + + const skia::ConvolutionFilter1D::Fixed* values = NULL; + int filter_offset = 0; + int filter_length = 0; + + // An all-zero filter is handled correctly, all factors ignored + static const float factors1[] = { 0.0f, 0.0f, 0.0f }; + filter.AddFilter(11, factors1, std::size(factors1)); + ASSERT_EQ(0, filter.max_filter()); + ASSERT_EQ(1, filter.num_values()); + + values = filter.FilterForValue(0, &filter_offset, &filter_length); + ASSERT_TRUE(values == NULL); // No values => NULL. + ASSERT_EQ(11, filter_offset); // Same as input offset. + ASSERT_EQ(0, filter_length); // But no factors since all are zeroes. + + // Zeroes on the left are ignored + static const float factors2[] = { 0.0f, 1.0f, 1.0f, 1.0f, 1.0f }; + filter.AddFilter(22, factors2, std::size(factors2)); + ASSERT_EQ(4, filter.max_filter()); + ASSERT_EQ(2, filter.num_values()); + + values = filter.FilterForValue(1, &filter_offset, &filter_length); + ASSERT_TRUE(values != NULL); + ASSERT_EQ(23, filter_offset); // 22 plus 1 leading zero + ASSERT_EQ(4, filter_length); // 5 - 1 leading zero + + // Zeroes on the right are ignored + static const float factors3[] = { 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 0.0f, 0.0f }; + filter.AddFilter(33, factors3, std::size(factors3)); + ASSERT_EQ(5, filter.max_filter()); + ASSERT_EQ(3, filter.num_values()); + + values = filter.FilterForValue(2, &filter_offset, &filter_length); + ASSERT_TRUE(values != NULL); + ASSERT_EQ(33, filter_offset); // 33, same as input due to no leading zero + ASSERT_EQ(5, filter_length); // 7 - 2 trailing zeroes + + // Zeroes in leading & trailing positions + static const float factors4[] = { 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 0.0f, 0.0f }; + filter.AddFilter(44, factors4, std::size(factors4)); + ASSERT_EQ(5, filter.max_filter()); // No change from existing value. + ASSERT_EQ(4, filter.num_values()); + + values = filter.FilterForValue(3, &filter_offset, &filter_length); + ASSERT_TRUE(values != NULL); + ASSERT_EQ(46, filter_offset); // 44 plus 2 leading zeroes + ASSERT_EQ(3, filter_length); // 7 - (2 leading + 2 trailing) zeroes + + // Zeroes surrounded by non-zero values are ignored + static const float factors5[] = { 0.0f, 0.0f, + 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, + 0.0f }; + filter.AddFilter(55, factors5, std::size(factors5)); + ASSERT_EQ(6, filter.max_filter()); + ASSERT_EQ(5, filter.num_values()); + + values = filter.FilterForValue(4, &filter_offset, &filter_length); + ASSERT_TRUE(values != NULL); + ASSERT_EQ(57, filter_offset); // 55 plus 2 leading zeroes + ASSERT_EQ(6, filter_length); // 9 - (2 leading + 1 trailing) zeroes + + // All-zero filters after the first one also work + static const float factors6[] = { 0.0f }; + filter.AddFilter(66, factors6, std::size(factors6)); + ASSERT_EQ(6, filter.max_filter()); + ASSERT_EQ(6, filter.num_values()); + + values = filter.FilterForValue(5, &filter_offset, &filter_length); + ASSERT_TRUE(values == NULL); // filter_length == 0 => values is NULL + ASSERT_EQ(66, filter_offset); // value passed in + ASSERT_EQ(0, filter_length); +} + +void VerifySIMD(unsigned int source_width, + unsigned int source_height, + unsigned int dest_width, + unsigned int dest_height) { + float filter[] = { 0.05f, -0.15f, 0.6f, 0.6f, -0.15f, 0.05f }; + // Preparing convolve coefficients. + ConvolutionFilter1D x_filter, y_filter; + for (unsigned int p = 0; p < dest_width; ++p) { + unsigned int offset = source_width * p / dest_width; + EXPECT_LT(offset, source_width); + x_filter.AddFilter(offset, filter, + std::min(std::size(filter), source_width - offset)); + } + x_filter.PaddingForSIMD(); + for (unsigned int p = 0; p < dest_height; ++p) { + unsigned int offset = source_height * p / dest_height; + y_filter.AddFilter( + offset, filter, + std::min(std::size(filter), source_height - offset)); + } + y_filter.PaddingForSIMD(); + + // Allocate input and output skia bitmap. + SkBitmap source, result_c, result_sse; + source.allocN32Pixels(source_width, source_height); + result_c.allocN32Pixels(dest_width, dest_height); + result_sse.allocN32Pixels(dest_width, dest_height); + + // Randomize source bitmap for testing. + unsigned char* src_ptr = static_cast(source.getPixels()); + for (int y = 0; y < source.height(); y++) { + for (unsigned int x = 0; x < source.rowBytes(); x++) + src_ptr[x] = rand() % 255; + src_ptr += source.rowBytes(); + } + + // Test both cases with different has_alpha. + for (int alpha = 0; alpha < 2; alpha++) { + // Convolve using C code. + base::TimeTicks resize_start; + base::TimeDelta delta_c, delta_sse; + unsigned char* r1 = static_cast(result_c.getPixels()); + unsigned char* r2 = static_cast(result_sse.getPixels()); + + resize_start = base::TimeTicks::Now(); + BGRAConvolve2D(static_cast(source.getPixels()), + static_cast(source.rowBytes()), (alpha != 0), x_filter, + y_filter, static_cast(result_c.rowBytes()), r1, false); + delta_c = base::TimeTicks::Now() - resize_start; + + resize_start = base::TimeTicks::Now(); + // Convolve using SSE2 code + BGRAConvolve2D(static_cast(source.getPixels()), + static_cast(source.rowBytes()), (alpha != 0), x_filter, + y_filter, static_cast(result_sse.rowBytes()), r2, true); + delta_sse = base::TimeTicks::Now() - resize_start; + + // Unfortunately I could not enable the performance check now. + // Most bots use debug version, and there are great difference between + // the code generation for intrinsic, etc. In release version speed + // difference was 150%-200% depend on alpha channel presence; + // while in debug version speed difference was 96%-120%. + // TODO(jiesun): optimize further until we could enable this for + // debug version too. + // EXPECT_LE(delta_sse, delta_c); + + int64_t c_us = delta_c.InMicroseconds(); + int64_t sse_us = delta_sse.InMicroseconds(); + VLOG(1) << "from:" << source_width << "x" << source_height + << " to:" << dest_width << "x" << dest_height + << (alpha ? " with alpha" : " w/o alpha"); + VLOG(1) << "c:" << c_us << " sse:" << sse_us; + VLOG(1) << "ratio:" << static_cast(c_us) / sse_us; + + // Comparing result. + for (unsigned int i = 0; i < dest_height; i++) { + EXPECT_FALSE(memcmp(r1, r2, dest_width * 4)); // RGBA always + r1 += result_c.rowBytes(); + r2 += result_sse.rowBytes(); + } + } +} + +TEST(Convolver, VerifySIMDEdgeCases) { + srand(static_cast(time(0))); + // Loop over all possible (small) image sizes + for (unsigned int width = 1; width < 20; width++) { + for (unsigned int height = 1; height < 20; height++) { + VerifySIMD(width, height, 8, 8); + VerifySIMD(8, 8, width, height); + } + } +} + +// Verify that lage upscales/downscales produce the same result +// with and without SIMD. +TEST(Convolver, VerifySIMDPrecision) { + int source_sizes[][2] = { {1920, 1080}, {1377, 523}, {325, 241} }; + int dest_sizes[][2] = { {1280, 1024}, {177, 123} }; + + srand(static_cast(time(0))); + + // Loop over some specific source and destination dimensions. + for (unsigned int i = 0; i < std::size(source_sizes); ++i) { + unsigned int source_width = source_sizes[i][0]; + unsigned int source_height = source_sizes[i][1]; + for (unsigned int j = 0; j < std::size(dest_sizes); ++j) { + unsigned int dest_width = dest_sizes[j][0]; + unsigned int dest_height = dest_sizes[j][1]; + VerifySIMD(source_width, source_height, dest_width, dest_height); + } + } +} + +TEST(Convolver, SeparableSingleConvolution) { + static const int kImgWidth = 1024; + static const int kImgHeight = 1024; + static const int kChannelCount = 3; + static const int kStrideSlack = 22; + ConvolutionFilter1D filter; + const float box[5] = { 0.2f, 0.2f, 0.2f, 0.2f, 0.2f }; + filter.AddFilter(0, box, 5); + + // Allocate a source image and set to 0. + const int src_row_stride = kImgWidth * kChannelCount + kStrideSlack; + int src_byte_count = src_row_stride * kImgHeight; + std::vector input; + const int signal_x = kImgWidth / 2; + const int signal_y = kImgHeight / 2; + input.resize(src_byte_count, 0); + // The image has a single impulse pixel in channel 1, smack in the middle. + const int non_zero_pixel_index = + signal_y * src_row_stride + signal_x * kChannelCount + 1; + input[non_zero_pixel_index] = 255; + + // Destination will be a single channel image with stide matching width. + const int dest_row_stride = kImgWidth; + const int dest_byte_count = dest_row_stride * kImgHeight; + std::vector output; + output.resize(dest_byte_count); + + // Apply convolution in X. + SingleChannelConvolveX1D(&input[0], src_row_stride, 1, kChannelCount, + filter, SkISize::Make(kImgWidth, kImgHeight), + &output[0], dest_row_stride, 0, 1, false); + for (int x = signal_x - 2; x <= signal_x + 2; ++x) + EXPECT_GT(output[signal_y * dest_row_stride + x], 0); + + EXPECT_EQ(output[signal_y * dest_row_stride + signal_x - 3], 0); + EXPECT_EQ(output[signal_y * dest_row_stride + signal_x + 3], 0); + + // Apply convolution in Y. + SingleChannelConvolveY1D(&input[0], src_row_stride, 1, kChannelCount, + filter, SkISize::Make(kImgWidth, kImgHeight), + &output[0], dest_row_stride, 0, 1, false); + for (int y = signal_y - 2; y <= signal_y + 2; ++y) + EXPECT_GT(output[y * dest_row_stride + signal_x], 0); + + EXPECT_EQ(output[(signal_y - 3) * dest_row_stride + signal_x], 0); + EXPECT_EQ(output[(signal_y + 3) * dest_row_stride + signal_x], 0); + + EXPECT_EQ(output[signal_y * dest_row_stride + signal_x - 1], 0); + EXPECT_EQ(output[signal_y * dest_row_stride + signal_x + 1], 0); + + // The main point of calling this is to invoke the routine on input without + // padding. + std::vector output2; + output2.resize(dest_byte_count); + SingleChannelConvolveX1D(&output[0], dest_row_stride, 0, 1, + filter, SkISize::Make(kImgWidth, kImgHeight), + &output2[0], dest_row_stride, 0, 1, false); + // This should be a result of 2D convolution. + for (int x = signal_x - 2; x <= signal_x + 2; ++x) { + for (int y = signal_y - 2; y <= signal_y + 2; ++y) + EXPECT_GT(output2[y * dest_row_stride + x], 0); + } + EXPECT_EQ(output2[0], 0); + EXPECT_EQ(output2[dest_row_stride - 1], 0); + EXPECT_EQ(output2[dest_byte_count - 1], 0); +} + +TEST(Convolver, SeparableSingleConvolutionEdges) { + // The purpose of this test is to check if the implementation treats correctly + // edges of the image. + static const int kImgWidth = 600; + static const int kImgHeight = 800; + static const int kChannelCount = 3; + static const int kStrideSlack = 22; + static const int kChannel = 1; + ConvolutionFilter1D filter; + const float box[5] = { 0.2f, 0.2f, 0.2f, 0.2f, 0.2f }; + filter.AddFilter(0, box, 5); + + // Allocate a source image and set to 0. + int src_row_stride = kImgWidth * kChannelCount + kStrideSlack; + int src_byte_count = src_row_stride * kImgHeight; + std::vector input(src_byte_count); + + // Draw a frame around the image. + for (int i = 0; i < src_byte_count; ++i) { + int row = i / src_row_stride; + int col = i % src_row_stride / kChannelCount; + int channel = i % src_row_stride % kChannelCount; + if (channel != kChannel || col > kImgWidth) { + input[i] = 255; + } else if (row == 0 || col == 0 || + col == kImgWidth - 1 || row == kImgHeight - 1) { + input[i] = 100; + } else if (row == 1 || col == 1 || + col == kImgWidth - 2 || row == kImgHeight - 2) { + input[i] = 200; + } else { + input[i] = 0; + } + } + + // Destination will be a single channel image with stide matching width. + int dest_row_stride = kImgWidth; + int dest_byte_count = dest_row_stride * kImgHeight; + std::vector output; + output.resize(dest_byte_count); + + // Apply convolution in X. + SingleChannelConvolveX1D(&input[0], src_row_stride, 1, kChannelCount, + filter, SkISize::Make(kImgWidth, kImgHeight), + &output[0], dest_row_stride, 0, 1, false); + + // Sadly, comparison is not as simple as retaining all values. + int invalid_values = 0; + const unsigned char first_value = output[0]; + EXPECT_NEAR(first_value, 100, 1); + for (int i = 0; i < dest_row_stride; ++i) { + if (output[i] != first_value) + ++invalid_values; + } + EXPECT_EQ(0, invalid_values); + + int test_row = 22; + EXPECT_NEAR(output[test_row * dest_row_stride], 100, 1); + EXPECT_NEAR(output[test_row * dest_row_stride + 1], 80, 1); + EXPECT_NEAR(output[test_row * dest_row_stride + 2], 60, 1); + EXPECT_NEAR(output[test_row * dest_row_stride + 3], 40, 1); + EXPECT_NEAR(output[(test_row + 1) * dest_row_stride - 1], 100, 1); + EXPECT_NEAR(output[(test_row + 1) * dest_row_stride - 2], 80, 1); + EXPECT_NEAR(output[(test_row + 1) * dest_row_stride - 3], 60, 1); + EXPECT_NEAR(output[(test_row + 1) * dest_row_stride - 4], 40, 1); + + SingleChannelConvolveY1D(&input[0], src_row_stride, 1, kChannelCount, + filter, SkISize::Make(kImgWidth, kImgHeight), + &output[0], dest_row_stride, 0, 1, false); + + int test_column = 42; + EXPECT_NEAR(output[test_column], 100, 1); + EXPECT_NEAR(output[test_column + dest_row_stride], 80, 1); + EXPECT_NEAR(output[test_column + dest_row_stride * 2], 60, 1); + EXPECT_NEAR(output[test_column + dest_row_stride * 3], 40, 1); + + EXPECT_NEAR(output[test_column + dest_row_stride * (kImgHeight - 1)], 100, 1); + EXPECT_NEAR(output[test_column + dest_row_stride * (kImgHeight - 2)], 80, 1); + EXPECT_NEAR(output[test_column + dest_row_stride * (kImgHeight - 3)], 60, 1); + EXPECT_NEAR(output[test_column + dest_row_stride * (kImgHeight - 4)], 40, 1); +} + +TEST(Convolver, SetUpGaussianConvolutionFilter) { + ConvolutionFilter1D smoothing_filter; + ConvolutionFilter1D gradient_filter; + SetUpGaussianConvolutionKernel(&smoothing_filter, 4.5f, false); + SetUpGaussianConvolutionKernel(&gradient_filter, 3.0f, true); + + int specified_filter_length; + int filter_offset; + int filter_length; + + const ConvolutionFilter1D::Fixed* smoothing_kernel = + smoothing_filter.GetSingleFilter( + &specified_filter_length, &filter_offset, &filter_length); + EXPECT_TRUE(smoothing_kernel); + std::vector fp_smoothing_kernel(filter_length); + std::transform(smoothing_kernel, + smoothing_kernel + filter_length, + fp_smoothing_kernel.begin(), + ConvolutionFilter1D::FixedToFloat); + // Should sum-up to 1 (nearly), and all values whould be in ]0, 1[. + EXPECT_NEAR(std::accumulate( + fp_smoothing_kernel.begin(), fp_smoothing_kernel.end(), 0.0f), + 1.0f, 0.01f); + EXPECT_GT(*std::min_element(fp_smoothing_kernel.begin(), + fp_smoothing_kernel.end()), 0.0f); + EXPECT_LT(*std::max_element(fp_smoothing_kernel.begin(), + fp_smoothing_kernel.end()), 1.0f); + + const ConvolutionFilter1D::Fixed* gradient_kernel = + gradient_filter.GetSingleFilter( + &specified_filter_length, &filter_offset, &filter_length); + EXPECT_TRUE(gradient_kernel); + std::vector fp_gradient_kernel(filter_length); + std::transform(gradient_kernel, + gradient_kernel + filter_length, + fp_gradient_kernel.begin(), + ConvolutionFilter1D::FixedToFloat); + // Should sum-up to 0, and all values whould be in ]-1.5, 1.5[. + EXPECT_NEAR(std::accumulate( + fp_gradient_kernel.begin(), fp_gradient_kernel.end(), 0.0f), + 0.0f, 0.01f); + EXPECT_GT(*std::min_element(fp_gradient_kernel.begin(), + fp_gradient_kernel.end()), -1.5f); + EXPECT_LT(*std::min_element(fp_gradient_kernel.begin(), + fp_gradient_kernel.end()), 0.0f); + EXPECT_LT(*std::max_element(fp_gradient_kernel.begin(), + fp_gradient_kernel.end()), 1.5f); + EXPECT_GT(*std::max_element(fp_gradient_kernel.begin(), + fp_gradient_kernel.end()), 0.0f); +} + +} // namespace skia diff --git a/ext/data/test_fonts/ChromiumAATTest.ttf b/ext/data/test_fonts/ChromiumAATTest.ttf new file mode 100644 index 0000000000000000000000000000000000000000..343d53aaeae6711158249aaf480edca415100877 GIT binary patch literal 3020 zcmcImZ)jUp6hHU9G&bF8+x5=|mip3`{#(}#{b;0#D$uF%ncW)#6Hmo160sS~55zLjoTsU3 zEs?SsvTd>HyhdJSmPj3g-Zjaid2+iVSbrGv?nKngSr$A~MI7_;M0$4eO6LoWu+L%r zJ4qgm(|4iU(EW|xpM=2Oq%J}i#Jn|`$;2Cu_$X`XR#i8P-7-KlcU|# z5B*-uwTy1eP|N$h*zXg_)@7p^{_WJ!YeZ$`=$mr7nYUi20Q64^dlIpuANt|!Z!cDD zUZhp(bx7#)*sC9uMz3Wli^?U;%SjPZG^|lBA+mjry|QR~a&*qS(w0y!`N)x+DWmK*lQO-g3hBLCCsh7c8Avf|So8Vb$W0TwC^aVvJu4>j}v=7{59D`-^`Jif-}Ux=pt`#rKkcLgG)D`_iWm9mQs$A_ zhlu)Wyfy88H7#D|xqjh-GJkv0tBAvltm@3GoX0O(iHZ6I8|Vu(to1(3>g!m=@eOZ| zd*Y|Nm%iG%mwDO4qvJ|upZmF?cCpTf)K@A^ggaUr9H?z#eS!V~wTJnJPSo${=o+m( zarhkDKlSX=jn+<|wsgaD(b=z^equfBSMrE;{egx$cT00ydpI2q_iucf#rng5JK?|E zve+q{UGS<6wzYU0>V;){ZU41B+t{X-g=5v_C+gY$&CiE+A6sCosugEuy}U)0^ayo{ z8z>?gV#SEcKyRbuI?!KR@)&GuZ){~WE(l}X5wvFZZt*PBAWcim@OAt~Vq8l4 zU1F8ivt1IqsEW-?Tt?NnPtI9)Lw?)A5Uf+2uL3S7d6c)LT%j7}yu>P1t3eyX1omwb z3wwtf!_9hB`CiIZTBB~au>yOqvPEKH|DnVRtyOMGtWu4-*TyRB>y@a)3}2y-B?ce0 zN@A62)hQdhVBe^`EV1zanZyb>cqLY;QGG>xF4Gbfg+G_j3XEgSY}{Vz+0nXjCn|P z$W7|8C22}yT=rzzGY&n6bQ}d{Icx=0m!X;98)^uML)hl4KwW_6}%{$OqsDH&uZcML^>5q9^|P!&r+z6ox{qM7D?HTC!yl^Sf?H2 z*$g-Erk$yT+;=5{mOI*#Syp%xy!kTWh+fEZQ=9xx-opiRj)P9NkWH9TL$Hk$@_DWy zrXuYUQ@+e^M9*Z9=9T13ZQLwyq{O6Mx{-;(PtjKSb$%XpOE&j!26O8U`E9c7*3Ok{fxwDr!_D;nJUWbHGwcjrL YO{a)xi)AgM-$MWMbIbY*BB!nX2fqsxMF0Q* literal 0 HcmV?d00001 diff --git a/ext/data/test_fonts/fuchsia_test_fonts_manifest.json b/ext/data/test_fonts/fuchsia_test_fonts_manifest.json new file mode 100644 index 000000000000..ced1febc2b6c --- /dev/null +++ b/ext/data/test_fonts/fuchsia_test_fonts_manifest.json @@ -0,0 +1,205 @@ +{ + "families": [ + { + "family": "Arimo", + "aliases": [ + "sans", + "sans serif", + "sans-serif", + "Arial", + "Helvetica" + ], + "fallback": true, + "fallback_group": "sans-serif", + "fonts": [ + { + "asset": "Arimo-Regular.ttf" + }, + { + "asset": "Arimo-Bold.ttf", + "weight": 700 + }, + { + "asset": "Arimo-Italic.ttf", + "slant": "italic" + }, + { + "asset": "Arimo-BoldItalic.ttf", + "weight": 700, + "slant": "italic" + } + ] + }, + { + "family": "Tinos", + "aliases": [ + "serif", + "Times", + "Times New Roman", + "Monaco", + "SubpixelPositioning" + ], + "fallback": true, + "fallback_group": "serif", + "fonts": [ + { + "asset": "Tinos-Regular.ttf" + }, + { + "asset": "Tinos-Bold.ttf", + "weight": 700 + }, + { + "asset": "Tinos-Italic.ttf", + "slant": "italic" + }, + { + "asset": "Tinos-BoldItalic.ttf", + "weight": 700, + "slant": "italic" + } + ] + }, + { + "family": "Cousine", + "aliases": [ + "mono", + "monospace", + "Courier", + "Courier New" + ], + "fallback": true, + "fallback_group": "monospace", + "fonts": [ + { + "asset": "Cousine-Regular.ttf" + }, + { + "asset": "Cousine-Bold.ttf", + "weight": 700 + }, + { + "asset": "Cousine-Italic.ttf", + "slant": "italic" + }, + { + "asset": "Cousine-BoldItalic.ttf", + "weight": 700, + "slant": "italic" + } + ] + }, + { + "family": "Gelasio", + "aliases": [ + "Georgia" + ], + "fallback": false, + "fonts": [ + { + "asset": "Gelasio-Regular.ttf" + }, + { + "asset": "Gelasio-Bold.ttf", + "weight": 700 + }, + { + "asset": "Gelasio-Italic.ttf", + "slant": "italic" + }, + { + "asset": "Gelasio-BoldItalic.ttf", + "weight": 700, + "slant": "italic" + } + ] + }, + { + "family": "Ahem", + "aliases": [ + "SubpixelPositioningAhem" + ], + "fallback": false, + "fallback_group": "sans-serif", + "fonts": [ + { + "asset": "Ahem.ttf" + } + ] + }, + { + "family": "DejaVu Sans", + "fallback": true, + "fonts": [ + { + "asset": "DejaVuSans.ttf" + } + ] + }, + { + "family": "Garuda", + "fallback": true, + "fonts": [ + { + "asset": "Garuda.ttf" + } + ] + }, + { + "family": "Lohit Devanagari", + "fallback": true, + "fonts": [ + { + "asset": "Lohit-Devanagari.ttf" + } + ] + }, + { + "family": "Lohit Gurmukhi", + "fallback": true, + "fonts": [ + { + "asset": "Lohit-Gurmukhi.ttf" + } + ] + }, + { + "family": "Lohit Tamil", + "fallback": true, + "fonts": [ + { + "asset": "Lohit-Tamil.ttf" + } + ] + }, + { + "family": "Mukti", + "fallback": true, + "fonts": [ + { + "asset": "MuktiNarrow.ttf" + } + ] + }, + { + "family": "Noto Sans Khmer", + "fallback": true, + "fonts": [ + { + "asset": "NotoSansKhmer-Regular.ttf", + "language": "km" + } + ] + }, + { + "family": "Noto Sans CJK JP", + "fallback": true, + "fonts": [ + { + "asset": "NotoSansCJK-VF.otf.ttc", + "language": "ja" + } + ] + } + ] +} diff --git a/ext/data/vectorcanvastest/basicdrawing/00_pc_clean.png b/ext/data/vectorcanvastest/basicdrawing/00_pc_clean.png new file mode 100644 index 0000000000000000000000000000000000000000..a5435f278bc5795ed2ef92f67fbaa63561454408 GIT binary patch literal 289 zcmeAS@N?(olHy`uVBq!ia0vp^DImpJrs65U{pG3@p0{%dHjUsLZadwrXI!P^Veuc5uTr9*QsdY#qT>&c?D zt=Gd9d{3|E+nt79ul9!Byxwl4TX`)wb?D=@=7^?=*Ru1h4qht{|H^wUJ}Is3wf0b@gqJWDj(cfi;jEWD77lt7Cnv9QOhEEYC+RmQ?Qx^L>%3ZEVUbrCEUfTqgoW2$y|D1ks~r|zcy+|W zQ?I62c;wX=3%plrERbH^vA}p43z+sY)_*fzm;cdeK7X=^j74NDB4ZI5i^y0+#v(En bk+FzAR^#);8L3rY00000NkvXXu0mjf0Gj(Q literal 0 HcmV?d00001 diff --git a/ext/data/vectorcanvastest/basicdrawing/02_vc_drawline_black.png b/ext/data/vectorcanvastest/basicdrawing/02_vc_drawline_black.png new file mode 100644 index 0000000000000000000000000000000000000000..c21fdf1c5777d2fd7b7765cfac3a6071ad7ca5c7 GIT binary patch literal 537 zcmV+!0_OdRP)pJrs65U{pG3@p0{%dHjUsLZadwrXI!P^Veuc5uTr9*QsdY#qT>&c?D zt=Gd9d{3|E+nt79ul9!Byxwl4TX`)wb?D=@=7^?=*Ru1h4qht{|H^wUJ}Is3wf0b@gqJWDj(cfi;jEWD77lt7Cnv9QOhEEYC+RmQ?Qx^L>%3ZEVUbrCEUfTqgoW2$y|D1ks~r|zcy+|W zQ?I62c;wX=3%plrERbH^vA}p43z+sY)_*fzm;cdeK7X=^j74NDB4ZI5i^y0+#v(En bk+FzAR^#);8L3rY00000NkvXXu0mjf0Gj(Q literal 0 HcmV?d00001 diff --git a/ext/data/vectorcanvastest/basicdrawing/03_pc_drawrect_green.png b/ext/data/vectorcanvastest/basicdrawing/03_pc_drawrect_green.png new file mode 100644 index 0000000000000000000000000000000000000000..dfc46a80d8a0c79a1e8222b5767205119b822204 GIT binary patch literal 417 zcmeAS@N?(olHy`uVBq!ia0vp^DImAL1D(*CnSr{ZQ3d~->xy=OJ_oa!bXRr#v@FftU}x>VlSAo zdp^$l{G&^6!?QV6dw2bleDyW!<;!=N;m5>8jOldAySMnz?_N z8Tb40T!;0GcP7rd{?f(j)at~mipN){cd8x9HmOUI{VKmVZENg;?s^n^JkTwsmcWAf z$C6FoN#^GCY`eSb`~J;R-}CRwul}`1X~U|M+N`Us_B3sHbwitb^|?KdHoQ8ZEx5XD zuhYh?)7s*zcZ8dSIj%2RqZF4Es$1Rp^?FzM^ZC7Z?cQTL^@6^^+mr+4;yD(;pl9%O L^>bP0l+XkK9HgrS literal 0 HcmV?d00001 diff --git a/ext/data/vectorcanvastest/basicdrawing/03_vc_drawrect_green.png b/ext/data/vectorcanvastest/basicdrawing/03_vc_drawrect_green.png new file mode 100644 index 0000000000000000000000000000000000000000..dfc46a80d8a0c79a1e8222b5767205119b822204 GIT binary patch literal 417 zcmeAS@N?(olHy`uVBq!ia0vp^DImAL1D(*CnSr{ZQ3d~->xy=OJ_oa!bXRr#v@FftU}x>VlSAo zdp^$l{G&^6!?QV6dw2bleDyW!<;!=N;m5>8jOldAySMnz?_N z8Tb40T!;0GcP7rd{?f(j)at~mipN){cd8x9HmOUI{VKmVZENg;?s^n^JkTwsmcWAf z$C6FoN#^GCY`eSb`~J;R-}CRwul}`1X~U|M+N`Us_B3sHbwitb^|?KdHoQ8ZEx5XD zuhYh?)7s*zcZ8dSIj%2RqZF4Es$1Rp^?FzM^ZC7Z?cQTL^@6^^+mr+4;yD(;pl9%O L^>bP0l+XkK9HgrS literal 0 HcmV?d00001 diff --git a/ext/data/vectorcanvastest/basicdrawing/04_pc_drawrect_noop.png b/ext/data/vectorcanvastest/basicdrawing/04_pc_drawrect_noop.png new file mode 100644 index 0000000000000000000000000000000000000000..dfc46a80d8a0c79a1e8222b5767205119b822204 GIT binary patch literal 417 zcmeAS@N?(olHy`uVBq!ia0vp^DImAL1D(*CnSr{ZQ3d~->xy=OJ_oa!bXRr#v@FftU}x>VlSAo zdp^$l{G&^6!?QV6dw2bleDyW!<;!=N;m5>8jOldAySMnz?_N z8Tb40T!;0GcP7rd{?f(j)at~mipN){cd8x9HmOUI{VKmVZENg;?s^n^JkTwsmcWAf z$C6FoN#^GCY`eSb`~J;R-}CRwul}`1X~U|M+N`Us_B3sHbwitb^|?KdHoQ8ZEx5XD zuhYh?)7s*zcZ8dSIj%2RqZF4Es$1Rp^?FzM^ZC7Z?cQTL^@6^^+mr+4;yD(;pl9%O L^>bP0l+XkK9HgrS literal 0 HcmV?d00001 diff --git a/ext/data/vectorcanvastest/basicdrawing/04_vc_drawrect_noop.png b/ext/data/vectorcanvastest/basicdrawing/04_vc_drawrect_noop.png new file mode 100644 index 0000000000000000000000000000000000000000..dfc46a80d8a0c79a1e8222b5767205119b822204 GIT binary patch literal 417 zcmeAS@N?(olHy`uVBq!ia0vp^DImAL1D(*CnSr{ZQ3d~->xy=OJ_oa!bXRr#v@FftU}x>VlSAo zdp^$l{G&^6!?QV6dw2bleDyW!<;!=N;m5>8jOldAySMnz?_N z8Tb40T!;0GcP7rd{?f(j)at~mipN){cd8x9HmOUI{VKmVZENg;?s^n^JkTwsmcWAf z$C6FoN#^GCY`eSb`~J;R-}CRwul}`1X~U|M+N`Us_B3sHbwitb^|?KdHoQ8ZEx5XD zuhYh?)7s*zcZ8dSIj%2RqZF4Es$1Rp^?FzM^ZC7Z?cQTL^@6^^+mr+4;yD(;pl9%O L^>bP0l+XkK9HgrS literal 0 HcmV?d00001 diff --git a/ext/data/vectorcanvastest/basicdrawing/05_pc_drawrect_noop.png b/ext/data/vectorcanvastest/basicdrawing/05_pc_drawrect_noop.png new file mode 100644 index 0000000000000000000000000000000000000000..69cc6dce63563f48339ba0aeece44c7192131fcc GIT binary patch literal 433 zcmeAS@N?(olHy`uVBq!ia0vp^DImqW(> zzvE}>O!}lGebJyV`R_a72jLM+A(v(pPh0!-<15BJ!AXrrm^xX7xK+eFbS6Y7Y;+Kh zNbcD$c|5wL(e(M1*B8Iu4_NQ+eO+hozUaLNUWdNOTOl58)l&ViG;GuLbBj!_Wf!x~ zf4O!4UCu97x1^U<&dpo0c#o8yY|);oz6_^5ZjYU>1^&O79d?`l%LjhA9T=A45cpN| zm0{k!_3BU6&rg{f33MDt-QE98_kU=zrNbPILodur3(gt;I9Bc^Exq_{?%m(#*Btx4 z{e6FEZJc1l)hAP%LS^F|BTC;)Z4FJ2D~u?8FtsCecih5=txu6LDojv+ vbP0l+XkK!bi6m literal 0 HcmV?d00001 diff --git a/ext/data/vectorcanvastest/basicdrawing/05_vc_drawrect_noop.png b/ext/data/vectorcanvastest/basicdrawing/05_vc_drawrect_noop.png new file mode 100644 index 0000000000000000000000000000000000000000..69cc6dce63563f48339ba0aeece44c7192131fcc GIT binary patch literal 433 zcmeAS@N?(olHy`uVBq!ia0vp^DImqW(> zzvE}>O!}lGebJyV`R_a72jLM+A(v(pPh0!-<15BJ!AXrrm^xX7xK+eFbS6Y7Y;+Kh zNbcD$c|5wL(e(M1*B8Iu4_NQ+eO+hozUaLNUWdNOTOl58)l&ViG;GuLbBj!_Wf!x~ zf4O!4UCu97x1^U<&dpo0c#o8yY|);oz6_^5ZjYU>1^&O79d?`l%LjhA9T=A45cpN| zm0{k!_3BU6&rg{f33MDt-QE98_kU=zrNbPILodur3(gt;I9Bc^Exq_{?%m(#*Btx4 z{e6FEZJc1l)hAP%LS^F|BTC;)Z4FJ2D~u?8FtsCecih5=txu6LDojv+ vbP0l+XkK!bi6m literal 0 HcmV?d00001 diff --git a/ext/data/vectorcanvastest/basicdrawing/06_pc_drawpaint_black.png b/ext/data/vectorcanvastest/basicdrawing/06_pc_drawpaint_black.png new file mode 100644 index 0000000000000000000000000000000000000000..9cbff6e164f3c0765951215d47f5aa77cd61672a GIT binary patch literal 109 zcmeAS@N?(olHy`uVBq!ia0vp^DImIxsd50E!_s``s eUjSs{26a58Qy3Um1bngtsqu96b6Mw<&;$S~8x#Ei literal 0 HcmV?d00001 diff --git a/ext/data/vectorcanvastest/basicdrawing/06_vc_drawpaint_black.png b/ext/data/vectorcanvastest/basicdrawing/06_vc_drawpaint_black.png new file mode 100644 index 0000000000000000000000000000000000000000..9cbff6e164f3c0765951215d47f5aa77cd61672a GIT binary patch literal 109 zcmeAS@N?(olHy`uVBq!ia0vp^DImIxsd50E!_s``s eUjSs{26a58Qy3Um1bngtsqu96b6Mw<&;$S~8x#Ei literal 0 HcmV?d00001 diff --git a/ext/data/vectorcanvastest/basicdrawing/07_pc_drawline_left_to_right.png b/ext/data/vectorcanvastest/basicdrawing/07_pc_drawline_left_to_right.png new file mode 100644 index 0000000000000000000000000000000000000000..bbdfc36cb56f835b073c47d32cfd4e198e8b7f86 GIT binary patch literal 126 zcmeAS@N?(olHy`uVBq!ia0vp^DIm*Z@M}(fSk2=ib}$!%I1s~l(u|R*UST^wNY2yM&t;ucLK6T{ Cza6ar literal 0 HcmV?d00001 diff --git a/ext/data/vectorcanvastest/basicdrawing/07_vc_drawline_left_to_right.png b/ext/data/vectorcanvastest/basicdrawing/07_vc_drawline_left_to_right.png new file mode 100644 index 0000000000000000000000000000000000000000..bbdfc36cb56f835b073c47d32cfd4e198e8b7f86 GIT binary patch literal 126 zcmeAS@N?(olHy`uVBq!ia0vp^DIm*Z@M}(fSk2=ib}$!%I1s~l(u|R*UST^wNY2yM&t;ucLK6T{ Cza6ar literal 0 HcmV?d00001 diff --git a/ext/data/vectorcanvastest/basicdrawing/08_pc_drawline_red.png b/ext/data/vectorcanvastest/basicdrawing/08_pc_drawline_red.png new file mode 100644 index 0000000000000000000000000000000000000000..9dc35f051e9e6179bf3d10985bf1ae523cd09117 GIT binary patch literal 271 zcmeAS@N?(olHy`uVBq!ia0vp^DImHa>_z&TYOenDFZPlK8;&pG;j#ppfe#^s*B8jS$ femn+-2G_ZdrZUg!;qmHa>_z&TYOenDFZPlK8;&pG;j#ppfe#^s*B8jS$ femn+-2G_ZdrZUg!;qmVHj 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 literal 0 HcmV?d00001 diff --git a/ext/data/vectorcanvastest/bitmaps/00_vc_opaque.png b/ext/data/vectorcanvastest/bitmaps/00_vc_opaque.png new file mode 100644 index 0000000000000000000000000000000000000000..812b1ca293a2f6db282d7be899d15a3c0f40c132 GIT binary patch literal 5140 zcmd6L`9BkmVHj zf|&&X5F%NbnK(z6t(UH4iCYvDb$_Y+;$ptrYMJe91B`2(=Vs^rcDVfkJqM8P zZvVR34cxwN&_nBVvXS4d{()>;karoaZcctxBn&erhB?b!u)-Oe$ zY&($S+nJ9Q8Qy10-3f{UuNE7dpsiCTOcMMK0GNe)$dp8@y9`P_Bv>yOyz}&Hw~lZX zTT8J(buIo-oc-bS-*N&W%x}6L(mDd`WW#hF&vvzv_fEo_JDI1@w>mLs`&w5M?*e1; zo}|@R-3)adn!t=E^;v#`^mLWU)^26OC+8(U$c; zn+x1ws_7BF8MphVAVV(>_g<+m2{@D*z8-?mQzAW1W(H4U^>v%f6|*vgs%q2H&krEj ztIhsNS*po+$7&`xm6iZL*UR?ZXQj?VmDgB%DiuIEOubqrs{)8?*NS-x+kHzj)}wp; zB#4z*r=)z3x-NP5pQRW&AasjKkDaDVsaZ<#22X$|sHs_jAZFbrZ8rLuJ@{Wnr9-YW z2C?B(pgPqgD=ue{4el&@91z;&0&Bwi$L>xEC6%n4#@0Rmi|${-$26vPzVcZBToH38 z^lx3mUQt1VGDVm~KVWwlrcu-u#&q)1kRsWv9;9`3waqo{1JLLTh5ZG>$8JpHBslY*kLe9zuc z0TKeB4;m8!e_bv1%JVQREnxtrDXX>cKfTGPkCbz~<(z!AJCi$$hncv{Ws-E0VA?WynB(L#_r1PrEp1M}qQF6$3oASU?v1{K@-FYzT=jSh2lI zxu;0=F(Aq+CwS{`u@5S`PI-yFs&q0DR9o%>ECx!Gdz&_2BBG!3nuZy|0u3S&g;j)g z%gah#@nNmeU8;?QsJYnY;~kvTqqW{io3)GSvbP-1Vmo0at~IYQPG_e~n+Mgp=O>cW z5!O5tHAP|?#x-kR(hf(5C%Yv|KIbj91BHrYg@4cQ{)8d8q|0CazwqB&;1r99Ic1Pf zmMHjqq+XbYQ_(V?%Ez-m+eb&0V=AmwZDu>g_3PrDo!ZO7$&AZl#fPb;zqk}-)tbdW zg?a_8mGsLu+|dAE+v|NVZ35k~CYnb(g; z`HZ&l4!y@-6_bJk}5nXJCsAsJGnbk)S&CcU5AA{jf0CZ$Rw}GU z+FX7TQyayVjcN}LB_1p4msi@T((1ZGZkf81=agV~#G%X*fo(zL!1c@DeZ$x}Q(5WH zL)V&d&3pFctj7AeC*So4C;EitW&eV+#}4tN6Biv>*s6BhJbzNjw{=ZpJh z1ql13`yK>#30W`ieSvYqHW7ncn}|2Y$^u~I8f;Dezg+qGz4tz98D{sh*@}{?79@ak zflD9G>r%n0ogMMYn~;MYL4K36ztgJQ-1@PI%}2+MHgPB0Q!WAn$9coVTbD*7lLbG) zq#^$0d9jGyu8-ik@rFs*)U8jGV**j?f=`wfbu=rWtHdAxL2b-d_FlW1hVPn|zR!`B zN$H}ge}F?g^VIjYE#c^vhV_vs>z?_p);Y4WalPyJx(S6q`Sn>rZJRhYBw-=Y0O0%t zq)qwE=yir&SXaABXDnU6A0#0oqjXJ;7@R+A`4FcRmfa@CEOXa~F@I~FG|X$fYQwgf zu3!0@h5Z0N&{JTm>-z5*9f~HsIn$Prq@~^vL}!eNAl{98t`kHUz&Y+1AT6$5B^16t(DNjuJEZ|mwxZ8RU?STLuwS)v4l#*u}sFl+5qF|#Q~!y=k+aT9K) zkp+Xn^`-roGuEP+4%^F!$G{LOD9k*qxWyp*GxlUv7hcTbXEjaLxx?E?xF|)zKjvI}Evp)4x!B_SYEn{I9rqLyg`$*9AIL{ATig)Ly zj|ud$2ZQLB`L}MAP@T^iUkkH3awprt4GSBo6P7hzy?yYypXae%?htfA>#cILjt19s zVR`c|=9U=%QI`m%;TdaLnZF_Ep^Vc285>g0^|}ExX_Kz(bLd((Q1QmiEkT8iy~z6g zTg+5gku}ncVl~Un>?YwI{#{nB*S!7pvp`o^N8N6u%_?_bvrE9L^-7_1M;%HC=z*ZM zwxJ1YZD3AQrfhPMU*qOW6U*+}S!X-w++I3xxL-`jJiK@n%a&^D)GoL=VGo**Z}l;oe5L0pQ6ZPd}YKQy^jP){gQdYE?Sn-G#Yv$B7~!nXt$ zz?M2On>nU3xAC#3cBesACVBiuvPqs_zjQwSG3x8}7bspr5W z&vXxNKl8wyt_^Z$i|oD!Jhy*txxFi_BC92h9Qt~4zwN&wn2+B%Mo!2Jq8PodB8idB z3%My7Sc=8vrYZIe<)Yuc4!BWLAuhnf%+kAuR7DTI-l1ZunsYiBpi6lUii+ zT~GR}0x%J^n~w4>8B7Ui8g04VT%3Hq>a&|jiIFdkgeFWU#CF8*o*WKcM%GiENU>1A zn5?$WtXgR1-gE!a;rFe|hI&)2_kO8brJzNp1YS$K$3OG&+Uz=^%FA5uM;^kNX1Z@$E!GnKu535sqB1$3oa+#5x+HN@?C-j9o5Ks=PZpM{cx1M8f8M~~bt z>_mvSJX{?7rd(MkRvZiR*c^h=lm4v+uDw~1!kC5Yc5~JgA5y5BnA3A}bRh`%3A(OJ zl+y%#L)|Dl43yM)!d0Q{^(OqbwtJ1QMLVJDwT^dL1q|jN3Mo80=6#sK_mi}|aN;Us zlVa7n?4w&|yH9|BX*6|KvBx}|=R5zUAooC1U-FTL4$@M3(JUK4$-?fzKzr*e>?7h( zY-M6)c@~`ET;sKUr;Mw|Zt@i#W-HDSHa>L`%O?9&u8a%Qe@-G{(8kA679>B<&o1D^ zx2i*K=;0w0naHwf?!ukmx$1ti0p6>Kv# zsak2We7y+|(s#uLUTHGX=Q2CpsX8@546k@^5j&sL&HbbLrw?d=)rA*K#>)qq28Xs= zM?sfIG9+Q<-FL*p&fV4$^kbyWL7?Ox6tjSUEDSmAC1PCETwP`-NnlH(##yFXBvs!? zyW6S<4(m@Uj)2o|kLn_iIe~EoZyekZK87b?hnZkFL!@d z_RTI15o6-z;ELGp5Y{nmUSqbPc#7{mgO>@( z+#hayUs5%Uz#+g`6}zUJa$PN^YTayg>^v|u&qUh0PxevaCE&*lfhPjUi+iS}e)v(k zp{LpfDZ7A!qUH%aK9@Eu3W+MUrdY~i(Fx6Bra5NjZ`Z3^=4=POO7uGI>~0M_=F;#L z6o_K}u3o_twbXSmbAAB8S)p#JR@j(V*X>^s`Bn{(BYa^;iU*~j)U*Cfuy{jFxqf_gUuZ+1LxNV6t(~_acmPxUeJRJ>p z*1x2wA9K`?eLf=bNvk^Qw*_yl&}XLbGZg$p{mz{v+j>d*x`gfr4P1h4s~C{Jij1qR zO`lh&dQV~r<{E$GmW!>WIRJ71(r2+rUv9Q<{JL8${GwMwVP$&*elwmb5xyh&{M#kP zI&&uNG}}umBldm;ibIMHppQze5@TGdHI%q$HK(4>GxDa}aPo)PWIdpT{Ak+KCdiHI z+Ab9-pQ4n+q%i(z>ht%Z8yc68y+h3;N&lft%b~8e9dJyGVuJaD@dfs86kAN!k+z%L zkBr-T+NRSyq}>5b@N_Us7<2#}#i}9TxcsbWl@pl8N#{?#5Ls8z$62w(s!y!mIRZ$! z@ZvefdEQlbB$P^Lg4$<@Sk16^M;|I*zOqkZ@P_cEUP#}0p*ADE*da0=$i6x-9-L?| z9}+$*J_`R_xuH*gWwGZQb521Dazi?>gPgcqfmw^PtIzCmuqM0YgFd!6dgMn2!=~#A zGpO?o7K1smSO2PvG}U*XQfM<1_4xinynbeYrpJaW11-sryRSCI2niTSm7GO6Du40* zGR_cI1N^EUnEji4MQUUc);T}#?$LA;20iYY9viDeMS(Yd|5-*=SQx=2<3F?z`24~m z!*>tyl$i)b04|;ZIox2_%j*WZvUJ!gZ}8kR=MqmsK9v4Dxi8DJ=Z*dNbC&P1rB~{2 z{==`ASx*-@7{qVzAnLw>)JC!sM`Eg&HI0PvIhfpScz(%U0uhPI{07m=+udH*!!-eC zd^|}59I{`7EI@Uea}LlsXNjmYcaeRV&*G`e6)u+XBo3A6nCab~Kcfljb@cYO&^(YF zWPF{V+lF1U`}0BL{uQGhl}j4yk0RPs@gK}`2~QBQ@}nJiRg7Ip+#+1|hGzI1$v>dk z3;gl{E8La|-4T{ab8y#|P*44oU#Ip-Jr1?^xW3Gvf3|#g-@vW)5>`sdwv0c~EC)^# u%#4VyKHd^1XB#4F8(4G(bpJn<10(I~I6&6A?f?EifYn`lGt8ZbN&g3G6)IH# literal 0 HcmV?d00001 diff --git a/ext/data/vectorcanvastest/bitmaps/01_pc_alpha.png b/ext/data/vectorcanvastest/bitmaps/01_pc_alpha.png new file mode 100644 index 0000000000000000000000000000000000000000..1d1342bbaa82c2d7123c6cf6a710477ff3d2bd56 GIT binary patch literal 2699 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 S_fMZpz}nKz;<>qB(*FQwI{jJz literal 0 HcmV?d00001 diff --git a/ext/data/vectorcanvastest/bitmaps/01_vc_alpha.png b/ext/data/vectorcanvastest/bitmaps/01_vc_alpha.png new file mode 100644 index 0000000000000000000000000000000000000000..1d1342bbaa82c2d7123c6cf6a710477ff3d2bd56 GIT binary patch literal 2699 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 S_fMZpz}nKz;<>qB(*FQwI{jJz literal 0 HcmV?d00001 diff --git a/ext/data/vectorcanvastest/bitmaps/bitmap_alpha.png b/ext/data/vectorcanvastest/bitmaps/bitmap_alpha.png new file mode 100644 index 0000000000000000000000000000000000000000..a19d09d06c74eb8aa4f54a01c7c8665eb4202551 GIT binary patch literal 422 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 fS~?my;0t!QNz!LGo+w%d3|IzFS3j3^P6Ap* z{>DezN;ut0a#!y7A3D?0%+f+$TUg&$KG|gU>xugs<_dAEh4t`5yUyg@M@L-<#vCeZ%&s zT)Q=crTz3tpjlmsN9A+5k8XIR@5{ZD$1$mKQbe}4;f}hwIsp>heV*SoZn&IsRDQuv zQ9Z?DnXPquOvGkQ-x4me`oJltrRv>cvg__{SoCzmwCf3ZJLbUrUleir zcgefAXHGJ?ZWL?N{&nenO5>%7j2)N$dy83|m(oAKIc>W94-eK2w|pNS^~{`=8OEGl v=3llWVzF1mW-qJuOVWSBum&l}?HBmvJ_YWMxmRTcj5-ESS3j3^P6EU09^tJ(<-j>ElDL_x)V{VjoYv zS2=yZ?euGxq_PRCR$Y9gwdz!AYKZ)l*q^sQI3nDG;XpzH0gGczZr6X>ax?Za^Xr6L z3+L4TJ|>mWdtc$%*2Dkye=&Y-b#Twu{@wPvU9Rn_Z>P>%<39E9uJ1fueA6eRhDuSy z_c}w@`vP-y0z|s`!mA!C+D2aAdnxE@l5O^`AML;5?pgob=4iX2@797hUiO>ireX#r c+^xTuLN+>{XVKnn0*p2WPgg&ebxsLQ0H^cHc>n+a literal 0 HcmV?d00001 diff --git a/ext/data/vectorcanvastest/circles/02_vc_circle_over_strike.png b/ext/data/vectorcanvastest/circles/02_vc_circle_over_strike.png new file mode 100644 index 0000000000000000000000000000000000000000..4d3d1b069eef90ed73934cb57df2eb94159767cc GIT binary patch literal 513 zcmeAS@N?(olHy`uVBq!ia0vp^DImn;4AzM6T7TZHuG9r1fIO9P~3`Tq0Glsk1lv+lgq;#J4ASFVzJ{8Y6* zMk0H1)$dO$!iBiKo}RXE+wi0ymQOp9=e79xgq$67V8$m!6o*~i`F;A0>i)^BUP*JM z);)Q>Wil&Q^1+8(^DjM7&;DF>&fsy-)h8eFzaAERutx6qlB-m&%Dxw z2=`$)7{9=kcZVk|*6-VOg)`gm?Bb)xzq;6N;G6lXuJFlTsW*|2d)KS1&U*IW+_1Ox znd)27qgfwP7G9d{+RbV`#6H~85%?|fLa2^v7B66fAqUcCR@9Gw7>*GCFm>%Uk> zE-(?3TK8plO7rJ6Kfl;yfBn={zmhR5m&@PJ>$1;kxro*8Q+6MfD?JoddgWPLF*Z{)^W}bE892;}ND#Rv~T`F%O*y z5ef)_FV=tA{$0EE@$u(1ioT6ojb*d9Ud!g&^e68|ROr@;SGIYEN}s*!FLvzK(sjAB zzj-AIuKX(+xc*Pxj|E8)HEX_If7*8XB&x}VYwoRnb?j*2CpR&5?kPte9TweZuVoW? zM7v%+vd;X@UO$yzJVBvXmsN;|KHX%LB{wDT=iLvE5XZgv!tlSUZq|Q=KXZ9ExGn$q z_G9SDzOuvH?KzLsAK?DlR4SO{xqf{_M}qBo{|IxtKS^ylSLW_%S(@?0skZc3hvo_77) z65hJpS=ax(Vt|Cv1())RJpWG`pUq6;Thp7_QuOZ)zZqj#HT%W&x!?XS?T@_9`D<0NEz z(BL)crQ-hs%+pU z-|JJ$9|o}|)%67yv;UkIE7<+y@`EOC@3p3Pw(h$k|G-m{J4I*0Mu%fe*ce~d{$sf9 z75R5Y7PF?{wL>A-*z30%t(eL)yFPZ=_Am28_NPr15`5~TrQ~I4`*gl=vTo3H|LIkl z8y8Hm`_miuWY}6k!*73-s^5J~X`B+_e0XWi zoNptxiPKFL)vT9b1AZ7tizkp1L_T5m{%?)!tyK`(PIt&ikC6`_#a zc<$PoGq2wszkU2wp8G%9IYDd7C8u4Qert|#e161r(Py8dk}tlupLw18TFU*J1-G~J z84LclJ{>$Y6d1yh2e%qIO7N>&L)*K6| zX20LBeX8+lYS-6;i@tkpNSn%?@9E}p^?&{%Dekk~x4gTiJ~-vHQoUPEcHQzs&&!Ft z@y)xC0!Q}R2dlNatv{zUE{R}1th)E*_msvf5g9wKoS($`ZS$*#bz!f+wDeD5&=zZL zo*QHutj!+wdTLIwf&0u2Q8P=-R*L^{PsJ0T%rXUqmy|g)e1U0(!PC{xWt~$(69A(! B6Kwzh literal 0 HcmV?d00001 diff --git a/ext/data/vectorcanvastest/circles/04_pc_mixed_stroke.png b/ext/data/vectorcanvastest/circles/04_pc_mixed_stroke.png new file mode 100644 index 0000000000000000000000000000000000000000..e4a044fa26be2076f4794043ac85c13028e79bf1 GIT binary patch literal 1180 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 L$Iy-L7t{X(-M@Ur literal 0 HcmV?d00001 diff --git a/ext/data/vectorcanvastest/clippingclean/00_vc_clipped.png b/ext/data/vectorcanvastest/clippingclean/00_vc_clipped.png new file mode 100644 index 0000000000000000000000000000000000000000..14ff949d624641c29bac4e41dc02782e61c73f61 GIT binary patch literal 1354 zcmd6n{XY|U0LQdrf^b(gmxU7#1=!3LZu~59wKEjGY=bXtRa~9d%8g1hAU)&F$&-?ZIyxu>5a!wrc+_DL=2>`GK>xDkG&d~2s zhpzV;6{-S&sty)?_%!L>JYNu_8wl@~UTYIKbrUDC&`yl%!|BY1eeSO&8yFb>v1-^5 zt4?9G&FjS828~+CZIYmZdv~znf?0{MbocKBTS)>V^wN_R=F;1M9Zx?e)fM6$P@Gyc zUSqlX5oPU5xV1eIW$>9BZjPTxk5H#3c~p7CNlSxMl~eEcyn=={lR72~AlEk{w1Gpx zjFm&dBh^`|;^C8o`ryXP!$);vxTM9gLN=a|nZGL>R0cicw$al`mz~a#L}c*kjwTy_ zW8CC{#qxC=@<=Iep)xSdntl_;c}`t%INYx#ON}J_1>CfOTUin0h0lH2ChhKjh_ zE&6%hT)Q2BkZD*0iRFnUl7qTeM~egWqKJH8C$+ElE>IK2=R{tln$T3z7gwJB>+74u za9s%NNmaH*U;W4*V$Nq>vsQ8UW3~^+i`X8lcq&VuL*Md85H#&j9f)mxIV z+9gMMy)O&1!AjD0foW%Ll~hLtC)@4@{IRPII*iTLwzwG)3(3z9stC-Uu!)feq4jdp zZwG3HAipW(6S5&>PW! zpdbr7^0R??u~|wiGAS8Z{!MlD7N~5=mJ?a?u@;5?o^X9yhC&oEgAvvC)E}P;f*Gye zpnRPwK4gp{D_)10P&Nw1J9C-*C4Khcv;_JA4)=#<@$^SKD(y(Z=M+cONIA;Yd+a6J zOH!7Ir&;yptwf+J(hLUFxgGqSusGSTYVv5h9F-`P<+{L(gjn(ZFv=lZ|69Xd!py$~ z9|l#7IZaR9HQN`C#B)PVeAs5m!;=ZgEeGs3DuYMA%?J@{LbH&^oa4l$`JS_im3O1% zx=GGH#6~;Lqivmcn7XcprijJ80p?%&S{M7snL_y2fhrtdFH(_=V1Z9_W;t6M;~o=8 z9zgUPfk*7aG}DV5JxLkYE-e=vaB*>Vy+>%(vK=t zUQpvybhN%>`B8K4;s&l8|A}(klVMPJPR-&R5UzEM*J6_4SCn>CP1FlhskyHcJ*Ei8 zPFy>cNS{`<bRT%r8lY0Gobx!iav%X3FB+d)}z^#6X%Au^?}6L$0IteD#9{rM-b7 zi#0D{Fd1hse4Q|k&x~h0v`p6H; z1oEM(+BJeq+BQGJ4nX+0;WZ+xA^E#2n>Qfo)b6c+oPqv-$21D?@7qtFkc`pRKN4Uu L$Iy-L7t{X(-M@Ur literal 0 HcmV?d00001 diff --git a/ext/data/vectorcanvastest/clippingclean/01_pc_unclipped.png b/ext/data/vectorcanvastest/clippingclean/01_pc_unclipped.png new file mode 100644 index 0000000000000000000000000000000000000000..436f9a5b7c3f81d885df05fce0dc2c6f50a0d470 GIT binary patch literal 4683 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 JqxbOH{{VGR0Z{+| literal 0 HcmV?d00001 diff --git a/ext/data/vectorcanvastest/clippingclean/01_vc_unclipped.png b/ext/data/vectorcanvastest/clippingclean/01_vc_unclipped.png new file mode 100644 index 0000000000000000000000000000000000000000..436f9a5b7c3f81d885df05fce0dc2c6f50a0d470 GIT binary patch literal 4683 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 JqxbOH{{VGR0Z{+| literal 0 HcmV?d00001 diff --git a/ext/data/vectorcanvastest/clippingcombined/00_pc_combined.png b/ext/data/vectorcanvastest/clippingcombined/00_pc_combined.png new file mode 100644 index 0000000000000000000000000000000000000000..14ff949d624641c29bac4e41dc02782e61c73f61 GIT binary patch literal 1354 zcmd6n{XY|U0LQdrf^b(gmxU7#1=!3LZu~59wKEjGY=bXtRa~9d%8g1hAU)&F$&-?ZIyxu>5a!wrc+_DL=2>`GK>xDkG&d~2s zhpzV;6{-S&sty)?_%!L>JYNu_8wl@~UTYIKbrUDC&`yl%!|BY1eeSO&8yFb>v1-^5 zt4?9G&FjS828~+CZIYmZdv~znf?0{MbocKBTS)>V^wN_R=F;1M9Zx?e)fM6$P@Gyc zUSqlX5oPU5xV1eIW$>9BZjPTxk5H#3c~p7CNlSxMl~eEcyn=={lR72~AlEk{w1Gpx zjFm&dBh^`|;^C8o`ryXP!$);vxTM9gLN=a|nZGL>R0cicw$al`mz~a#L}c*kjwTy_ zW8CC{#qxC=@<=Iep)xSdntl_;c}`t%INYx#ON}J_1>CfOTUin0h0lH2ChhKjh_ zE&6%hT)Q2BkZD*0iRFnUl7qTeM~egWqKJH8C$+ElE>IK2=R{tln$T3z7gwJB>+74u za9s%NNmaH*U;W4*V$Nq>vsQ8UW3~^+i`X8lcq&VuL*Md85H#&j9f)mxIV z+9gMMy)O&1!AjD0foW%Ll~hLtC)@4@{IRPII*iTLwzwG)3(3z9stC-Uu!)feq4jdp zZwG3HAipW(6S5&>PW! zpdbr7^0R??u~|wiGAS8Z{!MlD7N~5=mJ?a?u@;5?o^X9yhC&oEgAvvC)E}P;f*Gye zpnRPwK4gp{D_)10P&Nw1J9C-*C4Khcv;_JA4)=#<@$^SKD(y(Z=M+cONIA;Yd+a6J zOH!7Ir&;yptwf+J(hLUFxgGqSusGSTYVv5h9F-`P<+{L(gjn(ZFv=lZ|69Xd!py$~ z9|l#7IZaR9HQN`C#B)PVeAs5m!;=ZgEeGs3DuYMA%?J@{LbH&^oa4l$`JS_im3O1% zx=GGH#6~;Lqivmcn7XcprijJ80p?%&S{M7snL_y2fhrtdFH(_=V1Z9_W;t6M;~o=8 z9zgUPfk*7aG}DV5JxLkYE-e=vaB*>Vy+>%(vK=t zUQpvybhN%>`B8K4;s&l8|A}(klVMPJPR-&R5UzEM*J6_4SCn>CP1FlhskyHcJ*Ei8 zPFy>cNS{`<bRT%r8lY0Gobx!iav%X3FB+d)}z^#6X%Au^?}6L$0IteD#9{rM-b7 zi#0D{Fd1hse4Q|k&x~h0v`p6H; z1oEM(+BJeq+BQGJ4nX+0;WZ+xA^E#2n>Qfo)b6c+oPqv-$21D?@7qtFkc`pRKN4Uu L$Iy-L7t{X(-M@Ur literal 0 HcmV?d00001 diff --git a/ext/data/vectorcanvastest/clippingcombined/00_vc_combined.png b/ext/data/vectorcanvastest/clippingcombined/00_vc_combined.png new file mode 100644 index 0000000000000000000000000000000000000000..14ff949d624641c29bac4e41dc02782e61c73f61 GIT binary patch literal 1354 zcmd6n{XY|U0LQdrf^b(gmxU7#1=!3LZu~59wKEjGY=bXtRa~9d%8g1hAU)&F$&-?ZIyxu>5a!wrc+_DL=2>`GK>xDkG&d~2s zhpzV;6{-S&sty)?_%!L>JYNu_8wl@~UTYIKbrUDC&`yl%!|BY1eeSO&8yFb>v1-^5 zt4?9G&FjS828~+CZIYmZdv~znf?0{MbocKBTS)>V^wN_R=F;1M9Zx?e)fM6$P@Gyc zUSqlX5oPU5xV1eIW$>9BZjPTxk5H#3c~p7CNlSxMl~eEcyn=={lR72~AlEk{w1Gpx zjFm&dBh^`|;^C8o`ryXP!$);vxTM9gLN=a|nZGL>R0cicw$al`mz~a#L}c*kjwTy_ zW8CC{#qxC=@<=Iep)xSdntl_;c}`t%INYx#ON}J_1>CfOTUin0h0lH2ChhKjh_ zE&6%hT)Q2BkZD*0iRFnUl7qTeM~egWqKJH8C$+ElE>IK2=R{tln$T3z7gwJB>+74u za9s%NNmaH*U;W4*V$Nq>vsQ8UW3~^+i`X8lcq&VuL*Md85H#&j9f)mxIV z+9gMMy)O&1!AjD0foW%Ll~hLtC)@4@{IRPII*iTLwzwG)3(3z9stC-Uu!)feq4jdp zZwG3HAipW(6S5&>PW! zpdbr7^0R??u~|wiGAS8Z{!MlD7N~5=mJ?a?u@;5?o^X9yhC&oEgAvvC)E}P;f*Gye zpnRPwK4gp{D_)10P&Nw1J9C-*C4Khcv;_JA4)=#<@$^SKD(y(Z=M+cONIA;Yd+a6J zOH!7Ir&;yptwf+J(hLUFxgGqSusGSTYVv5h9F-`P<+{L(gjn(ZFv=lZ|69Xd!py$~ z9|l#7IZaR9HQN`C#B)PVeAs5m!;=ZgEeGs3DuYMA%?J@{LbH&^oa4l$`JS_im3O1% zx=GGH#6~;Lqivmcn7XcprijJ80p?%&S{M7snL_y2fhrtdFH(_=V1Z9_W;t6M;~o=8 z9zgUPfk*7aG}DV5JxLkYE-e=vaB*>Vy+>%(vK=t zUQpvybhN%>`B8K4;s&l8|A}(klVMPJPR-&R5UzEM*J6_4SCn>CP1FlhskyHcJ*Ei8 zPFy>cNS{`<bRT%r8lY0Gobx!iav%X3FB+d)}z^#6X%Au^?}6L$0IteD#9{rM-b7 zi#0D{Fd1hse4Q|k&x~h0v`p6H; z1oEM(+BJeq+BQGJ4nX+0;WZ+xA^E#2n>Qfo)b6c+oPqv-$21D?@7qtFkc`pRKN4Uu L$Iy-L7t{X(-M@Ur literal 0 HcmV?d00001 diff --git a/ext/data/vectorcanvastest/clippingintersect/00_pc_intersect.png b/ext/data/vectorcanvastest/clippingintersect/00_pc_intersect.png new file mode 100644 index 0000000000000000000000000000000000000000..1285dac0dbaebc19c10c02b23208dbde9aae576b GIT binary patch literal 1211 zcmeAS@N?(olHy`uVBq!ia0vp^DIm_0 z-uH_(F7v(i{!-Uk{rbw=(VH%Rx4mMY+$Q&-e$hr2lW7gdgf2udNqIXYbME;6?Bsr% z-;4~AhozSZer(H>vH$&byZ5bc^B>n7Tl(d>dcW}XAOof4zCPY;A8IzW^)5Z|`-^$F z(M+9d{{8cr7Whux@zCuJh<+hp~# zS~h&x^EOAWM@OtubiMAMgkM+h9)3ADQ>e%^fBNb#U)}W&+~N#edNaeQ_mIkqd-~U- zrEEohZeJL$zCF*b>c`(N?-GT!r5rn{;_2U?uHTow+5W(H4u(}c2k*TL-97z8vMFOF z+rJf-_VX+mcV-qny!7MT;^p>sF}9UIe}8_yeqOreiMx%(dy@I_0%@9lq-RqZgZE}TfTb1G^f885P(i^DUwaBJ)x|6|~5I$mO_R zH1BMmKcBkYTiJxIXI}9zn2ViYObK;ac4m&m)imwN?aDn{^-5>s z-RUR)$uDY{J!kFPsMC8A%O|#!8?7(hJ?FFO^Vfzt4TUm_2G{>eFgaYlxh>Ib^X17e z@10F~q*gLv-ri?N=d6`3-s-Ga$tO^>|Hh#Qik<#k(b+cg2VU;xfB8sx;!=&m z0e!S8b(688eTu}D>vm#I4Lyto-}dIrOg$~rXymyJz;^`rB5D&$GXIubWx1r}dz-(6Ou{7RQQ*&%b{$rw~Ie zUr0#k8;cj8fvNV)^FO(VeD01fR^nt2Cv{hrn8pUx0&5;`T#7H7(8A5 KT-G@yGywp23o-xz literal 0 HcmV?d00001 diff --git a/ext/data/vectorcanvastest/clippingintersect/00_vc_intersect.png b/ext/data/vectorcanvastest/clippingintersect/00_vc_intersect.png new file mode 100644 index 0000000000000000000000000000000000000000..1285dac0dbaebc19c10c02b23208dbde9aae576b GIT binary patch literal 1211 zcmeAS@N?(olHy`uVBq!ia0vp^DIm_0 z-uH_(F7v(i{!-Uk{rbw=(VH%Rx4mMY+$Q&-e$hr2lW7gdgf2udNqIXYbME;6?Bsr% z-;4~AhozSZer(H>vH$&byZ5bc^B>n7Tl(d>dcW}XAOof4zCPY;A8IzW^)5Z|`-^$F z(M+9d{{8cr7Whux@zCuJh<+hp~# zS~h&x^EOAWM@OtubiMAMgkM+h9)3ADQ>e%^fBNb#U)}W&+~N#edNaeQ_mIkqd-~U- zrEEohZeJL$zCF*b>c`(N?-GT!r5rn{;_2U?uHTow+5W(H4u(}c2k*TL-97z8vMFOF z+rJf-_VX+mcV-qny!7MT;^p>sF}9UIe}8_yeqOreiMx%(dy@I_0%@9lq-RqZgZE}TfTb1G^f885P(i^DUwaBJ)x|6|~5I$mO_R zH1BMmKcBkYTiJxIXI}9zn2ViYObK;ac4m&m)imwN?aDn{^-5>s z-RUR)$uDY{J!kFPsMC8A%O|#!8?7(hJ?FFO^Vfzt4TUm_2G{>eFgaYlxh>Ib^X17e z@10F~q*gLv-ri?N=d6`3-s-Ga$tO^>|Hh#Qik<#k(b+cg2VU;xfB8sx;!=&m z0e!S8b(688eTu}D>vm#I4Lyto-}dIrOg$~rXymyJz;^`rB5D&$GXIubWx1r}dz-(6Ou{7RQQ*&%b{$rw~Ie zUr0#k8;cj8fvNV)^FO(VeD01fR^nt2Cv{hrn8pUx0&5;`T#7H7(8A5 KT-G@yGywp23o-xz literal 0 HcmV?d00001 diff --git a/ext/data/vectorcanvastest/clippingpath/00_pc_path.png b/ext/data/vectorcanvastest/clippingpath/00_pc_path.png new file mode 100644 index 0000000000000000000000000000000000000000..8807649bb931cc9c4cf7e4484373523d0dd0afd1 GIT binary patch literal 1132 zcmeAS@N?(olHy`uVBq!ia0vp^DIm@sHNipB-f9FTR^Lg2Vfzy)DCpp$QT+!}hUhA@LkF$W_e`dkzi*7DoPHh&*vC+xN zsac`3&T_)xsXMmMyICr)Hrw;lSs$Zh#cy>f?UC2tulAn!;o(`Elw(?5Ot0o|TrKw@ ze$hq_sqO`+#DhQG@Av%t5*(Nysm=X%%?BUl$-VmZHs4a!C%D?a+q2=@nvDWgl_rlR zeGcC`VIj=0@O}TLms|DsY3DxgbePi?K5r+__PiQ?J%$-SZ=TUP7U`U|_FbUWxntMk zE7z@=sm?5TdWUj~X2Xkz#iz6Vx9G2#8yj1>;IBuT=>yhU^+|#M_fDU$pPTjdM0QxM z%)H2`{TsyQM)OI}+&DW?`%PYc84y{OD?H~dNPc|kSnuqoYt~9sn)}#3zO~}|rM+j* zee-*M@BG~OyoGY>;;TL`4PU=i-_9~~+oMH=f48m%DK1R$oMbhPA-yMCmWM^)zIjq) za|*XZM$@-fk1u|TdHyY}{Q34vY~h`jyl)SDWJv%+FZ-F9 zU97f2{Mq^K^0B(<1yg54tP?(W?szr(-QCu2e@B0;Q=bzl;_$2Xiu9y6CmZ?KtNniQ zeyMTy*QuA>!dp91?@aXRd&Ifx?(V>3CWUwJTjMldGW=!Kk#E>2a606Xvn1c?SuwKD znC6>4crxk8+_P(+hTT1*`(TB5e=@_QXAVpXazBsB9Sdu?xMj`BtBFZp+54t4bbK#= z{AMG^ghZ{M29po@a-V#|Qf54ZBVmtR!qja$jqdzk<9U%~!h#7k5>~o~%rZ~+I{I$f zU!OifrCIUV&tul|GQY2#GVHfLHrZsN&HR*|Cqm{N-cV>(W}H6Bz}XQtn7KnA`o{vt+6|OZnMf z-_GeWh|6dF|7!TI_e{mFgctk2p8bB@{_T^8aoYdS&plUN<7p!1m@GIXh<_}ZA$gw< T+o^a1OA!W7S3j3^P6@sHNipB-f9FTR^Lg2Vfzy)DCpp$QT+!}hUhA@LkF$W_e`dkzi*7DoPHh&*vC+xN zsac`3&T_)xsXMmMyICr)Hrw;lSs$Zh#cy>f?UC2tulAn!;o(`Elw(?5Ot0o|TrKw@ ze$hq_sqO`+#DhQG@Av%t5*(Nysm=X%%?BUl$-VmZHs4a!C%D?a+q2=@nvDWgl_rlR zeGcC`VIj=0@O}TLms|DsY3DxgbePi?K5r+__PiQ?J%$-SZ=TUP7U`U|_FbUWxntMk zE7z@=sm?5TdWUj~X2Xkz#iz6Vx9G2#8yj1>;IBuT=>yhU^+|#M_fDU$pPTjdM0QxM z%)H2`{TsyQM)OI}+&DW?`%PYc84y{OD?H~dNPc|kSnuqoYt~9sn)}#3zO~}|rM+j* zee-*M@BG~OyoGY>;;TL`4PU=i-_9~~+oMH=f48m%DK1R$oMbhPA-yMCmWM^)zIjq) za|*XZM$@-fk1u|TdHyY}{Q34vY~h`jyl)SDWJv%+FZ-F9 zU97f2{Mq^K^0B(<1yg54tP?(W?szr(-QCu2e@B0;Q=bzl;_$2Xiu9y6CmZ?KtNniQ zeyMTy*QuA>!dp91?@aXRd&Ifx?(V>3CWUwJTjMldGW=!Kk#E>2a606Xvn1c?SuwKD znC6>4crxk8+_P(+hTT1*`(TB5e=@_QXAVpXazBsB9Sdu?xMj`BtBFZp+54t4bbK#= z{AMG^ghZ{M29po@a-V#|Qf54ZBVmtR!qja$jqdzk<9U%~!h#7k5>~o~%rZ~+I{I$f zU!OifrCIUV&tul|GQY2#GVHfLHrZsN&HR*|Cqm{N-cV>(W}H6Bz}XQtn7KnA`o{vt+6|OZnMf z-_GeWh|6dF|7!TI_e{mFgctk2p8bB@{_T^8aoYdS&plUN<7p!1m@GIXh<_}ZA$gw< T+o^a1OA!W7S3j3^P6&ztiP z@lCg{WfmH&9U36(VX>I|2)9R2t)@D(XjlpISC%lU17KjgPNvbBmF_w>>8RQe$*NLzspRSTlRaU=A^#bR)S>ZWCNQ=(xsXwJ_>`3v z(MDU0g-on^Acb%~Md76e02`;P`W3Pfdt#tKAy)=rM3p<9KJ4RA6SIKfJVzfEnl-n>T3!ZOOcG z3pxY+F3rb#?47{&uaql>w`SwT4)NQ^!(tK2CL^|0DbkIFhCiyqA0^zFh)Z79=TVU6 z#g!QLAP(j6{uQxx_#)R^S1)O6*RII|9ns zChLV+JQM=45+~;KT7<{URBda$^9gmwgkN1ikTP{Rgg! zoN(*ta{Lto6D$oF;3SU5^@NwQ&bVp)rk5Zz)b8kJDuG8?{(%l%7<5u%E_eLX0l*qQ z0D2YU#z=~w)GIRDr{M|fq1t*TCg5VZ1ZI4I?6dk@b4W15_VzI6`EbZ$O96<=B!8)Q z;7|4+oy=l~95CqTbY9F-xN@svR;yS7OD$S-&YdEJPOpq~*tIsk?CblE${PGEql@0! zDVABL@?Tpm;YTb+T2QuRX)JXTN5V)SWS7Z|(LU=H@vsPX+D3ZnhC!hG4}tXAZBE%4 zad&=$g`2UZ8FB)--%nExe0i4pRrazHAmcmD7;4M1j&(x&WLfyR>EDHZk7mC zfPfMp=h)zm(VfFU0C!`K)jCts%|8y`*IUa_cL_e7Fw84%4d2;>G`GE4%HZ@#LPJYie2+rCed=SrU>$eIP^BmY=>I!%NT88$JS(*EarE99 O1Fp`VX!<$qFaH7_bhY0A literal 0 HcmV?d00001 diff --git a/ext/data/vectorcanvastest/clippingrect/00_vc_rect.png b/ext/data/vectorcanvastest/clippingrect/00_vc_rect.png new file mode 100644 index 0000000000000000000000000000000000000000..9c365e13dd519b3b5dacb148507ebd394300905a GIT binary patch literal 1459 zcmd6n{Xf$Q0LQ-@lBZTeW{l1$ccu(s<*7_U9(uSiPczSQ9yaA+*g7?g6M3nG&I?Vb z5!sq~n8?G)Lr(}Bu{-X})7W-v&ztiP z@lCg{WfmH&9U36(VX>I|2)9R2t)@D(XjlpISC%lU17KjgPNvbBmF_w>>8RQe$*NLzspRSTlRaU=A^#bR)S>ZWCNQ=(xsXwJ_>`3v z(MDU0g-on^Acb%~Md76e02`;P`W3Pfdt#tKAy)=rM3p<9KJ4RA6SIKfJVzfEnl-n>T3!ZOOcG z3pxY+F3rb#?47{&uaql>w`SwT4)NQ^!(tK2CL^|0DbkIFhCiyqA0^zFh)Z79=TVU6 z#g!QLAP(j6{uQxx_#)R^S1)O6*RII|9ns zChLV+JQM=45+~;KT7<{URBda$^9gmwgkN1ikTP{Rgg! zoN(*ta{Lto6D$oF;3SU5^@NwQ&bVp)rk5Zz)b8kJDuG8?{(%l%7<5u%E_eLX0l*qQ z0D2YU#z=~w)GIRDr{M|fq1t*TCg5VZ1ZI4I?6dk@b4W15_VzI6`EbZ$O96<=B!8)Q z;7|4+oy=l~95CqTbY9F-xN@svR;yS7OD$S-&YdEJPOpq~*tIsk?CblE${PGEql@0! zDVABL@?Tpm;YTb+T2QuRX)JXTN5V)SWS7Z|(LU=H@vsPX+D3ZnhC!hG4}tXAZBE%4 zad&=$g`2UZ8FB)--%nExe0i4pRrazHAmcmD7;4M1j&(x&WLfyR>EDHZk7mC zfPfMp=h)zm(VfFU0C!`K)jCts%|8y`*IUa_cL_e7Fw84%4d2;>G`GE4%HZ@#LPJYie2+rCed=SrU>$eIP^BmY=>I!%NT88$JS(*EarE99 O1Fp`VX!<$qFaH7_bhY0A literal 0 HcmV?d00001 diff --git a/ext/data/vectorcanvastest/diagonallines/00_pc_nw-se.png b/ext/data/vectorcanvastest/diagonallines/00_pc_nw-se.png new file mode 100644 index 0000000000000000000000000000000000000000..5736c3503adb1460574c6120f87f614501e7e01e GIT binary patch literal 536 zcmeAS@N?(olHy`uVBq!ia0vp^DImQI`53$GTly{P5(H1*i|NaGX9LY-n#p`|2Jlh)HM#rxKdDwFTAfmTYi#|sy)0* zuljH4_uti~-{j_~Ie1(9TeihL?TV5aZ$<6XdB0VaXv}@}^$UyrrPoJapR-wC z{=V(-p1A!U{-2@@OC=s}&2TY3oPF0OyW#q!hwuTH*nEl&E_soK5Y&ODf1l^Y#V z^5xdfyP40oR*GIbo&EpbhKbiFs%DF?50-u@7RP@5`>(k4_f{R%vil_#y_s6r?RHK3 z<*A9w!tednaa@1pbWT>>P4oXt_H5c1@JXwowI`fc$7Ii`1jPrToZYYDj&9I+8_M6k zD^7Kz#;Z`#?yk7jjT+BGrMsizo^I55vb4cfR9{fXtKwzC#P_PLuA8PyiOq~_ida&a znKbdSYPW0CbR)6ldyj0ic(t_usO5UG2$@f>nvQa=cZb0IRLg(|W#-mc}4I^|u%xXCry*@Ew&!<`KM~l}_jIjAK ztLv!n`hyWId()ByKTGwy9ylFwAXY#5Z`t9~5&yrwJ-@a`^h87>2Ju3^B4+W?7PrM3 Qz!+!nboFyt=akR{0F=rI%>V!Z literal 0 HcmV?d00001 diff --git a/ext/data/vectorcanvastest/diagonallines/00_vc_nw-se.png b/ext/data/vectorcanvastest/diagonallines/00_vc_nw-se.png new file mode 100644 index 0000000000000000000000000000000000000000..5736c3503adb1460574c6120f87f614501e7e01e GIT binary patch literal 536 zcmeAS@N?(olHy`uVBq!ia0vp^DImQI`53$GTly{P5(H1*i|NaGX9LY-n#p`|2Jlh)HM#rxKdDwFTAfmTYi#|sy)0* zuljH4_uti~-{j_~Ie1(9TeihL?TV5aZ$<6XdB0VaXv}@}^$UyrrPoJapR-wC z{=V(-p1A!U{-2@@OC=s}&2TY3oPF0OyW#q!hwuTH*nEl&E_soK5Y&ODf1l^Y#V z^5xdfyP40oR*GIbo&EpbhKbiFs%DF?50-u@7RP@5`>(k4_f{R%vil_#y_s6r?RHK3 z<*A9w!tednaa@1pbWT>>P4oXt_H5c1@JXwowI`fc$7Ii`1jPrToZYYDj&9I+8_M6k zD^7Kz#;Z`#?yk7jjT+BGrMsizo^I55vb4cfR9{fXtKwzC#P_PLuA8PyiOq~_ida&a znKbdSYPW0CbR)6ldyj0ic(t_usO5UG2$@f>nvQa=cZb0IRLg(|W#-mc}4I^|u%xXCry*@Ew&!<`KM~l}_jIjAK ztLv!n`hyWId()ByKTGwy9ylFwAXY#5Z`t9~5&yrwJ-@a`^h87>2Ju3^B4+W?7PrM3 Qz!+!nboFyt=akR{0F=rI%>V!Z literal 0 HcmV?d00001 diff --git a/ext/data/vectorcanvastest/diagonallines/01_pc_sw-ne.png b/ext/data/vectorcanvastest/diagonallines/01_pc_sw-ne.png new file mode 100644 index 0000000000000000000000000000000000000000..bfffd8a52a6c3b5e4a714a721238e979bdad5829 GIT binary patch literal 735 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 literal 0 HcmV?d00001 diff --git a/ext/data/vectorcanvastest/diagonallines/03_vc_se-nw.png b/ext/data/vectorcanvastest/diagonallines/03_vc_se-nw.png new file mode 100644 index 0000000000000000000000000000000000000000..362f6e727e7ed01bd68f8eb89f4c6d6cf986fa84 GIT binary patch literal 781 zcmV+o1M>WdP)Itg-D=Gwte5RqRS8a1&ox1Wyr!ZWMLVy zunbvPhAb>Y7M39k%aA*)ZA*C>WpeqvcUb#={{M;jwhfnuKgHYdQ9XFNSAY}(2Np!H7%9X~cPL(AQV1V~C3{6mA!BZs<`pc3q#0G4u>P{50gW!g`%y{K3XJ5QbfL)WnqQ(r?R|Kuo60XDPHkdX+6()uVAd? z?r^YIBo?KQ9_bZ^MVvTU^0H%1 zASJPT8L_63k{G=#Sd&RfEM97?DWxQ8FCEszQWBk)0&99HiNecxTX|Ng|MLF49R>ag zd4Jy40{_S~75JydUf>@b#SeZgsaF&~__6W|`)c=4Sy+ZFEJJ<)$0B#z7Y{yU00000 LNkvXXu0mjfG~iL z|LNZ^83#%~`LaOkZc2uP#0|seF+0*~s~YN+!W0O?4RiPOf1GR>_xOYRBb{mQd-Sgv s2xue2f`jjOK6szR%e;|k5Wpt>*sI(pMN74p0E2|V)78&qol`;+0BkU6ga7~l literal 0 HcmV?d00001 diff --git a/ext/data/vectorcanvastest/lineorientation/00_vc_horizontal.png b/ext/data/vectorcanvastest/lineorientation/00_vc_horizontal.png new file mode 100644 index 0000000000000000000000000000000000000000..46c9b0abe181330c3c2235e4e1a5aa94e628e609 GIT binary patch literal 319 zcmeAS@N?(olHy`uVBq!ia0vp^DIm#%_UgQND3 z|M6j~62d2L?@XSNc~yp+on5NyUwn!$G$_ z|EE3F?e_Tn2mFq3p~7`Xr5=C2%4-?f7=a28M$|O_ z->q5maLcj3r=BJEY-T?fZvI4yEr}Hws{II8`;lMB%e;|^&amm~{)~!8DJZT1xs3l> XoXp-wasKatA;jS6>gTe~DWM4fA!T{} literal 0 HcmV?d00001 diff --git a/ext/data/vectorcanvastest/lineorientation/01_vc_vertical.png b/ext/data/vectorcanvastest/lineorientation/01_vc_vertical.png new file mode 100644 index 0000000000000000000000000000000000000000..7f5f1f7f156499b915f3d28b7b3ee6972add07ef GIT binary patch literal 344 zcmeAS@N?(olHy`uVBq!ia0vp^DIm+>|%{k}1y- zE@XJ@k@TOpY^Pf@d!J7{dRt<%_%na~6X=SiD>~&nd of27stl)HLMvn4u2k>MT&7HQeNY&WMG0z-_!)78&qol`;+0NgfWLjV8( literal 0 HcmV?d00001 diff --git a/ext/data/vectorcanvastest/lineorientation/02_pc_horizontal_180.png b/ext/data/vectorcanvastest/lineorientation/02_pc_horizontal_180.png new file mode 100644 index 0000000000000000000000000000000000000000..5966df640eb1aee4692487b3bdcc54429430cad5 GIT binary patch literal 333 zcmeAS@N?(olHy`uVBq!ia0vp^DImhbTZ(wjCjZA67l8-K9< zFVBC--EKNBJ@4Kzi(L;YzP8C6c39)E=0L-J;|J>>Nw6icCb6>pna5Z!dfY%jTR_|3 t!FQe?t1a3ZBj^JUz6xjEvk}wn;$Jz9wtBC8vJV(c44$rjF6*2UngDadh%o>F literal 0 HcmV?d00001 diff --git a/ext/data/vectorcanvastest/lineorientation/02_vc_horizontal_180.png b/ext/data/vectorcanvastest/lineorientation/02_vc_horizontal_180.png new file mode 100644 index 0000000000000000000000000000000000000000..e43a844985d645e0e7789365e0afd2e4eccb60b7 GIT binary patch literal 348 zcmeAS@N?(olHy`uVBq!ia0vp^DIm1tkic~asA|+xf^P~wDcq@gein29NfR@L4OihN7K!w_%}b6-)y?S z=SI_WWsnKn|9V;LIg%whMCk+HbeFE3|9W}d&y-lbrG`AHo|F?iW4!h29;H%Xs4;lD L`njxgN@xNAOtgis literal 0 HcmV?d00001 diff --git a/ext/data/vectorcanvastest/lineorientation/03_pc_vertical_180.png b/ext/data/vectorcanvastest/lineorientation/03_pc_vertical_180.png new file mode 100644 index 0000000000000000000000000000000000000000..9ac482558f8782691d9c2de33dff73b3edf917b5 GIT binary patch literal 332 zcmeAS@N?(olHy`uVBq!ia0vp^DImj)Y=cGCRE z-}v3L`%UMC=h+>L(DOLWYuVNq0fdbEjUK#zB*B)%n#9WX=TyV~?qrD$(GF3G58BNC v^!gGN!sr7FzO81ucOw$)CXlbP0l+XkKCgE}4 literal 0 HcmV?d00001 diff --git a/ext/data/vectorcanvastest/lineorientation/03_vc_vertical_180.png b/ext/data/vectorcanvastest/lineorientation/03_vc_vertical_180.png new file mode 100644 index 0000000000000000000000000000000000000000..d9e033abf59ac635db2a42eea9b77c8f46f2fc64 GIT binary patch literal 351 zcmeAS@N?(olHy`uVBq!ia0vp^DIm-VNF8rLI1Kphi^5- z@43FVdQ&MBb@0LhDQi2wiq literal 0 HcmV?d00001 diff --git a/ext/data/vectorcanvastest/matrix/00_pc_translate1.png b/ext/data/vectorcanvastest/matrix/00_pc_translate1.png new file mode 100644 index 0000000000000000000000000000000000000000..fe27cb31581e35957899753a790126e5421c40dc GIT binary patch literal 5139 zcmc(D=RX?^)V5h$6}O^R&7z3WQZshcuA&G^jTo^iv1->t+M*~aYSbpeUBn2&T@U@-#eJ^S0;{(sR^{l%LY zDryP}jwvHO?dPFi$hpK6KEtff9c=||*7`>nP}*~2`v}Y&(=Dbh?#`I#@I}I|+Bw4| zP2-~&kKVW86-@8lLTsN~+pP7sMn6|>Vd`VIS{hcc5aiW^g?P9f_a^mmNSLWEs|2lF z1>88#cix!uxtn$t1Rq9U)ZQeRe(ictbFf{0V1nT^qTqLApqol`rlm;q@7WwX@w(sH zn5KTN6GV92|1-<}AME7Rj#EJR{wnh3_d~fqK}X(De_MDfrWQY-jy>;?h?s|s$KP9d{sRs5Q1BBpJH5M3 zr4)l#6b}AZr9kI4iC8oKQ#FO8HlAnG;s@^>UoV&<9209Cpdq$}*ir0g@_68|!p1wQ zjht;;<51_ae&OQ>bCKokj78f*@u+QJhc#QsMREC44af@Bp#y)iFI|xuOo5*l*z1(b zb;GL3L07M?{Nr;5n2ZO1uDvSj_)w8gicQ(f$YvjY))C=TZ?GSiAcQ)j^ z=kfWMkatIZj(?{7_}{oq4+r^ZxPz7b_TwIXqRlPjM18EZ1*Tnb8@{HLs&4g4dnV+@ zlYNMipRe-0M@qh}KteR7s4zb9Zqv?DZ;k2IaghOL!O zncG#o7{3PFIJV}S6TmRgJ@7Q<3wwn6#(Qe>5j)M$F%2OWofc0$vN`;P`BmOPMo!?s@~dDtrxyyR!#L4Iu^+- z0%O{U*|5*cW-V+|4d5 zr_yu>iv7%ept`3TbKaE{Dkx4@3U{8K!sNY5>^uI(ABRQDuxeW@PLXlp5G!KVtZ`sV z!9_d}Oi#tHfiQsS6XQT8=@IQu7vyf%w7?pwG6NdGWy>8mn&CXuv_>GVt+}C#Vq;Jk zP3HEk?~t>zderJ@DFTD_QJx+zn5Vg;CQ)W@=Z`aS<$%d4tCjnSGI}{2?FjCs2v#iv zQkv;2x&gB5Os~R0!J|eiMorBfPVFLtlwJ{N>Wr18nO1=*E`WB)tDCRj%ZD8C&xERf zt)A^fpQH|W+KT)2{2H98#zjP3C9V?)Hy@(JDKf?E2ff`a(P_Sb73biRS4v|dNW7DfWV#UM6zeAlM{tlwG6ij=`Zf7bQYrOGDd8jrI zbeY+5Bo=Xg5q&2Z)ZhKtXr z*8OS=jfA&UE|zFtieBvtUi*b%ny7rJy%iNXif{MB z!=V~t{`Pe`MV>Km3+pMo($nA~cT;8A`iy`^`sxJCmW}2d<9U96SHRKv>i!sA`Zrdi zhRxBkaR4MICw$lh7qzEuK0BH*guBd*wm@TGVy}Dls<+4kJFLkh5`W=zGR6lOupc0L zh*($kg=hfZn@wv|-DSK_?Iw=s4~

aof79mmk$_vZH4g>+d0PyykaAM#%{dk;$0; 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 V68muaZ`em+q;IBIqw_NE{{hC_KG^^O literal 0 HcmV?d00001 diff --git a/ext/data/vectorcanvastest/matrix/00_vc_translate1.png b/ext/data/vectorcanvastest/matrix/00_vc_translate1.png new file mode 100644 index 0000000000000000000000000000000000000000..fe27cb31581e35957899753a790126e5421c40dc GIT binary patch literal 5139 zcmc(D=RX?^)V5h$6}O^R&7z3WQZshcuA&G^jTo^iv1->t+M*~aYSbpeUBn2&T@U@-#eJ^S0;{(sR^{l%LY zDryP}jwvHO?dPFi$hpK6KEtff9c=||*7`>nP}*~2`v}Y&(=Dbh?#`I#@I}I|+Bw4| zP2-~&kKVW86-@8lLTsN~+pP7sMn6|>Vd`VIS{hcc5aiW^g?P9f_a^mmNSLWEs|2lF z1>88#cix!uxtn$t1Rq9U)ZQeRe(ictbFf{0V1nT^qTqLApqol`rlm;q@7WwX@w(sH zn5KTN6GV92|1-<}AME7Rj#EJR{wnh3_d~fqK}X(De_MDfrWQY-jy>;?h?s|s$KP9d{sRs5Q1BBpJH5M3 zr4)l#6b}AZr9kI4iC8oKQ#FO8HlAnG;s@^>UoV&<9209Cpdq$}*ir0g@_68|!p1wQ zjht;;<51_ae&OQ>bCKokj78f*@u+QJhc#QsMREC44af@Bp#y)iFI|xuOo5*l*z1(b zb;GL3L07M?{Nr;5n2ZO1uDvSj_)w8gicQ(f$YvjY))C=TZ?GSiAcQ)j^ z=kfWMkatIZj(?{7_}{oq4+r^ZxPz7b_TwIXqRlPjM18EZ1*Tnb8@{HLs&4g4dnV+@ zlYNMipRe-0M@qh}KteR7s4zb9Zqv?DZ;k2IaghOL!O zncG#o7{3PFIJV}S6TmRgJ@7Q<3wwn6#(Qe>5j)M$F%2OWofc0$vN`;P`BmOPMo!?s@~dDtrxyyR!#L4Iu^+- z0%O{U*|5*cW-V+|4d5 zr_yu>iv7%ept`3TbKaE{Dkx4@3U{8K!sNY5>^uI(ABRQDuxeW@PLXlp5G!KVtZ`sV z!9_d}Oi#tHfiQsS6XQT8=@IQu7vyf%w7?pwG6NdGWy>8mn&CXuv_>GVt+}C#Vq;Jk zP3HEk?~t>zderJ@DFTD_QJx+zn5Vg;CQ)W@=Z`aS<$%d4tCjnSGI}{2?FjCs2v#iv zQkv;2x&gB5Os~R0!J|eiMorBfPVFLtlwJ{N>Wr18nO1=*E`WB)tDCRj%ZD8C&xERf zt)A^fpQH|W+KT)2{2H98#zjP3C9V?)Hy@(JDKf?E2ff`a(P_Sb73biRS4v|dNW7DfWV#UM6zeAlM{tlwG6ij=`Zf7bQYrOGDd8jrI zbeY+5Bo=Xg5q&2Z)ZhKtXr z*8OS=jfA&UE|zFtieBvtUi*b%ny7rJy%iNXif{MB z!=V~t{`Pe`MV>Km3+pMo($nA~cT;8A`iy`^`sxJCmW}2d<9U96SHRKv>i!sA`Zrdi zhRxBkaR4MICw$lh7qzEuK0BH*guBd*wm@TGVy}Dls<+4kJFLkh5`W=zGR6lOupc0L zh*($kg=hfZn@wv|-DSK_?Iw=s4~

aof79mmk$_vZH4g>+d0PyykaAM#%{dk;$0; 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 V68muaZ`em+q;IBIqw_NE{{hC_KG^^O literal 0 HcmV?d00001 diff --git a/ext/data/vectorcanvastest/matrix/01_pc_translate2.png b/ext/data/vectorcanvastest/matrix/01_pc_translate2.png new file mode 100644 index 0000000000000000000000000000000000000000..406bf5776e14b42b13ba1ea0de07285d6c2f3887 GIT binary patch literal 4645 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 T_V@qZKLA}#um)VsA?p7CYj-Y< literal 0 HcmV?d00001 diff --git a/ext/data/vectorcanvastest/matrix/01_vc_translate2.png b/ext/data/vectorcanvastest/matrix/01_vc_translate2.png new file mode 100644 index 0000000000000000000000000000000000000000..406bf5776e14b42b13ba1ea0de07285d6c2f3887 GIT binary patch literal 4645 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 T_V@qZKLA}#um)VsA?p7CYj-Y< literal 0 HcmV?d00001 diff --git a/ext/data/vectorcanvastest/matrix/02_pc_scale.png b/ext/data/vectorcanvastest/matrix/02_pc_scale.png new file mode 100644 index 0000000000000000000000000000000000000000..9e94fb0c6f7e79cd03e1a3a807607cbebc164a0f GIT binary patch literal 6566 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ͫip)&EdW{GZTyT`fvlb9J>+Hk@@NP3Y7;3s$` z7EW~NM9{5WBh#!$16h{P#cf}W1I2K(VU)GS(_f)1q>J^R;BrJ?GlingkIEP6qUTAF{D>I^E6_?AQVu?r!ND-g+Q+5pnYF- zmaAxaDJySIRwlq$33o(FnO;TB>xMCZx!gQX^FLm{XDNNbcLz-~e0jh?in4$0qf*pw z1l?kJslm6q8Ncc2;kq;(0%6ZGvPxm|?>D`*t^R@H3sfl z-f0k}B)IpCc?a~K7b5su2}>=&;^yDxvB4zo@mJkm&usqHt9sl0{qJJr;q_I8db{_% zi#0sf=_>I>(*=`)kk44T(d?=G=X~LPn@KNh8X|gQ(tG9QSn$r17;P@nEONcQYoBd$ zAprEu6Is!otj0O|1F(4R70Lagz-YM|Iw{&;?=N5;r zFNV1mu?JCz5fy;?$Dah9S>c`fZ=`o_rDq2DSTlEDu0|sc(>-%6En zKa4fP7e8T`&%Qt%X<`63Ph*=rCG0bA;k?)d%aAdcC~Ao%yUM|u4Z2HJqYY40d*Rmh zQV$fVOJamkLCuI!CX{4#qu9UOAorL3s)MHU!z!rhwHrmD4SZa-GH5WKJhpT>pXL5# zt9dF7mxpJINL92dVq;&vV3KgJ#M=7H^wezabNlgU_WX-99q50iFp0cHgOaK@@Jltf zjoApKHeaUY(RnmSI=pUY3NK9Alsd-__yCw^L3TIbvLMqm9C}>PIH@Urt;2{xJ97y@ z-4FNDUFV*78+&R23RSvZ`u5yZcyx}A=0ydTktSYPKE~-=ML<+ysXf(J&MkLmXh^Fp zMlt~TmUif%lqrZn(?YS)(~rNop3qGJY~+J_#3t^ZrNAW+Hjswu$Lns5J2?PtBs14* zQr14=7h>Vh;VLlrThLhJA<%i}>ucF&nrd^PIiY6N+&~sY>GpxZSqY)vnoEPluLN!T zYqvbWo-&I{I6<9e*t>?ipNIQ+_Nvx4N#YjuxSBnBLK@2imUtg@r6T6Fr4yKrF4OX9CaN_|w$%MERb&Ct1Pi`Z`<*S{wCL#?D)Yi;68$-?ciA zmgr#>Q8$Ktl!`Klbj%hXtph6y5gE|rD19g{7a4Ufard3$l%@7SClE6`)M|0~ap}hE zwa`?Kw#f)Gi{?L*H-@BLQ-%6?M+Rt#`ALuDH*YuCPL zu6T@`wlXjmS-DZ>)ZWULg|&WJ^mye{y(Gm2ZJM>vv$VhIA&d5&#f)V13a=rqu7B*@ zWw3@Tvv=P;M`D$}DX9PC13{_u*SY!&jx-3)HSFMgaAZ#^NMw6?LP^J(%fti2SIU=9 z`^Dzc?g4jCOFivTzr~ld|CEj0ZO8h38Wh0*Vmb}#JV{6QNj@Z_e@b|J9PK&AM5{V_ ze~*^U|18iR%b=f>VBu|kM&Ir#-412>M%Bn#cWxP9G*18?Du!F?U|_{M$~Ve2F&!$- z$OqGhu_zZjQ$8pqY8;|=RH&}co)(d?WUbSZ(+ z#8I5jfo4qE*{89W^^xwPp9M_H+nNY_Z*Xgf+?f6opc~@UthfmgcKBacyH<$&`eLhp z&t<~h)!}|18`HnTMbW8Y_~;3%U5tdV0gpEYxb>s#c4{VENO~uOO=1=D{+ZjS zjfdS|6g7qTYyv>nQ=OM-lY0h6(5bf7UbS>3vlPSYKBr%yK2NSXeWq2a3ws=V_Aa+i z7*F=%c%}?a9v*tu+%#YF+SCsXRV`XC&vjtAwdHkB4{GE^DZWXp8JIYey(uoLZ_l`+ z5_^XBAu;(1clRrL$R!XRYxQ<)Ds=S!gziz@e^k|ruE#B5|E-a4t*%q~8n7m=w=uWU zyw^h{Q^;hKN#VkRp9f&$BoJ-MmM(+7>vg^f-dOh#0Vc3YcOa#iv3uJq6Dc2Fd6dsD zwcHg&$LP=K?}XIvc0-7@#BwK}8?Cr2yviL$Zjbqd1_x@!(57(fR2N!#-2E}xCTQ%% zr1zC@9*Vcre}lq*Ut;iuBU9D->Oc5b^(jIHsdX6&E>`c|8ofE6!(064yd!^M<>(XD z*ujylpx%dMPP@Lh~pcJWaXfYF8T}~q zPlt`Wtf11-0x)<DCvp9i)}E~FhH7&+fFJFJ>OMkQkQ=Ch@33Z#yn}s)0|YDILZz4`re;U z>c9N^!f0hP-2|!)0c_m+&L|cH>%r~+?Bh|KE+;MUddix4G^5V3K7--GCv}{ZM=aAA z)u7RQ$jVYwS80;Z<-q2+M*8-49FsUEAOGcG+E7)T3{16L7OV=R3-_pYge4@NP&&DI zloY4d$MHl2Y6>SZJ>k;40F0FXGC$|x5H~6*<&tY-wqCs3FrlWAl%}?!K22pZ!e9R} z&WG}gcaL{+?0OJtsi2#(!Qqy~tPsbXfSJ&eb1xnN?GD7-Ou1e>&==i`G>Q+T)vtPw zAnf=7!l3>C6vhXmpt4XN!`c!On3IaBXkQ^1W~E9?x-|KV@&R;RGz}@3Uc;~#Af1+- z@x6V*!eYabkC1Fb|7iS0xARt`64Rt#m>$dh-GRBa95!kqtH?3H=BnI;*8f&*llppC ze_m#CS85MjgI}0Zo|$A!Kc-R5F{C(@u7Y1P3asEj5uc&1$vG8({nMH6R4b-i4KHXQ zem-%eZu(D7I77lwRy^a!Xm;|N7jw>f8=o%>3N?F{2`u`-y;m@tCflZp zWomb}bTh>NRP? zR!mK3e3sXfN*qD>+&+%lS1%kUQjkT>7HdnhM1%Uxh^z+B9z^zWmV}IcJNYLNbu-e%K zX;hM9F(9Kb!X*CL>>|{5o`KEz+uKpwcg}fhp;PdgkOa;hBVfpcg99x`-%gDCg)Fru{YI z-JI+mPk^d{Q5^20J0?|Pa1+;Kh@H}k4aMMImBJjH#)_2qpeuKWX(#F zhU<@|zVcW3h*a%f>3Bu*EQnyVFq}54j40Jk5jIBSVu|Lg?iK>C=_E!iiRe4n7&v5ETq=dBRM{FwIoT*3 zeml7)mo0WcJbAg|oT&e{l#kca(9HTZ&nm;!iA>`qRu+zoVFPS+&5DOg#Sob|plEt@ zh>-q+Dv*i0w;koB>$V!9$AD|f!U8YvjH|=aVkaB{##=Z7 zQQMWlUncmzx+Xd+vlG6&;%zqgSUj@-Yd}iGP7fubVOxMratF5|gufmJ5Q~_Hzdyp+ znIU;~SVdOf;7qh!VgK0ISI+I;T$6g2J0cCee?|N2CBtk+o9vae-Q7%yvuu(mPsTF$ z;PR@yWKTW|9t%dz##+ki7iV%n3oMlB?I9&ci1yjYbYou%LdRkS)9xU7dnTR-wa8c{ z{Yg*sLZh|vYGiMia7eiOnV~-+`LT&7qYy}^c$@Cssi_5A0AlzYL z2t7&NtY>iDsh z6Dr9DzDIs%$zj)BSCX+h^p#L7Y03)lkO<|Tc^j2?z|VVSa#y3l12>yuCEwe@ESgya zqmi;ZPhtH#m~;d*BBw~xHMccD$G*vDZ|$4QUi8zbu_l7a7P|tS%qQ4jQZJUmMoJ>t z7kk%gtQy2ngATYo8$X%N(Sd&=(Z{#6;)5k($(iN-9rG6S z`lx8<`Hu;iH@7pNbul=$>d7}WM5?N66{;DXW3Yx?!oQ!}p3478c!-hAvpeXh-OH+J zQ3$Z>r^~GFK5qm$&pBPpb3hTSs4?iggL#!_olaFrH2+1VzqR(4HSkP=SXl! zePD@^G4L>yM~8bIc5#ryYO%9-2`=XH*Lu!x3FDx_y#=0&*S@OJzmoG+=~%;t_y7GX zTWzZ7%Q~c?dH7lVuzF*jVQFYYPP?ZlMre>ct+xeQQRSfbhm_}Sl80$G9h-v95v(g* zTkyYh;8EO~G14+Z;mF4Z?qw7@dKC<)V;mU z_;Y{hCVp4z7Lw?#xqVO4z3AInbJtg$?m$klv*U=4JWA-z|MG-B%Q1!}P4!?9oB#XO zm83J-y(9V&{do0wnT;0Kz0y3cBV3}?|09gDi{HSlF?kc@mJ6SKTXRIW|9G06M^UZJ zks0px45^<(f-O$z1QU`M5uuh-Pq~cZv?BGGv{U;)=+8lw);pJr1RH6Ur5U#p$_LU{ z2GPWf3w46Fz5I?IxguFQAG)wpGoK|O;@meDW{O9`<}YXE{UwK8r!cw8L)=^Q5#1^H z!n|KVIVV^Zif%}E{MZ|^&n_$co!ga8%rA>Ce%vDK9Vt9P8LT3RiZu!6L}aTy{Q*ww z(gKFLkp-LcdIs$ePWsy7`mJt_@jQNU%?(!@IqliMJU=(Cfw?p~SSMEqk6v6>J@I=P zENOd^9KN;(*}t>*dBa<;2(GeeG7@Qz_y4gJ4rlbLn-}MkNv6&0NCn$!g*YyHOAo6I zP~$Ntp>L}L@ne%*)3M=S2~nHwrrMfKMZv~p=^U@D;6$O&JKNUo5%$m=;^hQUlb*N$ z5#z}m7MM``@cz@BMu(+Q869RGcUk_}>%?KG2*mfklpqw#%`a(b_Q#az)ulju>!o#r zqzJddeZn_v)cVS(95G!Oc?=QJniyC7SKi{m<>sp*`&A>at&u=0^tjyeUFH!ws`J7G z3>)7<^rq>l6Kvm~-xF8CyJNVcVFtrO`(VTvc{#f%_o zl=n%jAV*t>JP>-5N=dNqWj7|o?BH?KP6*qRHRtzf5z36Al>obm(TIzMq{?@`cKv<- z_+Wxzm)dzYbOaPJUv=JGJ~(!8yodZfay5o+E=3aWdw`Ih_@*M*n^tQNF|@$p`+0u| zh4a*u>jy|Qe-L(wl3%qHzi`f<-x?s1QjpPW*r$ow?*Yf?g3eoZ1OBfh*qnSz37`Ph z%paaMbK>?Sj)mWQA(iqziJ$wd&XQ=ct_}lj1fq1T%#(SIqN7!`4y@g*RaJu8C9lKY z8rJ_AeD$AzJX={xJ_4^|@;Y7A$9=Vj2jG;4k{QnBy+G(VO)Y~Rl(1|1Sx}?=uI6>v zWGMHKc2tlY-I=p3@*Y?Da#aE5?V1LI0&eIqT3UJw&Ec>1h(5m9XGIdik-&@!p{Icc`qQpE3xSQVI5y1Sp$ ztKM}fwAq%Z3MZ3Ctk|WUfngHvMZp|*T=uLnYoG~abm7)KpyxWZ-G9{TP3iiMaJotl zlZ#AxtEknK?+S^Q?0TJ@PoWKse^5{&Y-e)wks^X;*jOMLUm8z<`lByhfOUgl;PKs&GDbZfSA0%E+t#xabfJ;xVGU z%;C69rS^a!u)0^V=zo#MEtrgI`B?%$L&(g z()`2f;=ZZjx_9OXeXN;Hs3;P9(+87cWbGFpROCR?g&|usz&7mXvxA@N-OR%-zMWZ_ z;G%lSw#xMv^g&=V+6o;nNL$&V`ScIBxkQ3-u_rBTc#<~zJ5e_7DB$qkh@J)sxQ`#cCpG?rF%R~R3v)8tZ)q$CuhQiTj+qa zDV{V8)!yA4gg~)O@Nkcz&-Ynh2Pst~S^$6o@SM^O9T-`=*R$eH1^7*1E ztZ$cuGx^gC426p3Xj;hzE5rlcqiBKXT%g*LC@r9o~P z`2NrW;&L$G=6@?(JX`)?1#ud(BgIz>i4$>r8E8z=Lyi~g&MVn@m|lL-KTc4ygsb-J zhmlJq;U0OsyaIkK#dzm{YZy7IEG#jqeBS;|xsCf={x|Ffe}-@|FSMLR>E*F9dCdMy zQO}3pcnZDv`){UO;ypaGxQ_fUXmhIg-MACY!6{C_{)oXAM&0{igAcHMf>BziBMwHK z?uKj5cd@C^;K;a>JWHo~+X-l#RT?>*JXYLT4evB{J;BsaHLmxeokU= zCmFDeY{ibrzR_gQ5)ye^gIGYeFofIErVeS z_EVP9`@y5w-L^L7X9ed0H$iLVBhT6y+O}W)I0T&RF2p!ze`b21{rW6nWb)OmXw_=~ z!2J8aUjQTQx-UE#z?lM;Jo~316PC#GcG2>y`g!;S-d0o>A1cXTH;?s(HHU=;$S}Nf3(t>ygIC`99k6mhVl%!&`tb3isMxJ^VzSA%L=P_jv%s7 zczG&!GXFJO8xI9o-S1r;@2Q(Hu<8Rfe5~}$1#!{Qukc?~HXed6k4?>pf#iOwo$mtd zb`8eQM{Wsf(fKijWNC-H0%Nx6|BmthlaTD2$j9s2&58m4Af;PwLEw{TYBn(C9d>xx zL^yx~s#g0$Vse|&Ke+Y-TVghgmS0)BLpqKttGn3#tpD!Q<>49v+AACu;#i;*K)QIb z9s$Y>Oz$uf&@nfzGX3e|dMy0up^FH%VSMRteA5kefP4Qc$n8q0JOSf0*!@|y1$Y5H zH_!DliG~jJR3LT(54rf{<)9&ncIY$HhK8W|Xk}4qRGDz28mbJud2eAj0Xuar|8|9ODlA(xVL*tf{-it~5 zMX*P;J*Y~_)YBJ`pTOkzC1`?Xk^vXDx&$J+C3?V&2+%8uh4jFk&>U_A-ajZgN3+7m zYtAsC{h74|s$7Tqoxf-{PBlBn{jiiDLhHG!Wa=hOc9Qb95D3Hyp1-k%%X;tME>+TT zd(ci2S&QI`h=vah9nJzP(R+WDSJ;efRXB6ZyheyhK7X!ez4Ey#B{jmZEr64HH`HxO zbpe<|i1*Av^;M4)i*8i+ zq2XvrvmyOYbo5zWyW9}TGPs+P$5FtOnZQ|&h&H#O-=`=5wM z2(M6m(P3e4;;kir<(%bbxGXH$sj9P~?m88|;hw4oq-Zno0k6w=^}ul-6WMZxab2r- zn%2F04EVBK4T?7)0TP(|tKGvc+Jsyru${|9rT*aoK)zA)a|?8CGebGEfOzJfKuuRO z!(^RT*9h~xz<&pxW`PMq_mE zSauf-;7EpgyWq_n0y^JHY>M%}{)t?p6!=(o&h@kT>+GcI$lU8p;p)HOPeK-zj~>fS zNlPt+FyWzM)b=q>ja_&14773^ZsQH_y~fiQ4h$=2w-%e)pFodER zg2MLJawO1K_m<6C6}79ZD}-x2-Vx3PP0o-9wtx3bDhM zIk!GTx$KXVGqbkBe-!!aWtv(xXrKrLK?-TwH%&ce!)e({fq>Qp1$QPbNpG@<*L~0{ zT6%VIuTjtTmM&PA$mC&wzVXgPyfx8Ebbvw@vAYPUz<=YjD6jiMDv1oV1_`YngwuN) z|7wAztBDYIA?IqQYT|*h=J-W#j?ikL`X*IXJRoX1;{wm?LxnC1X4VciCfj>~`d+kG$i^DNos{G+h+q3Eo}SmZWj|>@Ny_01Z4z8*GFs`A={3<>=?B%X zVo0BL|Bijc$2CP?3`k-4kKRlsN^L%gL9xJx_$Py{=#;sRn-I#0NES*odJgrIj>YA`%W$kjI;n^C+uQnj{EmF#soCJa?bQcQ z8=cqr8>Z(s@z1BLJ(^5YrjPN#e{1}YpXs9eHGwj;o!m#^i2=BZKx3mWk6CKb&I9Ms)dZJJ1xX?9;k$i(+%)ay{MD)8zs=7I zWd|=YTDbu9Pr+~Yo|~~AzXV^O^iiLv@1*^QSFz2{0XTz16@1TRspv5n$@(>98P;=e z78k0Fp&&$$ZirWqt_`@`#33~UgaAkg*v$&^z~rfXuoxU{Wu51H7l+xo`@!l(DbVkA z%s*p7!FLO~_PO?X`x#}Ie%}N5W*F<|4^K}o#+HwF**Ukzvg(#ijFBSQqu<(Tql~jk z%|%`wbWi&cjI6tP-`r6httSfs5iIz<89H*xjR|6%K3BW*{gk&}XgSLtAbN}>=D5dk4EdI6MWb|Pw_Y`XKR z8;X$g{wlkdip)-vm-df>M%AdwBj+_}B8&lAG7bLmD?>%yE^AWHN>z}B+YZ4Is#sUZ z7an}^y-Fx?3gQ}}%#J{a3=|^(OxW}KSL}i~GWnfTOA$kmB1Z1a*e_sNDVV&16%g~7?|*^S8Te4mUN>F)xfWHS zius;X`v4fTV31pT)Xm>FC+ODn0hhrg`rzR5XMg_`8Bd;cV7Fro7Z61#kdDJnL7X8p zn~MzyV~2Fbfx%P5iSwlnUp_La!paF^!T>$-^{=#KdLw>`OOC592_bW(7SK60kSSvhCQ`~eRB zq8NfT*J>~h%*LEFvYcF76N8HUzo^<*mh~B<7R0i|xP`SSgRfCMftGfm5*|Qq zCKZB#CO=enhDVR92~#8i6a$heF$vS?acC%9HCe!jwDX$aT_AqkXJPAlB_#`^OP+zw zzbB9PfXTbRT2#OsFSTI7*jWwW$DCwBHC0YPk*h^aElJ~4S}Dg1va5kX{Zr&uj#D3A zhwb`-lnNOx*m*gso2GAou0crHF~`JYVP>XZFrJlotpFWF4=dU1De?J0oFn8q#;fQ) zlb!a>yVqPcO*sJ$SJ}#{)av4+A}ybk3MB-5`*(Vx-Cs_g1kxa~*WMZv@TcMii%uS} zGoA;r@rkVf`0PAYUaxj|``!5&@_4@j9I_vBO9h>%Nasfl*RQr8E){TtQrtoJmnd+^GxXtlIaV=@oWohx34J+v7-Q5@ISsEHNYj3w1n$^VSM4jwh z(N&s{w|w1GEGS8A1h#WLXdA75;x_uD5i_J0L}rF1V!ey@KBgVt9vwDQdhHVc-d3O6LMu#9K*%x9pig&L3M8{yRI^S78NK=G0Mg$JNc|AZ&WDE>`0&!I!jlJ7~bL@o8z7GIX1v z8$Q8#IjG2OV9=0^p9GoqxB_A-8KYK|8?M%F8(%>Hh^po^;)gN`jMXzL+_O89F4Qx? zr&V}4iH@VdK&g@>R|Z2nk`(8^g+Vt41d6bG1~l!+nGyWB9w4pR*#bzKjHtlsvi@)F zseSe;zvFtvzbv|WvXRy3s<#*W(*~ieQWl?d*ss42QX%#AC)V~Y3jYkn@o&3&mtnbd zxJrwD5U>bY46-rg_CKFY`-`8N$JMY{yZv=~?$8*#zy_5~m(g46mFL z*AUTX&CeXQ)Ik)HqNOSEgb@vdYnF9W<6O0^DhE9exYtOQ0(|0mkX&jF1&N`SMD&F9=l7KYi9IRkhrWOly9Og*{kt5O^riUY|(d-)xXC zIoJ1cww`CX~^rqbA`ck_&e&vdy$cz{0d^9zrc|QV6^vkB~lZ^I_B9as~z~zaVyb@Umzy7{Kj{hCa?4sB!f;AQW4*^x7>U` z!_@i%({(}E*!A)9as}`Go4#W(zmz-}>Fu&5tv~)`#I0ujbv_AEPt1p%7opAjHV#fj zQm$X7I&@#fg#ve{yl^HBJaaC$PD5A|5r+8@+2EpH4iLZ+|GqQxjFo3}_X-0;UzTRc=XEBK@QX_cX9FH|W8L6p2c7qPL?yLz|m0^Hr^V*%bRXYdx`5dzx!Bc>u% zEJ<;YE8kutKMzUa?Oh;6)NN}eT5=~5ok3TB4uK(Q2W}#UWej6v9*Ui>wfHF$x~b{f zeStV-DyFTzD zAqvHjnIWXu_>MWCLBN!*D1*bu_|7rr6+4fX;1Ws&fGE~1R8{}eL$|!Y(&Bw`@iBAK zGuT(kbZ$Ar*+>R`ML{~zEwg80?{y6=4ISECa(uT}eDM9zeOV3dc;M7>miN;@f(36G z+xdOXYGv;wqp<%lh`=}ct%XD)!yvhOrw@L#6C+u8V)yEjL7f^JsZgpCz9z3$4Ohxd<$XU?T{}cDh5P(Nx(;OA4 z%m^sZ%def)kNVN@DqWOTgdle%=B>8IwY?SoC9Dta^iPZG8_{ zWx^P9KEHuJmls0}rEc+X?BXEHtnlMaFXm%Xte@bALv)ZL0>drWtM#46!{2qJ9-4v@ z({hsD7z;2*&iKggsonaBK@M{u-JN>rt&R)ZO^&%hR=JfXBKbvQg0E40Z!vFa{6IMo zU$Up!*AnG1Ff7I_r($M1MZO#T#Eb^feL<8Tc}WsB%?L$@!6OU$=>J`Q@sR@1HNN8e zm|mYq=RNq&mM)-k71vE`DxJAGP9PQ;<9$*xJXluKMx^CHZp4h9T78$z0rYoJ22Z?r z;Fl#3t&K6Uu>5njc5|xpi<)dyqVsfY-h1>moV;115U%YK_xd}t%$f!+wOvEm;yGEp z2t-r9Fuf22t$-c~T9+1yCHhhh)`k#*A@ra~qHsK7sw5yEAs;cYxCg8{*Qsb~<8YNp zxlyf*jQd%~oA!1k<9WB{`B!XZr63d+2hi%1~J_KB`GBeZ?^_P_|x_@0Cm@lf8+>K|Szyt{~d9v=B7Av$L~&_O10AJTRH- zI2m4Vg|As%Yq~4?WdL{QidSC2zD04`cH`#{gA7Oo5ixraA}%%Iy_QHQYug%!1B%Svf z5q$mXBAzBuVo}t#i^wjz&SId|`@KAJ2bquuOt=9xT& zbPQ<2*@dI^+&8(o^}J9S1=_&0U#w){6bGb`%1=!j=b1VFExR0M-PfgVe9=S;kxt&~ zeQN0;9Azn~6@MZWuV0(DsY=WVI<0^1Q-HX0G))_P(g9`+W%#5-kvZkroRgZ@(&*NB z3$1jL(U`Y6g$3?B9qc?F7du?{Ug~Q6LZ$5~0a@b62YtwaIkdDFy0d}O1E(=SzmhdCbXJ(gu{eRWkmt@eZ&F#E?;TcCFQzMjc*|yqq(ni#{^dNoSlGI(_@0V0oE|Vu?jdDR+wiVNKHBa=|dvOp6oJmCtNYk1t!N1vRrj-w@v5j*mu{s^EAyb0G`eOP+r z>BL&i34Fe`I5&7z%=!29gK1}u3h?(e6(ISsd3?(M+Q$FRDAN5;R;6miw&095$qYJB zu5rGKHIv@bA&!zvcB<)UGhT$3*9vLy8V8^=2d*f3(hJ#P0SH?_+F3@2#9X|4{Tf%e zMLe8kJOkSi?oKyr54UoFJsiLtn=bqdphd!E&@0OAOU^%))t26J&~P>g2=ODc4fgS> zzP;eh%WIqWOq_IGx*oUO44M{1kta`vM0nuRNXAEeek)L&;kT zZ;7tx>~NiStQD}ssyBlHeuW5azDkE_svt|Yp|J%hgOcyU&R?+Xs0mehr~STITok&Y z2$u!zvF-I~l36UJeue@#qtpzjMJ1Bcs^qvg^oH%Txw%a_i(=|BFc9u|Yk%w)8(D5` zK408k{))s%VT2kOvnb+v3^f^YCULa9talHZS6DX6krbOjpJROqtkYsf-UT%$jJ}C~Po%_A)`0oj)Z~&* z`bH&qYy9L#(*RZN8wKI7^@f9XN&MS64#<*eXwqv|F4FhKb*ElNv`%bpUnF4@6^M`w zr0q_;<<2QlembZhs&Wa7<4@B>I5uz8pYIUzPeGO+b}wwXrGtpF*_42S0Hb2=JE?xS zCo_PW?B(v=3^x;o0=~8W9B|x@a600sL|$M+rG1-V?n2H#EJ1` zmBX}C@B^hmCmUlsQ~TF0%l_v#r=%h1i)Azed_+!zbu{-om9!f#WuE^K$ux`hjFf7J z)uPviiIaBiEz=W|q5(I3l)ucfO zE>FwPxLv`QiBEeb1v09DEu-dxqn69XUVX!9JzSqs)4ad>SV0xOz5GD zx-iNLU=o6{VVS6X_PXO?#pUTrgpoB1mvdv~2x(n7?~?D6HVv3Y+njRtL01YBaW;A7}G%=f!`g^5RXPFcZclx3GT3JD63g#DR6|KaA|jkTp_JeGzm#ytjOR z5`5b0y%O~L=l4vLy+5o!g=v>QZD4$$m0K?BdV0`@O7_x~9lQoONuXKG&ZLPiiHSdt zod888#W7CD$c|aWW%f6^8HJBdFBYp7d@9+$YQwashO!GNA|^&vbA*OVoEBn4XIi*C zCDk6C+m)zT$X(*cf-3ldW^I>V`ZXA(@AWyJ$svw` zk-Uq&$|3OESaM=Q_mL=-O0{^{N97X3Dz2oZ>8AK*N5#D=^MXE#k_tb%k6NC$**S~B$;FfPh{M<9n5Ye_P6aXnknwKVec3aQ zFKz#_5EjErVgA9&HWw!MuNxQ|FQ5}gWvRm2RQ=+p&Z$Pu-_vPk@spDTRnx&hHmiY! z)o!9^@pUIWra&8~RLMTH2tg4Q;O0pYZrl{x&v{2qXfPyvtY41{NVZjKQW90?3DG6C zk5IBqtQa7lx`ioMPMeNCPPR~4uw#bbRAijMezU4S?)}V8dRNjB7Vwh3$r$%P0C~0L z0aHLxlLtsLZUO|4l{YX3-usZh@ATV2irm{ix;(GsKkU~$R3Xy%Usin6;*YNlg%>9% zYfkA97qVzxdAJ;L&SnW{V8Pt&!g7l!<*IWl5)p_yif`Y%B?*(;*OS3&KTK*woZ=2P z?4+(y=clJwqX+qDfcZaziJKbVC|j*#zr!u}%7m(Gpaz$^J18Cv8$uZn5#az%@%O=D zD&~vX)KNd)Rjy58#N@4?Z>fWygAUtQd1Q4n+qQZIES2tPT_ zQmAYnif<^yp6lRpz|f=p=!DVdr2N>f0;qr8g8}H`JQFS%Cf|{vvQ7=j(6LMU_{hm` z>467Yn3GrTjo0HYNvk&P3n$*atX=Ky?br426O8pXD!$m`vI!3v8wWyMLea?xNwq@t z6*GY!x$~vDPJ*w)8w^&TOKRHZ{#cnHWBe1uG%SB+B83e;6pbtH815d&I@@CtLiQN<0)8u`qsN?SHg)FcM?ScyR93ft8OSNNa6O0Cq%}R>(@DuFP(}-y=8tp|oMbW2k^|`3h@(=fdqA9(1?9fdMw6Breku8*Sd)@Ct%C zMofh`Sb0#<`VHSV?YBiu+8m8xkdj}J5rhs*wU0moOP93#Kn4E zf$slVWG4_rkZ9WNI3xYI+b^6uVzF?UJ-qy|>73(fcL$K6^q@Q+Rx<4J2QR^*S>O7%Dg`uT+2@{3Jz*^;Ad$tviY3VaRJL~{CW2mcmG@Q=+W zsUR6QMB$3%{WAUjEToYPoJB%$6@6hTv5E`I6=3Z&giOq|kM(t`nVQ%Rv=oX!2&neR zD@I-|3Ce6R_a?r;F@c)RUCNMgejaxI6_|f#clf6+o6g%fdt|#G zW3k_ya_TYP@AbeH)#}64=InhH*%x#ZkYosDvLe9jJ7&@y*5CAThLH`e*l&skE%R<< z?w~PRm$7_F$zVT<@WjLMyf=D+!^NQF<6TlYNTV{+Q=70H1yw0A`jGW$RQVOFXJ5Eb z)VwTbsq#h_Jrxy!dnV+UMi}f&+n$h-1qvf71pm@TZ2z;@S@*SW-FHeZu?^s%6F8I! zJ;QTUm}SETYvTPMF;`&0`TE5sIvd;L6@O8EEU;^@T~hq%qF6;w!PVO*L&vYFKX_fk&)L?r|9R{r&XJF~A_cb*TugKI30?}rt0v?py+>vE(`fRFmw z&nVLyU&i+inP=5$6^Jg^3ZWe%`AYb9PHdV%<+OnZ7pzhfIbu}+sfwVJuc8y;Jfp`U z*U7NsF-p>UT{aP>cu+z!;Q5i^)S~u{c*jBK^LqDU^HdWA`$`f6_%P~DspnnONn!f_ zjemq!6o-89?-MBQ!SR@b|4w{u;Lgs9q=f%mq!8of>?)RP%R@!AM4Vglq+ekBf;xAF z1vSCwg!#?fJM_%D-XeoCoJiP^V`)Xt6UFzH54?E zg{Z*(r!{&SgTX9&BPvIQi)H4G4xwII6is(nT)dSuas5>+_3K|nk@0%}(I|Z?Elg`u zsxIXdm#F_JYUTddObh=kSHoNxWJXG1=bt+?a7+UD@5EgK4L~==o!fCqd1_#=Ge^^Z z30rU)&zFp!$jMt#l9Tk$vxvJd#u;DkZZGb-otkP6k^-Fs?m4(oOo1PJpN~YQzP&xQ z-5Cd7B<6QDCy#91VvOHD+|`-l=Z_toJch46H9oJ6Q-y>7X6rge{eD5RgMXG!j`FbGFCBg(SR+6nj{xJz+{axoV{R^_>cg zg}1_ zx=kuJG^I%5P-@ zOmHnoC+3!a=KY(*TCw=2hg+c#ev-FUaRG(P z-q!>>d?UaMA@i&d2UW4LwqcyE2z?w@5FdSaf4|nq?BApO*hFb%xlXn>ty@SRSMu!a zvBk=hfWqL8+W{=;!?47tj%TdSPMYD~=zOrRa1izUkA}r!`;Ldmao~q&_ooYvfQQEC z%^E}99Lt@e#_sQCxDrsGM6dY~8)wqEScoPUxDoYclTXc3N$HGTwb~BpjS7?gkMsvU z)LxyDxR_EH{-27O%Gj-Z4j1L(h^!u`s4%fU+cfX8B9kpvZlN|wHwb={Y$4>twqxv- zW=%FI+znKOS^kKUkZFml58w5y;oetYjWf|$^}&;IbiO_~>kc0T1l_E0gj@%onon}1 zNZ;>b@p0Gcs{zh@o|kz(PF;-g1mXd0=_DTvgLhVIQwfq4T-rRNa0QYHe;4oPh}o%0 z)C)>bIx+nyDovSmZVbBHe7N2482#b4Uf^VlBFsjV1~i3E_^r4? zs_Uy}%16SGd()c-lUJD|O1M{t;U`i7H|CuydD1@EYlmOY+t)ZqiyQ9tbvwfZK|Uw# z`D&B^>hXn*{71Vplp(KS-_VM_oT|to8=`0KQG;F@4QtaF$**qRN>j~KQ*B}-Q#@uc zo@Qy~Q2(LZw(CqaQWK(*jzA|dj4dLaEc}+t?>sF*tds49;wFtBv}lOBa>y|I%I5Q> zYhnaYuq+@3A7z{zb!W$z5>nHTsylA=X{93t9FLpk^v|wjGPV1KT{&O$`kIbOJqTA7 zJ6%o}AJt#4KA+mqh+hI$?)P=Ct{=uEOd&~5lsi~(KHeDX6IDT^_3!{81?v@{Awgk= zBl+?t_YgJp9QJR`Gp{*gmuB6{%l$fyZ1f8D)CtOPmrw-wF4J+SxG%(-v=`<=`59qG z*q;sA-PlA5<1;KZ2(BqdblD?DA71v+<<{1} z7uDkTQ2d!gA&Omb;7!%6?v$W5`E8YT%u3tGPTRdfE0a%;WtvEsGRJ$GLA#75`}+3> zIio)S@M5JGGK1X0Y75P;Z9QZ(5|5p|MW{feZdVTz;m5no_^nTW7^8a6-z8qrNwvF4oD)e7Tttg6q;K)~67 zFzrZ5H!CWm4;GGL*9Zd-Zp&Z2&I``&3lv4d;zVB&dAXso5Sd_1z0}>Hb7PK3cpQSo z8UVpPD_5mw6fT9ERC|YiHWrG#b>x$HSbzST7<^UpIC|t$zc|AA1-tfp2p{nKOcU#k z_LSp~rD`2~=^I{d+mq)BI#0G}cx*kEhBMcK=Zd`RYNul5L^SF>arPws!SQkXv1_`* zQHwA085tdxB`ZelSW(}ug=G1+f$86x86t&tZ#as<`eCppc{G5^W;|YZ|5_D3uY&t$ zgOAj_EC9zi0js!X`md*BjkQ8!*2}SULDZZucD?v;1gw!=y;q>`)H2|u>XtYCkn#gWP{7(g`msyNuq?W)@w7_hOpI)C6jp5F;@^*gy- z9477pta)CHOiB$->)`sDzh7dNaZZw=tgPx zn;d<>BkZPVTyed4g{IF0n{=-K(Xn83xp-;A&QfWg2jzflW>hLO-0bJ#qcZZ*4^oFP zLU7FhVUs~Sz=@#dA^`I*1k4P`r2x)7a zz%Z?%imAdM6La7)WE#M(1sD867q-d~S}`PxbAvF0OHfAk+>M`(e})wyV6WH}AfB8; zo8nlV5b?G?TfXUZa3g^h44lqorwM~_mL+Ril(6w++e!e=>j8d$iSyHv6uPxaV>cVTdnQev6s<%?*q@f1=?j zNP&7YDXK1{%V?Pu`J!H5P#BU14YA+~A^#)tG$X=W^_z3N<35P8ti4J&fdW-(?%lGx z_FY11`F0M|sMOV>^g~=kq(}TTbhzACHV@!)GK67kLAnY7W8I;V zY=Fb~T4*dW`!@}3zGT2sE@x|RZ>%}`De6f6a|d>lo! z5r00QZy8Ui<1iz76V zr4a6R|7Ia3SIVzusk~u@D`sJH#h0`WE`kIryjG3Yu2gUQ)I;b~lWTjHurSbEPZ4L% zdV(hqgD@w~iIwWQ>uEcgd-MF{?b80FurETENV}OcHWoBj`{kR@={o+E?p~I!@`la? z4Xq1_>77V0hr@h~=&O}oa)+uY{`5Bqsc|qb>*8GaW(7{Y2TI=|E>n4Vn}23J zq~E`ggV0AqjlED-$sAlB^5YY@RMrZvh)pkQp>r@T-a9}@z5WjwJ-@np`1($X75)C{ ze)S3cYhiYDwS=nOwTJ2buMWST_cfLD2z1K~0{wKwI1q`xz+2pIv=Va6%=U%2A-`Di z8Gmv)6s2Xi0^G_}>NQ`NW^dnEvgbWA(m#P3U6?iE_t>^?rpm7kzb{^fpnXBL0xfwY zUtb?YwJM)OS#HX`BGUb5`hL)`1?g{fQCpg(ua#&J0T6|AfRo|)e-6kPr-PkX-^GzI z{HVO(XaC1<68Bg5e@053_O=-YN+sT?*5%BeZGTxM`Jh7xE$27;2`C4r$9qojQ_vW5 z*-Sg-R8AMn&y5UB|6|lFbIz$jk=Q43=9W1`5M^_&IDUMfN%d8qxGv%MvzDbEnUnlh1TIg)-sETbll_;Y#M#b1yWtE1N zwNadFN4}mDci@z7g2(4KV#SskHXK>3Tk=MATnOYpUj|=LEx)<&J=|VzOx;k!2;q6` zY1J&{3rC9UXoFKhGZ@xiaiWq`2@nXF63zI+=Q(ncip;V?@bo_oFCtPAor&aAP8-mM zkRX`NYWwaZ(km@;6t#20p`$ZvsXRC#2LI?cQg@@et_TAXmKyH+gCNNSVnVK+bKT>W z%<*mM7pT1YXl)WgLJ%RO zB+gZYCPhdh_E_?ilhS9qJBfBZ@iY-vi}ZzcLef95hnU99o`nHgkf}SQsK@XY4@)oG z_ttM-R;ds^vclw^4+*uo=~>aN!=8jtHKfx-^e*QL67Zs+_72EKJNqd?4Ww8hJk8?c zraHr&ZGA`QGZ%E?eR4ZK2)3}JXd^YF^98jnOgBEb<(gUgWorG*PrNhd^TvdO`~_c= z!^z^&ooVpnaPaf^eee?$A!5`ikmS7lMKIi(s&`fcZanyE`0|9lh0cx}@9QSBina0V zOQt2WG?}l1>7lelsw-w|yT5aFK}vhn#E&aDYbN91MkL{ea`_PgLL!4JXVqn3_mGaVaj2;WyVL2Jz4?~6WJS7w=-jFoL1{Jn7(ry1WF zzCHEI(>RA7$XX?=&MppKZYDr5demOR;N)ntGMX$cIj&yY zM%*Q|hDeOAj|H+tG_7ti=eULyk9c(gGa$@gAN>R<{aGtMDb&5}4q!$(J#F)V)rcnVUT zP1+kQauw~8v(}hLCnY1hblcIO^lRJi9R@cVg48=_KRcls^pGWF0xl)O2Di2HXwto) zC1&e);I49v;n13z;hN6JE4t1{zc;xD4LP6hj{4*xp^GR>B()~(6gu)fLicZ?=W_u5t*cm{%m~) zJVsd=@a6!BLcGhhES!FM7Xw`tY7a5MO?b2^pZQ{gCuiAy=^5MM19ZB$ndEh>e0rJY)IlR#8WO%P7wa*nD#GPsK1u77P0VEeXBe z+vZ2+A9TOE0u#OPX)`F6l3w<9pmlnG;!muXt5qvFlH~n(dvYa&? zrbND^NW@Svs9DDJ?u1Ff2Ai+$dJfg$6evjd3&awvs+>uuSjfhRYr)}}r9L>dxOKF+ zbs1n_Le@HB@RCo%8k`EH(T$&wmcHL$Lrma$Zz7j{b071Y&QiM9Mh|AK+ZA1v_Vx1N9lQYY2X6-x}- zynFc7d7Av7rF!wIW2NKWGki`b-q3L=%RhHvQ#wOv9GQ5C!=xVXHg#4erxN? z?0Jn#nM+OClzHYpbjgS?@N!(EEjVs*u%%=0dxG{mrZ(2tO3r25cb>`jU(z(AWwg(8 z$4k-}v0))r?_ygG!>j@^3dLirmn=gC$rhh3)cqO3g=|Z_VxOkw=w&1`ez+a1%TF1V z#!Q(xwCi+{Gnj=ZCLeG$8A5cgeq;Bydy)RW?8m&X*9WR#V_Ty3hxzllcbZ+kxO_QZ z+`2e(&-$WM*_xUe*y@x|*L;0_bXR-Qw+Yu~25JwYpF_1BCY{){UnrlhfbJ|gJ5~+- z(VVbAFP4~~pS`z7$;KV&atIXI*e6xo1cJ7(Fb~xzj?)YJ(ux(U;;dAiw>hv5pA78T zuB=~LL${02Ofn)v4=I>7*m#~hf~!B#E=E&mYLqlh@ptCpcedqr1o{X1wv+o`jJ~^Y z3V6P`33z_Idi)1|k#I#^z2-I3h=U*PS)T z$!%M6O`3ymZvXVd^DpYRDv1JivY;imOPXy2OLt$y3Ren|MlQ z5~By2d`VY~&szW`d38v&tXbIq0hc17-2eap literal 0 HcmV?d00001 diff --git a/ext/data/vectorcanvastest/patheffects/00_pc_dash_line.png b/ext/data/vectorcanvastest/patheffects/00_pc_dash_line.png new file mode 100644 index 0000000000000000000000000000000000000000..e08d3e2a3f5ef5fb2404906ed308ecc3be13d036 GIT binary patch literal 299 zcmeAS@N?(olHy`uVBq!ia0vp^DImgx2jIIrmrpC@VutFQD0D700mZbncC0Zer)&V^O9^ytV0Qk&o*Lv`RtT< Q1<>~lp00i_>zopr0DO~5M*si- literal 0 HcmV?d00001 diff --git a/ext/data/vectorcanvastest/patheffects/00_vc_dash_line.png b/ext/data/vectorcanvastest/patheffects/00_vc_dash_line.png new file mode 100644 index 0000000000000000000000000000000000000000..e08d3e2a3f5ef5fb2404906ed308ecc3be13d036 GIT binary patch literal 299 zcmeAS@N?(olHy`uVBq!ia0vp^DImgx2jIIrmrpC@VutFQD0D700mZbncC0Zer)&V^O9^ytV0Qk&o*Lv`RtT< Q1<>~lp00i_>zopr0DO~5M*si- literal 0 HcmV?d00001 diff --git a/ext/data/vectorcanvastest/patheffects/01_pc_dash_path.png b/ext/data/vectorcanvastest/patheffects/01_pc_dash_path.png new file mode 100644 index 0000000000000000000000000000000000000000..3a301354219fc0cc1a4ab25f02a019238b3c74ef GIT binary patch literal 348 zcmeAS@N?(olHy`uVBq!ia0vp^DImn(`OgE>`SpL<{|RqoLV-=IBaY9Goo}`0-JWxX zJV&^WaOH{Luebkp&iu!pA73}O&$fB+eqEAfTVsTM%v?jb)@94rUru5LLSL~zVo*^v z>+J4`#)!#tx4N!z0K=2#ZYhN+TuVvM71S00!R$?&w{?hinBI(x23ZA!v)-)H1sPLz gjmpNryxF9G;Ent<^HYC50Yi83*7^+!kcLU9x zJa?<>8V4{udG3}{n8LM`^jtx00T9gIw0T>HXou;|$mk=HA`);%P^1O5& z=8a4noBFlS*UMHf7q6)K{dJRke$j#Y-D0OD*plY%G)qomJ)V@C{`&n=YyTsR@-qKY z|Ngwm{^ZQ8vi%Q|H-1z9@$%Vi3ASSeyB@RU^Drl$P;Eb6kX~ND%W%s<|J~u4J&8cO zuRZa4sg=YEhQ8%{=6y{%!gXZUo5E6mq<{krn_Ct%<*c)I$ztaD0e0sxx4 Bp?ClQ literal 0 HcmV?d00001 diff --git a/ext/data/vectorcanvastest/patheffects/02_vc_dash_rect.png b/ext/data/vectorcanvastest/patheffects/02_vc_dash_rect.png new file mode 100644 index 0000000000000000000000000000000000000000..5344eee2e86349c30a3ce7bb26e00b6bbbf8aad4 GIT binary patch literal 395 zcmeAS@N?(olHy`uVBq!ia0vp^DImfYu+aQtu5U& zVQ>85*vAild`VF}zHUwB#HZ`l98TUb~-#kqj=%`ckuDU{97W!?C9?*HK-bBV8 p;et3i_53R(S}6hs1jLdDY|CEizuL(7yc-zE44$rjF6*2UngA8pt$6?d literal 0 HcmV?d00001 diff --git a/ext/data/vectorcanvastest/patheffects/03_pc_circle.png b/ext/data/vectorcanvastest/patheffects/03_pc_circle.png new file mode 100644 index 0000000000000000000000000000000000000000..4c267ef765d97bdcc14dcc58a0529e2202dc8434 GIT binary patch literal 497 zcmeAS@N?(olHy`uVBq!ia0vp^DImL93>l7QwgSHQ7g~R*w~P~ zaoN>hzhCz1KVp@$sM%CzZ>_LCGVr5xjZRo))Be1Dvd#w#d;IP^0MS>}+mCP9u)F@Q z#IA$xM|Yd_DS~XASnd^V2)4Un<09er>34g8ZgPDWxfbT6iPd4+KqrdsOy8OWRNsJ* z4tp~1Y8TW8An8du*Vg@4D41zduNGM+n)%jr*>#WNO>RFX|GKx)$Tu?deZ-d&CB3(; z#jcjh{XTlD*3Hj0#`F6cnd>s zIpLJ3B|F#JG?}PpvmERC^e%ES(2Hv{e$O-ILk;hSjScpH*taSW-L^Y+fgzC#KEZi!hn z|HF@QCtvmy`NdquR<(~i`P!Q_TS+aU^6hhq4_WfRk55oLA|&ALpu@P411QSa3P)goEF>k+x<|ExbOS~CCY zpE;XvCw=L@x_QAIvBxcwum0Sd>Ue(hp6`sj&*rSTt$TQ$dd+U`npf41Uk=!L|9B|u zH|@Um;g-oykN@s133~Rr>2=kG+H3F5+@3B`zfevgY}MSiM=jKR1C#d3PL|D2p5oPb z?m@ah<|d_%-REcVg1ocn{pM$mC{X~4iw{hkLOw5~rcN*cMlge?tDnm{r-UW|vS{jY literal 0 HcmV?d00001 diff --git a/ext/data/vectorcanvastest/pathorientation/00_pc_drawpath_ltr.png b/ext/data/vectorcanvastest/pathorientation/00_pc_drawpath_ltr.png new file mode 100644 index 0000000000000000000000000000000000000000..cefbf8700b3059145a5fedbb5705a799032d8d49 GIT binary patch literal 307 zcmeAS@N?(olHy`uVBq!ia0vp^DIm*$14Dtq)78&qol`;+0J!B|`~Uy| literal 0 HcmV?d00001 diff --git a/ext/data/vectorcanvastest/pathorientation/00_vc_drawpath_ltr.png b/ext/data/vectorcanvastest/pathorientation/00_vc_drawpath_ltr.png new file mode 100644 index 0000000000000000000000000000000000000000..cefbf8700b3059145a5fedbb5705a799032d8d49 GIT binary patch literal 307 zcmeAS@N?(olHy`uVBq!ia0vp^DIm*$14Dtq)78&qol`;+0J!B|`~Uy| literal 0 HcmV?d00001 diff --git a/ext/data/vectorcanvastest/pathorientation/01_pc_drawpath_rtl.png b/ext/data/vectorcanvastest/pathorientation/01_pc_drawpath_rtl.png new file mode 100644 index 0000000000000000000000000000000000000000..7bcd99885a80e9f9486f0d72bc666521cee6931d GIT binary patch literal 313 zcmeAS@N?(olHy`uVBq!ia0vp^DImL z|LNZ^83#%~`LaOkZc2uP#0|seF+0*~s~YN+!W0O?4RiPOf1GR>_xOYRBb{mQd-Sgv s2xue2f`jjOK6szR%e;|k5Wpt>*sI(pMN74p0E2|V)78&qol`;+0BkU6ga7~l literal 0 HcmV?d00001 diff --git a/ext/data/vectorcanvastest/pathorientation/01_vc_drawpath_rtl.png b/ext/data/vectorcanvastest/pathorientation/01_vc_drawpath_rtl.png new file mode 100644 index 0000000000000000000000000000000000000000..46c9b0abe181330c3c2235e4e1a5aa94e628e609 GIT binary patch literal 319 zcmeAS@N?(olHy`uVBq!ia0vp^DIm#%_UgQND3 z|M6j~62d2L?@XSNc~yp+on5N + +#include "base/feature_list.h" +#include "base/trace_event/trace_event.h" +#include "skia/ext/event_tracer_impl.h" +#include "third_party/skia/include/utils/SkEventTracer.h" + +namespace { +// Experiment with not deleting the Skia event tracer at process exit +// to measure the improvement in performance. See crbug.com/1329594 +BASE_FEATURE(kLeakSkiaEventTracerAtExit, + "LeakSkiaEventTracerAtExit", + base::FEATURE_DISABLED_BY_DEFAULT); +} // namespace + +namespace skia { + +class SkChromiumEventTracer: public SkEventTracer { + const uint8_t* getCategoryGroupEnabled(const char* name) override; + const char* getCategoryGroupName(const uint8_t* categoryEnabledFlag) override; + SkEventTracer::Handle addTraceEvent(char phase, + const uint8_t* categoryEnabledFlag, + const char* name, + uint64_t id, + int32_t numArgs, + const char** argNames, + const uint8_t* argTypes, + const uint64_t* argValues, + uint8_t flags) override; + void updateTraceEventDuration(const uint8_t* categoryEnabledFlag, + const char* name, + SkEventTracer::Handle handle) override; +}; + +const uint8_t* + SkChromiumEventTracer::getCategoryGroupEnabled(const char* name) { + return TRACE_EVENT_API_GET_CATEGORY_GROUP_ENABLED(name); +} + +const char* SkChromiumEventTracer::getCategoryGroupName( + const uint8_t* categoryEnabledFlag) { + return base::trace_event::TraceLog::GetCategoryGroupName(categoryEnabledFlag); +} + +SkEventTracer::Handle + SkChromiumEventTracer::addTraceEvent(char phase, + const uint8_t* categoryEnabledFlag, + const char* name, + uint64_t id, + int32_t numArgs, + const char** argNames, + const uint8_t* argTypes, + const uint64_t* argValues, + uint8_t flags) { + base::trace_event::TraceArguments args( + numArgs, argNames, argTypes, + reinterpret_cast(argValues)); + base::trace_event::TraceEventHandle handle = TRACE_EVENT_API_ADD_TRACE_EVENT( + phase, categoryEnabledFlag, name, trace_event_internal::kGlobalScope, id, + &args, flags); + SkEventTracer::Handle result; + memcpy(&result, &handle, sizeof(result)); + return result; +} + +void + SkChromiumEventTracer::updateTraceEventDuration( + const uint8_t* categoryEnabledFlag, + const char *name, + SkEventTracer::Handle handle) { + base::trace_event::TraceEventHandle traceEventHandle; + memcpy(&traceEventHandle, &handle, sizeof(handle)); + TRACE_EVENT_API_UPDATE_TRACE_EVENT_DURATION( + categoryEnabledFlag, name, traceEventHandle); +} + +} // namespace skia + +void InitSkiaEventTracer() { + // Initialize the binding to Skia's tracing events. Skia will + // take ownership of and clean up the memory allocated here. + SkEventTracer::SetInstance( + new skia::SkChromiumEventTracer(), + base::FeatureList::IsEnabled(kLeakSkiaEventTracerAtExit)); +} diff --git a/ext/event_tracer_impl.h b/ext/event_tracer_impl.h new file mode 100644 index 000000000000..8629a1ee6dd8 --- /dev/null +++ b/ext/event_tracer_impl.h @@ -0,0 +1,12 @@ +// Copyright 2014 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef SKIA_EXT_EVENT_TRACER_IMPL_H_ +#define SKIA_EXT_EVENT_TRACER_IMPL_H_ + +#include "include/core/SkTypes.h" + +SK_API void InitSkiaEventTracer(); + +#endif // SKIA_EXT_EVENT_TRACER_IMPL_H_ diff --git a/ext/fontmgr_default.cc b/ext/fontmgr_default.cc new file mode 100644 index 000000000000..fe9feb8e8702 --- /dev/null +++ b/ext/fontmgr_default.cc @@ -0,0 +1,34 @@ +// Copyright 2018 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "skia/ext/fontmgr_default.h" + +#include "third_party/skia/include/core/SkFontMgr.h" + +namespace { + +SkDEBUGCODE(bool g_factory_called;) + + // This is a purposefully leaky pointer that has ownership of the FontMgr. + SkFontMgr* g_fontmgr_override = nullptr; + +} // namespace + +namespace skia { + +void OverrideDefaultSkFontMgr(sk_sp fontmgr) { + SkASSERT(!g_factory_called); + + SkSafeUnref(g_fontmgr_override); + g_fontmgr_override = fontmgr.release(); +} + +} // namespace skia + +SK_API sk_sp SkFontMgr::Factory() { + SkDEBUGCODE(g_factory_called = true;); + + return g_fontmgr_override ? sk_ref_sp(g_fontmgr_override) + : skia::CreateDefaultSkFontMgr(); +} \ No newline at end of file diff --git a/ext/fontmgr_default.h b/ext/fontmgr_default.h new file mode 100644 index 000000000000..c49b5037456e --- /dev/null +++ b/ext/fontmgr_default.h @@ -0,0 +1,26 @@ +// Copyright 2019 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef SKIA_EXT_FONTMGR_DEFAULT_H_ +#define SKIA_EXT_FONTMGR_DEFAULT_H_ + +#include "third_party/skia/include/core/SkTypes.h" + +class SkFontMgr; +template +class sk_sp; + +namespace skia { + +// Allows to override the default SkFontMgr instance (returned from +// SkFontMgr::RefDefault()). Must be called before RefDefault() is called for +// the first time in the process. +SK_API void OverrideDefaultSkFontMgr(sk_sp fontmgr); + +// Create default SkFontMgr implementation for the current platform. +SK_API sk_sp CreateDefaultSkFontMgr(); + +} // namespace skia + +#endif // SKIA_EXT_FONTMGR_DEFAULT_H_ diff --git a/ext/fontmgr_default_android.cc b/ext/fontmgr_default_android.cc new file mode 100644 index 000000000000..9de53bfcda8c --- /dev/null +++ b/ext/fontmgr_default_android.cc @@ -0,0 +1,16 @@ +// Copyright 2016 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "skia/ext/fontmgr_default.h" + +#include "third_party/skia/include/core/SkFontMgr.h" +#include "third_party/skia/include/ports/SkFontMgr_android.h" + +namespace skia { + +SK_API sk_sp CreateDefaultSkFontMgr() { + return SkFontMgr_New_Android(nullptr); +} + +} // namespace skia \ No newline at end of file diff --git a/ext/fontmgr_default_fuchsia.cc b/ext/fontmgr_default_fuchsia.cc new file mode 100644 index 000000000000..788e7d2f0988 --- /dev/null +++ b/ext/fontmgr_default_fuchsia.cc @@ -0,0 +1,22 @@ +// Copyright 2018 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "skia/ext/fontmgr_default.h" + +#include +#include + +#include "base/fuchsia/process_context.h" +#include "third_party/skia/include/core/SkFontMgr.h" +#include "third_party/skia/include/ports/SkFontMgr_fuchsia.h" + +namespace skia { + +SK_API sk_sp CreateDefaultSkFontMgr() { + fuchsia::fonts::ProviderSyncPtr provider; + base::ComponentContextForProcess()->svc()->Connect(provider.NewRequest()); + return SkFontMgr_New_Fuchsia(std::move(provider)); +} + +} // namespace skia \ No newline at end of file diff --git a/ext/fontmgr_default_linux.cc b/ext/fontmgr_default_linux.cc new file mode 100644 index 000000000000..76927e7f0d88 --- /dev/null +++ b/ext/fontmgr_default_linux.cc @@ -0,0 +1,18 @@ +// Copyright 2016 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "skia/ext/fontmgr_default.h" + +#include "third_party/skia/include/core/SkFontMgr.h" +#include "third_party/skia/include/ports/SkFontConfigInterface.h" +#include "third_party/skia/include/ports/SkFontMgr_FontConfigInterface.h" + +namespace skia { + +SK_API sk_sp CreateDefaultSkFontMgr() { + sk_sp fci(SkFontConfigInterface::RefGlobal()); + return fci ? SkFontMgr_New_FCI(std::move(fci)) : nullptr; +} + +} // namespace skia \ No newline at end of file diff --git a/ext/fontmgr_default_win.cc b/ext/fontmgr_default_win.cc new file mode 100644 index 000000000000..65aee4b95144 --- /dev/null +++ b/ext/fontmgr_default_win.cc @@ -0,0 +1,16 @@ +// Copyright 2014 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "skia/ext/fontmgr_default.h" + +#include "third_party/skia/include/core/SkFontMgr.h" +#include "third_party/skia/include/ports/SkTypeface_win.h" + +namespace skia { + +SK_API sk_sp CreateDefaultSkFontMgr() { + return SkFontMgr_New_DirectWrite(); +} + +} // namespace skia diff --git a/ext/fontmgr_fuchsia_unittest.cc b/ext/fontmgr_fuchsia_unittest.cc new file mode 100644 index 000000000000..91b8ac0b4c5c --- /dev/null +++ b/ext/fontmgr_fuchsia_unittest.cc @@ -0,0 +1,63 @@ +// Copyright 2018 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include + +#include "skia/ext/test_fonts_fuchsia.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "third_party/skia/include/core/SkFontMgr.h" +#include "third_party/skia/include/core/SkTypeface.h" +#include "third_party/skia/include/ports/SkFontMgr_fuchsia.h" + +namespace skia { + +// Tests for SkFontMgr_Fuchsia in Skia. +class FuchsiaFontManagerTest : public testing::Test { + public: + FuchsiaFontManagerTest() + : font_manager_( + SkFontMgr_New_Fuchsia(GetTestFontsProvider().BindSync())) {} + + protected: + sk_sp font_manager_; +}; + +// Verify that SkTypeface objects are cached. +TEST_F(FuchsiaFontManagerTest, Caching) { + sk_sp sans( + font_manager_->matchFamilyStyle("sans", SkFontStyle())); + EXPECT_TRUE(sans); + + sk_sp sans2( + font_manager_->matchFamilyStyle("sans", SkFontStyle())); + + // Expect that the same SkTypeface is returned for both requests. + EXPECT_EQ(sans.get(), sans2.get()); + + // Request serif and verify that a different SkTypeface is returned. + sk_sp serif( + font_manager_->matchFamilyStyle("serif", SkFontStyle())); + EXPECT_NE(sans.get(), serif.get()); +} + +// Verify that SkTypeface can outlive the manager. +TEST_F(FuchsiaFontManagerTest, TypefaceOutlivesManager) { + sk_sp sans( + font_manager_->matchFamilyStyle("sans", SkFontStyle())); + font_manager_.reset(); +} + +// Verify that we can query a font after releasing a previous instance. +TEST_F(FuchsiaFontManagerTest, ReleaseThenCreateAgain) { + sk_sp serif( + font_manager_->matchFamilyStyle("serif", SkFontStyle())); + EXPECT_TRUE(serif); + serif.reset(); + + sk_sp serif2( + font_manager_->matchFamilyStyle("serif", SkFontStyle())); + EXPECT_TRUE(serif2); +} + +} // namespace skia diff --git a/ext/google_logging.cc b/ext/google_logging.cc new file mode 100644 index 000000000000..6955ff02a7fb --- /dev/null +++ b/ext/google_logging.cc @@ -0,0 +1,46 @@ +// Copyright 2012 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// This file provides integration with Google-style "base/logging.h" assertions +// for Skia SkASSERT. If you don't want this, you can link with another file +// that provides integration with the logging of your choice. + +#include "base/logging.h" +#include "base/strings/stringprintf.h" +#include "third_party/skia/include/core/SkTypes.h" + +void SkDebugf_FileLine(const char* file, int line, const char* format, ...) { +#if DCHECK_IS_ON() + int severity = logging::LOG_ERROR; +#else + int severity = logging::LOG_INFO; +#endif + if (severity < logging::GetMinLogLevel()) + return; + + va_list ap; + va_start(ap, format); + + std::string msg; + base::StringAppendV(&msg, format, ap); + va_end(ap); + + logging::LogMessage(file, line, severity).stream() << msg; +} + +void SkAbort_FileLine(const char* file, int line, const char* format, ...) { + int severity = logging::LOG_FATAL; + + va_list ap; + va_start(ap, format); + + std::string msg; + base::StringAppendV(&msg, format, ap); + va_end(ap); + + logging::LogMessage(file, line, severity).stream() << msg; + sk_abort_no_print(); + // Extra safety abort(). + abort(); +} diff --git a/ext/image_operations.cc b/ext/image_operations.cc new file mode 100644 index 000000000000..2d57273f2408 --- /dev/null +++ b/ext/image_operations.cc @@ -0,0 +1,417 @@ +// Copyright 2012 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include +#include + +#include +#include + +#include "skia/ext/image_operations.h" + +#include "base/check.h" +#include "base/containers/stack_container.h" +#include "base/metrics/histogram_macros.h" +#include "base/notreached.h" +#include "base/numerics/math_constants.h" +#include "base/time/time.h" +#include "base/trace_event/trace_event.h" +#include "build/build_config.h" +#include "skia/ext/convolver.h" +#include "third_party/skia/include/core/SkColorPriv.h" +#include "third_party/skia/include/core/SkRect.h" + +namespace skia { + +namespace { + +// Returns the ceiling/floor as an integer. +inline int CeilInt(float val) { + return static_cast(ceil(val)); +} +inline int FloorInt(float val) { + return static_cast(floor(val)); +} + +// Filter function computation ------------------------------------------------- + +// Evaluates the box filter, which goes from -0.5 to +0.5. +float EvalBox(float x) { + return (x >= -0.5f && x < 0.5f) ? 1.0f : 0.0f; +} + +// Evaluates the Lanczos filter of the given filter size window for the given +// position. +// +// |filter_size| is the width of the filter (the "window"), outside of which +// the value of the function is 0. Inside of the window, the value is the +// normalized sinc function: +// lanczos(x) = sinc(x) * sinc(x / filter_size); +// where +// sinc(x) = sin(pi*x) / (pi*x); +float EvalLanczos(int filter_size, float x) { + if (x <= -filter_size || x >= filter_size) + return 0.0f; // Outside of the window. + if (x > -std::numeric_limits::epsilon() && + x < std::numeric_limits::epsilon()) + return 1.0f; // Special case the discontinuity at the origin. + float xpi = x * base::kPiFloat; + return (sin(xpi) / xpi) * // sinc(x) + sin(xpi / filter_size) / (xpi / filter_size); // sinc(x/filter_size) +} + +// Evaluates the Hamming filter of the given filter size window for the given +// position. +// +// The filter covers [-filter_size, +filter_size]. Outside of this window +// the value of the function is 0. Inside of the window, the value is sinus +// cardinal multiplied by a recentered Hamming function. The traditional +// Hamming formula for a window of size N and n ranging in [0, N-1] is: +// hamming(n) = 0.54 - 0.46 * cos(2 * pi * n / (N-1))) +// In our case we want the function centered for x == 0 and at its minimum +// on both ends of the window (x == +/- filter_size), hence the adjusted +// formula: +// hamming(x) = (0.54 - +// 0.46 * cos(2 * pi * (x - filter_size)/ (2 * filter_size))) +// = 0.54 - 0.46 * cos(pi * x / filter_size - pi) +// = 0.54 + 0.46 * cos(pi * x / filter_size) +float EvalHamming(int filter_size, float x) { + if (x <= -filter_size || x >= filter_size) + return 0.0f; // Outside of the window. + if (x > -std::numeric_limits::epsilon() && + x < std::numeric_limits::epsilon()) + return 1.0f; // Special case the sinc discontinuity at the origin. + const float xpi = x * base::kPiFloat; + + return ((sin(xpi) / xpi) * // sinc(x) + (0.54f + 0.46f * cos(xpi / filter_size))); // hamming(x) +} + +// ResizeFilter ---------------------------------------------------------------- + +// Encapsulates computation and storage of the filters required for one complete +// resize operation. +class ResizeFilter { + public: + ResizeFilter(ImageOperations::ResizeMethod method, + int src_full_width, int src_full_height, + int dest_width, int dest_height, + const SkIRect& dest_subset); + + ResizeFilter(const ResizeFilter&) = delete; + ResizeFilter& operator=(const ResizeFilter&) = delete; + + // Returns the filled filter values. + const ConvolutionFilter1D& x_filter() { return x_filter_; } + const ConvolutionFilter1D& y_filter() { return y_filter_; } + + private: + // Returns the number of pixels that the filer spans, in filter space (the + // destination image). + float GetFilterSupport(float scale) { + switch (method_) { + case ImageOperations::RESIZE_BOX: + // The box filter just scales with the image scaling. + return 0.5f; // Only want one side of the filter = /2. + case ImageOperations::RESIZE_HAMMING1: + // The Hamming filter takes as much space in the source image in + // each direction as the size of the window = 1 for Hamming1. + return 1.0f; + case ImageOperations::RESIZE_LANCZOS3: + // The Lanczos filter takes as much space in the source image in + // each direction as the size of the window = 3 for Lanczos3. + return 3.0f; + default: + NOTREACHED(); + return 1.0f; + } + } + + // Computes one set of filters either horizontally or vertically. The caller + // will specify the "min" and "max" rather than the bottom/top and + // right/bottom so that the same code can be re-used in each dimension. + // + // |src_depend_lo| and |src_depend_size| gives the range for the source + // depend rectangle (horizontally or vertically at the caller's discretion + // -- see above for what this means). + // + // Likewise, the range of destination values to compute and the scale factor + // for the transform is also specified. + void ComputeFilters(int src_size, + int dest_subset_lo, int dest_subset_size, + float scale, + ConvolutionFilter1D* output); + + // Computes the filter value given the coordinate in filter space. + inline float ComputeFilter(float pos) { + switch (method_) { + case ImageOperations::RESIZE_BOX: + return EvalBox(pos); + case ImageOperations::RESIZE_HAMMING1: + return EvalHamming(1, pos); + case ImageOperations::RESIZE_LANCZOS3: + return EvalLanczos(3, pos); + default: + NOTREACHED(); + return 0; + } + } + + ImageOperations::ResizeMethod method_; + + ConvolutionFilter1D x_filter_; + ConvolutionFilter1D y_filter_; +}; + +ResizeFilter::ResizeFilter(ImageOperations::ResizeMethod method, + int src_full_width, + int src_full_height, + int dest_width, + int dest_height, + const SkIRect& dest_subset) + : method_(method) { + // method_ will only ever refer to an "algorithm method". + SkASSERT((ImageOperations::RESIZE_FIRST_ALGORITHM_METHOD <= method) && + (method <= ImageOperations::RESIZE_LAST_ALGORITHM_METHOD)); + + float scale_x = static_cast(dest_width) / + static_cast(src_full_width); + float scale_y = static_cast(dest_height) / + static_cast(src_full_height); + + ComputeFilters(src_full_width, dest_subset.fLeft, dest_subset.width(), + scale_x, &x_filter_); + ComputeFilters(src_full_height, dest_subset.fTop, dest_subset.height(), + scale_y, &y_filter_); +} + +// TODO(egouriou): Take advantage of periods in the convolution. +// Practical resizing filters are periodic outside of the border area. +// For Lanczos, a scaling by a (reduced) factor of p/q (q pixels in the +// source become p pixels in the destination) will have a period of p. +// A nice consequence is a period of 1 when downscaling by an integral +// factor. Downscaling from typical display resolutions is also bound +// to produce interesting periods as those are chosen to have multiple +// small factors. +// Small periods reduce computational load and improve cache usage if +// the coefficients can be shared. For periods of 1 we can consider +// loading the factors only once outside the borders. +void ResizeFilter::ComputeFilters(int src_size, + int dest_subset_lo, int dest_subset_size, + float scale, + ConvolutionFilter1D* output) { + int dest_subset_hi = dest_subset_lo + dest_subset_size; // [lo, hi) + + // When we're doing a magnification, the scale will be larger than one. This + // means the destination pixels are much smaller than the source pixels, and + // that the range covered by the filter won't necessarily cover any source + // pixel boundaries. Therefore, we use these clamped values (max of 1) for + // some computations. + float clamped_scale = std::min(1.0f, scale); + + // This is how many source pixels from the center we need to count + // to support the filtering function. + float src_support = GetFilterSupport(clamped_scale) / clamped_scale; + + // Speed up the divisions below by turning them into multiplies. + float inv_scale = 1.0f / scale; + + base::StackVector filter_values; + base::StackVector fixed_filter_values; + + // Loop over all pixels in the output range. We will generate one set of + // filter values for each one. Those values will tell us how to blend the + // source pixels to compute the destination pixel. + for (int dest_subset_i = dest_subset_lo; dest_subset_i < dest_subset_hi; + dest_subset_i++) { + // Reset the arrays. We don't declare them inside so they can re-use the + // same malloc-ed buffer. + filter_values->clear(); + fixed_filter_values->clear(); + + // This is the pixel in the source directly under the pixel in the dest. + // Note that we base computations on the "center" of the pixels. To see + // why, observe that the destination pixel at coordinates (0, 0) in a 5.0x + // downscale should "cover" the pixels around the pixel with *its center* + // at coordinates (2.5, 2.5) in the source, not those around (0, 0). + // Hence we need to scale coordinates (0.5, 0.5), not (0, 0). + float src_pixel = (static_cast(dest_subset_i) + 0.5f) * inv_scale; + + // Compute the (inclusive) range of source pixels the filter covers. + int src_begin = std::max(0, FloorInt(src_pixel - src_support)); + int src_end = std::min(src_size - 1, CeilInt(src_pixel + src_support)); + + // Compute the unnormalized filter value at each location of the source + // it covers. + float filter_sum = 0.0f; // Sub of the filter values for normalizing. + for (int cur_filter_pixel = src_begin; cur_filter_pixel <= src_end; + cur_filter_pixel++) { + // Distance from the center of the filter, this is the filter coordinate + // in source space. We also need to consider the center of the pixel + // when comparing distance against 'src_pixel'. In the 5x downscale + // example used above the distance from the center of the filter to + // the pixel with coordinates (2, 2) should be 0, because its center + // is at (2.5, 2.5). + float src_filter_dist = + ((static_cast(cur_filter_pixel) + 0.5f) - src_pixel); + + // Since the filter really exists in dest space, map it there. + float dest_filter_dist = src_filter_dist * clamped_scale; + + // Compute the filter value at that location. + float filter_value = ComputeFilter(dest_filter_dist); + filter_values->push_back(filter_value); + + filter_sum += filter_value; + } + DCHECK(!filter_values->empty()) << "We should always get a filter!"; + + // The filter must be normalized so that we don't affect the brightness of + // the image. Convert to normalized fixed point. + int16_t fixed_sum = 0; + for (size_t i = 0; i < filter_values->size(); i++) { + int16_t cur_fixed = output->FloatToFixed(filter_values[i] / filter_sum); + fixed_sum += cur_fixed; + fixed_filter_values->push_back(cur_fixed); + } + + // The conversion to fixed point will leave some rounding errors, which + // we add back in to avoid affecting the brightness of the image. We + // arbitrarily add this to the center of the filter array (this won't always + // be the center of the filter function since it could get clipped on the + // edges, but it doesn't matter enough to worry about that case). + int16_t leftovers = output->FloatToFixed(1.0f) - fixed_sum; + fixed_filter_values[fixed_filter_values->size() / 2] += leftovers; + + // Now it's ready to go. + output->AddFilter(src_begin, &fixed_filter_values[0], + static_cast(fixed_filter_values->size())); + } + + output->PaddingForSIMD(); +} + +ImageOperations::ResizeMethod ResizeMethodToAlgorithmMethod( + ImageOperations::ResizeMethod method) { + // Convert any "Quality Method" into an "Algorithm Method" + if (method >= ImageOperations::RESIZE_FIRST_ALGORITHM_METHOD && + method <= ImageOperations::RESIZE_LAST_ALGORITHM_METHOD) { + return method; + } + // The call to ImageOperationsGtv::Resize() above took care of + // GPU-acceleration in the cases where it is possible. So now we just + // pick the appropriate software method for each resize quality. + switch (method) { + // Users of RESIZE_GOOD are willing to trade a lot of quality to + // get speed, allowing the use of linear resampling to get hardware + // acceleration (SRB). Hence any of our "good" software filters + // will be acceptable, and we use the fastest one, Hamming-1. + case ImageOperations::RESIZE_GOOD: + // Users of RESIZE_BETTER are willing to trade some quality in order + // to improve performance, but are guaranteed not to devolve to a linear + // resampling. In visual tests we see that Hamming-1 is not as good as + // Lanczos-2, however it is about 40% faster and Lanczos-2 itself is + // about 30% faster than Lanczos-3. The use of Hamming-1 has been deemed + // an acceptable trade-off between quality and speed. + case ImageOperations::RESIZE_BETTER: + return ImageOperations::RESIZE_HAMMING1; + default: + return ImageOperations::RESIZE_LANCZOS3; + } +} + +} // namespace + +// Resize ---------------------------------------------------------------------- + +// static +SkBitmap ImageOperations::Resize(const SkPixmap& source, + ResizeMethod method, + int dest_width, + int dest_height, + const SkIRect& dest_subset, + SkBitmap::Allocator* allocator) { + TRACE_EVENT2("disabled-by-default-skia", "ImageOperations::Resize", + "src_pixels", source.width() * source.height(), "dst_pixels", + dest_width * dest_height); + // Ensure that the ResizeMethod enumeration is sound. + SkASSERT(((RESIZE_FIRST_QUALITY_METHOD <= method) && + (method <= RESIZE_LAST_QUALITY_METHOD)) || + ((RESIZE_FIRST_ALGORITHM_METHOD <= method) && + (method <= RESIZE_LAST_ALGORITHM_METHOD))); + + // Time how long this takes to see if it's a problem for users. + base::TimeTicks resize_start = base::TimeTicks::Now(); + + // If the size of source or destination is 0, i.e. 0x0, 0xN or Nx0, just + // return empty. + if (source.width() < 1 || source.height() < 1 || + dest_width < 1 || dest_height < 1) + return SkBitmap(); + + SkIRect dest = {0, 0, dest_width, dest_height}; + DCHECK(dest.contains(dest_subset)) + << "The supplied subset does not fall within the destination image."; + + method = ResizeMethodToAlgorithmMethod(method); + // Check that we deal with an "algorithm methods" from this point onward. + SkASSERT((ImageOperations::RESIZE_FIRST_ALGORITHM_METHOD <= method) && + (method <= ImageOperations::RESIZE_LAST_ALGORITHM_METHOD)); + + if (!source.addr() || source.colorType() != kN32_SkColorType) + return SkBitmap(); + + ResizeFilter filter(method, source.width(), source.height(), + dest_width, dest_height, dest_subset); + + // Get a source bitmap encompassing this touched area. We construct the + // offsets and row strides such that it looks like a new bitmap, while + // referring to the old data. + const uint8_t* source_subset = + reinterpret_cast(source.addr()); + + // Convolve into the result. + SkBitmap result; + result.setInfo( + source.info().makeWH(dest_subset.width(), dest_subset.height())); + if (!result.tryAllocPixels(allocator) || !result.readyToDraw()) + return SkBitmap(); + + BGRAConvolve2D(source_subset, static_cast(source.rowBytes()), + !source.isOpaque(), filter.x_filter(), filter.y_filter(), + static_cast(result.rowBytes()), + static_cast(result.getPixels()), + true); + + base::TimeDelta delta = base::TimeTicks::Now() - resize_start; + UMA_HISTOGRAM_TIMES("Image.ResampleMS", delta); + + return result; +} + +// static +SkBitmap ImageOperations::Resize(const SkBitmap& source, + ResizeMethod method, + int dest_width, + int dest_height, + const SkIRect& dest_subset, + SkBitmap::Allocator* allocator) { + SkPixmap pixmap; + if (!source.peekPixels(&pixmap)) + return SkBitmap(); + return Resize(pixmap, method, dest_width, dest_height, dest_subset, + allocator); +} + +// static +SkBitmap ImageOperations::Resize(const SkBitmap& source, + ResizeMethod method, + int dest_width, int dest_height, + SkBitmap::Allocator* allocator) { + SkIRect dest_subset = { 0, 0, dest_width, dest_height }; + return Resize(source, method, dest_width, dest_height, dest_subset, + allocator); +} + +} // namespace skia diff --git a/ext/image_operations.h b/ext/image_operations.h new file mode 100644 index 000000000000..60283ada8a24 --- /dev/null +++ b/ext/image_operations.h @@ -0,0 +1,122 @@ +// Copyright 2011 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef SKIA_EXT_IMAGE_OPERATIONS_H_ +#define SKIA_EXT_IMAGE_OPERATIONS_H_ + +#include "third_party/skia/include/core/SkBitmap.h" +#include "third_party/skia/include/core/SkPixmap.h" +#include "third_party/skia/include/core/SkTypes.h" + +struct SkIRect; + +namespace skia { + +class SK_API ImageOperations { + public: + enum ResizeMethod { + // + // Quality Methods + // + // Those enumeration values express a desired quality/speed tradeoff. + // They are translated into an algorithm-specific method that depends + // on the capabilities (CPU, GPU) of the underlying platform. + // It is possible for all three methods to be mapped to the same + // algorithm on a given platform. + + // Good quality resizing. Fastest resizing with acceptable visual quality. + // This is typically intended for use during interactive layouts + // where slower platforms may want to trade image quality for large + // increase in resizing performance. + // + // For example the resizing implementation may devolve to linear + // filtering if this enables GPU acceleration to be used. + // + // Note that the underlying resizing method may be determined + // on the fly based on the parameters for a given resize call. + // For example an implementation using a GPU-based linear filter + // in the common case may still use a higher-quality software-based + // filter in cases where using the GPU would actually be slower - due + // to too much latency - or impossible - due to image format or size + // constraints. + RESIZE_GOOD, + + // Medium quality resizing. Close to high quality resizing (better + // than linear interpolation) with potentially some quality being + // traded-off for additional speed compared to RESIZE_BEST. + // + // This is intended, for example, for generation of large thumbnails + // (hundreds of pixels in each dimension) from large sources, where + // a linear filter would produce too many artifacts but where + // a RESIZE_HIGH might be too costly time-wise. + RESIZE_BETTER, + + // High quality resizing. The algorithm is picked to favor image quality. + RESIZE_BEST, + + // + // Algorithm-specific enumerations + // + + // Box filter. This is a weighted average of all of the pixels touching + // the destination pixel. For enlargement, this is nearest neighbor. + // + // You probably don't want this, it is here for testing since it is easy to + // compute. Use RESIZE_LANCZOS3 instead. + RESIZE_BOX, + + // 1-cycle Hamming filter. This is tall is the middle and falls off towards + // the window edges but without going to 0. This is about 40% faster than + // a 2-cycle Lanczos. + RESIZE_HAMMING1, + + // 3-cycle Lanczos filter. This is tall in the middle, goes negative on + // each side, then oscillates 2 more times. It gives nice sharp edges. + RESIZE_LANCZOS3, + + // enum aliases for first and last methods by algorithm or by quality. + RESIZE_FIRST_QUALITY_METHOD = RESIZE_GOOD, + RESIZE_LAST_QUALITY_METHOD = RESIZE_BEST, + RESIZE_FIRST_ALGORITHM_METHOD = RESIZE_BOX, + RESIZE_LAST_ALGORITHM_METHOD = RESIZE_LANCZOS3, + }; + + // Resizes the given source bitmap using the specified resize method, so that + // the entire image is (dest_size) big. The dest_subset is the rectangle in + // this destination image that should actually be returned. + // + // The output image will be (dest_subset.width(), dest_subset.height()). This + // will save work if you do not need the entire bitmap. + // + // The destination subset must be smaller than the destination image. + // + // Note that color space is not taken into account, and so callers wanting + // color-correct results should provide a source bitmap that uses a linear + // color space. + static SkBitmap Resize(const SkPixmap& source, + ResizeMethod method, + int dest_width, + int dest_height, + const SkIRect& dest_subset, + SkBitmap::Allocator* allocator = NULL); + static SkBitmap Resize(const SkBitmap& source, + ResizeMethod method, + int dest_width, int dest_height, + const SkIRect& dest_subset, + SkBitmap::Allocator* allocator = NULL); + + // Alternate version for resizing and returning the entire bitmap rather than + // a subset. + static SkBitmap Resize(const SkBitmap& source, + ResizeMethod method, + int dest_width, int dest_height, + SkBitmap::Allocator* allocator = NULL); + + private: + ImageOperations(); // Class for scoping only. +}; + +} // namespace skia + +#endif // SKIA_EXT_IMAGE_OPERATIONS_H_ diff --git a/ext/image_operations_bench.cc b/ext/image_operations_bench.cc new file mode 100644 index 000000000000..6f9035a56711 --- /dev/null +++ b/ext/image_operations_bench.cc @@ -0,0 +1,291 @@ +// Copyright 2011 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// This small program is used to measure the performance of the various +// resize algorithms offered by the ImageOperations::Resize function. +// It will generate an empty source bitmap, and rescale it to specified +// dimensions. It will repeat this operation multiple time to get more accurate +// average throughput. Because it uses elapsed time to do its math, it is only +// accurate on an idle system (but that approach was deemed more accurate +// than the use of the times() call. +// To present a single number in MB/s, it calculates the 'speed' by taking +// source surface + destination surface and dividing by the elapsed time. +// This number is somewhat reasonable way to measure this, given our current +// implementation which somewhat scales this way. + +#include +#include +#include + +#include "base/command_line.h" +#include "base/format_macros.h" +#include "base/strings/string_number_conversions.h" +#include "base/strings/string_split.h" +#include "base/strings/string_util.h" +#include "base/strings/utf_string_conversions.h" +#include "base/time/time.h" +#include "build/build_config.h" +#include "skia/ext/image_operations.h" +#include "third_party/skia/include/core/SkBitmap.h" +#include "third_party/skia/include/core/SkRect.h" + +namespace { + +struct StringMethodPair { + const char* name; + skia::ImageOperations::ResizeMethod method; +}; +#define ADD_METHOD(x) { #x, skia::ImageOperations::RESIZE_##x } +const StringMethodPair resize_methods[] = { + ADD_METHOD(GOOD), + ADD_METHOD(BETTER), + ADD_METHOD(BEST), + ADD_METHOD(BOX), + ADD_METHOD(HAMMING1), + ADD_METHOD(LANCZOS3), +}; + +// converts a string into one of the image operation method to resize. +// Returns true on success, false otherwise. +bool StringToMethod(const std::string& arg, + skia::ImageOperations::ResizeMethod* method) { + for (size_t i = 0; i < std::size(resize_methods); ++i) { + if (base::EqualsCaseInsensitiveASCII(arg, resize_methods[i].name)) { + *method = resize_methods[i].method; + return true; + } + } + return false; +} + +const char* MethodToString(skia::ImageOperations::ResizeMethod method) { + for (size_t i = 0; i < std::size(resize_methods); ++i) { + if (method == resize_methods[i].method) { + return resize_methods[i].name; + } + } + return "unknown"; +} + +// Prints all supported resize methods +void PrintMethods() { + bool print_comma = false; + for (size_t i = 0; i < std::size(resize_methods); ++i) { + if (print_comma) { + printf(","); + } else { + print_comma = true; + } + printf(" %s", resize_methods[i].name); + } +} + +// Returns the number of bytes that the bitmap has. This number is different +// from what SkBitmap::getSize() returns since it does not take into account +// the stride. The difference between the stride and the width can be large +// because of the alignment constraints on bitmaps created for SRB scaling +// (32 pixels) as seen on GTV platforms. Using this metric instead of the +// getSize seemed to be a more accurate representation of the work done (even +// though in terms of memory bandwidth that might be similar because of the +// cache line size). +int GetBitmapSize(const SkBitmap* bitmap) { + return bitmap->height() * bitmap->bytesPerPixel() * bitmap->width(); +} + +// Simple class to represent dimensions of a bitmap (width, height). +class Dimensions { + public: + Dimensions() + : width_(0), + height_(0) {} + + void set(int w, int h) { + width_ = w; + height_ = h; + } + + int width() const { + return width_; + } + + int height() const { + return height_; + } + + bool IsValid() const { + return (width_ > 0 && height_ > 0); + } + + // On failure, will set its state in such a way that IsValid will return + // false. + void FromString(const std::string& arg) { + std::vector strings = base::SplitStringPiece( + arg, "x", base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL); + if (strings.size() != 2 || + base::StringToInt(strings[0], &width_) == false || + base::StringToInt(strings[1], &height_) == false) { + width_ = -1; // force the dimension object to be invalid. + } + } + private: + int width_; + int height_; +}; + +// main class used for the benchmarking. +class Benchmark { + public: + static const int kDefaultNumberIterations; + static const skia::ImageOperations::ResizeMethod kDefaultResizeMethod; + + Benchmark() + : num_iterations_(kDefaultNumberIterations), + method_(kDefaultResizeMethod) {} + + // Returns true if command line parsing was successful, false otherwise. + bool ParseArgs(const base::CommandLine* command_line); + + // Returns true if successful, false otherwise. + bool Run() const; + + static void Usage(); + private: + int num_iterations_; + skia::ImageOperations::ResizeMethod method_; + Dimensions source_; + Dimensions dest_; +}; + +// static +const int Benchmark::kDefaultNumberIterations = 1024; +const skia::ImageOperations::ResizeMethod Benchmark::kDefaultResizeMethod = + skia::ImageOperations::RESIZE_LANCZOS3; + +// argument management +void Benchmark::Usage() { + printf("image_operations_bench -source wxh -destination wxh " + "[-iterations i] [-method m] [-help]\n" + " -source wxh: specify source width and height\n" + " -destination wxh: specify destination width and height\n" + " -iter i: perform i iterations (default:%d)\n" + " -method m: use method m (default:%s), which can be:", + Benchmark::kDefaultNumberIterations, + MethodToString(Benchmark::kDefaultResizeMethod)); + PrintMethods(); + printf("\n -help: prints this help and exits\n"); +} + +bool Benchmark::ParseArgs(const base::CommandLine* command_line) { + const base::CommandLine::SwitchMap& switches = command_line->GetSwitches(); + bool fNeedHelp = false; + + for (base::CommandLine::SwitchMap::const_iterator iter = switches.begin(); + iter != switches.end(); + ++iter) { + const std::string& s = iter->first; + std::string value; +#if BUILDFLAG(IS_WIN) + value = base::WideToUTF8(iter->second); +#else + value = iter->second; +#endif + if (s == "source") { + source_.FromString(value); + } else if (s == "destination") { + dest_.FromString(value); + } else if (s == "iterations") { + if (base::StringToInt(value, &num_iterations_) == false) { + fNeedHelp = true; + } + } else if (s == "method") { + if (!StringToMethod(value, &method_)) { + printf("Invalid method '%s' specified\n", value.c_str()); + fNeedHelp = true; + } + } else { + fNeedHelp = true; + } + } + + if (num_iterations_ <= 0) { + printf("Invalid number of iterations: %d\n", num_iterations_); + fNeedHelp = true; + } + if (!source_.IsValid()) { + printf("Invalid source dimensions specified\n"); + fNeedHelp = true; + } + if (!dest_.IsValid()) { + printf("Invalid dest dimensions specified\n"); + fNeedHelp = true; + } + if (fNeedHelp == true) { + return false; + } + return true; +} + +// actual benchmark. +bool Benchmark::Run() const { + SkBitmap source; + source.allocN32Pixels(source_.width(), source_.height()); + source.eraseARGB(0, 0, 0, 0); + + SkBitmap dest; + + const base::TimeTicks start = base::TimeTicks::Now(); + + for (int i = 0; i < num_iterations_; ++i) { + dest = skia::ImageOperations::Resize(source, + method_, + dest_.width(), dest_.height()); + } + + const int64_t elapsed_us = (base::TimeTicks::Now() - start).InMicroseconds(); + + const uint64_t num_bytes = static_cast(num_iterations_) * + (GetBitmapSize(&source) + GetBitmapSize(&dest)); + + printf("%" PRIu64 " MB/s,\telapsed = %" PRIu64 " source=%d dest=%d\n", + static_cast(elapsed_us == 0 ? 0 : num_bytes / elapsed_us), + static_cast(elapsed_us), GetBitmapSize(&source), + GetBitmapSize(&dest)); + + return true; +} + +// A small class to automatically call Reset on the global command line to +// avoid nasty valgrind complaints for the leak of the global command line. +class CommandLineAutoReset { + public: + CommandLineAutoReset(int argc, char** argv) { + base::CommandLine::Init(argc, argv); + } + ~CommandLineAutoReset() { + base::CommandLine::Reset(); + } + + const base::CommandLine* Get() const { + return base::CommandLine::ForCurrentProcess(); + } +}; + +} // namespace + +int main(int argc, char** argv) { + Benchmark bench; + CommandLineAutoReset command_line(argc, argv); + + if (!bench.ParseArgs(command_line.Get())) { + Benchmark::Usage(); + return 1; + } + + if (!bench.Run()) { + printf("Failed to run benchmark\n"); + return 1; + } + + return 0; +} diff --git a/ext/image_operations_unittest.cc b/ext/image_operations_unittest.cc new file mode 100644 index 000000000000..b3a1ef50c6e3 --- /dev/null +++ b/ext/image_operations_unittest.cc @@ -0,0 +1,581 @@ +// Copyright 2012 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "skia/ext/image_operations.h" + +#include +#include + +#include +#include +#include +#include + +#include "base/compiler_specific.h" +#include "base/files/file_util.h" +#include "base/numerics/math_constants.h" +#include "base/strings/string_util.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "third_party/skia/include/core/SkBitmap.h" +#include "third_party/skia/include/core/SkColorSpace.h" +#include "third_party/skia/include/core/SkImageInfo.h" +#include "third_party/skia/include/core/SkRect.h" +#include "ui/gfx/codec/png_codec.h" +#include "ui/gfx/geometry/size.h" + +namespace { + +// Computes the average pixel value for the given range, inclusive. +uint32_t AveragePixel(const SkBitmap& bmp, + int x_min, int x_max, + int y_min, int y_max) { + float accum[4] = {0, 0, 0, 0}; + int count = 0; + for (int y = y_min; y <= y_max; y++) { + for (int x = x_min; x <= x_max; x++) { + uint32_t cur = *bmp.getAddr32(x, y); + accum[0] += SkColorGetB(cur); + accum[1] += SkColorGetG(cur); + accum[2] += SkColorGetR(cur); + accum[3] += SkColorGetA(cur); + count++; + } + } + + return SkColorSetARGB(static_cast(accum[3] / count), + static_cast(accum[2] / count), + static_cast(accum[1] / count), + static_cast(accum[0] / count)); +} + +// Computes the average pixel (/color) value for the given colors. +SkColor AveragePixel(const SkColor colors[], size_t color_count) { + float accum[4] = { 0.0f, 0.0f, 0.0f, 0.0f }; + for (size_t i = 0; i < color_count; ++i) { + const SkColor cur = colors[i]; + accum[0] += static_cast(SkColorGetA(cur)); + accum[1] += static_cast(SkColorGetR(cur)); + accum[2] += static_cast(SkColorGetG(cur)); + accum[3] += static_cast(SkColorGetB(cur)); + } + const SkColor average_color = + SkColorSetARGB(static_cast(accum[0] / color_count), + static_cast(accum[1] / color_count), + static_cast(accum[2] / color_count), + static_cast(accum[3] / color_count)); + return average_color; +} + +void PrintPixel(const SkBitmap& bmp, + int x_min, int x_max, + int y_min, int y_max) { + char str[128]; + + for (int y = y_min; y <= y_max; ++y) { + for (int x = x_min; x <= x_max; ++x) { + const uint32_t cur = *bmp.getAddr32(x, y); + base::snprintf(str, sizeof(str), "bmp[%d,%d] = %08X", x, y, cur); + ADD_FAILURE() << str; + } + } +} + +// Returns the euclidian distance between two RGBA colors interpreted +// as 4-components vectors. +// +// Notes: +// - This is a really poor definition of color distance. Yet it +// is "good enough" for our uses here. +// - More realistic measures like the various Delta E formulas defined +// by CIE are way more complex and themselves require the RGBA to +// to transformed into CIELAB (typically via sRGB first). +// - The static_cast below are needed to avoid interpreting "negative" +// differences as huge positive values. +float ColorsEuclidianDistance(const SkColor a, const SkColor b) { + int b_int_diff = static_cast(SkColorGetB(a) - SkColorGetB(b)); + int g_int_diff = static_cast(SkColorGetG(a) - SkColorGetG(b)); + int r_int_diff = static_cast(SkColorGetR(a) - SkColorGetR(b)); + int a_int_diff = static_cast(SkColorGetA(a) - SkColorGetA(b)); + + float b_float_diff = static_cast(b_int_diff); + float g_float_diff = static_cast(g_int_diff); + float r_float_diff = static_cast(r_int_diff); + float a_float_diff = static_cast(a_int_diff); + + return sqrtf((b_float_diff * b_float_diff) + (g_float_diff * g_float_diff) + + (r_float_diff * r_float_diff) + (a_float_diff * a_float_diff)); +} + +// Returns true if each channel of the given two colors are "close." This is +// used for comparing colors where rounding errors may cause off-by-one. +bool ColorsClose(uint32_t a, uint32_t b) { + return abs(static_cast(SkColorGetB(a) - SkColorGetB(b))) < 2 && + abs(static_cast(SkColorGetG(a) - SkColorGetG(b))) < 2 && + abs(static_cast(SkColorGetR(a) - SkColorGetR(b))) < 2 && + abs(static_cast(SkColorGetA(a) - SkColorGetA(b))) < 2; +} + +void FillDataToBitmap(int w, int h, SkBitmap* bmp) { + bmp->allocPixels(SkImageInfo::MakeN32Premul(w, h, SkColorSpace::MakeSRGB())); + + for (int y = 0; y < h; ++y) { + for (int x = 0; x < w; ++x) { + const uint8_t component = static_cast(y * w + x); + const SkColor pixel = SkColorSetARGB(component, component, + component, component); + *bmp->getAddr32(x, y) = pixel; + } + } +} + +// Draws a checkerboard pattern into the w x h bitmap passed in. +// Each rectangle is rect_w in width, rect_h in height. +// The colors alternate between color1 and color2, color1 being used +// in the rectangle at the top left corner. +void DrawCheckerToBitmap(int w, int h, + SkColor color1, SkColor color2, + int rect_w, int rect_h, + SkBitmap* bmp) { + ASSERT_GT(rect_w, 0); + ASSERT_GT(rect_h, 0); + ASSERT_NE(color1, color2); + + bmp->allocPixels(SkImageInfo::MakeN32Premul(w, h, SkColorSpace::MakeSRGB())); + + for (int y = 0; y < h; ++y) { + bool y_bit = (((y / rect_h) & 0x1) == 0); + + for (int x = 0; x < w; ++x) { + bool x_bit = (((x / rect_w) & 0x1) == 0); + + bool use_color2 = (x_bit != y_bit); // xor + + *bmp->getAddr32(x, y) = (use_color2 ? color2 : color1); + } + } +} + +// DEBUG_BITMAP_GENERATION (0 or 1) controls whether the routines +// to save the test bitmaps are present. By default the test just fails +// without reading/writing files but it is then convenient to have +// a simple way to make the failing tests write out the input/output images +// to check them visually. +#define DEBUG_BITMAP_GENERATION (0) + +#if DEBUG_BITMAP_GENERATION +void SaveBitmapToPNG(const SkBitmap& bmp, const char* path) { + std::vector png; + gfx::PNGCodec::ColorFormat color_format = gfx::PNGCodec::FORMAT_RGBA; + if (!gfx::PNGCodec::Encode( + reinterpret_cast(bmp.getPixels()), + color_format, gfx::Size(bmp.width(), bmp.height()), + static_cast(bmp.rowBytes()), + false, std::vector(), &png)) { + FAIL() << "Failed to encode image"; + } + + const base::FilePath fpath(path); + if (!base::WriteFile(fpath, png)) { + FAIL() << "Failed to write dest \"" << path << '"'; + } +} +#endif // #if DEBUG_BITMAP_GENERATION + +void CheckResampleToSame(skia::ImageOperations::ResizeMethod method) { + // Make our source bitmap. + const int src_w = 16, src_h = 34; + SkBitmap src; + FillDataToBitmap(src_w, src_h, &src); + + // Do a resize of the full bitmap to the same size. The lanczos filter is good + // enough that we should get exactly the same image for output. + SkBitmap results = skia::ImageOperations::Resize(src, method, src_w, src_h); + ASSERT_EQ(src_w, results.width()); + ASSERT_EQ(src_h, results.height()); + EXPECT_TRUE(results.colorSpace() && results.colorSpace()->isSRGB()); + + for (int y = 0; y < src_h; y++) { + for (int x = 0; x < src_w; x++) { + EXPECT_EQ(*src.getAddr32(x, y), *results.getAddr32(x, y)); + } + } +} + +// Types defined outside of the ResizeShouldAverageColors test to allow +// use of the std::size() macro. +// +// 'max_color_distance_override' is used in a max() call together with +// the value of 'max_color_distance' defined in a TestedPixel instance. +// Hence a value of 0.0 in 'max_color_distance_override' means +// "use the pixel-specific value" and larger values can be used to allow +// worse computation errors than provided in a TestedPixel instance. +struct TestedResizeMethod { + skia::ImageOperations::ResizeMethod method; + const char* name; + float max_color_distance_override; +}; + +struct TestedPixel { + int x; + int y; + float max_color_distance; + const char* name; +}; + +// Helper function used by the test "ResizeShouldAverageColors" below. +// Note that ASSERT_EQ does a "return;" on failure, hence we can't have +// a "bool" return value to reflect success. Hence "all_pixels_pass" +void CheckResizeMethodShouldAverageGrid( + const SkBitmap& src, + const TestedResizeMethod& tested_method, + int dest_w, int dest_h, SkColor average_color, + bool* method_passed) { + *method_passed = false; + + const TestedPixel tested_pixels[] = { + // Corners + { 0, 0, 2.3f, "Top left corner" }, + { 0, dest_h - 1, 2.3f, "Bottom left corner" }, + { dest_w - 1, 0, 2.3f, "Top right corner" }, + { dest_w - 1, dest_h - 1, 2.3f, "Bottom right corner" }, + // Middle points of each side + { dest_w / 2, 0, 1.0f, "Top middle" }, + { dest_w / 2, dest_h - 1, 1.0f, "Bottom middle" }, + { 0, dest_h / 2, 1.0f, "Left middle" }, + { dest_w - 1, dest_h / 2, 1.0f, "Right middle" }, + // Center + { dest_w / 2, dest_h / 2, 1.0f, "Center" } + }; + + // Resize the src + const skia::ImageOperations::ResizeMethod method = tested_method.method; + + SkBitmap dest = skia::ImageOperations::Resize(src, method, dest_w, dest_h); + ASSERT_EQ(dest_w, dest.width()); + ASSERT_EQ(dest_h, dest.height()); + EXPECT_TRUE(dest.colorSpace() && dest.colorSpace()->isSRGB()); + + // Check that pixels match the expected average. + float max_observed_distance = 0.0f; + bool all_pixels_ok = true; + + for (size_t pixel_index = 0; pixel_index < std::size(tested_pixels); + ++pixel_index) { + const TestedPixel& tested_pixel = tested_pixels[pixel_index]; + + const int x = tested_pixel.x; + const int y = tested_pixel.y; + const float max_allowed_distance = + std::max(tested_pixel.max_color_distance, + tested_method.max_color_distance_override); + + const SkColor actual_color = *dest.getAddr32(x, y); + + // Check that the pixels away from the border region are very close + // to the expected average color + float distance = ColorsEuclidianDistance(average_color, actual_color); + + EXPECT_LE(distance, max_allowed_distance) + << "Resizing method: " << tested_method.name + << ", pixel tested: " << tested_pixel.name + << "(" << x << ", " << y << ")" + << std::hex << std::showbase + << ", expected (avg) hex: " << average_color + << ", actual hex: " << actual_color; + + if (distance > max_allowed_distance) { + all_pixels_ok = false; + } + if (distance > max_observed_distance) { + max_observed_distance = distance; + } + } + + if (!all_pixels_ok) { + ADD_FAILURE() << "Maximum observed color distance for method " + << tested_method.name << ": " << max_observed_distance; + +#if DEBUG_BITMAP_GENERATION + char path[128]; + base::snprintf(path, sizeof(path), + "/tmp/ResizeShouldAverageColors_%s_dest.png", + tested_method.name); + SaveBitmapToPNG(dest, path); +#endif // #if DEBUG_BITMAP_GENERATION + } + + *method_passed = all_pixels_ok; +} + + +} // namespace + +// Helper tests that saves bitmaps to PNGs in /tmp/ to visually check +// that the bitmap generation functions work as expected. +// Those tests are not enabled by default as verification is done +// manually/visually, however it is convenient to leave the functions +// in place. +#if 0 && DEBUG_BITMAP_GENERATION +TEST(ImageOperations, GenerateGradientBitmap) { + // Make our source bitmap. + const int src_w = 640, src_h = 480; + SkBitmap src; + FillDataToBitmap(src_w, src_h, &src); + + SaveBitmapToPNG(src, "/tmp/gradient_640x480.png"); +} + +TEST(ImageOperations, GenerateGridBitmap) { + const int src_w = 640, src_h = 480, src_grid_pitch = 10, src_grid_width = 4; + const SkColor grid_color = SK_ColorRED, background_color = SK_ColorBLUE; + SkBitmap src; + DrawGridToBitmap(src_w, src_h, + background_color, grid_color, + src_grid_pitch, src_grid_width, + &src); + + SaveBitmapToPNG(src, "/tmp/grid_640x408_10_4_red_blue.png"); +} + +TEST(ImageOperations, GenerateCheckerBitmap) { + const int src_w = 640, src_h = 480, rect_w = 10, rect_h = 4; + const SkColor color1 = SK_ColorRED, color2 = SK_ColorBLUE; + SkBitmap src; + DrawCheckerToBitmap(src_w, src_h, color1, color2, rect_w, rect_h, &src); + + SaveBitmapToPNG(src, "/tmp/checker_640x408_10_4_red_blue.png"); +} +#endif // #if ... && DEBUG_BITMAP_GENERATION + +// Makes the bitmap 50% the size as the original using a box filter. This is +// an easy operation that we can check the results for manually. +TEST(ImageOperations, Halve) { + // Make our source bitmap. + int src_w = 30, src_h = 38; + SkBitmap src; + FillDataToBitmap(src_w, src_h, &src); + + // Do a halving of the full bitmap. + SkBitmap actual_results = skia::ImageOperations::Resize( + src, skia::ImageOperations::RESIZE_BOX, src_w / 2, src_h / 2); + ASSERT_EQ(src_w / 2, actual_results.width()); + ASSERT_EQ(src_h / 2, actual_results.height()); + EXPECT_TRUE(actual_results.colorSpace() && + actual_results.colorSpace()->isSRGB()); + + // Compute the expected values & compare. + for (int y = 0; y < actual_results.height(); y++) { + for (int x = 0; x < actual_results.width(); x++) { + // Note that those expressions take into account the "half-pixel" + // offset that comes into play due to considering the coordinates + // of the center of the pixels. So x * 2 is a simplification + // of ((x+0.5) * 2 - 1) and (x * 2 + 1) is really (x + 0.5) * 2. + int first_x = x * 2; + int last_x = std::min(src_w - 1, x * 2 + 1); + + int first_y = y * 2; + int last_y = std::min(src_h - 1, y * 2 + 1); + + const uint32_t expected_color = AveragePixel(src, + first_x, last_x, + first_y, last_y); + const uint32_t actual_color = *actual_results.getAddr32(x, y); + const bool close = ColorsClose(expected_color, actual_color); + EXPECT_TRUE(close); + if (!close) { + char str[128]; + base::snprintf(str, sizeof(str), + "exp[%d,%d] = %08X, actual[%d,%d] = %08X", + x, y, expected_color, x, y, actual_color); + ADD_FAILURE() << str; + PrintPixel(src, first_x, last_x, first_y, last_y); + } + } + } +} + +TEST(ImageOperations, HalveSubset) { + // Make our source bitmap. + int src_w = 16, src_h = 34; + SkBitmap src; + FillDataToBitmap(src_w, src_h, &src); + + // Do a halving of the full bitmap. + SkBitmap full_results = skia::ImageOperations::Resize( + src, skia::ImageOperations::RESIZE_BOX, src_w / 2, src_h / 2); + ASSERT_EQ(src_w / 2, full_results.width()); + ASSERT_EQ(src_h / 2, full_results.height()); + EXPECT_TRUE(full_results.colorSpace() && full_results.colorSpace()->isSRGB()); + + // Now do a halving of a a subset, recall the destination subset is in the + // destination coordinate system (max = half of the original image size). + SkIRect subset_rect = { 2, 3, 3, 6 }; + SkBitmap subset_results = skia::ImageOperations::Resize( + src, skia::ImageOperations::RESIZE_BOX, + src_w / 2, src_h / 2, subset_rect); + ASSERT_EQ(subset_rect.width(), subset_results.width()); + ASSERT_EQ(subset_rect.height(), subset_results.height()); + EXPECT_TRUE(subset_results.colorSpace() && + subset_results.colorSpace()->isSRGB()); + + // The computed subset and the corresponding subset of the original image + // should be the same. + for (int y = 0; y < subset_rect.height(); y++) { + for (int x = 0; x < subset_rect.width(); x++) { + ASSERT_EQ( + *full_results.getAddr32(x + subset_rect.fLeft, y + subset_rect.fTop), + *subset_results.getAddr32(x, y)); + } + } +} + +TEST(ImageOperations, InvalidParams) { + // Make our source bitmap. + SkBitmap src; + src.allocPixels(SkImageInfo::MakeA8(16, 34)); + + // Scale it, don't die. + SkBitmap full_results = skia::ImageOperations::Resize( + src, skia::ImageOperations::RESIZE_BOX, 10, 20); +} + +// Resamples an image to the same image, it should give the same result. +TEST(ImageOperations, ResampleToSameHamming1) { + CheckResampleToSame(skia::ImageOperations::RESIZE_HAMMING1); +} + +TEST(ImageOperations, ResampleToSameLanczos3) { + CheckResampleToSame(skia::ImageOperations::RESIZE_LANCZOS3); +} + +// Check that all Good/Better/Best, Box, Lanczos2 and Lanczos3 generate purple +// when resizing a 4x8 red/blue checker pattern by 1/16x1/16. +TEST(ImageOperations, ResizeShouldAverageColors) { + // Make our source bitmap. + const int src_w = 640, src_h = 480, checker_rect_w = 4, checker_rect_h = 8; + const SkColor checker_color1 = SK_ColorRED, checker_color2 = SK_ColorBLUE; + + const int dest_w = src_w / (4 * checker_rect_w); + const int dest_h = src_h / (2 * checker_rect_h); + + // Compute the expected (average) color + const SkColor colors[] = { checker_color1, checker_color2 }; + const SkColor average_color = AveragePixel(colors, std::size(colors)); + + static const TestedResizeMethod tested_methods[] = { + { skia::ImageOperations::RESIZE_GOOD, "GOOD", 0.0f }, + { skia::ImageOperations::RESIZE_BETTER, "BETTER", 0.0f }, + { skia::ImageOperations::RESIZE_BEST, "BEST", 0.0f }, + { skia::ImageOperations::RESIZE_BOX, "BOX", 0.0f }, + { skia::ImageOperations::RESIZE_HAMMING1, "HAMMING1", 0.0f }, + { skia::ImageOperations::RESIZE_LANCZOS3, "LANCZOS3", 0.0f }, + }; + + // Create our source bitmap. + SkBitmap src; + DrawCheckerToBitmap(src_w, src_h, + checker_color1, checker_color2, + checker_rect_w, checker_rect_h, + &src); + + // For each method, downscale by 16 in each dimension, + // and check each tested pixel against the expected average color. + bool all_methods_ok = true; + + for (size_t method_index = 0; method_index < std::size(tested_methods); + ++method_index) { + bool pass = true; + CheckResizeMethodShouldAverageGrid(src, + tested_methods[method_index], + dest_w, dest_h, average_color, + &pass); + if (!pass) { + all_methods_ok = false; + } + } + + if (!all_methods_ok) { +#if DEBUG_BITMAP_GENERATION + SaveBitmapToPNG(src, "/tmp/ResizeShouldAverageColors_src.png"); +#endif // #if DEBUG_BITMAP_GENERATION + } +} + +static double sinc(double x) { + if (x == 0.0) return 1.0; + x *= base::kPiDouble; + return sin(x) / x; +} + +static double lanczos3(double offset) { + if (fabs(offset) >= 3) return 0.0; + return sinc(offset) * sinc(offset / 3.0); +} + +TEST(ImageOperations, ScaleUp) { + const int src_w = 3; + const int src_h = 3; + const int dst_w = 9; + const int dst_h = 9; + SkBitmap src; + src.allocPixels( + SkImageInfo::MakeN32Premul(src_w, src_h, SkColorSpace::MakeSRGB())); + + for (int src_y = 0; src_y < src_h; ++src_y) { + for (int src_x = 0; src_x < src_w; ++src_x) { + *src.getAddr32(src_x, src_y) = + SkColorSetARGB(255, 10 + src_x * 100, 10 + src_y * 100, 0); + } + } + + SkBitmap dst = skia::ImageOperations::Resize( + src, + skia::ImageOperations::RESIZE_LANCZOS3, + dst_w, dst_h); + EXPECT_TRUE(dst.colorSpace() && dst.colorSpace()->isSRGB()); + for (int dst_y = 0; dst_y < dst_h; ++dst_y) { + for (int dst_x = 0; dst_x < dst_w; ++dst_x) { + float dst_x_in_src = (dst_x + 0.5) * src_w / dst_w; + float dst_y_in_src = (dst_y + 0.5) * src_h / dst_h; + float a = 0.0f; + float r = 0.0f; + float g = 0.0f; + float b = 0.0f; + float sum = 0.0f; + for (int src_y = 0; src_y < src_h; ++src_y) { + for (int src_x = 0; src_x < src_w; ++src_x) { + double coeff = + lanczos3(src_x + 0.5 - dst_x_in_src) * + lanczos3(src_y + 0.5 - dst_y_in_src); + sum += coeff; + SkColor tmp = *src.getAddr32(src_x, src_y); + a += coeff * SkColorGetA(tmp); + r += coeff * SkColorGetR(tmp); + g += coeff * SkColorGetG(tmp); + b += coeff * SkColorGetB(tmp); + } + } + a /= sum; + r /= sum; + g /= sum; + b /= sum; + if (a < 0.0f) a = 0.0f; + if (r < 0.0f) r = 0.0f; + if (g < 0.0f) g = 0.0f; + if (b < 0.0f) b = 0.0f; + if (a > 255.0f) a = 255.0f; + if (r > 255.0f) r = 255.0f; + if (g > 255.0f) g = 255.0f; + if (b > 255.0f) b = 255.0f; + SkColor dst_color = *dst.getAddr32(dst_x, dst_y); + EXPECT_LE(fabs(SkColorGetA(dst_color) - a), 1.5f); + EXPECT_LE(fabs(SkColorGetR(dst_color) - r), 1.5f); + EXPECT_LE(fabs(SkColorGetG(dst_color) - g), 1.5f); + EXPECT_LE(fabs(SkColorGetB(dst_color) - b), 1.5f); + if (HasFailure()) { + return; + } + } + } +} diff --git a/ext/legacy_display_globals.cc b/ext/legacy_display_globals.cc new file mode 100644 index 000000000000..dc9e6e08dc79 --- /dev/null +++ b/ext/legacy_display_globals.cc @@ -0,0 +1,44 @@ +// Copyright 2020 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "skia/ext/legacy_display_globals.h" + +namespace skia { + +namespace { +SkPixelGeometry g_pixel_geometry = kRGB_H_SkPixelGeometry; +} + +// static +void LegacyDisplayGlobals::SetCachedPixelGeometry( + SkPixelGeometry pixel_geometry) { + g_pixel_geometry = pixel_geometry; +} + +// static +SkPixelGeometry LegacyDisplayGlobals::GetCachedPixelGeometry() { + return g_pixel_geometry; +} + +// static +SkSurfaceProps LegacyDisplayGlobals::GetSkSurfaceProps() { + return GetSkSurfaceProps(0); +} + +// static +SkSurfaceProps LegacyDisplayGlobals::GetSkSurfaceProps(uint32_t flags) { + return SkSurfaceProps{flags, g_pixel_geometry}; +} + +SkSurfaceProps LegacyDisplayGlobals::ComputeSurfaceProps( + bool can_use_lcd_text) { + uint32_t flags = 0; + if (can_use_lcd_text) { + return LegacyDisplayGlobals::GetSkSurfaceProps(flags); + } + // Use unknown pixel geometry to disable LCD text. + return SkSurfaceProps{flags, kUnknown_SkPixelGeometry}; +} + +} // namespace skia diff --git a/ext/legacy_display_globals.h b/ext/legacy_display_globals.h new file mode 100644 index 000000000000..d9bb164cc7cf --- /dev/null +++ b/ext/legacy_display_globals.h @@ -0,0 +1,27 @@ +// Copyright 2020 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef SKIA_EXT_LEGACY_DISPLAY_GLOBALS_H_ +#define SKIA_EXT_LEGACY_DISPLAY_GLOBALS_H_ + +#include "third_party/skia/include/core/SkSurfaceProps.h" + +namespace skia { + +class SK_API LegacyDisplayGlobals { + public: + static void SetCachedPixelGeometry(SkPixelGeometry pixel_geometry); + static SkPixelGeometry GetCachedPixelGeometry(); + + // Returns a SkSurfaceProps with the cached geometry settings. + static SkSurfaceProps GetSkSurfaceProps(); + static SkSurfaceProps GetSkSurfaceProps(uint32_t flags); + + // Will turn off LCD text if |can_use_lcd_text| is false. + static SkSurfaceProps ComputeSurfaceProps(bool can_use_lcd_text); +}; + +} // namespace skia + +#endif // SKIA_EXT_LEGACY_DISPLAY_GLOBALS_H_ diff --git a/ext/opacity_filter_canvas.cc b/ext/opacity_filter_canvas.cc new file mode 100644 index 000000000000..56b4711fd52c --- /dev/null +++ b/ext/opacity_filter_canvas.cc @@ -0,0 +1,70 @@ +// Copyright 2015 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "skia/ext/opacity_filter_canvas.h" +#include "third_party/skia/include/core/SkPaint.h" + +namespace skia { + +OpacityFilterCanvas::OpacityFilterCanvas(SkCanvas* canvas, + float opacity, + bool disable_image_filtering) + : INHERITED(canvas), + opacity_(opacity), + disable_image_filtering_(disable_image_filtering) {} + +bool OpacityFilterCanvas::onFilter(SkPaint& paint) const { + if (opacity_ < 1.f) + paint.setAlphaf(paint.getAlphaf() * opacity_); + + return true; +} + +void OpacityFilterCanvas::onDrawImage2(const SkImage* image, + SkScalar dx, + SkScalar dy, + const SkSamplingOptions& sampling, + const SkPaint* paint) { + this->INHERITED::onDrawImage2( + image, dx, dy, disable_image_filtering_ ? SkSamplingOptions() : sampling, + paint); +} + +void OpacityFilterCanvas::onDrawImageRect2(const SkImage* image, + const SkRect& src, + const SkRect& dst, + const SkSamplingOptions& sampling, + const SkPaint* paint, + SrcRectConstraint constraint) { + this->INHERITED::onDrawImageRect2( + image, src, dst, + disable_image_filtering_ ? SkSamplingOptions() : sampling, paint, + constraint); +} + +void OpacityFilterCanvas::onDrawEdgeAAImageSet2( + const ImageSetEntry imageSet[], + int count, + const SkPoint dstClips[], + const SkMatrix preViewMatrices[], + const SkSamplingOptions& sampling, + const SkPaint* paint, + SrcRectConstraint constraint) { + this->INHERITED::onDrawEdgeAAImageSet2( + imageSet, count, dstClips, preViewMatrices, + disable_image_filtering_ ? SkSamplingOptions() : sampling, paint, + constraint); +} + +void OpacityFilterCanvas::onDrawPicture(const SkPicture* picture, + const SkMatrix* matrix, + const SkPaint* paint) { + SkPaint filteredPaint(paint ? *paint : SkPaint()); + if (this->onFilter(filteredPaint)) { + // Unfurl pictures in order to filter nested paints. + this->SkCanvas::onDrawPicture(picture, matrix, &filteredPaint); + } +} + +} // namespace skia diff --git a/ext/opacity_filter_canvas.h b/ext/opacity_filter_canvas.h new file mode 100644 index 000000000000..84cd13ab539e --- /dev/null +++ b/ext/opacity_filter_canvas.h @@ -0,0 +1,57 @@ +// Copyright 2015 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef SKIA_EXT_OPACITY_FILTER_CANVAS_H_ +#define SKIA_EXT_OPACITY_FILTER_CANVAS_H_ + +#include "third_party/skia/include/utils/SkPaintFilterCanvas.h" + +namespace skia { + +// This filter canvas allows setting an opacity on every draw call to a canvas, +// and to disable image filtering. Note that the opacity setting is only +// correct in very limited conditions: when there is only zero or one opaque, +// nonlayer draw for every pixel in the surface. +class SK_API OpacityFilterCanvas : public SkPaintFilterCanvas { + public: + OpacityFilterCanvas(SkCanvas* canvas, + float opacity, + bool disable_image_filtering); + + protected: + bool onFilter(SkPaint& paint) const override; + + void onDrawImage2(const SkImage*, + SkScalar dx, + SkScalar dy, + const SkSamplingOptions&, + const SkPaint*) override; + void onDrawImageRect2(const SkImage*, + const SkRect& src, + const SkRect& dst, + const SkSamplingOptions&, + const SkPaint*, + SrcRectConstraint) override; + void onDrawEdgeAAImageSet2(const ImageSetEntry imageSet[], + int count, + const SkPoint dstClips[], + const SkMatrix preViewMatrices[], + const SkSamplingOptions&, + const SkPaint*, + SrcRectConstraint) override; + + void onDrawPicture(const SkPicture* picture, + const SkMatrix* matrix, + const SkPaint* paint) override; + + private: + typedef SkPaintFilterCanvas INHERITED; + + float opacity_; + bool disable_image_filtering_; +}; + +} // namespace skia + +#endif // SKIA_EXT_OPACITY_FILTER_CANVAS_H_ diff --git a/ext/platform_canvas.cc b/ext/platform_canvas.cc new file mode 100644 index 000000000000..b323e591cfa2 --- /dev/null +++ b/ext/platform_canvas.cc @@ -0,0 +1,71 @@ +// Copyright 2011 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "skia/ext/platform_canvas.h" + +#include "base/check.h" +#include "base/memory/ptr_util.h" +#include "build/build_config.h" +#include "third_party/skia/include/core/SkTypes.h" + +namespace skia { + +SkBitmap ReadPixels(SkCanvas* canvas) { + SkBitmap bitmap; + bitmap.allocPixels(canvas->imageInfo()); + if (!canvas->readPixels(bitmap, 0, 0)) + bitmap.reset(); + return bitmap; +} + +bool GetWritablePixels(SkCanvas* canvas, SkPixmap* result) { + if (!canvas || !result) { + return false; + } + + SkImageInfo info; + size_t row_bytes; + void* pixels = canvas->accessTopLayerPixels(&info, &row_bytes); + if (!pixels) { + result->reset(); + return false; + } + + result->reset(info, pixels, row_bytes); + return true; +} + +#if !defined(WIN32) + +std::unique_ptr CreatePlatformCanvasWithPixels( + int width, + int height, + bool is_opaque, + uint8_t* data, + OnFailureType failureType) { + + SkBitmap bitmap; + bitmap.setInfo(SkImageInfo::MakeN32(width, height, + is_opaque ? kOpaque_SkAlphaType : kPremul_SkAlphaType)); + + if (data) { + bitmap.setPixels(data); + } else { + if (!bitmap.tryAllocPixels()) { + CHECK(failureType != CRASH_ON_FAILURE); + return nullptr; + } + + // Follow the logic in SkCanvas::createDevice(), initialize the bitmap if + // it is not opaque. + if (!is_opaque) + bitmap.eraseARGB(0, 0, 0, 0); + } + + return std::make_unique(bitmap); +} + +#endif + +} // namespace skia diff --git a/ext/platform_canvas.h b/ext/platform_canvas.h new file mode 100644 index 000000000000..34d8b858f5f0 --- /dev/null +++ b/ext/platform_canvas.h @@ -0,0 +1,114 @@ +// Copyright 2011 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef SKIA_EXT_PLATFORM_CANVAS_H_ +#define SKIA_EXT_PLATFORM_CANVAS_H_ + +#include +#include + +#include "build/build_config.h" + +#if BUILDFLAG(IS_WIN) +#include +#endif + +// The platform-specific device will include the necessary platform headers +// to get the surface type. + +#include "third_party/skia/include/core/SkBitmap.h" +#include "third_party/skia/include/core/SkCanvas.h" + +// A PlatformCanvas is a software-rasterized SkCanvas which is *also* +// addressable by the platform-specific drawing API (GDI, Core Graphics, +// Cairo...). + +namespace skia { + +// +// Note about error handling. +// +// Creating a canvas can fail at times, most often because we fail to allocate +// the backing-store (pixels). This can be from out-of-memory, or something +// more opaque, like GDI or cairo reported a failure. +// +// To allow the caller to handle the failure, every Create... factory takes an +// enum as its last parameter. The default value is kCrashOnFailure. If the +// caller passes kReturnNullOnFailure, then the caller is responsible to check +// the return result. +// +enum OnFailureType { + CRASH_ON_FAILURE, + RETURN_NULL_ON_FAILURE +}; + +#if defined(WIN32) + // The shared_section parameter is passed to gfx::PlatformDevice::create. + // See it for details. +SK_API std::unique_ptr CreatePlatformCanvasWithSharedSection( + int width, + int height, + bool is_opaque, + HANDLE shared_section, + OnFailureType failure_type); + +// Returns the NativeDrawingContext to use for native platform drawing calls. +SK_API HDC GetNativeDrawingContext(SkCanvas* canvas); + +#elif defined(__linux__) || defined(__FreeBSD__) || defined(__OpenBSD__) || \ + defined(__sun) || defined(ANDROID) || defined(__APPLE__) || \ + defined(__Fuchsia__) +// Construct a canvas from the given memory region. The memory is not cleared +// first. @data must be, at least, @height * StrideForWidth(@width) bytes. +SK_API std::unique_ptr CreatePlatformCanvasWithPixels( + int width, + int height, + bool is_opaque, + uint8_t* data, + OnFailureType failure_type); +#endif + +inline std::unique_ptr CreatePlatformCanvas(int width, + int height, + bool is_opaque) { +#if defined(WIN32) + return CreatePlatformCanvasWithSharedSection(width, height, is_opaque, 0, + CRASH_ON_FAILURE); +#else + return CreatePlatformCanvasWithPixels(width, height, is_opaque, nullptr, + CRASH_ON_FAILURE); +#endif +} + +inline std::unique_ptr TryCreateBitmapCanvas(int width, + int height, + bool is_opaque) { +#if defined(WIN32) + return CreatePlatformCanvasWithSharedSection(width, height, is_opaque, 0, + RETURN_NULL_ON_FAILURE); +#else + return CreatePlatformCanvasWithPixels(width, height, is_opaque, nullptr, + RETURN_NULL_ON_FAILURE); +#endif +} + +// Copies pixels from the SkCanvas into an SkBitmap, fetching pixels from +// GPU memory if necessary. +// +// The bitmap will remain empty if we can't allocate enough memory for a copy +// of the pixels. +SK_API SkBitmap ReadPixels(SkCanvas* canvas); + +// Gives the pixmap passed in *writable* access to the pixels backing this +// canvas. All writes to the pixmap should be visible if the canvas is +// raster-backed. +// +// Returns false on failure: if either argument is nullptr, or if the +// pixels can not be retrieved from the canvas. In the latter case resets +// the pixmap to empty. +SK_API bool GetWritablePixels(SkCanvas* canvas, SkPixmap* pixmap); + +} // namespace skia + +#endif // SKIA_EXT_PLATFORM_CANVAS_H_ diff --git a/ext/platform_canvas_unittest.cc b/ext/platform_canvas_unittest.cc new file mode 100644 index 000000000000..4b7aec936870 --- /dev/null +++ b/ext/platform_canvas_unittest.cc @@ -0,0 +1,369 @@ +// Copyright 2012 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// TODO(awalker): clean up the const/non-const reference handling in this test + +#include "skia/ext/platform_canvas.h" + +#include + +#include "base/memory/raw_ref.h" +#include "build/build_config.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "third_party/skia/include/core/SkBitmap.h" +#include "third_party/skia/include/core/SkCanvas.h" +#include "third_party/skia/include/core/SkColor.h" +#include "third_party/skia/include/core/SkColorPriv.h" +#include "third_party/skia/include/core/SkPixelRef.h" + +// Native drawing context is only used/supported on Windows. +#if BUILDFLAG(IS_WIN) + +namespace skia { + +namespace { + +// Uses DstATop transfer mode with SK_ColorBLACK 0xff000000: +// the destination pixel will end up with 0xff alpha +// if it was transparent black (0x0) before the blend, +// it will be set to opaque black (0xff000000). +// if it has nonzero alpha before the blend, +// it will retain its color. +void MakeOpaque(SkCanvas* canvas, int x, int y, int width, int height) { + if (width <= 0 || height <= 0) + return; + + SkRect rect; + rect.setXYWH(SkIntToScalar(x), SkIntToScalar(y), + SkIntToScalar(width), SkIntToScalar(height)); + SkPaint paint; + paint.setColor(SK_ColorBLACK); + paint.setBlendMode(SkBlendMode::kDstATop); + canvas->drawRect(rect, paint); +} + +bool IsOfColor(const SkBitmap& bitmap, int x, int y, uint32_t color) { + // For masking out the alpha values. + static uint32_t alpha_mask = + static_cast(SK_A32_MASK) << SK_A32_SHIFT; + return (*bitmap.getAddr32(x, y) | alpha_mask) == (color | alpha_mask); +} + +// Return true if the canvas is filled to canvas_color, and contains a single +// rectangle filled to rect_color. This function ignores the alpha channel, +// since Windows will sometimes clear the alpha channel when drawing, and we +// will fix that up later in cases it's necessary. +bool VerifyRect(const SkCanvas& canvas, + uint32_t canvas_color, uint32_t rect_color, + int x, int y, int w, int h) { + const SkBitmap bitmap = skia::ReadPixels(const_cast(&canvas)); + + for (int cur_y = 0; cur_y < bitmap.height(); cur_y++) { + for (int cur_x = 0; cur_x < bitmap.width(); cur_x++) { + if (cur_x >= x && cur_x < x + w && + cur_y >= y && cur_y < y + h) { + // Inside the square should be rect_color + if (!IsOfColor(bitmap, cur_x, cur_y, rect_color)) + return false; + } else { + // Outside the square should be canvas_color + if (!IsOfColor(bitmap, cur_x, cur_y, canvas_color)) + return false; + } + } + } + return true; +} + +#if !defined(USE_AURA) && !BUILDFLAG(IS_MAC) +// Return true if canvas has something that passes for a rounded-corner +// rectangle. Basically, we're just checking to make sure that the pixels in the +// middle are of rect_color and pixels in the corners are of canvas_color. +bool VerifyRoundedRect(const SkCanvas& canvas, + uint32_t canvas_color, + uint32_t rect_color, + int x, + int y, + int w, + int h) { + SkPixmap pixmap; + ASSERT_TRUE(canvas.peekPixels(&pixmap)); + SkBitmap bitmap; + bitmap.installPixels(pixmap); + + // Check corner points first. They should be of canvas_color. + if (!IsOfColor(bitmap, x, y, canvas_color)) return false; + if (!IsOfColor(bitmap, x + w, y, canvas_color)) return false; + if (!IsOfColor(bitmap, x, y + h, canvas_color)) return false; + if (!IsOfColor(bitmap, x + w, y, canvas_color)) return false; + + // Check middle points. They should be of rect_color. + if (!IsOfColor(bitmap, (x + w / 2), y, rect_color)) return false; + if (!IsOfColor(bitmap, x, (y + h / 2), rect_color)) return false; + if (!IsOfColor(bitmap, x + w, (y + h / 2), rect_color)) return false; + if (!IsOfColor(bitmap, (x + w / 2), y + h, rect_color)) return false; + + return true; +} +#endif + +// Checks whether there is a white canvas with a black square at the given +// location in pixels (not in the canvas coordinate system). +bool VerifyBlackRect(const SkCanvas& canvas, int x, int y, int w, int h) { + return VerifyRect(canvas, SK_ColorWHITE, SK_ColorBLACK, x, y, w, h); +} + +// Check that every pixel in the canvas is a single color. +bool VerifyCanvasColor(const SkCanvas& canvas, uint32_t canvas_color) { + return VerifyRect(canvas, canvas_color, 0, 0, 0, 0, 0); +} + +void DrawNativeRect(SkCanvas& canvas, int x, int y, int w, int h) { + HDC dc = skia::GetNativeDrawingContext(&canvas); + + RECT inner_rc; + inner_rc.left = x; + inner_rc.top = y; + inner_rc.right = x + w; + inner_rc.bottom = y + h; + FillRect(dc, &inner_rc, reinterpret_cast(GetStockObject(BLACK_BRUSH))); +} + +// Clips the contents of the canvas to the given rectangle. This will be +// intersected with any existing clip. +void AddClip(SkCanvas& canvas, int x, int y, int w, int h) { + SkRect rect; + rect.setXYWH(SkIntToScalar(x), SkIntToScalar(y), SkIntToScalar(w), + SkIntToScalar(h)); + canvas.clipRect(rect); +} + +class LayerSaver { + public: + LayerSaver(SkCanvas& canvas, int x, int y, int w, int h) + : canvas_(canvas), + x_(x), + y_(y), + w_(w), + h_(h) { + SkRect bounds; + bounds.setLTRB(SkIntToScalar(x_), SkIntToScalar(y_), SkIntToScalar(right()), + SkIntToScalar(bottom())); + canvas_->saveLayer(&bounds, NULL); + canvas.clear(SkColorSetARGB(0, 0, 0, 0)); + } + + ~LayerSaver() { canvas_->restore(); } + + int x() const { return x_; } + int y() const { return y_; } + int w() const { return w_; } + int h() const { return h_; } + + // Returns the EXCLUSIVE far bounds of the layer. + int right() const { return x_ + w_; } + int bottom() const { return y_ + h_; } + + private: + const raw_ref canvas_; + int x_, y_, w_, h_; +}; + +// Size used for making layers in many of the below tests. +const int kLayerX = 2; +const int kLayerY = 3; +const int kLayerW = 9; +const int kLayerH = 7; + +// Size used by some tests to draw a rectangle inside the layer. +const int kInnerX = 4; +const int kInnerY = 5; +const int kInnerW = 2; +const int kInnerH = 3; + +} + +// This just checks that our checking code is working properly, it just uses +// regular skia primitives. +TEST(PlatformCanvas, SkLayer) { + // Create the canvas initialized to opaque white. + std::unique_ptr canvas = CreatePlatformCanvas(16, 16, true); + canvas->drawColor(SK_ColorWHITE); + + // Make a layer and fill it completely to make sure that the bounds are + // correct. + { + LayerSaver layer(*canvas, kLayerX, kLayerY, kLayerW, kLayerH); + canvas->drawColor(SK_ColorBLACK); + } + EXPECT_TRUE(VerifyBlackRect(*canvas, kLayerX, kLayerY, kLayerW, kLayerH)); +} + +// Test native clipping. +TEST(PlatformCanvas, ClipRegion) { + // Initialize a white canvas + std::unique_ptr canvas = CreatePlatformCanvas(16, 16, true); + canvas->drawColor(SK_ColorWHITE); + EXPECT_TRUE(VerifyCanvasColor(*canvas, SK_ColorWHITE)); + + // Test that initially the canvas has no clip region, by filling it + // with a black rectangle. + // Note: Don't use LayerSaver, since internally it sets a clip region. + DrawNativeRect(*canvas, 0, 0, 16, 16); + EXPECT_TRUE(VerifyCanvasColor(*canvas, SK_ColorBLACK)); + + // Test that intersecting disjoint clip rectangles sets an empty clip region + canvas->drawColor(SK_ColorWHITE); + EXPECT_TRUE(VerifyCanvasColor(*canvas, SK_ColorWHITE)); + { + LayerSaver layer(*canvas, 0, 0, 16, 16); + AddClip(*canvas, 2, 3, 4, 5); + AddClip(*canvas, 4, 9, 10, 10); + DrawNativeRect(*canvas, 0, 0, 16, 16); + } + EXPECT_TRUE(VerifyCanvasColor(*canvas, SK_ColorWHITE)); +} + +// Test the layers get filled properly by native rendering. +TEST(PlatformCanvas, FillLayer) { + // Create the canvas initialized to opaque white. + std::unique_ptr canvas(CreatePlatformCanvas(16, 16, true)); + + // Make a layer and fill it completely to make sure that the bounds are + // correct. + canvas->drawColor(SK_ColorWHITE); + { + LayerSaver layer(*canvas, kLayerX, kLayerY, kLayerW, kLayerH); + DrawNativeRect(*canvas, 0, 0, 100, 100); + MakeOpaque(canvas.get(), 0, 0, 100, 100); + } + EXPECT_TRUE(VerifyBlackRect(*canvas, kLayerX, kLayerY, kLayerW, kLayerH)); + + // Make a layer and fill it partially to make sure the translation is correct. + canvas->drawColor(SK_ColorWHITE); + { + LayerSaver layer(*canvas, kLayerX, kLayerY, kLayerW, kLayerH); + DrawNativeRect(*canvas, kInnerX, kInnerY, kInnerW, kInnerH); + MakeOpaque(canvas.get(), kInnerX, kInnerY, kInnerW, kInnerH); + } + EXPECT_TRUE(VerifyBlackRect(*canvas, kInnerX, kInnerY, kInnerW, kInnerH)); + + // Add a clip on the layer and fill to make sure clip is correct. + canvas->drawColor(SK_ColorWHITE); + { + LayerSaver layer(*canvas, kLayerX, kLayerY, kLayerW, kLayerH); + canvas->save(); + AddClip(*canvas, kInnerX, kInnerY, kInnerW, kInnerH); + DrawNativeRect(*canvas, 0, 0, 100, 100); + MakeOpaque(canvas.get(), kInnerX, kInnerY, kInnerW, kInnerH); + canvas->restore(); + } + EXPECT_TRUE(VerifyBlackRect(*canvas, kInnerX, kInnerY, kInnerW, kInnerH)); + + // Add a clip and then make the layer to make sure the clip is correct. + canvas->drawColor(SK_ColorWHITE); + canvas->save(); + AddClip(*canvas, kInnerX, kInnerY, kInnerW, kInnerH); + { + LayerSaver layer(*canvas, kLayerX, kLayerY, kLayerW, kLayerH); + DrawNativeRect(*canvas, 0, 0, 100, 100); + MakeOpaque(canvas.get(), 0, 0, 100, 100); + } + canvas->restore(); + EXPECT_TRUE(VerifyBlackRect(*canvas, kInnerX, kInnerY, kInnerW, kInnerH)); +} + +// Test that translation + make layer works properly. +TEST(PlatformCanvas, TranslateLayer) { + // Create the canvas initialized to opaque white. + std::unique_ptr canvas = CreatePlatformCanvas(16, 16, true); + + // Make a layer and fill it completely to make sure that the bounds are + // correct. + canvas->drawColor(SK_ColorWHITE); + canvas->save(); + canvas->translate(1, 1); + { + LayerSaver layer(*canvas, kLayerX, kLayerY, kLayerW, kLayerH); + DrawNativeRect(*canvas, 0, 0, 100, 100); + MakeOpaque(canvas.get(), 0, 0, 100, 100); + } + canvas->restore(); + EXPECT_TRUE(VerifyBlackRect(*canvas, kLayerX + 1, kLayerY + 1, + kLayerW, kLayerH)); + + // Translate then make the layer. + canvas->drawColor(SK_ColorWHITE); + canvas->save(); + canvas->translate(1, 1); + { + LayerSaver layer(*canvas, kLayerX, kLayerY, kLayerW, kLayerH); + DrawNativeRect(*canvas, kInnerX, kInnerY, kInnerW, kInnerH); + MakeOpaque(canvas.get(), kInnerX, kInnerY, kInnerW, kInnerH); + } + canvas->restore(); + EXPECT_TRUE(VerifyBlackRect(*canvas, kInnerX + 1, kInnerY + 1, + kInnerW, kInnerH)); + + // Make the layer then translate. + canvas->drawColor(SK_ColorWHITE); + canvas->save(); + { + LayerSaver layer(*canvas, kLayerX, kLayerY, kLayerW, kLayerH); + canvas->translate(1, 1); + DrawNativeRect(*canvas, kInnerX, kInnerY, kInnerW, kInnerH); + MakeOpaque(canvas.get(), kInnerX, kInnerY, kInnerW, kInnerH); + } + canvas->restore(); + EXPECT_TRUE(VerifyBlackRect(*canvas, kInnerX + 1, kInnerY + 1, + kInnerW, kInnerH)); + + // Translate both before and after, and have a clip. + canvas->drawColor(SK_ColorWHITE); + canvas->save(); + canvas->translate(1, 1); + { + LayerSaver layer(*canvas, kLayerX, kLayerY, kLayerW, kLayerH); + canvas->drawColor(SK_ColorWHITE); + canvas->translate(1, 1); + AddClip(*canvas, kInnerX + 1, kInnerY + 1, kInnerW - 1, kInnerH - 1); + DrawNativeRect(*canvas, 0, 0, 100, 100); + MakeOpaque(canvas.get(), kLayerX, kLayerY, kLayerW, kLayerH); + } + canvas->restore(); + EXPECT_TRUE(VerifyBlackRect(*canvas, kInnerX + 3, kInnerY + 3, + kInnerW - 1, kInnerH - 1)); + +// TODO(dglazkov): Figure out why this fails on Mac (antialiased clipping?), +// modify test and remove this guard. +#if !defined(USE_AURA) + // Translate both before and after, and have a path clip. + canvas->drawColor(SK_ColorWHITE); + canvas->save(); + canvas->translate(1, 1); + { + LayerSaver layer(*canvas, kLayerX, kLayerY, kLayerW, kLayerH); + canvas->drawColor(SK_ColorWHITE); + canvas->translate(1, 1); + + SkPath path; + SkRect rect; + rect.iset(kInnerX - 1, kInnerY - 1, + kInnerX + kInnerW, kInnerY + kInnerH); + const SkScalar kRadius = 2.0; + path.addRoundRect(rect, kRadius, kRadius); + canvas->clipPath(path); + + DrawNativeRect(*canvas, 0, 0, 100, 100); + MakeOpaque(canvas.get(), kLayerX, kLayerY, kLayerW, kLayerH); + } + canvas->restore(); + EXPECT_TRUE(VerifyRoundedRect(*canvas, SK_ColorWHITE, SK_ColorBLACK, + kInnerX + 1, kInnerY + 1, kInnerW, kInnerH)); +#endif +} + +} // namespace skia + +#endif // BUILDFLAG(IS_WIN) diff --git a/ext/raster_handle_allocator_win.cc b/ext/raster_handle_allocator_win.cc new file mode 100644 index 000000000000..b704c8c00093 --- /dev/null +++ b/ext/raster_handle_allocator_win.cc @@ -0,0 +1,164 @@ +// Copyright 2017 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include + +#include +#include +#include + +#include "base/debug/gdi_debug_util_win.h" +#include "base/logging.h" +#include "base/memory/ptr_util.h" +#include "base/win/win_util.h" +#include "skia/ext/legacy_display_globals.h" +#include "skia/ext/platform_canvas.h" +#include "skia/ext/skia_utils_win.h" +#include "third_party/skia/include/core/SkMatrix.h" +#include "third_party/skia/include/core/SkPath.h" +#include "third_party/skia/include/core/SkRect.h" +#include "third_party/skia/include/core/SkRefCnt.h" + +namespace { + +struct HDCContextRec { + HDC hdc_; + HBITMAP prev_bitmap_; +}; + +static void DeleteHDCCallback(void*, void* context) { + DCHECK_NE(context, nullptr); + const HDCContextRec* rec = static_cast(context); + + // Must select back in the old bitmap before we delete the hdc, and so we can + // recover the new_bitmap that we allocated, so we can delete it. + HBITMAP new_bitmap = + static_cast(SelectObject(rec->hdc_, rec->prev_bitmap_)); + bool success = DeleteObject(new_bitmap); + DCHECK(success); + success = DeleteDC(rec->hdc_); + DCHECK(success); + delete rec; +} + +// Allocate the layer and fill in the fields for the Rec, or return false +// on error. +static bool Create(int width, + int height, + HANDLE shared_section, + bool do_clear, + SkRasterHandleAllocator::Rec* rec) { + void* pixels; + base::win::ScopedBitmap new_bitmap = + skia::CreateHBitmapXRGB8888(width, height, shared_section, &pixels); + if (!new_bitmap.is_valid()) { + LOG(ERROR) << "CreateHBitmap failed"; + return false; + } + + // The HBITMAP is 32-bit RGB data. A size_t causes a type change from int when + // multiplying against the dimensions. + const size_t bpp = 4; + if (do_clear) + memset(pixels, 0, width * bpp * height); + + HDC hdc = CreateCompatibleDC(nullptr); + if (!hdc) + return false; + + SetGraphicsMode(hdc, GM_ADVANCED); + + // The |new_bitmap| will be destroyed by |rec|'s DeleteHDCCallback(). + HBITMAP prev_bitmap = + static_cast(SelectObject(hdc, new_bitmap.release())); + DCHECK(prev_bitmap); + + rec->fReleaseProc = DeleteHDCCallback; + rec->fReleaseCtx = new HDCContextRec{hdc, prev_bitmap}; + rec->fPixels = pixels; + rec->fRowBytes = width * bpp; + rec->fHandle = hdc; + return true; +} + +/** + * Subclass of SkRasterHandleAllocator that returns an HDC as its "handle". + */ +class GDIAllocator : public SkRasterHandleAllocator { + public: + GDIAllocator() {} + + bool allocHandle(const SkImageInfo& info, Rec* rec) override { + SkASSERT(info.colorType() == kN32_SkColorType); + return Create(info.width(), info.height(), nullptr, !info.isOpaque(), rec); + } + + void updateHandle(Handle handle, + const SkMatrix& ctm, + const SkIRect& clip_bounds) override { + HDC hdc = static_cast(handle); + skia::LoadTransformToDC(hdc, ctm); + + HRGN hrgn = CreateRectRgnIndirect(&skia::SkIRectToRECT(clip_bounds)); + int result = SelectClipRgn(hdc, hrgn); + DCHECK(result != ERROR); + result = DeleteObject(hrgn); + DCHECK(result != 0); + } +}; + +void unmap_view_proc(void* pixels, void*) { + UnmapViewOfFile(pixels); +} + +} // namespace + +namespace skia { + +std::unique_ptr CreatePlatformCanvasWithSharedSection( + int width, + int height, + bool is_opaque, + HANDLE shared_section, + OnFailureType failure_type) { + SkAlphaType alpha = is_opaque ? kOpaque_SkAlphaType : kPremul_SkAlphaType; + SkImageInfo info = SkImageInfo::MakeN32(width, height, alpha); + // 32-bit RGB data as we're using N32 |info|. A size_t causes a type change + // from int when multiplying against the dimensions. + const size_t bpp = 4; + + // This function contains an implementation of a Skia platform bitmap for + // drawing and compositing graphics. The original implementation uses Windows + // GDI to create the backing bitmap memory, however it's possible for a + // process to not have access to GDI which will cause this code to fail. It's + // possible to detect when GDI is unavailable and instead directly map the + // shared memory as the bitmap. + if (base::win::IsUser32AndGdi32Available()) { + SkRasterHandleAllocator::Rec rec; + if (Create(width, height, shared_section, false, &rec)) + return SkRasterHandleAllocator::MakeCanvas( + std::make_unique(), info, &rec); + } else { + DCHECK(shared_section != NULL); + void* pixels = MapViewOfFile(shared_section, FILE_MAP_WRITE, 0, 0, + width * bpp * height); + if (pixels) { + SkBitmap bitmap; + if (bitmap.installPixels(info, pixels, width * bpp, unmap_view_proc, + nullptr)) { + return std::make_unique( + bitmap, LegacyDisplayGlobals::GetSkSurfaceProps()); + } + } + } + + CHECK(failure_type != CRASH_ON_FAILURE); + return nullptr; +} + +HDC GetNativeDrawingContext(SkCanvas* canvas) { + return canvas ? static_cast(canvas->accessTopRasterHandle()) : nullptr; +} + +} // namespace skia diff --git a/ext/recursive_gaussian_convolution.cc b/ext/recursive_gaussian_convolution.cc new file mode 100644 index 000000000000..9eb32bc2302c --- /dev/null +++ b/ext/recursive_gaussian_convolution.cc @@ -0,0 +1,271 @@ +// Copyright 2013 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include +#include +#include + +#include "base/check_op.h" +#include "base/notreached.h" +#include "skia/ext/recursive_gaussian_convolution.h" + +namespace skia { + +namespace { + +// Takes the value produced by accumulating element-wise product of image with +// a kernel and brings it back into range. +// All of the filter scaling factors are in fixed point with kShiftBits bits of +// fractional part. +template +inline unsigned char FloatTo8(float f) { + int a = static_cast(f + 0.5f); + if (take_absolute) + a = std::abs(a); + else if (a < 0) + return 0; + if (a < 256) + return a; + return 255; +} + +template +inline float ForwardFilter(float in_n_1, + float in_n, + float in_n1, + const std::vector& w, + int n, + const float* b) { + switch (order) { + case RecursiveFilter::FUNCTION: + return b[0] * in_n + b[1] * w[n-1] + b[2] * w[n-2] + b[3] * w[n-3]; + case RecursiveFilter::FIRST_DERIVATIVE: + return b[0] * 0.5f * (in_n1 - in_n_1) + + b[1] * w[n-1] + b[2] * w[n-2] + b[3] * w[n-3]; + case RecursiveFilter::SECOND_DERIVATIVE: + return b[0] * (in_n - in_n_1) + + b[1] * w[n-1] + b[2] * w[n-2] + b[3] * w[n-3]; + } + + NOTREACHED(); + return 0.0f; +} + +template +inline float BackwardFilter(const std::vector& out, + int n, + float w_n, + float w_n1, + const float* b) { + switch (order) { + case RecursiveFilter::FUNCTION: + case RecursiveFilter::FIRST_DERIVATIVE: + return b[0] * w_n + + b[1] * out[n + 1] + b[2] * out[n + 2] + b[3] * out[n + 3]; + case RecursiveFilter::SECOND_DERIVATIVE: + return b[0] * (w_n1 - w_n) + + b[1] * out[n + 1] + b[2] * out[n + 2] + b[3] * out[n + 3]; + } + NOTREACHED(); + return 0.0f; +} + +template +unsigned char SingleChannelRecursiveFilter( + const unsigned char* const source_data, + int source_pixel_stride, + int source_row_stride, + int row_width, + int row_count, + unsigned char* const output, + int output_pixel_stride, + int output_row_stride, + const float* b) { + const int intermediate_buffer_size = row_width + 6; + std::vector w(intermediate_buffer_size); + const unsigned char* in = source_data; + unsigned char* out = output; + unsigned char max_output = 0; + for (int r = 0; r < row_count; + ++r, in += source_row_stride, out += output_row_stride) { + // Compute forward filter. + // First initialize start of the w (temporary) vector. + if (order == RecursiveFilter::FUNCTION) + w[0] = w[1] = w[2] = in[0]; + else + w[0] = w[1] = w[2] = 0.0f; + // Note that special-casing of w[3] is needed because of derivatives. + w[3] = ForwardFilter( + in[0], in[0], in[source_pixel_stride], w, 3, b); + int n = 4; + int c = 1; + int byte_index = source_pixel_stride; + for (; c < row_width - 1; ++c, ++n, byte_index += source_pixel_stride) { + w[n] = ForwardFilter(in[byte_index - source_pixel_stride], + in[byte_index], + in[byte_index + source_pixel_stride], + w, n, b); + } + + // The value of w corresponding to the last image pixel needs to be computed + // separately, again because of derivatives. + w[n] = ForwardFilter(in[byte_index - source_pixel_stride], + in[byte_index], + in[byte_index], + w, n, b); + // Now three trailing bytes set to the same value as current w[n]. + w[n + 1] = w[n]; + w[n + 2] = w[n]; + w[n + 3] = w[n]; + + // Now apply the back filter. + float w_n1 = w[n + 1]; + int output_index = (row_width - 1) * output_pixel_stride; + for (; c >= 0; output_index -= output_pixel_stride, --c, --n) { + float w_n = BackwardFilter(w, n, w[n], w_n1, b); + w_n1 = w[n]; + w[n] = w_n; + out[output_index] = FloatTo8(w_n); + max_output = std::max(max_output, out[output_index]); + } + } + return max_output; +} + +unsigned char SingleChannelRecursiveFilter( + const unsigned char* const source_data, + int source_pixel_stride, + int source_row_stride, + int row_width, + int row_count, + unsigned char* const output, + int output_pixel_stride, + int output_row_stride, + const float* b, + RecursiveFilter::Order order, + bool absolute_values) { + if (absolute_values) { + switch (order) { + case RecursiveFilter::FUNCTION: + return SingleChannelRecursiveFilter( + source_data, source_pixel_stride, source_row_stride, + row_width, row_count, + output, output_pixel_stride, output_row_stride, b); + case RecursiveFilter::FIRST_DERIVATIVE: + return SingleChannelRecursiveFilter< + RecursiveFilter::FIRST_DERIVATIVE, true>( + source_data, source_pixel_stride, source_row_stride, + row_width, row_count, + output, output_pixel_stride, output_row_stride, b); + case RecursiveFilter::SECOND_DERIVATIVE: + return SingleChannelRecursiveFilter< + RecursiveFilter::SECOND_DERIVATIVE, true>( + source_data, source_pixel_stride, source_row_stride, + row_width, row_count, + output, output_pixel_stride, output_row_stride, b); + } + } else { + switch (order) { + case RecursiveFilter::FUNCTION: + return SingleChannelRecursiveFilter( + source_data, source_pixel_stride, source_row_stride, + row_width, row_count, + output, output_pixel_stride, output_row_stride, b); + case RecursiveFilter::FIRST_DERIVATIVE: + return SingleChannelRecursiveFilter< + RecursiveFilter::FIRST_DERIVATIVE, false>( + source_data, source_pixel_stride, source_row_stride, + row_width, row_count, + output, output_pixel_stride, output_row_stride, b); + case RecursiveFilter::SECOND_DERIVATIVE: + return SingleChannelRecursiveFilter< + RecursiveFilter::SECOND_DERIVATIVE, false>( + source_data, source_pixel_stride, source_row_stride, + row_width, row_count, + output, output_pixel_stride, output_row_stride, b); + } + } + + NOTREACHED(); + return 0; +} + +} + +float RecursiveFilter::qFromSigma(float sigma) { + DCHECK_GE(sigma, 0.5f); + if (sigma <= 2.5f) + return 3.97156f - 4.14554f * std::sqrt(1.0f - 0.26891f * sigma); + return 0.98711f * sigma - 0.96330f; +} + +void RecursiveFilter::computeCoefficients(float q, float b[4]) { + b[0] = 1.57825f + 2.44413f * q + 1.4281f * q * q + 0.422205f * q * q * q; + b[1] = 2.4413f * q + 2.85619f * q * q + 1.26661f * q * q * q; + b[2] = - 1.4281f * q * q - 1.26661f * q * q * q; + b[3] = 0.422205f * q * q * q; + + // The above is exactly like in the paper. To cut down on computations, + // we can fix up these numbers a bit now. + float b_norm = 1.0f - (b[1] + b[2] + b[3]) / b[0]; + b[1] /= b[0]; + b[2] /= b[0]; + b[3] /= b[0]; + b[0] = b_norm; +} + +RecursiveFilter::RecursiveFilter(float sigma, Order order) + : order_(order), q_(qFromSigma(sigma)) { + computeCoefficients(q_, b_); +} + +unsigned char SingleChannelRecursiveGaussianX(const unsigned char* source_data, + int source_byte_row_stride, + int input_channel_index, + int input_channel_count, + const RecursiveFilter& filter, + const SkISize& image_size, + unsigned char* output, + int output_byte_row_stride, + int output_channel_index, + int output_channel_count, + bool absolute_values) { + return SingleChannelRecursiveFilter(source_data + input_channel_index, + input_channel_count, + source_byte_row_stride, + image_size.width(), + image_size.height(), + output + output_channel_index, + output_channel_count, + output_byte_row_stride, + filter.b(), + filter.order(), + absolute_values); +} + +unsigned char SingleChannelRecursiveGaussianY(const unsigned char* source_data, + int source_byte_row_stride, + int input_channel_index, + int input_channel_count, + const RecursiveFilter& filter, + const SkISize& image_size, + unsigned char* output, + int output_byte_row_stride, + int output_channel_index, + int output_channel_count, + bool absolute_values) { + return SingleChannelRecursiveFilter(source_data + input_channel_index, + source_byte_row_stride, + input_channel_count, + image_size.height(), + image_size.width(), + output + output_channel_index, + output_byte_row_stride, + output_channel_count, + filter.b(), + filter.order(), + absolute_values); +} + +} // namespace skia diff --git a/ext/recursive_gaussian_convolution.h b/ext/recursive_gaussian_convolution.h new file mode 100644 index 000000000000..8134ca21e08c --- /dev/null +++ b/ext/recursive_gaussian_convolution.h @@ -0,0 +1,71 @@ +// Copyright 2013 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef SKIA_EXT_RECURSIVE_GAUSSIAN_CONVOLUTION_H_ +#define SKIA_EXT_RECURSIVE_GAUSSIAN_CONVOLUTION_H_ + +#include "skia/ext/convolver.h" +#include "third_party/skia/include/core/SkSize.h" +#include "third_party/skia/include/core/SkTypes.h" + +namespace skia { + +// RecursiveFilter, paired with SingleChannelRecursiveGaussianX and +// SingleChannelRecursiveGaussianY routines below implement recursive filters as +// described in 'Recursive implementation of the Gaussian filter' (Young, Vliet) +// (1995). Single-letter variable names mirror exactly the usage in the paper to +// ease reading and analysis. +class RecursiveFilter { + public: + enum Order { + FUNCTION, + FIRST_DERIVATIVE, + SECOND_DERIVATIVE + }; + + static float qFromSigma(float sigma); + static void computeCoefficients(float q, float b[4]); + SK_API RecursiveFilter(float sigma, Order order); + + Order order() const { return order_; } + const float* b() const { return b_; } + + private: + Order order_; + float q_; + float b_[4]; +}; + +// Applies a gaussian recursive filter given as |filter| to a single channel at +// |input_channel_index| to image given in |source_data| along X axis. +// The output is placed into |output| into channel |output_channel_index|. +SK_API unsigned char SingleChannelRecursiveGaussianX( + const unsigned char* source_data, + int source_byte_row_stride, + int input_channel_index, + int input_channel_count, + const RecursiveFilter& filter, + const SkISize& image_size, + unsigned char* output, + int output_byte_row_stride, + int output_channel_index, + int output_channel_count, + bool absolute_values); + +// Applies a gaussian recursive filter along Y axis. +SK_API unsigned char SingleChannelRecursiveGaussianY( + const unsigned char* source_data, + int source_byte_row_stride, + int input_channel_index, + int input_channel_count, + const RecursiveFilter& filter, + const SkISize& image_size, + unsigned char* output, + int output_byte_row_stride, + int output_channel_index, + int output_channel_count, + bool absolute_values); +} // namespace skia + +#endif // SKIA_EXT_RECURSIVE_GAUSSIAN_CONVOLUTION_H_ diff --git a/ext/recursive_gaussian_convolution_unittest.cc b/ext/recursive_gaussian_convolution_unittest.cc new file mode 100644 index 000000000000..8f47112f08d8 --- /dev/null +++ b/ext/recursive_gaussian_convolution_unittest.cc @@ -0,0 +1,394 @@ +// Copyright 2013 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include +#include +#include + +#include "base/files/file_path.h" +#include "base/files/file_util.h" +#include "base/ranges/algorithm.h" +#include "base/time/time.h" +#include "skia/ext/convolver.h" +#include "skia/ext/recursive_gaussian_convolution.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "third_party/skia/include/core/SkPoint.h" +#include "third_party/skia/include/core/SkRect.h" + +namespace { + +int ComputeRowStride(int width, int channel_count, int stride_slack) { + return width * channel_count + stride_slack; +} + +SkIPoint MakeImpulseImage(std::vector* image, + int width, + int height, + int channel_index, + int channel_count, + int stride_slack) { + const int src_row_stride = ComputeRowStride( + width, channel_count, stride_slack); + const int src_byte_count = src_row_stride * height; + const int signal_x = width / 2; + const int signal_y = height / 2; + + image->resize(src_byte_count, 0); + const int non_zero_pixel_index = + signal_y * src_row_stride + signal_x * channel_count + channel_index; + (*image)[non_zero_pixel_index] = 255; + return SkIPoint::Make(signal_x, signal_y); +} + +SkIRect MakeBoxImage(std::vector* image, + int width, + int height, + int channel_index, + int channel_count, + int stride_slack, + int box_width, + int box_height, + unsigned char value) { + const int src_row_stride = ComputeRowStride( + width, channel_count, stride_slack); + const int src_byte_count = src_row_stride * height; + const SkIRect box = SkIRect::MakeXYWH((width - box_width) / 2, + (height - box_height) / 2, + box_width, box_height); + + image->resize(src_byte_count, 0); + for (int y = box.top(); y < box.bottom(); ++y) { + for (int x = box.left(); x < box.right(); ++x) + (*image)[y * src_row_stride + x * channel_count + channel_index] = value; + } + + return box; +} + +int ComputeBoxSum(const std::vector& image, + const SkIRect& box, + int image_width) { + // Compute the sum of all pixels in the box. Assume byte stride 1 and row + // stride same as image_width. + int sum = 0; + for (int y = box.top(); y < box.bottom(); ++y) { + for (int x = box.left(); x < box.right(); ++x) + sum += image[y * image_width + x]; + } + + return sum; +} + +} // namespace + +namespace skia { + +TEST(RecursiveGaussian, SmoothingMethodComparison) { + static const int kImgWidth = 512; + static const int kImgHeight = 220; + static const int kChannelIndex = 3; + static const int kChannelCount = 3; + static const int kStrideSlack = 22; + + std::vector input; + SkISize image_size = SkISize::Make(kImgWidth, kImgHeight); + MakeImpulseImage( + &input, kImgWidth, kImgHeight, kChannelIndex, kChannelCount, + kStrideSlack); + + // Destination will be a single channel image with stide matching width. + const int dest_row_stride = kImgWidth; + const int dest_byte_count = dest_row_stride * kImgHeight; + std::vector intermediate(dest_byte_count); + std::vector intermediate2(dest_byte_count); + std::vector control(dest_byte_count); + std::vector output(dest_byte_count); + + const int src_row_stride = ComputeRowStride( + kImgWidth, kChannelCount, kStrideSlack); + + const float kernel_sigma = 2.5f; + ConvolutionFilter1D filter; + SetUpGaussianConvolutionKernel(&filter, kernel_sigma, false); + // Process the control image. + SingleChannelConvolveX1D(&input[0], src_row_stride, + kChannelIndex, kChannelCount, + filter, image_size, + &intermediate[0], dest_row_stride, 0, 1, false); + SingleChannelConvolveY1D(&intermediate[0], dest_row_stride, 0, 1, + filter, image_size, + &control[0], dest_row_stride, 0, 1, false); + + // Now try the same using the other method. + RecursiveFilter recursive_filter(kernel_sigma, RecursiveFilter::FUNCTION); + SingleChannelRecursiveGaussianY(&input[0], src_row_stride, + kChannelIndex, kChannelCount, + recursive_filter, image_size, + &intermediate2[0], dest_row_stride, + 0, 1, false); + SingleChannelRecursiveGaussianX(&intermediate2[0], dest_row_stride, 0, 1, + recursive_filter, image_size, + &output[0], dest_row_stride, 0, 1, false); + + // We cannot expect the results to be really the same. In particular, + // the standard implementation is computed in completely fixed-point, while + // recursive is done in floating point and squeezed back into char*. On top + // of that, its characteristics are a bit different (consult the paper). + EXPECT_NEAR(std::accumulate(intermediate.begin(), intermediate.end(), 0), + std::accumulate(intermediate2.begin(), intermediate2.end(), 0), + 50); + int difference_count = 0; + std::vector::const_iterator i1, i2; + for (i1 = control.begin(), i2 = output.begin(); + i1 != control.end(); ++i1, ++i2) { + if ((*i1 != 0) != (*i2 != 0)) + difference_count++; + } + + EXPECT_LE(difference_count, 44); // 44 is 2 * PI * r (r == 7, spot size). +} + +TEST(RecursiveGaussian, SmoothingImpulse) { + static const int kImgWidth = 200; + static const int kImgHeight = 300; + static const int kChannelIndex = 3; + static const int kChannelCount = 3; + static const int kStrideSlack = 22; + + std::vector input; + SkISize image_size = SkISize::Make(kImgWidth, kImgHeight); + const SkIPoint centre_point = MakeImpulseImage( + &input, kImgWidth, kImgHeight, kChannelIndex, kChannelCount, + kStrideSlack); + + // Destination will be a single channel image with stide matching width. + const int dest_row_stride = kImgWidth; + const int dest_byte_count = dest_row_stride * kImgHeight; + std::vector intermediate(dest_byte_count); + std::vector output(dest_byte_count); + + const int src_row_stride = ComputeRowStride( + kImgWidth, kChannelCount, kStrideSlack); + + const float kernel_sigma = 5.0f; + RecursiveFilter recursive_filter(kernel_sigma, RecursiveFilter::FUNCTION); + SingleChannelRecursiveGaussianY(&input[0], src_row_stride, + kChannelIndex, kChannelCount, + recursive_filter, image_size, + &intermediate[0], dest_row_stride, + 0, 1, false); + SingleChannelRecursiveGaussianX(&intermediate[0], dest_row_stride, 0, 1, + recursive_filter, image_size, + &output[0], dest_row_stride, 0, 1, false); + + // Check we got the expected impulse response. + const int cx = centre_point.x(); + const int cy = centre_point.y(); + unsigned char value_x = output[dest_row_stride * cy + cx]; + unsigned char value_y = value_x; + EXPECT_GT(value_x, 0); + for (int offset = 0; + offset < std::min(kImgWidth, kImgHeight) && (value_y > 0 || value_x > 0); + ++offset) { + // Symmetricity and monotonicity along X. + EXPECT_EQ(output[dest_row_stride * cy + cx - offset], + output[dest_row_stride * cy + cx + offset]); + EXPECT_LE(output[dest_row_stride * cy + cx - offset], value_x); + value_x = output[dest_row_stride * cy + cx - offset]; + + // Symmetricity and monotonicity along Y. + EXPECT_EQ(output[dest_row_stride * (cy - offset) + cx], + output[dest_row_stride * (cy + offset) + cx]); + EXPECT_LE(output[dest_row_stride * (cy - offset) + cx], value_y); + value_y = output[dest_row_stride * (cy - offset) + cx]; + + // Symmetricity along X/Y (not really assured, but should be close). + EXPECT_NEAR(value_x, value_y, 1); + } + + // Smooth the inverse now. + std::vector output2(dest_byte_count); + base::ranges::transform(input, input.begin(), + [](unsigned char c) { return 255U - c; }); + SingleChannelRecursiveGaussianY(&input[0], src_row_stride, + kChannelIndex, kChannelCount, + recursive_filter, image_size, + &intermediate[0], dest_row_stride, + 0, 1, false); + SingleChannelRecursiveGaussianX(&intermediate[0], dest_row_stride, 0, 1, + recursive_filter, image_size, + &output2[0], dest_row_stride, 0, 1, false); + // The image should be the reverse of output, but permitting for rounding + // we will only claim that wherever output is 0, output2 should be 255. + // There still can be differences at the edges of the object. + std::vector::const_iterator i1, i2; + int difference_count = 0; + for (i1 = output.begin(), i2 = output2.begin(); + i1 != output.end(); ++i1, ++i2) { + // The line below checks (*i1 == 0 <==> *i2 == 255). + if ((*i1 != 0 && *i2 == 255) && ! (*i1 == 0 && *i2 != 255)) + ++difference_count; + } + EXPECT_LE(difference_count, 8); +} + +TEST(RecursiveGaussian, FirstDerivative) { + static const int kImgWidth = 512; + static const int kImgHeight = 1024; + static const int kChannelIndex = 2; + static const int kChannelCount = 4; + static const int kStrideSlack = 22; + static const int kBoxSize = 400; + + std::vector input; + const SkISize image_size = SkISize::Make(kImgWidth, kImgHeight); + const SkIRect box = MakeBoxImage( + &input, kImgWidth, kImgHeight, kChannelIndex, kChannelCount, + kStrideSlack, kBoxSize, kBoxSize, 200); + + // Destination will be a single channel image with stide matching width. + const int dest_row_stride = kImgWidth; + const int dest_byte_count = dest_row_stride * kImgHeight; + std::vector output_x(dest_byte_count); + std::vector output_y(dest_byte_count); + std::vector output(dest_byte_count); + + const int src_row_stride = ComputeRowStride( + kImgWidth, kChannelCount, kStrideSlack); + + const float kernel_sigma = 3.0f; + const int spread = 4 * kernel_sigma; + RecursiveFilter recursive_filter(kernel_sigma, + RecursiveFilter::FIRST_DERIVATIVE); + SingleChannelRecursiveGaussianX(&input[0], src_row_stride, + kChannelIndex, kChannelCount, + recursive_filter, image_size, + &output_x[0], dest_row_stride, + 0, 1, true); + SingleChannelRecursiveGaussianY(&input[0], src_row_stride, + kChannelIndex, kChannelCount, + recursive_filter, image_size, + &output_y[0], dest_row_stride, + 0, 1, true); + + // In test code we can assume adding the two up should do fine. + std::vector::const_iterator ix, iy; + std::vector::iterator target; + for (target = output.begin(), ix = output_x.begin(), iy = output_y.begin(); + target < output.end(); ++target, ++ix, ++iy) { + *target = *ix + *iy; + } + + SkIRect inflated_rect(box); + inflated_rect.outset(spread, spread); + SkIRect deflated_rect(box); + deflated_rect.inset(spread, spread); + + int image_total = ComputeBoxSum(output, + SkIRect::MakeWH(kImgWidth, kImgHeight), + kImgWidth); + int box_inflated = ComputeBoxSum(output, inflated_rect, kImgWidth); + int box_deflated = ComputeBoxSum(output, deflated_rect, kImgWidth); + EXPECT_EQ(box_deflated, 0); + EXPECT_EQ(image_total, box_inflated); + + // Try inverted image. Behaviour should be very similar (modulo rounding). + base::ranges::transform(input, input.begin(), + [](unsigned char c) { return 255U - c; }); + SingleChannelRecursiveGaussianX(&input[0], src_row_stride, + kChannelIndex, kChannelCount, + recursive_filter, image_size, + &output_x[0], dest_row_stride, + 0, 1, true); + SingleChannelRecursiveGaussianY(&input[0], src_row_stride, + kChannelIndex, kChannelCount, + recursive_filter, image_size, + &output_y[0], dest_row_stride, + 0, 1, true); + + for (target = output.begin(), ix = output_x.begin(), iy = output_y.begin(); + target < output.end(); ++target, ++ix, ++iy) { + *target = *ix + *iy; + } + + image_total = ComputeBoxSum(output, + SkIRect::MakeWH(kImgWidth, kImgHeight), + kImgWidth); + box_inflated = ComputeBoxSum(output, inflated_rect, kImgWidth); + box_deflated = ComputeBoxSum(output, deflated_rect, kImgWidth); + + EXPECT_EQ(box_deflated, 0); + EXPECT_EQ(image_total, box_inflated); +} + +TEST(RecursiveGaussian, SecondDerivative) { + static const int kImgWidth = 700; + static const int kImgHeight = 500; + static const int kChannelIndex = 0; + static const int kChannelCount = 2; + static const int kStrideSlack = 42; + static const int kBoxSize = 200; + + std::vector input; + SkISize image_size = SkISize::Make(kImgWidth, kImgHeight); + const SkIRect box = MakeBoxImage( + &input, kImgWidth, kImgHeight, kChannelIndex, kChannelCount, + kStrideSlack, kBoxSize, kBoxSize, 200); + + // Destination will be a single channel image with stide matching width. + const int dest_row_stride = kImgWidth; + const int dest_byte_count = dest_row_stride * kImgHeight; + std::vector output_x(dest_byte_count); + std::vector output_y(dest_byte_count); + std::vector output(dest_byte_count); + + const int src_row_stride = ComputeRowStride( + kImgWidth, kChannelCount, kStrideSlack); + + const float kernel_sigma = 5.0f; + const int spread = 8 * kernel_sigma; + RecursiveFilter recursive_filter(kernel_sigma, + RecursiveFilter::SECOND_DERIVATIVE); + SingleChannelRecursiveGaussianX(&input[0], src_row_stride, + kChannelIndex, kChannelCount, + recursive_filter, image_size, + &output_x[0], dest_row_stride, + 0, 1, true); + SingleChannelRecursiveGaussianY(&input[0], src_row_stride, + kChannelIndex, kChannelCount, + recursive_filter, image_size, + &output_y[0], dest_row_stride, + 0, 1, true); + + // In test code we can assume adding the two up should do fine. + std::vector::const_iterator ix, iy; + std::vector::iterator target; + for (target = output.begin(),ix = output_x.begin(), iy = output_y.begin(); + target < output.end(); ++target, ++ix, ++iy) { + *target = *ix + *iy; + } + + int image_total = ComputeBoxSum(output, + SkIRect::MakeWH(kImgWidth, kImgHeight), + kImgWidth); + int box_inflated = ComputeBoxSum(output, + SkIRect::MakeLTRB(box.left() - spread, + box.top() - spread, + box.right() + spread, + box.bottom() + spread), + kImgWidth); + int box_deflated = ComputeBoxSum(output, + SkIRect::MakeLTRB(box.left() + spread, + box.top() + spread, + box.right() - spread, + box.bottom() - spread), + kImgWidth); + // Since second derivative is not really used and implemented mostly + // for the sake of completeness, we do not verify the detail (that dip + // in the middle). But it is there. + EXPECT_EQ(box_deflated, 0); + EXPECT_EQ(image_total, box_inflated); +} + +} // namespace skia diff --git a/ext/rgba_to_yuva.cc b/ext/rgba_to_yuva.cc new file mode 100644 index 000000000000..eef4397595f7 --- /dev/null +++ b/ext/rgba_to_yuva.cc @@ -0,0 +1,119 @@ +// Copyright 2021 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "skia/ext/rgba_to_yuva.h" + +#include + +#include "base/logging.h" +#include "base/notreached.h" +#include "third_party/skia/include/core/SkBlendMode.h" +#include "third_party/skia/include/core/SkCanvas.h" +#include "third_party/skia/include/core/SkClipOp.h" +#include "third_party/skia/include/core/SkColor.h" +#include "third_party/skia/include/core/SkColorFilter.h" +#include "third_party/skia/include/core/SkPaint.h" +#include "third_party/skia/include/core/SkRect.h" +#include "third_party/skia/include/effects/SkColorMatrix.h" + +namespace skia { + +namespace { + +SkRect GetSubsampledRect(const SkRect& rect, + const std::array& subsampling_factors) { + return SkRect::MakeXYWH(rect.x() * subsampling_factors[0], + rect.y() * subsampling_factors[1], + rect.width() * subsampling_factors[0], + rect.height() * subsampling_factors[1]); +} + +} // namespace + +void BlitRGBAToYUVA(SkImage* src_image, + SkSurface* dst_surfaces[SkYUVAInfo::kMaxPlanes], + const SkYUVAInfo& dst_yuva_info, + const SkRect& dst_region, + bool clear_destination) { + // Rectangle representing the entire destination image: + const SkRect dst_image_rect = SkRect::Make(dst_yuva_info.dimensions()); + const SkRect src_rect = SkRect::Make(src_image->bounds()); + // Region of destination image that is supposed to be populated: + const SkRect dst_rect = dst_region.isEmpty() ? dst_image_rect : dst_region; + + DCHECK(dst_image_rect.contains(dst_rect)); + + // Permutation matrices to select the appropriate YUVA channels for each + // output plane. + constexpr SkColorMatrix xxxY(0, 0, 0, 0, 0, // + 0, 0, 0, 0, 0, // + 0, 0, 0, 0, 0, // + 1, 0, 0, 0, 0); + constexpr SkColorMatrix UVx1(0, 1, 0, 0, 0, // + 0, 0, 1, 0, 0, // + 0, 0, 0, 0, 0, // + 0, 0, 0, 1, 0); + + // Only Y_UV has been tested. + SkColorMatrix permutation_matrices[SkYUVAInfo::kMaxPlanes]; + switch (dst_yuva_info.planeConfig()) { + case SkYUVAInfo::PlaneConfig::kY_UV: + permutation_matrices[0] = xxxY; + permutation_matrices[1] = UVx1; + break; + default: + DLOG(ERROR) << "Unsupported plane configuration."; + return; + } + SkColorMatrix rgb_to_yuv_matrix = + SkColorMatrix::RGBtoYUV(dst_yuva_info.yuvColorSpace()); + + // Blit each plane. + for (int plane = 0; plane < dst_yuva_info.numPlanes(); ++plane) { + SkCanvas* plane_canvas = dst_surfaces[plane]->getCanvas(); + + SkColorMatrix color_matrix = rgb_to_yuv_matrix; + color_matrix.postConcat(permutation_matrices[plane]); + + SkSamplingOptions sampling_options(SkFilterMode::kLinear); + + SkPaint paint; + paint.setBlendMode(SkBlendMode::kSrc); + + // Blend the input image over black before performing RGB to YUV + // conversion, to match un-accelerated versions. + paint.setColorFilter(SkColorFilters::Compose( + SkColorFilters::Matrix(color_matrix), + SkColorFilters::Blend(SK_ColorBLACK, SkBlendMode::kDstOver))); + + // Subsampling factors are determined by the ratios of the entire image's + // width & height to the dimensions of the passed in surfaces (which should + // also span the entire logical image): + std::array subsampling_factors = { + static_cast(dst_surfaces[plane]->width()) / + dst_yuva_info.dimensions().width(), + static_cast(dst_surfaces[plane]->height()) / + dst_yuva_info.dimensions().height(), + }; + + if (clear_destination && dst_image_rect != dst_rect) { + // If we were told to clear the destination prior to blitting and we know + // the blit won't populate the entire destination image, issue the draw + // call that fills the destination with black and takes into account the + // color conversion needed. + SkPaint clear_paint(paint); + clear_paint.setColor(SK_ColorBLACK); + + plane_canvas->drawPaint(clear_paint); + } + + const SkRect plane_dst_rect = + GetSubsampledRect(dst_rect, subsampling_factors); + plane_canvas->drawImageRect(src_image, src_rect, plane_dst_rect, + sampling_options, &paint, + SkCanvas::kFast_SrcRectConstraint); + } +} + +} // namespace skia diff --git a/ext/rgba_to_yuva.h b/ext/rgba_to_yuva.h new file mode 100644 index 000000000000..9ece36df97e5 --- /dev/null +++ b/ext/rgba_to_yuva.h @@ -0,0 +1,30 @@ +// Copyright 2021 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef SKIA_EXT_RGBA_TO_YUVA_H_ +#define SKIA_EXT_RGBA_TO_YUVA_H_ + +#include "third_party/skia/include/core/SkImage.h" +#include "third_party/skia/include/core/SkRect.h" +#include "third_party/skia/include/core/SkSurface.h" +#include "third_party/skia/include/core/SkYUVAInfo.h" + +namespace skia { + +// Copy `src_image` from RGBA to the YUVA planes specified in `dst_surfaces`, +// using the color space and plane configuration information specified in +// `dst_yuva_info`. `dst_yuva_info` describes the entire destination image - the +// results of the blit operation will be placed in its subregion, described by +// `dst_region`. If a default-constructed `dst_region` is passed in, the entire +// destination image will be written to. If `clear_destination` is true, the +// entire destination image will be cleared with black before the blit. +SK_API void BlitRGBAToYUVA(SkImage* src_image, + SkSurface* dst_surfaces[SkYUVAInfo::kMaxPlanes], + const SkYUVAInfo& dst_yuva_info, + const SkRect& dst_region = SkRect::MakeEmpty(), + bool clear_destination = false); + +} // namespace skia + +#endif // SKIA_EXT_RGBA_TO_YUVA_H_ diff --git a/ext/skcolorspace_primaries.cc b/ext/skcolorspace_primaries.cc new file mode 100644 index 000000000000..1b4fb02a9c29 --- /dev/null +++ b/ext/skcolorspace_primaries.cc @@ -0,0 +1,71 @@ +// Copyright 2022 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "skia/ext/skcolorspace_primaries.h" + +#include +#include + +bool operator==(const SkColorSpacePrimaries& a, + const SkColorSpacePrimaries& b) { + return a.fRX == b.fRX && a.fRY == b.fRY && a.fGX == b.fGX && a.fGY == b.fGY && + a.fBX == b.fBX && a.fBY == b.fBY && a.fWX == b.fWX && a.fWY == b.fWY; +} + +bool operator!=(const SkColorSpacePrimaries& a, + const SkColorSpacePrimaries& b) { + return !(a == b); +} + +namespace skia { + +std::string SkColorSpacePrimariesToString( + const SkColorSpacePrimaries& primaries) { + if (primaries == SkNamedPrimariesExt::kInvalid) + return "invalid"; + + std::stringstream ss; + ss << std::fixed << std::setprecision(4); + ss << "{"; + if (primaries == SkNamedPrimariesExt::kSRGB) + ss << "name:'srgb', "; + else if (primaries == SkNamedPrimariesExt::kP3) + ss << "name:'p3', "; + else if (primaries == SkNamedPrimariesExt::kRec2020) + ss << "name:'rec2020', "; + ss << "r:[" << primaries.fRX << ", " << primaries.fRY << "], "; + ss << "g:[" << primaries.fGX << ", " << primaries.fGY << "], "; + ss << "b:[" << primaries.fBX << ", " << primaries.fRY << "], "; + ss << "w:[" << primaries.fWX << ", " << primaries.fWY << "]"; + ss << "}"; + return ss.str(); +} + +SkColorSpacePrimaries GetD65PrimariesFromToXYZD50Matrix( + const skcms_Matrix3x3& m_d50) { + constexpr float kD65_X = 0.3127f; + constexpr float kD65_Y = 0.3290f; + skcms_Matrix3x3 adapt_d65_to_d50; + skcms_AdaptToXYZD50(kD65_X, kD65_Y, &adapt_d65_to_d50); + + skcms_Matrix3x3 adapt_d50_to_d65; + skcms_Matrix3x3_invert(&adapt_d65_to_d50, &adapt_d50_to_d65); + + const skcms_Matrix3x3 m = skcms_Matrix3x3_concat(&adapt_d50_to_d65, &m_d50); + const float sum_R = m.vals[0][0] + m.vals[1][0] + m.vals[2][0]; + const float sum_G = m.vals[0][1] + m.vals[1][1] + m.vals[2][1]; + const float sum_B = m.vals[0][2] + m.vals[1][2] + m.vals[2][2]; + SkColorSpacePrimaries primaries; + primaries.fRX = m.vals[0][0] / sum_R; + primaries.fRY = m.vals[1][0] / sum_R; + primaries.fGX = m.vals[0][1] / sum_G; + primaries.fGY = m.vals[1][1] / sum_G; + primaries.fBX = m.vals[0][2] / sum_B; + primaries.fBY = m.vals[1][2] / sum_B; + primaries.fWX = kD65_X; + primaries.fWY = kD65_Y; + return primaries; +} + +} // namespace skia diff --git a/ext/skcolorspace_primaries.h b/ext/skcolorspace_primaries.h new file mode 100644 index 000000000000..fba487df588d --- /dev/null +++ b/ext/skcolorspace_primaries.h @@ -0,0 +1,129 @@ +// Copyright 2022 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef SKIA_EXT_SKCOLORSPACE_PRIMARIES_H_ +#define SKIA_EXT_SKCOLORSPACE_PRIMARIES_H_ + +#include "third_party/skia/include/core/SkColorSpace.h" + +#include + +// TODO(https://crbug.com/skia/13721): Add these operators to Skia source. +SK_API bool operator==(const SkColorSpacePrimaries& a, + const SkColorSpacePrimaries& b); + +SK_API bool operator!=(const SkColorSpacePrimaries& a, + const SkColorSpacePrimaries& b); + +namespace skia { + +// Display SkColorSpacePrimaries as a string. +SK_API std::string SkColorSpacePrimariesToString( + const SkColorSpacePrimaries& primaries); + +// Given a matrix that transforms to XYZD50, compute the primaries with a D65 +// white point that would produce this matrix. +SK_API SkColorSpacePrimaries +GetD65PrimariesFromToXYZD50Matrix(const skcms_Matrix3x3& m); + +} // namespace skia + +namespace SkNamedPrimariesExt { + +//////////////////////////////////////////////////////////////////////////////// +// Color primaries defined by ITU-T H.273, table 2. Names are given by the first +// specification referenced in the value's row. + +// Rec. ITU-R BT.709-6, value 1. +static constexpr SkColorSpacePrimaries kRec709 = { + 0.64f, 0.33f, 0.3f, 0.6f, 0.15f, 0.06f, 0.3127f, 0.329f}; + +// Rec. ITU-R BT.470-6 System M (historical), value 4. +static constexpr SkColorSpacePrimaries kRec470SystemM = { + 0.67f, 0.33f, 0.21f, 0.71f, 0.14f, 0.08f, 0.31f, 0.316f}; + +// Rec. ITU-R BT.470-6 System B, G (historical), value 5. +static constexpr SkColorSpacePrimaries kRec470SystemBG = { + 0.64f, 0.33f, 0.29f, 0.60f, 0.15f, 0.06f, 0.3127f, 0.3290f}; + +// Rec. ITU-R BT.601-7 525, value 6. +static constexpr SkColorSpacePrimaries kRec601 = { + 0.630f, 0.340f, 0.310f, 0.595f, 0.155f, 0.070f, 0.3127f, 0.3290f}; + +// SMPTE ST 240, value 7 (functionally the same as value 6). +static constexpr SkColorSpacePrimaries kSMPTE_ST_240 = kRec601; + +// Generic film (colour filters using Illuminant C), value 8. +static constexpr SkColorSpacePrimaries kGenericFilm = { + 0.681f, 0.319f, 0.243f, 0.692f, 0.145f, 0.049f, 0.310f, 0.316f}; + +// Rec. ITU-R BT.2020-2, value 9. +static constexpr SkColorSpacePrimaries kRec2020{ + 0.708f, 0.292f, 0.170f, 0.797f, 0.131f, 0.046f, 0.3127f, 0.3290f}; + +// SMPTE ST 428-1, value 10. +static constexpr SkColorSpacePrimaries kSMPTE_ST_428_1 = { + 1.f, 0.f, 0.f, 1.f, 0.f, 0.f, 1.f / 3.f, 1.f / 3.f}; + +// SMPTE RP 431-2, value 11. +static constexpr SkColorSpacePrimaries kSMPTE_RP_431_2 = { + 0.680f, 0.320f, 0.265f, 0.690f, 0.150f, 0.060f, 0.314f, 0.351f}; + +// SMPTE EG 432-1, value 12. +static constexpr SkColorSpacePrimaries kSMPTE_EG_432_1 = { + 0.680f, 0.320f, 0.265f, 0.690f, 0.150f, 0.060f, 0.3127f, 0.3290f}; + +// No corresponding industry specification identified, value 22. +// This is sometimes referred to as EBU 3213-E, but that document doesn't +// specify these values. +static constexpr SkColorSpacePrimaries kITU_T_H273_Value22 = { + 0.630f, 0.340f, 0.295f, 0.605f, 0.155f, 0.077f, 0.3127f, 0.3290f}; + +//////////////////////////////////////////////////////////////////////////////// +// CSS Color Level 4 predefined and xyz color spaces. + +// 'srgb' +static constexpr SkColorSpacePrimaries kSRGB = kRec709; + +// 'display-p3' (and also 'p3' as a color gamut). +static constexpr SkColorSpacePrimaries kP3 = kSMPTE_EG_432_1; + +// 'a98-rgb' +static constexpr SkColorSpacePrimaries kA98RGB = { + 0.64f, 0.33f, 0.21f, 0.71f, 0.15f, 0.06f, 0.3127f, 0.3290f}; + +// 'prophoto-rgb' +static constexpr SkColorSpacePrimaries kProPhotoRGB = { + 0.7347f, 0.2653f, 0.1596f, 0.8404f, 0.0366f, 0.0001f, 0.34567f, 0.35850f}; + +// 'rec2020' (as both a predefined color space and color gamut). +// The value kRec2020 is already defined above. + +// 'xyzd50' +static constexpr SkColorSpacePrimaries kXYZD50 = { + 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.34567f, 0.35850f}; + +// 'xyz' and 'xyzd65' +static constexpr SkColorSpacePrimaries kXYZD65 = {1.0f, 0.0f, 0.0f, 1.0f, + 0.0f, 0.0f, 0.3127f, 0.3290f}; + +//////////////////////////////////////////////////////////////////////////////// +// Additional helper color primaries. + +// Invalid primaries, initialized to zero. +static constexpr SkColorSpacePrimaries kInvalid = {0}; + +// The GenericRGB space on macOS. +static constexpr SkColorSpacePrimaries kAppleGenericRGB = { + 0.63002f, 0.34000f, 0.29505f, 0.60498f, + 0.15501f, 0.07701f, 0.3127f, 0.3290f}; + +// Primaries where the colors are rotated and the gamut is huge. Good for +// testing. +static constexpr SkColorSpacePrimaries kWideGamutColorSpin = { + 0.01f, 0.98f, 0.01f, 0.01f, 0.98f, 0.01f, 0.3127f, 0.3290f}; + +} // namespace SkNamedPrimariesExt + +#endif // SKIA_EXT_SKCOLORSPACE_PRIMARIES_H_ diff --git a/ext/skcolorspace_primaries_unittest.cc b/ext/skcolorspace_primaries_unittest.cc new file mode 100644 index 000000000000..6edff75e5773 --- /dev/null +++ b/ext/skcolorspace_primaries_unittest.cc @@ -0,0 +1,59 @@ +// Copyright 2022 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "skia/ext/skcolorspace_primaries.h" + +#include "testing/gtest/include/gtest/gtest.h" + +namespace skia { +namespace { + +constexpr float kEpsilon = 0.0001; + +TEST(SkiaUtils, PrimariesD65) { + // DCI P3 (D65) + const auto p3 = SkNamedPrimariesExt::kP3; + + skcms_Matrix3x3 matrix; + EXPECT_TRUE(p3.toXYZD50(&matrix)); + const auto primaries_from_matrix = GetD65PrimariesFromToXYZD50Matrix(matrix); + + // The retrieved primaries from the matrix should be the same as the original + // primaries, because the original primaries had a D65 white point. + EXPECT_NEAR(p3.fRX, primaries_from_matrix.fRX, kEpsilon); + EXPECT_NEAR(p3.fRY, primaries_from_matrix.fRY, kEpsilon); + EXPECT_NEAR(p3.fGX, primaries_from_matrix.fGX, kEpsilon); + EXPECT_NEAR(p3.fGY, primaries_from_matrix.fGY, kEpsilon); + EXPECT_NEAR(p3.fBX, primaries_from_matrix.fBX, kEpsilon); + EXPECT_NEAR(p3.fBY, primaries_from_matrix.fBY, kEpsilon); + EXPECT_NEAR(p3.fWX, primaries_from_matrix.fWX, kEpsilon); + EXPECT_NEAR(p3.fWY, primaries_from_matrix.fWY, kEpsilon); +} + +TEST(SkiaUtils, PrimariesD50) { + // ProPhoto (which has a D50 white point) + const auto pro_photo = SkNamedPrimariesExt::kProPhotoRGB; + + // Convert primaries to a matrix. + skcms_Matrix3x3 pro_photo_matrix; + EXPECT_TRUE(pro_photo.toXYZD50(&pro_photo_matrix)); + + // The convert the matrix back to primaries with a D65 white point. + const auto d65 = GetD65PrimariesFromToXYZD50Matrix(pro_photo_matrix); + + // And then convert the D65 primaries to a matrix. + skcms_Matrix3x3 d65_matrix; + EXPECT_TRUE(d65.toXYZD50(&d65_matrix)); + + // The two matrices should be the same, but the primaries will not be. + EXPECT_FALSE(pro_photo == d65); + for (size_t i = 0; i < 3; ++i) { + for (size_t j = 0; j < 3; ++j) { + EXPECT_NEAR(pro_photo_matrix.vals[i][j], d65_matrix.vals[i][j], kEpsilon); + } + } +} + +} // namespace +} // namespace skia diff --git a/ext/skcolorspace_trfn.h b/ext/skcolorspace_trfn.h new file mode 100644 index 000000000000..96872b729f72 --- /dev/null +++ b/ext/skcolorspace_trfn.h @@ -0,0 +1,102 @@ +// Copyright 2022 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef SKIA_EXT_SKCOLORSPACE_TRFN_H_ +#define SKIA_EXT_SKCOLORSPACE_TRFN_H_ + +#include "third_party/skia/include/core/SkColorSpace.h" + +namespace SkNamedTransferFnExt { + +//////////////////////////////////////////////////////////////////////////////// +// Color primaries defined by ITU-T H.273, table 3. Names are given by the first +// specification referenced in the value's row. + +// Rec. ITU-R BT.709-6, value 1. +static constexpr skcms_TransferFunction kRec709 = {2.222222222222f, + 0.909672415686f, + 0.090327584314f, + 0.222222222222f, + 0.081242858299f, + 0.f, + 0.f}; + +// Rec. ITU-R BT.470-6 System M (historical) assumed display gamma 2.2, value 4. +static constexpr skcms_TransferFunction kRec470SystemM = {2.2f, 1.f}; + +// Rec. ITU-R BT.470-6 System B, G (historical) assumed display gamma 2.8, +// value 5. +static constexpr skcms_TransferFunction kRec470SystemBG = {2.8f, 1.f}; + +// Rec. ITU-R BT.601-7, same as kRec709, value 6. +static constexpr skcms_TransferFunction kRec601 = kRec709; + +// SMPTE ST 240, value 7. +static constexpr skcms_TransferFunction kSMPTE_ST_240 = {2.222222222222f, + 0.899626676224f, + 0.100373323776f, + 0.25f, + 0.091286342118f, + 0.f, + 0.f}; + +// IEC 61966-2-4, value 11, same as kRec709 (but is explicitly extended). +static constexpr skcms_TransferFunction kIEC61966_2_4 = kRec709; + +// IEC 61966-2-1 sRGB, value 13. This is almost equal to +// SkNamedTransferFnExt::kSRGB. The differences are rounding errors that +// cause test failures (and should be unified). +static constexpr skcms_TransferFunction kIEC61966_2_1 = { + 2.4, 0.947867345704f, 0.052132654296f, 0.077399380805f, 0.040449937172f}; + +// Rec. ITU-R BT.2020-2 (10-bit system), value 14. +static constexpr skcms_TransferFunction kRec2020_10bit = kRec709; + +// Rec. ITU-R BT.2020-2 (12-bit system), value 15. +static constexpr skcms_TransferFunction kRec2020_12bit = kRec709; + +// SMPTE ST 428-1, value 17. +static constexpr skcms_TransferFunction kSMPTE_ST_428_1 = {2.6f, + 1.034080527699f}; + +//////////////////////////////////////////////////////////////////////////////// +// CSS Color Level 4 predefined color spaces. + +// 'srgb', 'display-p3' +static constexpr skcms_TransferFunction kSRGB = kIEC61966_2_1; + +// 'a98-rgb' +static constexpr skcms_TransferFunction kA98RGB = {2.2f, 1.}; + +// 'prophoto-rgb' +static constexpr skcms_TransferFunction kProPhotoRGB = {1.8f, 1.}; + +// 'rec2020' uses the same transfer function as kRec709. +static constexpr skcms_TransferFunction kRec2020 = kRec709; + +//////////////////////////////////////////////////////////////////////////////// +// Additional helper transfer functions. + +// Invalid primaries, initialized to zero. +static constexpr skcms_TransferFunction kInvalid = {0}; + +// The interpretation of kRec709 that is produced by accelerated video decode +// on macOS. +static constexpr skcms_TransferFunction kRec709Apple = {1.961f, 1.}; + +// If the sRGB transfer function is f(x), then this transfer function is +// f(x * 1023 / 510). This function gives 510 values to SDR content, and can +// reach a maximum brightnes of 4.99x SDR brightness. +static constexpr skcms_TransferFunction kSRGBExtended1023Over510 = { + SkNamedTransferFnExt::kSRGB.g, + SkNamedTransferFnExt::kSRGB.a * 1023 / 510, + SkNamedTransferFnExt::kSRGB.b, + SkNamedTransferFnExt::kSRGB.c * 1023 / 510, + SkNamedTransferFnExt::kSRGB.d * 1023 / 510, + SkNamedTransferFnExt::kSRGB.e, + SkNamedTransferFnExt::kSRGB.f}; + +} // namespace SkNamedTransferFnExt + +#endif // SKIA_EXT_SKCOLORSPACE_TRFN_H_ diff --git a/ext/skia_histogram.cc b/ext/skia_histogram.cc new file mode 100644 index 000000000000..fca0ec2a7900 --- /dev/null +++ b/ext/skia_histogram.cc @@ -0,0 +1,46 @@ +// Copyright 2016 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "skia/ext/skia_histogram.h" + +#include +#include "base/metrics/histogram_macros_internal.h" + +namespace skia { + +// Wrapper around HISTOGRAM_POINTER_USE - mimics UMA_HISTOGRAM_BOOLEAN but +// allows for an external atomic_histogram_pointer. +void HistogramBoolean(std::atomic_uintptr_t* atomic_histogram_pointer, + const char* name, + bool sample) { + HISTOGRAM_POINTER_USE( + atomic_histogram_pointer, name, AddBoolean(sample), + base::BooleanHistogram::FactoryGet( + name, base::HistogramBase::kUmaTargetedHistogramFlag)); +} + +// Wrapper around HISTOGRAM_POINTER_USE - mimics UMA_HISTOGRAM_EXACT_LINEAR but +// allows for an external atomic_histogram_pointer. +void HistogramExactLinear(std::atomic_uintptr_t* atomic_histogram_pointer, + const char* name, + int sample, + int value_max) { + HISTOGRAM_POINTER_USE(atomic_histogram_pointer, name, Add(sample), + base::LinearHistogram::FactoryGet( + name, 1, value_max, value_max + 1, + base::HistogramBase::kUmaTargetedHistogramFlag)); +} + +// Wrapper around HISTOGRAM_POINTER_USE - mimics UMA_HISTOGRAM_MEMORY_KB but +// allows for an external atomic_histogram_pointer. +void HistogramMemoryKB(std::atomic_uintptr_t* atomic_histogram_pointer, + const char* name, + int sample) { + HISTOGRAM_POINTER_USE(atomic_histogram_pointer, name, Add(sample), + base::Histogram::FactoryGet( + name, 1000, 500000, 50, + base::HistogramBase::kUmaTargetedHistogramFlag)); +} + +} // namespace skia diff --git a/ext/skia_histogram.h b/ext/skia_histogram.h new file mode 100644 index 000000000000..6548118a78e1 --- /dev/null +++ b/ext/skia_histogram.h @@ -0,0 +1,49 @@ +// Copyright 2016 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef SKIA_EXT_SKIA_HISTOGRAM_H_ +#define SKIA_EXT_SKIA_HISTOGRAM_H_ + +#include + +#include +#include + +// This file exposes Chrome's histogram functionality to Skia, without bringing +// in any Chrome specific headers. To achieve the same level of optimization as +// is present in Chrome, we need to use an inlined atomic pointer. This macro +// defines a placeholder atomic which will be inlined into the call-site. This +// placeholder is passed to the actual histogram logic in Chrome. +#define SK_HISTOGRAM_POINTER_HELPER(function, ...) \ + do { \ + static std::atomic_uintptr_t atomic_histogram_pointer; \ + function(std::addressof(atomic_histogram_pointer), __VA_ARGS__); \ + } while (0) + +#define SK_HISTOGRAM_BOOLEAN(name, sample) \ + SK_HISTOGRAM_POINTER_HELPER(skia::HistogramBoolean, "Skia." name, sample) + +#define SK_HISTOGRAM_EXACT_LINEAR(name, sample, value_max) \ + SK_HISTOGRAM_POINTER_HELPER(skia::HistogramExactLinear, "Skia." name, \ + sample, value_max) + +#define SK_HISTOGRAM_MEMORY_KB(name, sample) \ + SK_HISTOGRAM_POINTER_HELPER(skia::HistogramMemoryKB, "Skia." name, sample) + +namespace skia { + +void HistogramBoolean(std::atomic_uintptr_t* atomic_histogram_pointer, + const char* name, + bool sample); +void HistogramExactLinear(std::atomic_uintptr_t* atomic_histogram_pointer, + const char* name, + int sample, + int value_max); +void HistogramMemoryKB(std::atomic_uintptr_t* atomic_histogram_pointer, + const char* name, + int sample); + +} // namespace skia + +#endif // SKIA_EXT_SKIA_HISTOGRAM_H_ diff --git a/ext/skia_memory_dump_provider.cc b/ext/skia_memory_dump_provider.cc new file mode 100644 index 000000000000..9ab9830cb75a --- /dev/null +++ b/ext/skia_memory_dump_provider.cc @@ -0,0 +1,52 @@ +// Copyright 2015 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "skia_memory_dump_provider.h" + +#include "base/trace_event/memory_allocator_dump.h" +#include "base/trace_event/memory_dump_manager.h" +#include "base/trace_event/process_memory_dump.h" +#include "skia/ext/skia_trace_memory_dump_impl.h" +#include "third_party/skia/include/core/SkGraphics.h" + +namespace skia { + +// static +SkiaMemoryDumpProvider* SkiaMemoryDumpProvider::GetInstance() { + return base::Singleton< + SkiaMemoryDumpProvider, + base::LeakySingletonTraits>::get(); +} + +SkiaMemoryDumpProvider::SkiaMemoryDumpProvider() = default; + +SkiaMemoryDumpProvider::~SkiaMemoryDumpProvider() = default; + +bool SkiaMemoryDumpProvider::OnMemoryDump( + const base::trace_event::MemoryDumpArgs& args, + base::trace_event::ProcessMemoryDump* process_memory_dump) { + if (args.level_of_detail == + base::trace_event::MemoryDumpLevelOfDetail::BACKGROUND) { + auto* glyph_cache_dump = + process_memory_dump->CreateAllocatorDump("skia/sk_glyph_cache"); + glyph_cache_dump->AddScalar( + base::trace_event::MemoryAllocatorDump::kNameSize, + base::trace_event::MemoryAllocatorDump::kUnitsBytes, + SkGraphics::GetFontCacheUsed()); + auto* resource_cache_dump = + process_memory_dump->CreateAllocatorDump("skia/sk_resource_cache"); + resource_cache_dump->AddScalar( + base::trace_event::MemoryAllocatorDump::kNameSize, + base::trace_event::MemoryAllocatorDump::kUnitsBytes, + SkGraphics::GetResourceCacheTotalBytesUsed()); + return true; + } + SkiaTraceMemoryDumpImpl skia_dumper(args.level_of_detail, + process_memory_dump); + SkGraphics::DumpMemoryStatistics(&skia_dumper); + + return true; +} + +} // namespace skia diff --git a/ext/skia_memory_dump_provider.h b/ext/skia_memory_dump_provider.h new file mode 100644 index 000000000000..7a1812544d27 --- /dev/null +++ b/ext/skia_memory_dump_provider.h @@ -0,0 +1,36 @@ +// Copyright 2015 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef SKIA_EXT_SKIA_MEMORY_DUMP_PROVIDER_H_ +#define SKIA_EXT_SKIA_MEMORY_DUMP_PROVIDER_H_ + +#include "base/memory/singleton.h" +#include "base/trace_event/memory_dump_provider.h" +#include "third_party/skia/include/core/SkTypes.h" + +namespace skia { + +class SK_API SkiaMemoryDumpProvider + : public base::trace_event::MemoryDumpProvider { + public: + static SkiaMemoryDumpProvider* GetInstance(); + + SkiaMemoryDumpProvider(const SkiaMemoryDumpProvider&) = delete; + SkiaMemoryDumpProvider& operator=(const SkiaMemoryDumpProvider&) = delete; + + // base::trace_event::MemoryDumpProvider implementation: + bool OnMemoryDump( + const base::trace_event::MemoryDumpArgs& args, + base::trace_event::ProcessMemoryDump* process_memory_dump) override; + + private: + friend struct base::DefaultSingletonTraits; + + SkiaMemoryDumpProvider(); + ~SkiaMemoryDumpProvider() override; +}; + +} // namespace skia + +#endif // SKIA_EXT_SKIA_MEMORY_DUMP_PROVIDER_H_ diff --git a/ext/skia_memory_dump_provider_unittest.cc b/ext/skia_memory_dump_provider_unittest.cc new file mode 100644 index 000000000000..0ccccf7a0c15 --- /dev/null +++ b/ext/skia_memory_dump_provider_unittest.cc @@ -0,0 +1,26 @@ +// Copyright 2015 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "skia/ext/skia_memory_dump_provider.h" + +#include + +#include "base/trace_event/process_memory_dump.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace skia { + +// Tests if the skia dump provider dumps without crashing. +TEST(SkiaMemoryDumpProviderTest, OnMemoryDump) { + base::trace_event::MemoryDumpArgs dump_args = { + base::trace_event::MemoryDumpLevelOfDetail::DETAILED}; + std::unique_ptr process_memory_dump( + new base::trace_event::ProcessMemoryDump(dump_args)); + SkiaMemoryDumpProvider::GetInstance()->OnMemoryDump( + dump_args, process_memory_dump.get()); + + ASSERT_TRUE(process_memory_dump->GetAllocatorDump("skia/sk_glyph_cache")); +} + +} // namespace skia diff --git a/ext/skia_trace_memory_dump_impl.cc b/ext/skia_trace_memory_dump_impl.cc new file mode 100644 index 000000000000..522cb7e2d354 --- /dev/null +++ b/ext/skia_trace_memory_dump_impl.cc @@ -0,0 +1,91 @@ +// Copyright 2015 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "skia/ext/skia_trace_memory_dump_impl.h" + +#include "base/trace_event/memory_allocator_dump.h" +#include "base/trace_event/memory_dump_manager.h" +#include "base/trace_event/process_memory_dump.h" +#include "skia/ext/SkDiscardableMemory_chrome.h" + +namespace skia { + +namespace { +const char kMallocBackingType[] = "malloc"; +} + +SkiaTraceMemoryDumpImpl::SkiaTraceMemoryDumpImpl( + base::trace_event::MemoryDumpLevelOfDetail level_of_detail, + base::trace_event::ProcessMemoryDump* process_memory_dump) + : SkiaTraceMemoryDumpImpl("", level_of_detail, process_memory_dump) {} + +SkiaTraceMemoryDumpImpl::SkiaTraceMemoryDumpImpl( + const std::string& dump_name_prefix, + base::trace_event::MemoryDumpLevelOfDetail level_of_detail, + base::trace_event::ProcessMemoryDump* process_memory_dump) + : dump_name_prefix_(dump_name_prefix), + process_memory_dump_(process_memory_dump), + request_level_( + level_of_detail == base::trace_event::MemoryDumpLevelOfDetail::LIGHT + ? SkTraceMemoryDump::kLight_LevelOfDetail + : SkTraceMemoryDump::kObjectsBreakdowns_LevelOfDetail) {} + +SkiaTraceMemoryDumpImpl::~SkiaTraceMemoryDumpImpl() = default; + +void SkiaTraceMemoryDumpImpl::dumpNumericValue(const char* dumpName, + const char* valueName, + const char* units, + uint64_t value) { + auto* dump = process_memory_dump_->GetOrCreateAllocatorDump(dumpName); + dump->AddScalar(valueName, units, value); +} + +void SkiaTraceMemoryDumpImpl::dumpStringValue(const char* dump_name, + const char* value_name, + const char* value) { + auto* dump = process_memory_dump_->GetOrCreateAllocatorDump(dump_name); + dump->AddString(value_name, "", value); +} + +void SkiaTraceMemoryDumpImpl::setMemoryBacking(const char* dumpName, + const char* backingType, + const char* backingObjectId) { + if (strcmp(backingType, kMallocBackingType) == 0) { + auto* dump = process_memory_dump_->GetOrCreateAllocatorDump(dumpName); + const char* system_allocator_name = + base::trace_event::MemoryDumpManager::GetInstance() + ->system_allocator_pool_name(); + if (system_allocator_name) { + process_memory_dump_->AddSuballocation(dump->guid(), + system_allocator_name); + } + } else { + NOTREACHED(); + } +} + +void SkiaTraceMemoryDumpImpl::setDiscardableMemoryBacking( + const char* dumpName, + const SkDiscardableMemory& discardableMemoryObject) { + std::string name = dump_name_prefix_ + dumpName; + DCHECK(!process_memory_dump_->GetAllocatorDump(name)); + const SkDiscardableMemoryChrome& discardable_memory_obj = + static_cast(discardableMemoryObject); + auto* dump = discardable_memory_obj.CreateMemoryAllocatorDump( + name.c_str(), process_memory_dump_); + DCHECK(dump); +} + +SkTraceMemoryDump::LevelOfDetail SkiaTraceMemoryDumpImpl::getRequestedDetails() + const { + return request_level_; +} + +bool SkiaTraceMemoryDumpImpl::shouldDumpWrappedObjects() const { + // Chrome already dumps objects it imports into Skia. Avoid duplicate dumps + // by asking Skia not to dump them. + return false; +} + +} // namespace skia diff --git a/ext/skia_trace_memory_dump_impl.h b/ext/skia_trace_memory_dump_impl.h new file mode 100644 index 000000000000..72deb2f54f25 --- /dev/null +++ b/ext/skia_trace_memory_dump_impl.h @@ -0,0 +1,77 @@ +// Copyright 2015 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef SKIA_EXT_SKIA_TRACE_MEMORY_DUMP_IMPL_H_ +#define SKIA_EXT_SKIA_TRACE_MEMORY_DUMP_IMPL_H_ + +#include + +#include + +#include "base/memory/raw_ptr.h" +#include "base/trace_event/memory_dump_request_args.h" +#include "third_party/skia/include/core/SkTraceMemoryDump.h" + +namespace base { +namespace trace_event { +class ProcessMemoryDump; +} +} + +namespace skia { + +class SK_API SkiaTraceMemoryDumpImpl : public SkTraceMemoryDump { + public: + // This should never outlive the OnMemoryDump call since the + // ProcessMemoryDump is valid only in that timeframe. Optional + // |dump_name_prefix| argument specifies the prefix appended to the dump + // name skia provides. By default it is taken as empty string. + SkiaTraceMemoryDumpImpl( + base::trace_event::MemoryDumpLevelOfDetail level_of_detail, + base::trace_event::ProcessMemoryDump* process_memory_dump); + + SkiaTraceMemoryDumpImpl( + const std::string& dump_name_prefix, + base::trace_event::MemoryDumpLevelOfDetail level_of_detail, + base::trace_event::ProcessMemoryDump* process_memory_dump); + + SkiaTraceMemoryDumpImpl(const SkiaTraceMemoryDumpImpl&) = delete; + SkiaTraceMemoryDumpImpl& operator=(const SkiaTraceMemoryDumpImpl&) = delete; + + ~SkiaTraceMemoryDumpImpl() override; + + // SkTraceMemoryDump implementation: + void dumpNumericValue(const char* dumpName, + const char* valueName, + const char* units, + uint64_t value) override; + void dumpStringValue(const char* dump_name, + const char* value_name, + const char* value) override; + void setMemoryBacking(const char* dumpName, + const char* backingType, + const char* backingObjectId) override; + void setDiscardableMemoryBacking( + const char* dumpName, + const SkDiscardableMemory& discardableMemoryObject) override; + LevelOfDetail getRequestedDetails() const override; + bool shouldDumpWrappedObjects() const override; + + protected: + base::trace_event::ProcessMemoryDump* process_memory_dump() { + return process_memory_dump_; + } + + private: + std::string dump_name_prefix_; + + raw_ptr process_memory_dump_; + + // Stores the level of detail for the current dump. + LevelOfDetail request_level_; +}; + +} // namespace skia + +#endif // SKIA_EXT_SKIA_TRACE_MEMORY_DUMP_IMPL_H_ diff --git a/ext/skia_utils_base.cc b/ext/skia_utils_base.cc new file mode 100644 index 000000000000..745845773cba --- /dev/null +++ b/ext/skia_utils_base.cc @@ -0,0 +1,122 @@ +// Copyright 2013 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "skia/ext/skia_utils_base.h" + +#include + +#include "base/pickle.h" +#include "base/strings/stringprintf.h" +#include "third_party/skia/include/core/SkBitmap.h" +#include "third_party/skia/include/core/SkData.h" +#include "third_party/skia/include/core/SkImage.h" +#include "third_party/skia/include/core/SkImageInfo.h" +#include "third_party/skia/include/core/SkSerialProcs.h" + +namespace skia { + +bool ReadSkString(base::PickleIterator* iter, SkString* str) { + size_t reply_length; + const char* reply_text; + + if (!iter->ReadData(&reply_text, &reply_length)) + return false; + + if (str) + str->set(reply_text, reply_length); + return true; +} + +bool ReadSkFontIdentity(base::PickleIterator* iter, + SkFontConfigInterface::FontIdentity* identity) { + uint32_t reply_id; + uint32_t reply_ttcIndex; + size_t reply_length; + const char* reply_text; + + if (!iter->ReadUInt32(&reply_id) || + !iter->ReadUInt32(&reply_ttcIndex) || + !iter->ReadData(&reply_text, &reply_length)) + return false; + + if (identity) { + identity->fID = reply_id; + identity->fTTCIndex = reply_ttcIndex; + identity->fString.set(reply_text, reply_length); + } + return true; +} + +bool ReadSkFontStyle(base::PickleIterator* iter, SkFontStyle* style) { + uint16_t reply_weight; + uint16_t reply_width; + uint16_t reply_slant; + + if (!iter->ReadUInt16(&reply_weight) || + !iter->ReadUInt16(&reply_width) || + !iter->ReadUInt16(&reply_slant)) + return false; + + if (style) { + *style = SkFontStyle(reply_weight, + reply_width, + static_cast(reply_slant)); + } + return true; +} + +void WriteSkString(base::Pickle* pickle, const SkString& str) { + pickle->WriteData(str.c_str(), str.size()); +} + +void WriteSkFontIdentity(base::Pickle* pickle, + const SkFontConfigInterface::FontIdentity& identity) { + pickle->WriteUInt32(identity.fID); + pickle->WriteUInt32(identity.fTTCIndex); + WriteSkString(pickle, identity.fString); +} + +void WriteSkFontStyle(base::Pickle* pickle, SkFontStyle style) { + pickle->WriteUInt16(style.weight()); + pickle->WriteUInt16(style.width()); + pickle->WriteUInt16(style.slant()); +} + +bool SkBitmapToN32OpaqueOrPremul(const SkBitmap& in, SkBitmap* out) { + DCHECK(out); + if (in.colorType() == kUnknown_SkColorType && + in.alphaType() == kUnknown_SkAlphaType && in.empty() && in.isNull()) { + // Default-initialized bitmaps convert to the same. + *out = SkBitmap(); + return true; + } + const SkImageInfo& info = in.info(); + const bool stride_matches_width = in.rowBytes() == info.minRowBytes(); + if (stride_matches_width && info.colorType() == kN32_SkColorType && + (info.alphaType() == kPremul_SkAlphaType || + info.alphaType() == kOpaque_SkAlphaType)) { + // Shallow copy if the data is already in the right format. + *out = in; + return true; + } + + SkImageInfo new_info = + info.makeColorType(kN32_SkColorType) + .makeAlphaType(info.alphaType() == kOpaque_SkAlphaType + ? kOpaque_SkAlphaType + : kPremul_SkAlphaType); + if (!out->tryAllocPixels(new_info, 0)) { + return false; + } + if (!in.readPixels(out->pixmap())) { + return false; + } + return true; +} + +std::string SkColorToHexString(SkColor color) { + return base::StringPrintf("#%02X%02X%02X", SkColorGetR(color), + SkColorGetG(color), SkColorGetB(color)); +} +} // namespace skia diff --git a/ext/skia_utils_base.h b/ext/skia_utils_base.h new file mode 100644 index 000000000000..7e0cb42e5503 --- /dev/null +++ b/ext/skia_utils_base.h @@ -0,0 +1,64 @@ +// Copyright 2013 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef SKIA_EXT_SKIA_UTILS_BASE_H_ +#define SKIA_EXT_SKIA_UTILS_BASE_H_ + +#include "third_party/skia/include/core/SkColor.h" +#include "third_party/skia/include/core/SkFlattenable.h" +#include "third_party/skia/include/ports/SkFontConfigInterface.h" + +namespace base { +class Pickle; +class PickleIterator; +} + +class SkBitmap; +class SkFlattenable; + +namespace skia { + +// Return true if the pickle/iterator contains a string. If so, and if str +// is not null, copy that string into str. +SK_API bool ReadSkString(base::PickleIterator* iter, SkString* str); + +// Return true if the pickle/iterator contains a FontIdentity. If so, and if +// identity is not null, copy it into identity. +SK_API bool ReadSkFontIdentity(base::PickleIterator* iter, + SkFontConfigInterface::FontIdentity* identity); + +// Return true if the pickle/iterator contains a SkFontStyle. If so, and if +// style is not null, copy it into style. +SK_API bool ReadSkFontStyle(base::PickleIterator* iter, SkFontStyle* style); + +// Writes str into the request pickle. +SK_API void WriteSkString(base::Pickle* pickle, const SkString& str); + +// Writes identity into the request pickle. +SK_API void WriteSkFontIdentity( + base::Pickle* pickle, + const SkFontConfigInterface::FontIdentity& identity); + +// Writes style into the request pickle. +SK_API void WriteSkFontStyle(base::Pickle* pickle, SkFontStyle style); + +// Converts an SkBitmap to an Opaque or Premul N32 SkBitmap with stride matching +// the width of each row. If the input is has the right format (N32 Opaque or +// Premul) without stride padding already, this assigns `in` to `out`, sharing +// the backing pixels. `out` may or may not be GPU-backed. +// +// If unsuccessful, returns false, but |out| may be modified. +// +// This should be called as early as possible at IPC endpoints from +// less-privileged contexts (e.g. on a message from the renderer process) if the +// code handling the SkBitmap wants to work with an N32 type, rather than +// delaying this conversion until a later time. +SK_API bool SkBitmapToN32OpaqueOrPremul(const SkBitmap& in, SkBitmap* out); + +// Returns hex string representation for the |color| in "#FFFFFF" format. +SK_API std::string SkColorToHexString(SkColor color); + +} // namespace skia + +#endif // SKIA_EXT_SKIA_UTILS_BASE_H_ diff --git a/ext/skia_utils_base_unittest.cc b/ext/skia_utils_base_unittest.cc new file mode 100644 index 000000000000..2219f1f061b2 --- /dev/null +++ b/ext/skia_utils_base_unittest.cc @@ -0,0 +1,97 @@ +// Copyright 2020 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "skia/ext/skia_utils_base.h" + +#include "testing/gtest/include/gtest/gtest.h" +#include "third_party/skia/include/core/SkBitmap.h" +#include "third_party/skia/include/core/SkColor.h" +#include "third_party/skia/include/core/SkImageInfo.h" + +namespace skia { +namespace { + +#define EXPECT_EQ_BITMAP(a, b) \ + do { \ + EXPECT_EQ(a.pixmap().addr(), b.pixmap().addr()); \ + EXPECT_EQ(a.pixmap().rowBytes(), b.pixmap().rowBytes()); \ + EXPECT_EQ(a.pixmap().info(), b.pixmap().info()); \ + } while (false) + +TEST(SkiaUtilsBase, ConvertNullToN32) { + SkBitmap bitmap; + SkBitmap out; + EXPECT_TRUE(SkBitmapToN32OpaqueOrPremul(bitmap, &out)); + // Returned a copy of the input bitmap. + EXPECT_EQ_BITMAP(bitmap, out); +} + +TEST(SkiaUtilsBase, ConvertValidToN32) { + SkBitmap bitmap; + bitmap.allocN32Pixels(10, 12); + SkBitmap out; + EXPECT_TRUE(SkBitmapToN32OpaqueOrPremul(bitmap, &out)); + // Returned a copy of the input bitmap. + EXPECT_EQ_BITMAP(bitmap, out); +} + +TEST(SkiaUtilsBase, ConvertWeirdStrideToN32) { + int width = 10; + int height = 12; + + SkBitmap bitmap; + // Stride is > 4 * width. + bitmap.allocPixels(SkImageInfo::MakeN32(width, height, kPremul_SkAlphaType), + width * 4 + 4); + + SkBitmap out; + EXPECT_TRUE(SkBitmapToN32OpaqueOrPremul(bitmap, &out)); + // The stride was converted. + EXPECT_NE(bitmap.rowBytes(), out.rowBytes()); + EXPECT_EQ(out.rowBytes(), width * 4u); +} + +TEST(SkiaUtilsBase, ConvertWeirdFormatToN32) { + int width = 10; + int height = 12; + + // A format smaller than N32. + { + SkBitmap bitmap; + bitmap.allocPixels(SkImageInfo::MakeA8(width, height)); + + SkBitmap out; + EXPECT_TRUE(SkBitmapToN32OpaqueOrPremul(bitmap, &out)); + // The format was converted. + EXPECT_NE(bitmap.rowBytes(), out.rowBytes()); + EXPECT_NE(bitmap.info().colorType(), out.info().colorType()); + EXPECT_EQ(out.rowBytes(), width * 4u); + EXPECT_EQ(out.info().colorType(), kN32_SkColorType); + } + + // A format larger than N32. + { + SkBitmap bitmap; + bitmap.allocPixels(SkImageInfo::Make(width, height, kRGBA_F16_SkColorType, + kPremul_SkAlphaType)); + + SkBitmap out; + EXPECT_TRUE(SkBitmapToN32OpaqueOrPremul(bitmap, &out)); + // The format was converted. + EXPECT_NE(bitmap.rowBytes(), out.rowBytes()); + EXPECT_NE(bitmap.info().colorType(), out.info().colorType()); + EXPECT_EQ(out.rowBytes(), width * 4u); + EXPECT_EQ(out.info().colorType(), kN32_SkColorType); + } +} + +TEST(SkiaUtilsBase, ConvertSkColorToHexString) { + EXPECT_EQ(SkColorToHexString(SK_ColorBLUE), "#0000FF"); + EXPECT_EQ(SkColorToHexString(SK_ColorRED), "#FF0000"); + EXPECT_EQ(SkColorToHexString(SK_ColorGREEN), "#00FF00"); + EXPECT_EQ(SkColorToHexString(SK_ColorWHITE), "#FFFFFF"); +} + +} // namespace +} // namespace skia diff --git a/ext/skia_utils_ios.h b/ext/skia_utils_ios.h new file mode 100644 index 000000000000..3027ae1bff9c --- /dev/null +++ b/ext/skia_utils_ios.h @@ -0,0 +1,54 @@ +// Copyright 2012 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef SKIA_EXT_SKIA_UTILS_IOS_H_ +#define SKIA_EXT_SKIA_UTILS_IOS_H_ + +#include +#include + +#include "third_party/skia/include/core/SkBitmap.h" +#include "third_party/skia/include/core/SkColor.h" + +#ifdef __OBJC__ +@class UIColor; +@class UIImage; +@class NSData; +#else +class UIColor; +class UIImage; +class NSData; +#endif + +namespace skia { + +// Draws a CGImage into an SkBitmap of the given size. +SK_API SkBitmap CGImageToSkBitmap(CGImageRef image, + CGSize size, + bool is_opaque); + +// Given an SkBitmap and a color space, return an autoreleased UIImage. +SK_API UIImage* SkBitmapToUIImageWithColorSpace(const SkBitmap& skia_bitmap, + CGFloat scale, + CGColorSpaceRef color_space); + +// Decodes all image representations inside the data into a vector of SkBitmaps. +// Returns a vector of all the successfully decoded representations or an empty +// vector if none can be decoded. +SK_API std::vector ImageDataToSkBitmaps(NSData* image_data); + +// Decodes all image representations inside the data into a vector of SkBitmaps. +// If a representation is bigger than max_size (either width or height), it is +// ignored. +// Returns a vector of all the successfully decoded representations or an empty +// vector if none can be decoded. +SK_API std::vector ImageDataToSkBitmapsWithMaxSize(NSData* image_data, + CGFloat max_size); + +// Returns a UIColor for an SKColor. Used by iOS downstream. +SK_API UIColor* UIColorFromSkColor(SkColor color); + +} // namespace skia + +#endif // SKIA_EXT_SKIA_UTILS_IOS_H_ diff --git a/ext/skia_utils_ios.mm b/ext/skia_utils_ios.mm new file mode 100644 index 000000000000..cedf4e3b3f2a --- /dev/null +++ b/ext/skia_utils_ios.mm @@ -0,0 +1,136 @@ +// Copyright 2012 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "skia/ext/skia_utils_ios.h" + +#import +#import +#include +#include + +#include "base/ios/ios_util.h" +#include "base/logging.h" +#include "base/mac/scoped_cftyperef.h" +#include "third_party/skia/include/utils/mac/SkCGUtils.h" + +namespace { + +const uint8_t kICOHeaderMagic[4] = {0x00, 0x00, 0x01, 0x00}; + +// Returns whether the data encodes an ico image. +bool EncodesIcoImage(NSData* image_data) { + if (image_data.length < std::size(kICOHeaderMagic)) + return false; + return memcmp(kICOHeaderMagic, image_data.bytes, + std::size(kICOHeaderMagic)) == 0; +} + +} // namespace + +namespace skia { + +SkBitmap CGImageToSkBitmap(CGImageRef image, CGSize size, bool is_opaque) { + SkBitmap bitmap; + if (!image) + return bitmap; + + if (!bitmap.tryAllocN32Pixels(size.width, size.height, is_opaque)) + return bitmap; + + void* data = bitmap.getPixels(); + + // Allocate a bitmap context with 4 components per pixel (BGRA). Apple + // recommends these flags for improved CG performance. +#define HAS_ARGB_SHIFTS(a, r, g, b) \ + (SK_A32_SHIFT == (a) && SK_R32_SHIFT == (r) \ + && SK_G32_SHIFT == (g) && SK_B32_SHIFT == (b)) +#if defined(SK_CPU_LENDIAN) && HAS_ARGB_SHIFTS(24, 16, 8, 0) + base::ScopedCFTypeRef color_space( + CGColorSpaceCreateDeviceRGB()); + base::ScopedCFTypeRef context(CGBitmapContextCreate( + data, size.width, size.height, 8, size.width * 4, color_space, + uint32_t{kCGImageAlphaPremultipliedFirst} | kCGBitmapByteOrder32Host)); +#else +#error We require that Skia's and CoreGraphics's recommended \ + image memory layout match. +#endif +#undef HAS_ARGB_SHIFTS + + DCHECK(context); + if (!context) + return bitmap; + + CGRect imageRect = CGRectMake(0.0, 0.0, size.width, size.height); + CGContextSetBlendMode(context, kCGBlendModeCopy); + CGContextDrawImage(context, imageRect, image); + + return bitmap; +} + +UIImage* SkBitmapToUIImageWithColorSpace(const SkBitmap& skia_bitmap, + CGFloat scale, + CGColorSpaceRef color_space) { + if (skia_bitmap.isNull()) + return nil; + + // First convert SkBitmap to CGImageRef. + base::ScopedCFTypeRef cg_image( + SkCreateCGImageRefWithColorspace(skia_bitmap, color_space)); + + // Now convert to UIImage. + return [UIImage imageWithCGImage:cg_image.get() + scale:scale + orientation:UIImageOrientationUp]; +} + +std::vector ImageDataToSkBitmaps(NSData* image_data) { + return ImageDataToSkBitmapsWithMaxSize(image_data, CGFLOAT_MAX); +} + +std::vector ImageDataToSkBitmapsWithMaxSize(NSData* image_data, + CGFloat max_size) { + DCHECK(image_data); + + // On iOS 8.1.1 |CGContextDrawImage| crashes when processing images included + // in .ico files that are 88x88 pixels or larger (http://crbug.com/435068). + bool skip_images_88x88_or_larger = + base::ios::IsRunningOnOrLater(8, 1, 1) && EncodesIcoImage(image_data); + + base::ScopedCFTypeRef empty_dictionary( + CFDictionaryCreate(NULL, NULL, NULL, 0, NULL, NULL)); + std::vector frames; + + base::ScopedCFTypeRef source( + CGImageSourceCreateWithData((CFDataRef)image_data, empty_dictionary)); + + size_t count = CGImageSourceGetCount(source); + for (size_t index = 0; index < count; ++index) { + base::ScopedCFTypeRef cg_image( + CGImageSourceCreateImageAtIndex(source, index, empty_dictionary)); + + CGSize size = CGSizeMake(CGImageGetWidth(cg_image), + CGImageGetHeight(cg_image)); + if (size.width > max_size || size.height > max_size) + continue; + if (size.width >= 88 && size.height >= 88 && skip_images_88x88_or_larger) + continue; + + const SkBitmap bitmap = CGImageToSkBitmap(cg_image, size, false); + if (!bitmap.empty()) + frames.push_back(bitmap); + } + + DLOG_IF(WARNING, frames.size() != count) << "Only decoded " << frames.size() + << " frames for " << count << " expected."; + return frames; +} + +UIColor* UIColorFromSkColor(SkColor color) { + return [UIColor colorWithRed:SkColorGetR(color) / 255.0f + green:SkColorGetG(color) / 255.0f + blue:SkColorGetB(color) / 255.0f + alpha:SkColorGetA(color) / 255.0f]; +} + +} // namespace skia diff --git a/ext/skia_utils_ios_unittest.mm b/ext/skia_utils_ios_unittest.mm new file mode 100644 index 000000000000..88452f7fa382 --- /dev/null +++ b/ext/skia_utils_ios_unittest.mm @@ -0,0 +1,780 @@ +// Copyright 2012 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "skia/ext/skia_utils_ios.h" + +#import +#import + +#include "base/base64.h" +#include "base/ios/ios_util.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace { + +typedef testing::Test SkiaUtilsIosTest; + +// This is a base64 encoded version of google.com ico file. generated by +// curl http://www.google.com/favicon.ico | base64 -b 76 +const char kIcoEncodedData[] = + "AAABAAIAEBAAAAEAIABoBAAAJgAAACAgAAABACAAqBAAAI4EAAAoAAAAEAAAACAAAAABACAAAAAA" + "AAAEAAASCwAAEgsAAAAAAAAAAAAA9IVCSvSFQuf0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/" + "9IVC//SFQv/0hUL/9IVC//SFQv/0hULk9IVCSvSFQub0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0" + "hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQuf0hUL/9IVC//SFQv/0hUL/9Y1O//rI" + "q//+7+f//eXX//vUvf/7z7X/96Fu//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC" + "//vYwv/97OH/9ZRZ//SFQv/0hUL/9IhG//zbx//3om7/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/" + "9IVC//SFQv/97uX/+buW//SFQv/0hUL/9IVC//SFQv/5upT/+9O6//SFQv/0hUL/9IVC//SFQv/0" + "hUL/9IVC//SFQv/0hUL/+b6b//zezP/0iEf/9IVC//SFQv/1klf//ezh//vPtP/0hUL/9IVC//SF" + "Qv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/3qXr/+siq//m8lv/5wqD//vTu//3t4//1klb/9IVC" + "//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0h0b//vbx//zi0//1j1H/" + "9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/2nmn/+bmS/////v/4" + "sIX/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/5uJH///v5//eo" + "ef/1jU//+82y//afav/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL//vXw" + "//vOs//0hUL/9IVC//ekcf/96+D/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/" + "9IVC//728v/4sIX/9IVC//SFQv/4s4n///v4//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0" + "hUL/9IVC//SFQv/6yKn/+byX//SFQv/0hkT//eTV//vWv//0hUL/9IVC//SFQv/0hUL/9IVC//SF" + "Qv/0hUL/9IVC//SFQv/0hUL/9IZE//m6lP/5u5b//OHQ///+/f/6y6//96d3//SFQv/0hUL/9IVC" + "//SFQv/0hULm9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/" + "9IVC//SFQv/0hULm9IVCSfSFQub0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0" + "hUL/9IVC//SFQv/0hULm9IVCSQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAoAAAAIAAAAEAAAAABACAAAAAAAAAQAAASCwAAEgsA" + "AAAAAAAAAAAA9IVCAPSFQif0hUKt9IVC8vSFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/" + "9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0" + "hUL/9IVC//SFQvL0hUKt9IVCJ/SFQgD0hUIo9IVC7/SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SF" + "Qv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC" + "//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hULv9IVCKPSFQq30hUL/9IVC//SFQv/0hUL/" + "9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0" + "hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUKt9IVC8fSF" + "Qv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC" + "//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/" + "9IVC//SFQvP0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9YtL//i2jv/8" + "28f//vLr///7+P///Pv//vTu//3n2v/6zbH/96Nw//SFQ//0hUL/9IVC//SFQv/0hUL/9IVC//SF" + "Qv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC" + "//ekcv/+8+z////////////+9fD/+9K5//m9mf/4to7/+buV//vSuf/++PT//OPT//aYYP/0hUL/" + "9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0" + "hUL/9IVC//SFQv/2l13///r3/////////fv/+b2Z//SIRv/0hUL/9IVC//SFQv/0hUL/9IVC//WN" + "T//84M///vXv//aZYf/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC" + "//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//vPtP////////////i0i//0hUL/9IVC//SFQv/0hUL/" + "9IVC//SFQv/0hUL/9IVC//WQUv///Pr//OPU//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0" + "hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL//eTV///////+9O7/9IVD//SF" + "Qv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//3m2P//////9ppi//SFQv/0hUL/9IVC" + "//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/718H/" + "//////3s4f/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL//vDn///////4" + "soj/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SF" + "Qv/0hUL/9IVC//erff////////38//WTWP/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC" + "//iziv////////////iwhf/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/" + "9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//rMsP///////eXW//WSVv/0hUL/9IVC//SFQv/0" + "hUL/9IVC//SFQv/4sYb///z7/////////Pv/9ZFV//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SF" + "Qv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//ixhv/+8Of//vn1" + "//rMsP/4rH//9plh//WQUv/1j1L/+s2x//////////////////m9mf/0hUL/9IVC//SFQv/0hUL/" + "9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0" + "hUL/9IVC//SGQ//2nmn/+buW//vNsv/82sb//e3j/////////////////////v/5wZ//9IVC//SF" + "Qv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC" + "//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/83Mj////////////++fb/" + "+K+C//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0" + "hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9ZRZ////" + "/////////vTt//aaYv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC" + "//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/" + "9IVC//SFQv/1lFr////////////6xqf/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0" + "hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SF" + "Qv/0hUL/9IVC//ehbf/70bj//end//3o2////v3///////3l1//0iEb/9IVC//SFQv/0hUL/9IVC" + "//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/" + "9IVC//SFQv/0hUL/9IVC//SFQv/5wqD////////////96t7/96Z2//WOUP/2nWf//NvH//zcyP/1" + "i0z/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SF" + "Qv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/96l6/////////////vLr//WPUf/0hUL/9IVC" + "//SFQv/0h0b//end//3k1f/0iUn/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/" + "9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/8387////////////4" + "sYf/9IVC//SFQv/0hUL/9IVC//SFQv/6w6L///////nBn//0hUL/9IVC//SFQv/0hUL/9IVC//SF" + "Qv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC" + "///69////////vj1//SIR//0hUL/9IVC//SFQv/0hUL/9IVC//m+mv///////e3j//SFQv/0hUL/" + "9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0" + "hUL/9IVC//SFQv/0hUL///r3///////8387/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/+syw////" + "///++fb/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC" + "//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/95NX///////vUvP/0hUL/9IVC//SFQv/0hUL/" + "9IVC//SFQv/97OH///////7y6//0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0" + "hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//i2jv///////N/O//SF" + "Qv/0hUL/9IVC//SFQv/0hUL/96Nx////////////+s2x//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC" + "//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/" + "9IdF//zh0P//+/j/9ZJW//SFQv/0hUL/9IVC//SKSv/96t7///////738v/1k1f/9IVC//SFQv/0" + "hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SF" + "Qv/0hUL/9IVC//SFQv/0hUL/9YxN//vUvf/96+D/96Z0//WNT//3om///ebY/////////Pv/+LKI" + "//WVW//0h0X/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/" + "9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//agbP/7zbL//enc//749P//" + "//////////////////////////3r4P/3p3f/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SF" + "Qv/0hULx9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC" + "//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/" + "9IVC//SFQv/0hUL/9IVC8/SFQq30hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0" + "hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SF" + "Qv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUKt9IVCJ/SFQu/0hUL/9IVC//SFQv/0hUL/9IVC" + "//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/" + "9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC7/SFQif0hUIA9IVCJfSFQq30" + "hULx9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SF" + "Qv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC//SFQv/0hUL/9IVC8fSFQq30hUIl9IVC" + "AIAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAACAAAAB"; + +NSData* StringToNSData(const char* base64encodedData) { + std::string output; + EXPECT_TRUE(base::Base64Decode(base64encodedData, &output)); + return [NSData dataWithBytes:output.data() length:output.length()]; +} + +NSData* InvalidData(NSUInteger length) { + NSMutableData* data=[NSMutableData dataWithLength:length]; + memset([data mutableBytes], 0xFF, length); + return data; +} + +TEST_F(SkiaUtilsIosTest, ImageDataToSkBitmaps) { + std::vector bitmaps( + skia::ImageDataToSkBitmaps(StringToNSData(kIcoEncodedData))); + + EXPECT_EQ(2UL, bitmaps.size()); + EXPECT_EQ(32, bitmaps[0].width()); + EXPECT_EQ(32, bitmaps[0].height()); + EXPECT_EQ(16, bitmaps[1].width()); + EXPECT_EQ(16, bitmaps[1].height()); +} + +TEST_F(SkiaUtilsIosTest, ImageTooLarge) { + std::vector bitmaps(skia::ImageDataToSkBitmapsWithMaxSize( + StringToNSData(kIcoEncodedData), 20)); + EXPECT_EQ(1UL, bitmaps.size()); + EXPECT_EQ(16, bitmaps[0].width()); + EXPECT_EQ(16, bitmaps[0].height()); +} + +TEST_F(SkiaUtilsIosTest, ImageSizeOnTheEdge) { + std::vector bitmaps(skia::ImageDataToSkBitmapsWithMaxSize( + StringToNSData(kIcoEncodedData), 16)); + EXPECT_EQ(1UL, bitmaps.size()); + EXPECT_EQ(16, bitmaps[0].width()); + EXPECT_EQ(16, bitmaps[0].height()); +} + +TEST_F(SkiaUtilsIosTest, InvalidDataFailure) { + std::vector bitmaps1(skia::ImageDataToSkBitmaps(InvalidData(1))); + EXPECT_EQ(0UL, bitmaps1.size()); + std::vector bitmaps2(skia::ImageDataToSkBitmaps(InvalidData(10))); + EXPECT_EQ(0UL, bitmaps2.size()); + std::vector bitmaps3(skia::ImageDataToSkBitmaps(InvalidData(100))); + EXPECT_EQ(0UL, bitmaps3.size()); + std::vector bitmaps4(skia::ImageDataToSkBitmaps(InvalidData(1000))); + EXPECT_EQ(0UL, bitmaps4.size()); + std::vector bitmaps5(skia::ImageDataToSkBitmaps(InvalidData(5000))); + EXPECT_EQ(0UL, bitmaps5.size()); +} + +TEST_F(SkiaUtilsIosTest, EmptyDataFailure) { + std::vector bitmaps(skia::ImageDataToSkBitmaps([NSData data])); + + EXPECT_EQ(0UL, bitmaps.size()); +} + +// This is a base64 encoded version of ico file containing a 88x88 image. +// generated by: convert -size 88x88 xc:white test.ico; base64 -b -i test.ico +const char kIco88x88EncodedData[] = + "AAABAAEAWFgAAAEAIABIfQAAFgAAACgAAABYAAAAsAAAAAEAIAAAAAAAAHkAAAAAAAAAAAAAAA" + "AAAAAAAAD/AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//" + "AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//" + "8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD/" + "/wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP" + "//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA" + "//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AA" + "D//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8A" + "AP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//w" + "AA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//" + "AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//" + "8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD/" + "/wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP" + "//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA" + "//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AA" + "D//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8A" + "AP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//w" + "AA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//" + "AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//" + "8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD/" + "/wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP" + "//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA" + "//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AA" + "D//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8A" + "AP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//w" + "AA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//" + "AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//" + "8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD/" + "/wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP" + "//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA" + "//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AA" + "D//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8A" + "AP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//w" + "AA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//" + "AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//" + "8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD/" + "/wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP" + "//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA" + "//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AA" + "D//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8A" + "AP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//w" + "AA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//" + "AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//" + "8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD/" + "/wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP" + "//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA" + "//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AA" + "D//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8A" + "AP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//w" + "AA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//" + "AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//" + "8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD/" + "/wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP" + "//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA" + "//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AA" + "D//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8A" + "AP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//w" + "AA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//" + "AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//" + "8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD/" + "/wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP" + "//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA" + "//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AA" + "D//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8A" + "AP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//w" + "AA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//" + "AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//" + "8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD/" + "/wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP" + "//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA" + "//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AA" + "D//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8A" + "AP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//w" + "AA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//" + "AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//" + "8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD/" + "/wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP" + "//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA" + "//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AA" + "D//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8A" + "AP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//w" + "AA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//" + "AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//" + "8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD/" + "/wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP" + "//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA" + "//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AA" + "D//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8A" + "AP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//w" + "AA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//" + "AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//" + "8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD/" + "/wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP" + "//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA" + "//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AA" + "D//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8A" + "AP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//w" + "AA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//" + "AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//" + "8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD/" + "/wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP" + "//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA" + "//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AA" + "D//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8A" + "AP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//w" + "AA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//" + "AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//" + "8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD/" + "/wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP" + "//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA" + "//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AA" + "D//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8A" + "AP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//w" + "AA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//" + "AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//" + "8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD/" + "/wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP" + "//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA" + "//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AA" + "D//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8A" + "AP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//w" + "AA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//" + "AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//" + "8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD/" + "/wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP" + "//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA" + "//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AA" + "D//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8A" + "AP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//w" + "AA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//" + "AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//" + "8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD/" + "/wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP" + "//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA" + "//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AA" + "D//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8A" + "AP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//w" + "AA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//" + "AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//" + "8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD/" + "/wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP" + "//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA" + "//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AA" + "D//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8A" + "AP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//w" + "AA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//" + "AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//" + "8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD/" + "/wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP" + "//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA" + "//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AA" + "D//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8A" + "AP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//w" + "AA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//" + "AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//" + "8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD/" + "/wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP" + "//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA" + "//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AA" + "D//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8A" + "AP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//w" + "AA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//" + "AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//" + "8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD/" + "/wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP" + "//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA" + "//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AA" + "D//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8A" + "AP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//w" + "AA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//" + "AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//" + "8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD/" + "/wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP" + "//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA" + "//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AA" + "D//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8A" + "AP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//w" + "AA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//" + "AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//" + "8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD/" + "/wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP" + "//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA" + "//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AA" + "D//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8A" + "AP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//w" + "AA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//" + "AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//" + "8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD/" + "/wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP" + "//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA" + "//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AA" + "D//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8A" + "AP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//w" + "AA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//" + "AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//" + "8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD/" + "/wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP" + "//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA" + "//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AA" + "D//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8A" + "AP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//w" + "AA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//" + "AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//" + "8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD/" + "/wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP" + "//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA" + "//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AA" + "D//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8A" + "AP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//w" + "AA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//" + "AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//" + "8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD/" + "/wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP" + "//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA" + "//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AA" + "D//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8A" + "AP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//w" + "AA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//" + "AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//" + "8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD/" + "/wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP" + "//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA" + "//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AA" + "D//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8A" + "AP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//w" + "AA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//" + "AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//" + "8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD/" + "/wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP" + "//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA" + "//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AA" + "D//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8A" + "AP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//w" + "AA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//" + "AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//" + "8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD/" + "/wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP" + "//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA" + "//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AA" + "D//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8A" + "AP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//w" + "AA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//" + "AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//" + "8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD/" + "/wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP" + "//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA" + "//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AA" + "D//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8A" + "AP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//w" + "AA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//" + "AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//" + "8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD/" + "/wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP" + "//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA" + "//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AA" + "D//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8A" + "AP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//w" + "AA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//" + "AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//" + "8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD/" + "/wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP" + "//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA" + "//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AA" + "D//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8A" + "AP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//w" + "AA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//" + "AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//" + "8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD/" + "/wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP" + "//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA" + "//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AA" + "D//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8A" + "AP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//w" + "AA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//" + "AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//" + "8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD/" + "/wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP" + "//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA" + "//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AA" + "D//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8A" + "AP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//w" + "AA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//" + "AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//" + "8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD/" + "/wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP" + "//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA" + "//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AA" + "D//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8A" + "AP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//w" + "AA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//" + "AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//" + "8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD/" + "/wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP" + "//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA" + "//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AA" + "D//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8A" + "AP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//w" + "AA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//" + "AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//" + "8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD/" + "/wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP" + "//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA" + "//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AA" + "D//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8A" + "AP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//w" + "AA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//" + "AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//" + "8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD/" + "/wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP" + "//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA" + "//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AA" + "D//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8A" + "AP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//w" + "AA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//" + "AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//" + "8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD/" + "/wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP" + "//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA" + "//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AA" + "D//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8A" + "AP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//w" + "AA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//" + "AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//" + "8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD/" + "/wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP" + "//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA" + "//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AA" + "D//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8A" + "AP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//w" + "AA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//" + "AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//" + "8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD/" + "/wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP" + "//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA" + "//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AA" + "D//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8A" + "AP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//w" + "AA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//" + "AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//" + "8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD/" + "/wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP" + "//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA" + "//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AA" + "D//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8A" + "AP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//w" + "AA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//" + "AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//" + "8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD/" + "/wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP" + "//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA" + "//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AA" + "D//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8A" + "AP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//w" + "AA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//" + "AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//" + "8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD/" + "/wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP" + "//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA" + "//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AA" + "D//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8A" + "AP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//w" + "AA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//" + "AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//" + "8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD/" + "/wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP" + "//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA" + "//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AA" + "D//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8A" + "AP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//w" + "AA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//" + "AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//" + "8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD/" + "/wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP" + "//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA" + "//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AA" + "D//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8A" + "AP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//w" + "AA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//" + "AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//" + "8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD/" + "/wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP" + "//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA" + "//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AA" + "D//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8A" + "AP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//w" + "AA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//" + "AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//" + "8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD/" + "/wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP" + "//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA" + "//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AA" + "D//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8A" + "AP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//w" + "AA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//" + "AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//" + "8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD/" + "/wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP" + "//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA" + "//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AA" + "D//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8A" + "AP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//w" + "AA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//" + "AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//" + "8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD/" + "/wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP" + "//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA" + "//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AA" + "D//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8A" + "AP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//w" + "AA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//" + "AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//" + "8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD/" + "/wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP" + "//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA" + "//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AA" + "D//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8A" + "AP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//w" + "AA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//" + "AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//" + "8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD/" + "/wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP" + "//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA" + "//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AA" + "D//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8A" + "AP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//w" + "AA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//" + "AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//" + "8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD/" + "/wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP" + "//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA" + "//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AA" + "D//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8A" + "AP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//w" + "AA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//" + "AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//" + "8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD/" + "/wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP" + "//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA" + "//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AA" + "D//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8A" + "AP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//w" + "AA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//" + "AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//" + "8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD/" + "/wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP" + "//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA" + "//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AA" + "D//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8A" + "AP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//w" + "AA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//" + "AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//" + "8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD/" + "/wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP" + "//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA" + "//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AA" + "D//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8A" + "AP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//w" + "AA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//" + "AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//" + "8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD/" + "/wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP" + "//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA" + "//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AA" + "D//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8A" + "AP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//w" + "AA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//" + "AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//" + "8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD/" + "/wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP" + "//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA" + "//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AA" + "D//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8A" + "AP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//w" + "AA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//" + "AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//" + "8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD/" + "/wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP" + "//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA" + "//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AA" + "D//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8A" + "AP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//w" + "AA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//" + "AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//" + "8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD/" + "/wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP" + "//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA" + "//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AA" + "D//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8A" + "AP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//w" + "AA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//" + "AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//" + "8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD/" + "/wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP" + "//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA" + "//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AA" + "D//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8A" + "AP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//w" + "AA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//" + "AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//" + "8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD/" + "/wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP" + "//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA" + "//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AA" + "D//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8A" + "AP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//w" + "AA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//" + "AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//" + "8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD/" + "/wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP" + "//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA" + "//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AA" + "D//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8A" + "AP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//w" + "AA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//" + "AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//" + "8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD/" + "/wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP" + "//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA" + "//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AA" + "D//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8A" + "AP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//w" + "AA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//" + "AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//" + "8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD/" + "/wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP" + "//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA" + "//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AA" + "D//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8A" + "AP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//w" + "AA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//" + "AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//" + "8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD/" + "/wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP" + "//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA" + "//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AA" + "D//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8A" + "AP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//w" + "AA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//" + "AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//" + "8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD/" + "/wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP" + "//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA" + "//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AA" + "D//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8A" + "AP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//w" + "AA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//" + "AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//" + "8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD/" + "/wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP" + "//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA" + "//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AA" + "D//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8A" + "AP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//w" + "AA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//" + "AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//" + "8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD/" + "/wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP" + "//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA" + "//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AA" + "D//wAA//8AAP//AAD/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAA"; + +TEST_F(SkiaUtilsIosTest, Image88x88OrLarger) { + std::vector bitmaps( + skia::ImageDataToSkBitmaps(StringToNSData(kIco88x88EncodedData))); + if (base::ios::IsRunningOnOrLater(8, 1, 1)) + EXPECT_EQ(0UL, bitmaps.size()); + else + EXPECT_EQ(1UL, bitmaps.size()); +} + +TEST_F(SkiaUtilsIosTest, UIColorFromSkColor) { + SkColor color = SkColorSetARGB(50, 100, 150, 200); + UIColor* ios_color = skia::UIColorFromSkColor(color); + CGFloat red, green, blue, alpha; + [ios_color getRed:&red green:&green blue:&blue alpha:&alpha]; + EXPECT_EQ(50, static_cast(alpha * 255 + 0.5f)); + EXPECT_EQ(100, static_cast(red * 255 + 0.5f)); + EXPECT_EQ(150, static_cast(green * 255 + 0.5f)); + EXPECT_EQ(200, static_cast(blue * 255 + 0.5f)); +} + +} // namespace + diff --git a/ext/skia_utils_mac.h b/ext/skia_utils_mac.h new file mode 100644 index 000000000000..efff021ebe10 --- /dev/null +++ b/ext/skia_utils_mac.h @@ -0,0 +1,98 @@ +// Copyright 2012 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef SKIA_EXT_SKIA_UTILS_MAC_H_ +#define SKIA_EXT_SKIA_UTILS_MAC_H_ + +#include + +#include "base/mac/scoped_cftyperef.h" +#include "third_party/skia/include/core/SkBitmap.h" +#include "third_party/skia/include/core/SkColor.h" +#include "third_party/skia/include/core/SkPixmap.h" + +struct SkIRect; +struct SkRect; +class SkMatrix; +using NSSize = CGSize; + +#ifdef __OBJC__ +@class NSBitmapImageRep; +@class NSImage; +@class NSImageRep; +@class NSColor; +#else +class NSBitmapImageRep; +class NSImage; +class NSImageRep; +class NSColor; +#endif + +namespace skia { + +// Matrix converters. +SK_API CGAffineTransform SkMatrixToCGAffineTransform(const SkMatrix& matrix); + +// Rectangle converters. +SK_API SkRect CGRectToSkRect(const CGRect& rect); + +// Converts a Skia rect to a CoreGraphics CGRect. +CGRect SkIRectToCGRect(const SkIRect& rect); +CGRect SkRectToCGRect(const SkRect& rect); + +// Converts NSColor to an SKColor. +// NSColor has a number of methods that return system colors (i.e. controlled by +// user preferences). This function converts the color given by an NSColor class +// method to an SkColor. Official documentation suggests developers only rely on +// +[NSColor selectedTextBackgroundColor] and +[NSColor selectedControlColor], +// but other colors give a good baseline. For many, a gradient is involved; the +// palette chosen based on the enum value given by +[NSColor currentColorTint]. +// Apple's documentation also suggests to use NSColorList, but the system color +// list is just populated with class methods on NSColor. +SK_API SkColor NSSystemColorToSkColor(NSColor* color); + +// Converts CGColorRef to the ARGB layout Skia expects. The given CGColorRef +// should be in the sRGB color space and include alpha. +SK_API SkColor CGColorRefToSkColor(CGColorRef color); + +// Converts a Skia ARGB color to CGColorRef. Assumes sRGB color space. +SK_API base::ScopedCFTypeRef CGColorCreateFromSkColor( + SkColor color); + +// Converts NSColor to ARGB. Returns raw rgb values and does no colorspace +// conversion. Only valid for colors in calibrated and device color spaces. +SK_API SkColor NSDeviceColorToSkColor(NSColor* color); + +// Converts ARGB in the specified color space to NSColor. +// Prefer sRGB over calibrated colors. +SK_API NSColor* SkColorToCalibratedNSColor(SkColor color); +SK_API NSColor* SkColorToDeviceNSColor(SkColor color); +SK_API NSColor* SkColorToSRGBNSColor(SkColor color); + +// Converts a CGImage to a SkBitmap. +SK_API SkBitmap CGImageToSkBitmap(CGImageRef image); + +// Draws an NSImage with a given size into a SkBitmap. +SK_API SkBitmap NSImageToSkBitmapWithColorSpace(NSImage* image, + bool is_opaque, + CGColorSpaceRef color_space); + +// Draws an NSImageRep with a given size into a SkBitmap. +SK_API SkBitmap NSImageRepToSkBitmapWithColorSpace(NSImageRep* image, + NSSize size, + bool is_opaque, + CGColorSpaceRef colorspace); + +// Given an SkBitmap, return an autoreleased NSBitmapImageRep. +SK_API NSBitmapImageRep* SkBitmapToNSBitmapImageRepWithColorSpace( + const SkBitmap& skiaBitmap, + CGColorSpaceRef colorSpace); + +// Given an SkBitmap and a color space, return an autoreleased NSImage. +SK_API NSImage* SkBitmapToNSImageWithColorSpace(const SkBitmap& icon, + CGColorSpaceRef colorSpace); + +} // namespace skia + +#endif // SKIA_EXT_SKIA_UTILS_MAC_H_ diff --git a/ext/skia_utils_mac.mm b/ext/skia_utils_mac.mm new file mode 100644 index 000000000000..495c99f642ce --- /dev/null +++ b/ext/skia_utils_mac.mm @@ -0,0 +1,257 @@ +// Copyright 2012 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "skia/ext/skia_utils_mac.h" + +#import +#include + +#include + +#include "base/check.h" +#include "base/mac/mac_util.h" +#include "base/mac/scoped_cftyperef.h" +#include "base/mac/scoped_nsobject.h" +#include "skia/ext/platform_canvas.h" +#include "third_party/skia/include/utils/mac/SkCGUtils.h" + +namespace { + +// Draws an NSImage or an NSImageRep with a given size into a SkBitmap. +SkBitmap NSImageOrNSImageRepToSkBitmapWithColorSpace( + NSImage* image, + NSImageRep* image_rep, + NSSize size, + bool is_opaque, + CGColorSpaceRef color_space) { + // Only image or image_rep should be provided, not both. + DCHECK((image != nullptr) ^ (image_rep != nullptr)); + + SkBitmap bitmap; + if (!bitmap.tryAllocN32Pixels(size.width, size.height, is_opaque)) + return bitmap; // Return |bitmap| which should respond true to isNull(). + + + void* data = bitmap.getPixels(); + + // Allocate a bitmap context with 4 components per pixel (BGRA). Apple + // recommends these flags for improved CG performance. +#define HAS_ARGB_SHIFTS(a, r, g, b) \ + (SK_A32_SHIFT == (a) && SK_R32_SHIFT == (r) \ + && SK_G32_SHIFT == (g) && SK_B32_SHIFT == (b)) +#if defined(SK_CPU_LENDIAN) && HAS_ARGB_SHIFTS(24, 16, 8, 0) + base::ScopedCFTypeRef context(CGBitmapContextCreate( + data, size.width, size.height, 8, size.width * 4, color_space, + uint32_t{kCGImageAlphaPremultipliedFirst} | kCGBitmapByteOrder32Host)); +#else +#error We require that Skia's and CoreGraphics's recommended \ + image memory layout match. +#endif +#undef HAS_ARGB_SHIFTS + + // Something went really wrong. Best guess is that the bitmap data is invalid. + DCHECK(context); + + [NSGraphicsContext saveGraphicsState]; + + NSGraphicsContext* context_cocoa = + [NSGraphicsContext graphicsContextWithCGContext:context flipped:NO]; + [NSGraphicsContext setCurrentContext:context_cocoa]; + + NSRect drawRect = NSMakeRect(0, 0, size.width, size.height); + if (image) { + [image drawInRect:drawRect + fromRect:NSZeroRect + operation:NSCompositingOperationCopy + fraction:1.0]; + } else { + [image_rep drawInRect:drawRect + fromRect:NSZeroRect + operation:NSCompositingOperationCopy + fraction:1.0 + respectFlipped:NO + hints:nil]; + } + + [NSGraphicsContext restoreGraphicsState]; + + return bitmap; +} + +} // namespace + +namespace skia { + +CGAffineTransform SkMatrixToCGAffineTransform(const SkMatrix& matrix) { + // CGAffineTransforms don't support perspective transforms, so make sure + // we don't get those. + DCHECK(matrix[SkMatrix::kMPersp0] == 0.0f); + DCHECK(matrix[SkMatrix::kMPersp1] == 0.0f); + DCHECK(matrix[SkMatrix::kMPersp2] == 1.0f); + + return CGAffineTransformMake(matrix[SkMatrix::kMScaleX], + matrix[SkMatrix::kMSkewY], + matrix[SkMatrix::kMSkewX], + matrix[SkMatrix::kMScaleY], + matrix[SkMatrix::kMTransX], + matrix[SkMatrix::kMTransY]); +} + +SkRect CGRectToSkRect(const CGRect& rect) { + SkRect sk_rect = { + rect.origin.x, rect.origin.y, CGRectGetMaxX(rect), CGRectGetMaxY(rect) + }; + return sk_rect; +} + +CGRect SkIRectToCGRect(const SkIRect& rect) { + CGRect cg_rect = { + { rect.fLeft, rect.fTop }, + { rect.fRight - rect.fLeft, rect.fBottom - rect.fTop } + }; + return cg_rect; +} + +CGRect SkRectToCGRect(const SkRect& rect) { + CGRect cg_rect = { + { rect.fLeft, rect.fTop }, + { rect.fRight - rect.fLeft, rect.fBottom - rect.fTop } + }; + return cg_rect; +} + +SkColor NSSystemColorToSkColor(NSColor* color) { + // System colors use the an NSNamedColorSpace called "System", so first step + // is to convert the color into something that can be worked with. + NSColor* device_color = + [color colorUsingColorSpace:[NSColorSpace deviceRGBColorSpace]]; + if (device_color) + return NSDeviceColorToSkColor(device_color); + + // Sometimes the conversion is not possible, but we can get an approximation + // by going through a CGColorRef. Note that simply using NSColor methods for + // accessing components for system colors results in exceptions like + // "-numberOfComponents not valid for the NSColor NSNamedColorSpace System + // windowBackgroundColor; need to first convert colorspace." Hence the + // conversion first to CGColor. + CGColorRef cg_color = [color CGColor]; + const size_t component_count = CGColorGetNumberOfComponents(cg_color); + if (component_count == 4) + return CGColorRefToSkColor(cg_color); + + CHECK(component_count == 1 || component_count == 2); + // 1-2 components means a grayscale channel and maybe an alpha channel, which + // CGColorRefToSkColor will not like. But RGB is additive, so the conversion + // is easy (RGB to grayscale is less easy). + const CGFloat* components = CGColorGetComponents(cg_color); + CGFloat alpha = component_count == 2 ? components[1] : 1.0f; + return SkColor4f{components[0], components[0], components[0], alpha} + .toSkColor(); +} + +SkColor CGColorRefToSkColor(CGColorRef color) { + base::ScopedCFTypeRef cg_color( + CGColorCreateCopyByMatchingToColorSpace(base::mac::GetSRGBColorSpace(), + kCGRenderingIntentDefault, color, + nullptr)); + DCHECK(CGColorGetNumberOfComponents(color) == 4); + const CGFloat* components = CGColorGetComponents(cg_color); + return SkColor4f{components[0], components[1], components[2], components[3]} + .toSkColor(); +} + +base::ScopedCFTypeRef CGColorCreateFromSkColor(SkColor color) { + CGFloat components[] = { + SkColorGetR(color) / 255.0f, SkColorGetG(color) / 255.0f, + SkColorGetB(color) / 255.0f, SkColorGetA(color) / 255.0f}; + return base::ScopedCFTypeRef( + CGColorCreate(base::mac::GetSRGBColorSpace(), components)); +} + +// Converts NSColor to ARGB +SkColor NSDeviceColorToSkColor(NSColor* color) { + DCHECK([color colorSpace] == [NSColorSpace genericRGBColorSpace] || + [color colorSpace] == [NSColorSpace deviceRGBColorSpace]); + CGFloat red, green, blue, alpha; + color = [color colorUsingColorSpace:[NSColorSpace deviceRGBColorSpace]]; + [color getRed:&red green:&green blue:&blue alpha:&alpha]; + return SkColor4f{red, green, blue, alpha}.toSkColor(); +} + +// Converts ARGB to NSColor. +NSColor* SkColorToCalibratedNSColor(SkColor color) { + return [NSColor colorWithCalibratedRed:SkColorGetR(color) / 255.0f + green:SkColorGetG(color) / 255.0f + blue:SkColorGetB(color) / 255.0f + alpha:SkColorGetA(color) / 255.0f]; +} + +NSColor* SkColorToDeviceNSColor(SkColor color) { + return [NSColor colorWithDeviceRed:SkColorGetR(color) / 255.0f + green:SkColorGetG(color) / 255.0f + blue:SkColorGetB(color) / 255.0f + alpha:SkColorGetA(color) / 255.0f]; +} + +NSColor* SkColorToSRGBNSColor(SkColor color) { + const CGFloat components[] = { + SkColorGetR(color) / 255.0f, SkColorGetG(color) / 255.0f, + SkColorGetB(color) / 255.0f, SkColorGetA(color) / 255.0f}; + return [NSColor colorWithColorSpace:[NSColorSpace sRGBColorSpace] + components:components + count:4]; +} + +SkBitmap CGImageToSkBitmap(CGImageRef image) { + SkBitmap bitmap; + if (image && SkCreateBitmapFromCGImage(&bitmap, image)) + return bitmap; + return SkBitmap(); +} + +SkBitmap NSImageToSkBitmapWithColorSpace( + NSImage* image, bool is_opaque, CGColorSpaceRef color_space) { + return NSImageOrNSImageRepToSkBitmapWithColorSpace( + image, nil, [image size], is_opaque, color_space); +} + +SkBitmap NSImageRepToSkBitmapWithColorSpace(NSImageRep* image_rep, + NSSize size, + bool is_opaque, + CGColorSpaceRef color_space) { + return NSImageOrNSImageRepToSkBitmapWithColorSpace( + nil, image_rep, size, is_opaque, color_space); +} + +NSBitmapImageRep* SkBitmapToNSBitmapImageRepWithColorSpace( + const SkBitmap& skiaBitmap, + CGColorSpaceRef colorSpace) { + // First convert SkBitmap to CGImageRef. + base::ScopedCFTypeRef cgimage( + SkCreateCGImageRefWithColorspace(skiaBitmap, colorSpace)); + if (!cgimage) + return nil; + + // Now convert to NSBitmapImageRep. + base::scoped_nsobject bitmap( + [[NSBitmapImageRep alloc] initWithCGImage:cgimage]); + return [bitmap.release() autorelease]; +} + +NSImage* SkBitmapToNSImageWithColorSpace(const SkBitmap& skiaBitmap, + CGColorSpaceRef colorSpace) { + if (skiaBitmap.isNull()) + return nil; + + base::scoped_nsobject image([[NSImage alloc] init]); + NSBitmapImageRep* imageRep = + SkBitmapToNSBitmapImageRepWithColorSpace(skiaBitmap, colorSpace); + if (!imageRep) + return nil; + [image addRepresentation:imageRep]; + [image setSize:NSMakeSize(skiaBitmap.width(), skiaBitmap.height())]; + return [image.release() autorelease]; +} + +} // namespace skia diff --git a/ext/skia_utils_mac_unittest.mm b/ext/skia_utils_mac_unittest.mm new file mode 100644 index 000000000000..06553d4ba421 --- /dev/null +++ b/ext/skia_utils_mac_unittest.mm @@ -0,0 +1,214 @@ +// Copyright 2012 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "skia/ext/skia_utils_mac.h" + +#import + +#include "base/mac/foundation_util.h" +#include "base/mac/mac_util.h" +#include "base/mac/scoped_nsobject.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "third_party/skia/include/core/SkColorSpace.h" +#include "ui/gfx/scoped_ns_graphics_context_save_gstate_mac.h" + +namespace { + +class SkiaUtilsMacTest : public testing::Test { + public: + enum class TestColor { + kRed, + kBlue, + }; + + enum class ColorType { + k24Bit, // kN32_SkColorType + k16Bit, // kARGB_4444_SkColorType + }; + + // Creates a test bitmap of the specified color and color type. + SkBitmap CreateSkBitmap(int width, + int height, + TestColor test_color, + ColorType color_type); + + // Creates a red image. + NSImage* CreateNSImage(int width, int height); + + // Checks that the given bitmap rep is actually the correct color. + void TestImageRep(NSBitmapImageRep* image_rep, TestColor test_color); + + // Checks that the given bitmap is red. + void TestSkBitmap(const SkBitmap& bitmap); + + // Tests `SkBitmapToNSImageWithColorSpace` for a specific combination of color + // and color type. Creates a bitmap with `CreateSkBitmap`, converts it into an + // `NSImage`, then tests it with `TestImageRep`. + void ShapeHelper(int width, + int height, + TestColor test_color, + ColorType color_type); +}; + +SkBitmap SkiaUtilsMacTest::CreateSkBitmap(int width, + int height, + TestColor test_color, + ColorType color_type) { + SkColorType sk_color_type = color_type == ColorType::k24Bit + ? kN32_SkColorType + : kARGB_4444_SkColorType; + SkImageInfo info = + SkImageInfo::Make(width, height, sk_color_type, kPremul_SkAlphaType, + SkColorSpace::MakeSRGB()); + + SkBitmap bitmap; + bitmap.allocPixels(info); + + if (test_color == TestColor::kRed) + bitmap.eraseARGB(0xff, 0xff, 0, 0); + else + bitmap.eraseARGB(0xff, 0, 0, 0xff); + + return bitmap; +} + +NSImage* SkiaUtilsMacTest::CreateNSImage(int width, int height) { + // An `NSBitmapImageRep` can only be created with a handful of named color + // spaces, and sRGB isn't one. Do a retagging after creation to switch it. + base::scoped_nsobject initial_bitmap( + [[NSBitmapImageRep alloc] + initWithBitmapDataPlanes:nil + pixelsWide:width + pixelsHigh:height + bitsPerSample:8 + samplesPerPixel:4 + hasAlpha:YES + isPlanar:NO + colorSpaceName:NSCalibratedRGBColorSpace + bitmapFormat:0 + bytesPerRow:4 * width + bitsPerPixel:32]); + NSBitmapImageRep* bitmap = [initial_bitmap + bitmapImageRepByRetaggingWithColorSpace:NSColorSpace.sRGBColorSpace]; + + { + gfx::ScopedNSGraphicsContextSaveGState scopedGState; + [NSGraphicsContext + setCurrentContext:[NSGraphicsContext + graphicsContextWithBitmapImageRep:bitmap]]; + + CGFloat comps[] = {1.0, 0.0, 0.0, 1.0}; + NSColor* color = [NSColor colorWithColorSpace:NSColorSpace.sRGBColorSpace + components:comps + count:4]; + [color set]; + NSRectFill(NSMakeRect(0, 0, width, height)); + } + + base::scoped_nsobject image( + [[NSImage alloc] initWithSize:NSMakeSize(width, height)]); + [image addRepresentation:bitmap]; + + return [image.release() autorelease]; +} + +void SkiaUtilsMacTest::TestImageRep(NSBitmapImageRep* image_rep, + TestColor test_color) { + // Get the color of a pixel and make sure it looks fine. + int x = image_rep.size.width > 17 ? 17 : 0; + int y = image_rep.size.height > 17 ? 17 : 0; + NSColor* color = [image_rep colorAtX:x y:y]; + + ASSERT_EQ(4, color.numberOfComponents); + CGFloat color_components[4]; + [color getComponents:color_components]; + const CGFloat& red = color_components[0]; + const CGFloat& green = color_components[1]; + const CGFloat& blue = color_components[2]; + const CGFloat& alpha = color_components[3]; + + // Be a little tolerant of floating point rounding, maybe, but everything is + // done in SRGB so there should be no color space conversion affecting things. + if (test_color == TestColor::kRed) { + EXPECT_GT(red, 0.9995); + EXPECT_LT(blue, 0.0005); + } else { + EXPECT_LT(red, 0.0005); + EXPECT_GT(blue, 0.9995); + } + EXPECT_LT(green, 0.0005); + EXPECT_GT(alpha, 0.9995); +} + +void SkiaUtilsMacTest::TestSkBitmap(const SkBitmap& bitmap) { + int x = bitmap.width() > 17 ? 17 : 0; + int y = bitmap.height() > 17 ? 17 : 0; + SkColor color = bitmap.getColor(x, y); + + EXPECT_EQ(255u, SkColorGetR(color)); + EXPECT_EQ(0u, SkColorGetB(color)); + EXPECT_EQ(0u, SkColorGetG(color)); + EXPECT_EQ(255u, SkColorGetA(color)); +} + +void SkiaUtilsMacTest::ShapeHelper(int width, + int height, + TestColor test_color, + ColorType color_type) { + SkBitmap bitmap(CreateSkBitmap(width, height, test_color, color_type)); + + // Confirm size + NSImage* image = skia::SkBitmapToNSImageWithColorSpace( + bitmap, base::mac::GetSRGBColorSpace()); + EXPECT_DOUBLE_EQ(image.size.width, (CGFloat)width); + EXPECT_DOUBLE_EQ(image.size.height, (CGFloat)height); + + EXPECT_TRUE(image.representations.count == 1); + EXPECT_TRUE([image.representations.lastObject + isKindOfClass:[NSBitmapImageRep class]]); + TestImageRep(base::mac::ObjCCastStrict( + image.representations.lastObject), + test_color); +} + +TEST_F(SkiaUtilsMacTest, BitmapToNSImage_RedSquare64x64) { + ShapeHelper(64, 64, TestColor::kRed, ColorType::k24Bit); +} + +TEST_F(SkiaUtilsMacTest, BitmapToNSImage_BlueRectangle199x19) { + ShapeHelper(199, 19, TestColor::kBlue, ColorType::k24Bit); +} + +TEST_F(SkiaUtilsMacTest, BitmapToNSImage_BlueRectangle444) { + ShapeHelper(200, 200, TestColor::kBlue, ColorType::k16Bit); +} + +TEST_F(SkiaUtilsMacTest, BitmapToNSBitmapImageRep_BlueRectangle20x30) { + int width = 20; + int height = 30; + + SkBitmap bitmap( + CreateSkBitmap(width, height, TestColor::kBlue, ColorType::k24Bit)); + NSBitmapImageRep* imageRep = skia::SkBitmapToNSBitmapImageRepWithColorSpace( + bitmap, base::mac::GetSRGBColorSpace()); + + EXPECT_DOUBLE_EQ(width, imageRep.size.width); + EXPECT_DOUBLE_EQ(height, imageRep.size.height); + TestImageRep(imageRep, TestColor::kBlue); +} + +TEST_F(SkiaUtilsMacTest, NSImageRepToSkBitmap) { + int width = 10; + int height = 15; + + NSImage* image = CreateNSImage(width, height); + EXPECT_EQ(1u, image.representations.count); + NSBitmapImageRep* imageRep = base::mac::ObjCCastStrict( + image.representations.lastObject); + SkBitmap bitmap(skia::NSImageRepToSkBitmapWithColorSpace( + imageRep, image.size, false, base::mac::GetSRGBColorSpace())); + TestSkBitmap(bitmap); +} + +} // namespace diff --git a/ext/skia_utils_win.cc b/ext/skia_utils_win.cc new file mode 100644 index 000000000000..65ac862444f5 --- /dev/null +++ b/ext/skia_utils_win.cc @@ -0,0 +1,393 @@ +// Copyright 2006-2008 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "skia/ext/skia_utils_win.h" + +#include +#include + +#include "base/check_op.h" +#include "base/debug/gdi_debug_util_win.h" +#include "base/numerics/checked_math.h" +#include "base/win/scoped_hdc.h" +#include "base/win/scoped_hglobal.h" +#include "skia/ext/legacy_display_globals.h" +#include "skia/ext/skia_utils_base.h" +#include "third_party/skia/include/core/SkColorSpace.h" +#include "third_party/skia/include/core/SkRect.h" +#include "third_party/skia/include/core/SkSurface.h" +#include "third_party/skia/include/core/SkTypes.h" + +namespace { + +static_assert(offsetof(RECT, left) == offsetof(SkIRect, fLeft), "o1"); +static_assert(offsetof(RECT, top) == offsetof(SkIRect, fTop), "o2"); +static_assert(offsetof(RECT, right) == offsetof(SkIRect, fRight), "o3"); +static_assert(offsetof(RECT, bottom) == offsetof(SkIRect, fBottom), "o4"); +static_assert(sizeof(RECT().left) == sizeof(SkIRect().fLeft), "o5"); +static_assert(sizeof(RECT().top) == sizeof(SkIRect().fTop), "o6"); +static_assert(sizeof(RECT().right) == sizeof(SkIRect().fRight), "o7"); +static_assert(sizeof(RECT().bottom) == sizeof(SkIRect().fBottom), "o8"); +static_assert(sizeof(RECT) == sizeof(SkIRect), "o9"); + +void CreateBitmapHeaderWithColorDepth(LONG width, + LONG height, + WORD color_depth, + BITMAPINFOHEADER* hdr) { + // These values are shared with gfx::PlatformDevice. + hdr->biSize = sizeof(BITMAPINFOHEADER); + hdr->biWidth = width; + hdr->biHeight = -height; // Minus means top-down bitmap. + hdr->biPlanes = 1; + hdr->biBitCount = color_depth; + hdr->biCompression = BI_RGB; // No compression. + hdr->biSizeImage = 0; + hdr->biXPelsPerMeter = 1; + hdr->biYPelsPerMeter = 1; + hdr->biClrUsed = 0; + hdr->biClrImportant = 0; +} + +// Fills in a BITMAPV5HEADER structure. This is to be used for images that have +// an alpha channel and are in the ARGB8888 format. This is because DIBV5 has an +// explicit mask for each component which default to XRGB and we manually set +// flag so the alpha channel is the first byte. This is not supported by the +// older-style BITMAPINFOHEADER. +void CreateBitmapV5HeaderForARGB8888(LONG width, + LONG height, + LONG image_size, + BITMAPV5HEADER* hdr) { + memset(hdr, 0, sizeof(BITMAPV5HEADER)); + hdr->bV5Size = sizeof(BITMAPV5HEADER); + hdr->bV5Width = width; + // If height is positive this means that the image will be bottom-up. + hdr->bV5Height = height; + hdr->bV5Planes = 1; + hdr->bV5BitCount = 32; + hdr->bV5Compression = BI_RGB; + hdr->bV5AlphaMask = 0xff000000; + hdr->bV5CSType = LCS_WINDOWS_COLOR_SPACE; + hdr->bV5Intent = LCS_GM_IMAGES; + hdr->bV5ClrUsed = 0; + hdr->bV5ClrImportant = 0; + hdr->bV5ProfileData = 0; +} + +} // namespace + +namespace skia { + +POINT SkPointToPOINT(const SkPoint& point) { + POINT win_point = { + SkScalarRoundToInt(point.fX), SkScalarRoundToInt(point.fY) + }; + return win_point; +} + +SkRect RECTToSkRect(const RECT& rect) { + SkRect sk_rect = { SkIntToScalar(rect.left), SkIntToScalar(rect.top), + SkIntToScalar(rect.right), SkIntToScalar(rect.bottom) }; + return sk_rect; +} + +SkColor COLORREFToSkColor(COLORREF color) { +#ifndef _MSC_VER + return SkColorSetRGB(GetRValue(color), GetGValue(color), GetBValue(color)); +#else + // ARGB = 0xFF000000 | ((0BGR -> RGB0) >> 8) + return 0xFF000000u | (_byteswap_ulong(color) >> 8); +#endif +} + +COLORREF SkColorToCOLORREF(SkColor color) { +#ifndef _MSC_VER + return RGB(SkColorGetR(color), SkColorGetG(color), SkColorGetB(color)); +#else + // 0BGR = ((ARGB -> BGRA) >> 8) + return (_byteswap_ulong(color) >> 8); +#endif +} + +void InitializeDC(HDC context) { + // Enables world transformation. + // If the GM_ADVANCED graphics mode is set, GDI always draws arcs in the + // counterclockwise direction in logical space. This is equivalent to the + // statement that, in the GM_ADVANCED graphics mode, both arc control points + // and arcs themselves fully respect the device context's world-to-device + // transformation. + BOOL res = SetGraphicsMode(context, GM_ADVANCED); + SkASSERT(res != 0); + + // Enables dithering. + res = SetStretchBltMode(context, HALFTONE); + SkASSERT(res != 0); + // As per SetStretchBltMode() documentation, SetBrushOrgEx() must be called + // right after. + res = SetBrushOrgEx(context, 0, 0, NULL); + SkASSERT(res != 0); + + // Sets up default orientation. + res = SetArcDirection(context, AD_CLOCKWISE); + SkASSERT(res != 0); + + // Sets up default colors. + res = SetBkColor(context, RGB(255, 255, 255)); + SkASSERT(res != CLR_INVALID); + res = SetTextColor(context, RGB(0, 0, 0)); + SkASSERT(res != CLR_INVALID); + res = SetDCBrushColor(context, RGB(255, 255, 255)); + SkASSERT(res != CLR_INVALID); + res = SetDCPenColor(context, RGB(0, 0, 0)); + SkASSERT(res != CLR_INVALID); + + // Sets up default transparency. + res = SetBkMode(context, OPAQUE); + SkASSERT(res != 0); + res = SetROP2(context, R2_COPYPEN); + SkASSERT(res != 0); +} + +void LoadTransformToDC(HDC dc, const SkMatrix& matrix) { + XFORM xf; + xf.eM11 = matrix[SkMatrix::kMScaleX]; + xf.eM21 = matrix[SkMatrix::kMSkewX]; + xf.eDx = matrix[SkMatrix::kMTransX]; + xf.eM12 = matrix[SkMatrix::kMSkewY]; + xf.eM22 = matrix[SkMatrix::kMScaleY]; + xf.eDy = matrix[SkMatrix::kMTransY]; + SetWorldTransform(dc, &xf); +} + +void CopyHDC(HDC source, HDC destination, int x, int y, bool is_opaque, + const RECT& src_rect, const SkMatrix& transform) { + + int copy_width = src_rect.right - src_rect.left; + int copy_height = src_rect.bottom - src_rect.top; + + // We need to reset the translation for our bitmap or (0,0) won't be in the + // upper left anymore + SkMatrix identity; + identity.reset(); + + LoadTransformToDC(source, identity); + if (is_opaque) { + BitBlt(destination, + x, + y, + copy_width, + copy_height, + source, + src_rect.left, + src_rect.top, + SRCCOPY); + } else { + SkASSERT(copy_width != 0 && copy_height != 0); + BLENDFUNCTION blend_function = {AC_SRC_OVER, 0, 255, AC_SRC_ALPHA}; + GdiAlphaBlend(destination, + x, + y, + copy_width, + copy_height, + source, + src_rect.left, + src_rect.top, + copy_width, + copy_height, + blend_function); + } + LoadTransformToDC(source, transform); +} + +SkImageInfo PrepareAllocation(HDC context, BITMAP* backing) { + HBITMAP backing_handle = + static_cast(GetCurrentObject(context, OBJ_BITMAP)); + const size_t backing_size = sizeof *backing; + return (GetObject(backing_handle, backing_size, backing) == backing_size) + ? SkImageInfo::MakeN32Premul(backing->bmWidth, backing->bmHeight) + : SkImageInfo(); +} + +sk_sp MapPlatformSurface(HDC context) { + BITMAP backing; + const SkImageInfo size(PrepareAllocation(context, &backing)); + SkSurfaceProps props = skia::LegacyDisplayGlobals::GetSkSurfaceProps(); + return size.isEmpty() + ? nullptr + : SkSurface::MakeRasterDirect(size, backing.bmBits, + backing.bmWidthBytes, &props); +} + +SkBitmap MapPlatformBitmap(HDC context) { + BITMAP backing; + const SkImageInfo size(PrepareAllocation(context, &backing)); + SkBitmap bitmap; + if (!size.isEmpty()) + bitmap.installPixels(size, backing.bmBits, size.minRowBytes()); + return bitmap; +} + +void CreateBitmapHeaderForN32SkBitmap(const SkBitmap& bitmap, + BITMAPINFOHEADER* hdr) { + // Native HBITMAPs are XRGB-backed, and we expect SkBitmaps that we will use + // with them to also be of the same format. + CHECK_EQ(bitmap.colorType(), kN32_SkColorType); + // The header will be for an RGB bitmap with 32 bits-per-pixel. The SkBitmap + // data to go into the bitmap should be of the same size. If the SkBitmap + // SkColorType is for a larger number of bits-per-pixel, copying the SkBitmap + // into the HBITMAP for this header would cause a write out-of-bounds. + CHECK_EQ(4, bitmap.info().bytesPerPixel()); + // The HBITMAP's bytes will always be tightly packed so we expect the SkBitmap + // to be also. Row padding would mean the number of bytes in the SkBitmap and + // in the HBITMAP for this header would be different, which can cause out-of- + // bound reads or writes. + CHECK_EQ(bitmap.rowBytes(), bitmap.width() * static_cast(4)); + + CreateBitmapHeaderWithColorDepth(bitmap.width(), bitmap.height(), 32, hdr); +} + +HGLOBAL CreateHGlobalForByteArray( + const std::vector& byte_array) { + HGLOBAL hglobal = ::GlobalAlloc(GHND, byte_array.size()); + if (!hglobal) { + return nullptr; + } + base::win::ScopedHGlobal global_mem(hglobal); + if (!global_mem.get()) { + ::GlobalFree(hglobal); + return nullptr; + } + memcpy(global_mem.get(), byte_array.data(), byte_array.size()); + + return hglobal; +} + +HGLOBAL CreateDIBV5ImageDataFromN32SkBitmap(const SkBitmap& bitmap) { + // While DIBV5 support bit flags which would allow us to put channels in a any + // order, we require an ARGB format because it is more convenient to use. + CHECK_EQ(bitmap.colorType(), kN32_SkColorType); + // The header will be for an ARGB bitmap with 32 bits-per-pixel. The SkBitmap + // data to go into the bitmap should be of the same size. If the SkBitmap + // SkColorType is for a larger number of bits-per-pixel, copying the SkBitmap + // into the DIBV5ImageData for this header would cause a write out-of-bounds. + CHECK_EQ(4, bitmap.info().bytesPerPixel()); + // The DIBV5ImageData bytes will always be tightly packed so we expect the + // SkBitmap to be also. Row padding would mean the number of bytes in the + // SkBitmap and in the DIBV5ImageData for this header would be different, + // which can cause out-of- bound reads or writes. + CHECK_EQ(bitmap.rowBytes(), bitmap.width() * static_cast(4)); + + int width = bitmap.width(); + int height = bitmap.height(); + size_t bytes; + // Native DIBV5 bitmaps store 32-bit ARGB data, and the SkBitmap used with it + // must also, as verified at the start of this function. A size_t type causes + // a type change from int when multiplying. + constexpr size_t bpp = 4; + if (!base::CheckMul(height, base::CheckMul(width, bpp)).AssignIfValid(&bytes)) + return nullptr; + + HGLOBAL hglobal = ::GlobalAlloc(GHND, sizeof(BITMAPV5HEADER) + bytes); + if (hglobal == nullptr) + return nullptr; + + base::win::ScopedHGlobal header(hglobal); + if (!header.get()) { + ::GlobalFree(hglobal); + return nullptr; + } + + CreateBitmapV5HeaderForARGB8888(width, height, bytes, header.get()); + auto* dst_pixels = + reinterpret_cast(header.get()) + sizeof(BITMAPV5HEADER); + + // CreateBitmapV5HeaderForARGB8888 creates a bitmap with a positive height as + // stated in the image's header. Having a positive value implies that the + // image is stored bottom-up. As skia uses the opposite, we have to flip + // vertically so the image's content while copying in the DIBV5 data structure + // to account for that. In theory, we could use a negative value to avoid the + // flip, but not all programs treat a negative value properly. + + SkImageInfo infoSRGB = bitmap.info() + .makeColorSpace(SkColorSpace::MakeSRGB()) + .makeWH(bitmap.width(), 1); + + const size_t row_bytes = bitmap.rowBytes(); + + for (size_t line = 0; line < height; line++) { + size_t flipped_line_index = height - 1 - line; + auto* current_dst = dst_pixels + (row_bytes * flipped_line_index); + bool success = bitmap.readPixels(infoSRGB, current_dst, row_bytes, 0, line); + DCHECK(success); + } + return hglobal; +} + +base::win::ScopedBitmap CreateHBitmapFromN32SkBitmap(const SkBitmap& bitmap) { + BITMAPINFOHEADER header; + CreateBitmapHeaderForN32SkBitmap(bitmap, &header); + + int width = bitmap.width(); + int height = bitmap.height(); + + size_t bytes; + // Native HBITMAPs store 32-bit RGB data, and the SkBitmap used with it must + // also, as verified by CreateBitmapHeaderForN32SkBitmap(). A size_t type + // causes a type change from int when multiplying. + const size_t bpp = 4; + if (!base::CheckMul(height, base::CheckMul(width, bpp)).AssignIfValid(&bytes)) + return {}; + + void* bits; + HBITMAP hbitmap; + { + base::win::ScopedGetDC screen_dc(nullptr); + // By giving a null hSection, the |bits| will be destroyed when the + // |hbitmap| is destroyed. + hbitmap = + CreateDIBSection(screen_dc, reinterpret_cast(&header), + DIB_RGB_COLORS, &bits, nullptr, 0); + } + if (hbitmap) { + memcpy(bits, bitmap.getPixels(), bytes); + } else { + // If CreateDIBSection() failed, try to get some useful information out + // before we crash for post-mortem analysis. + base::debug::CollectGDIUsageAndDie(&header, nullptr); + } + + return base::win::ScopedBitmap(hbitmap); +} + +void CreateBitmapHeaderForXRGB888(int width, + int height, + BITMAPINFOHEADER* hdr) { + CreateBitmapHeaderWithColorDepth(width, height, 32, hdr); +} + +base::win::ScopedBitmap CreateHBitmapXRGB8888(int width, + int height, + HANDLE shared_section, + void** data) { + // CreateDIBSection fails to allocate anything if we try to create an empty + // bitmap, so just create a minimal bitmap. + if ((width == 0) || (height == 0)) { + width = 1; + height = 1; + } + + BITMAPINFOHEADER hdr = {0}; + CreateBitmapHeaderWithColorDepth(width, height, 32, &hdr); + HBITMAP hbitmap = CreateDIBSection(NULL, reinterpret_cast(&hdr), + 0, data, shared_section, 0); + + // If CreateDIBSection() failed, try to get some useful information out + // before we crash for post-mortem analysis. + if (!hbitmap) + base::debug::CollectGDIUsageAndDie(&hdr, shared_section); + + return base::win::ScopedBitmap(hbitmap); +} + +} // namespace skia + diff --git a/ext/skia_utils_win.h b/ext/skia_utils_win.h new file mode 100644 index 000000000000..6fb826396e7d --- /dev/null +++ b/ext/skia_utils_win.h @@ -0,0 +1,127 @@ +// Copyright 2006-2008 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef SKIA_EXT_SKIA_UTILS_WIN_H_ +#define SKIA_EXT_SKIA_UTILS_WIN_H_ + +#include + +#include "base/win/scoped_gdi_object.h" +#include "third_party/skia/include/core/SkBitmap.h" +#include "third_party/skia/include/core/SkColor.h" +#include "third_party/skia/include/core/SkImageInfo.h" +#include "third_party/skia/include/core/SkMatrix.h" +#include "third_party/skia/include/core/SkRefCnt.h" + +#include "build/build_config.h" +#include + +struct SkIRect; +struct SkPoint; +struct SkRect; +class SkSurface; +typedef unsigned long DWORD; +typedef DWORD COLORREF; +typedef struct tagPOINT POINT; +typedef struct tagRECT RECT; + +namespace skia { + +// Converts a Skia point to a Windows POINT. +POINT SkPointToPOINT(const SkPoint& point); + +// Converts a Windows RECT to a Skia rect. +SkRect RECTToSkRect(const RECT& rect); + +// Converts a Windows RECT to a Skia rect. +// Both use same in-memory format. Verified by static_assert in +// skia_utils_win.cc. +inline const SkIRect& RECTToSkIRect(const RECT& rect) { + return reinterpret_cast(rect); +} + +// Converts a Skia rect to a Windows RECT. +// Both use same in-memory format. Verified by static_assert in +// skia_utils_win.cc. +inline const RECT& SkIRectToRECT(const SkIRect& rect) { + return reinterpret_cast(rect); +} + +// Converts COLORREFs (0BGR) to the ARGB layout Skia expects. +SK_API SkColor COLORREFToSkColor(COLORREF color); + +// Converts ARGB to COLORREFs (0BGR). +SK_API COLORREF SkColorToCOLORREF(SkColor color); + +// Initializes the default settings and colors in a device context. +SK_API void InitializeDC(HDC context); + +// Converts scale, skew, and translation to Windows format and sets it on the +// HDC. +SK_API void LoadTransformToDC(HDC dc, const SkMatrix& matrix); + +// Copies |src_rect| from source into destination. +// Takes a potentially-slower path if |is_opaque| is false. +// Sets |transform| on source afterwards! +SK_API void CopyHDC(HDC source, HDC destination, int x, int y, bool is_opaque, + const RECT& src_rect, const SkMatrix& transform); + +// Creates a surface writing to the pixels backing |context|'s bitmap. +// Returns null on error. +SK_API sk_sp MapPlatformSurface(HDC context); + +// Creates a bitmap backed by the same pixels backing the HDC's bitmap. +// Returns an empty bitmap on error. The HDC's bitmap is assumed to be formatted +// as 32-bits-per-pixel XRGB8888, as created by CreateHBitmapXRGB8888(). +SK_API SkBitmap MapPlatformBitmap(HDC context); + +// Fills in a BITMAPINFOHEADER structure to hold the pixel data from |bitmap|. +// The |bitmap| must be have a color type of kN32_SkColorType, and the header +// will be for a bitmap with 32-bits-per-pixel RGB data (the high bits are +// unused in each pixel). +SK_API void CreateBitmapHeaderForN32SkBitmap(const SkBitmap& bitmap, + BITMAPINFOHEADER* hdr); + +// Creates a globally allocated memory containing the given byte array. The +// returned handle to the global memory is allocated by ::GlobalAlloc(), and +// must be explicitly freed by ::GlobalFree(), unless ownership is passed to the +// Win32 API. On failure, it returns null. +SK_API HGLOBAL +CreateHGlobalForByteArray(const std::vector& byte_array); + +// Creates an HBITMAP backed by 32-bits-per-pixel RGB data (the high bits are +// unused in each pixel), with dimensions and the RGBC pixel data from the +// SkBitmap. Any alpha channel values are copied into the HBITMAP but are not +// used. Can return a null HBITMAP on any failure to create the HBITMAP. +SK_API base::win::ScopedBitmap CreateHBitmapFromN32SkBitmap( + const SkBitmap& bitmap); + +// Creates an image in the DIBV5 format. On success this function returns a +// handle to an allocated memory block containing a DIBV5 header followed by the +// pixel data. If the bitmap creation fails, it returns null. This is preferred +// in some cases over the HBITMAP format because it handles transparency better. +// The returned handle to the global memory is allocated by ::GlobalAlloc(), and +// must be explicitly freed by ::GlobalFree(), unless ownership is passed to the +// Win32 API. +SK_API HGLOBAL CreateDIBV5ImageDataFromN32SkBitmap(const SkBitmap& bitmap); + +// Fills in a BITMAPINFOHEADER structure given the bitmap's size. The header +// will be for a bitmap with 32-bits-per-pixel RGB data (the high bits are +// unused in each pixel). +SK_API void CreateBitmapHeaderForXRGB888(int width, + int height, + BITMAPINFOHEADER* hdr); + +// Creates an HBITMAP backed by 32-bits-per-pixel RGB data (the high bits are +// unused in each pixel). +SK_API base::win::ScopedBitmap CreateHBitmapXRGB8888( + int width, + int height, + HANDLE shared_section = nullptr, + void** data = nullptr); + +} // namespace skia + +#endif // SKIA_EXT_SKIA_UTILS_WIN_H_ + diff --git a/ext/skottie_unittest.cc b/ext/skottie_unittest.cc new file mode 100644 index 000000000000..2bd6067c9ad7 --- /dev/null +++ b/ext/skottie_unittest.cc @@ -0,0 +1,56 @@ +// Copyright 2018 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "testing/gtest/include/gtest/gtest.h" +#include "third_party/skia/include/core/SkPixmap.h" +#include "third_party/skia/include/core/SkStream.h" +#include "third_party/skia/include/core/SkSurface.h" +#include "third_party/skia/modules/skottie/include/Skottie.h" + +TEST(Skottie, Basic) { + // Just a solid green layer. + static constexpr char anim_data[] = + "{" + " \"v\" : \"4.12.0\"," + " \"fr\": 30," + " \"w\" : 400," + " \"h\" : 200," + " \"ip\": 0," + " \"op\": 150," + " \"assets\": []," + + " \"layers\": [" + " {" + " \"ty\": 1," + " \"sw\": 400," + " \"sh\": 200," + " \"sc\": \"#00ff00\"," + " \"ip\": 0," + " \"op\": 150" + " }" + " ]" + "}"; + + SkMemoryStream stream(anim_data, strlen(anim_data)); + auto anim = skottie::Animation::Make(&stream); + + ASSERT_TRUE(anim); + EXPECT_EQ(strcmp(anim->version().c_str(), "4.12.0"), 0); + EXPECT_EQ(anim->size().width(), 400.0f); + EXPECT_EQ(anim->size().height(), 200.0f); + EXPECT_EQ(anim->duration(), 5.0f); + + auto surface = SkSurface::MakeRasterN32Premul(400, 200); + anim->seek(0); + anim->render(surface->getCanvas()); + + SkPixmap pixmap; + ASSERT_TRUE(surface->peekPixels(&pixmap)); + + for (int i = 0; i < pixmap.width(); ++i) { + for (int j = 0; j < pixmap.height(); ++j) { + EXPECT_EQ(pixmap.getColor(i, j), 0xff00ff00); + } + } +} diff --git a/ext/test_fonts.h b/ext/test_fonts.h new file mode 100644 index 000000000000..fb0863394606 --- /dev/null +++ b/ext/test_fonts.h @@ -0,0 +1,16 @@ +// Copyright 2019 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef SKIA_EXT_TEST_FONTS_H_ +#define SKIA_EXT_TEST_FONTS_H_ + +namespace skia { + +// Configures the process to use //third_party/test_fonts. Should be called +// early, before default instance of SkFontMgr is created. +void InitializeSkFontMgrForTest(); + +} // namespace skia + +#endif // SKIA_EXT_TEST_FONTS_H_ diff --git a/ext/test_fonts_fuchsia.cc b/ext/test_fonts_fuchsia.cc new file mode 100644 index 000000000000..2c4173da39cb --- /dev/null +++ b/ext/test_fonts_fuchsia.cc @@ -0,0 +1,20 @@ +// Copyright 2019 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "skia/ext/test_fonts_fuchsia.h" + +#include + +#include "skia/ext/fontmgr_default.h" +#include "third_party/skia/include/core/SkFontMgr.h" +#include "third_party/skia/include/ports/SkFontMgr_fuchsia.h" + +namespace skia { + +void InitializeSkFontMgrForTest() { + OverrideDefaultSkFontMgr( + SkFontMgr_New_Fuchsia(GetTestFontsProvider().BindSync())); +} + +} // namespace skia diff --git a/ext/test_fonts_fuchsia.h b/ext/test_fonts_fuchsia.h new file mode 100644 index 000000000000..a6cc8d6409e1 --- /dev/null +++ b/ext/test_fonts_fuchsia.h @@ -0,0 +1,18 @@ +// Copyright 2019 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef SKIA_EXT_TEST_FONTS_FUCHSIA_H_ +#define SKIA_EXT_TEST_FONTS_FUCHSIA_H_ + +#include + +namespace skia { + +// Returns a handle to a fuchsia.fonts.Provider that serves the fonts in +// the package's test_fonts directory. +fuchsia::fonts::ProviderHandle GetTestFontsProvider(); + +} // namespace skia + +#endif // SKIA_EXT_TEST_FONTS_FUCHSIA_H_ diff --git a/ext/test_fonts_fuchsia_cfv2.cc b/ext/test_fonts_fuchsia_cfv2.cc new file mode 100644 index 000000000000..27552740d642 --- /dev/null +++ b/ext/test_fonts_fuchsia_cfv2.cc @@ -0,0 +1,23 @@ +// Copyright 2022 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "skia/ext/test_fonts_fuchsia.h" + +#include +#include + +#include "base/fuchsia/process_context.h" + +namespace skia { + +fuchsia::fonts::ProviderHandle GetTestFontsProvider() { + // //build/config/fuchsia/test/test_fonts.shard.test-cml must be in the + // current test component's manifest. It configures a fonts.Provider to serve + // fonts from the package's test_fonts directory for the test process. + fuchsia::fonts::ProviderHandle provider; + base::ComponentContextForProcess()->svc()->Connect(provider.NewRequest()); + return provider; +} + +} // namespace skia diff --git a/ext/test_fonts_mac.mm b/ext/test_fonts_mac.mm new file mode 100644 index 000000000000..18cb9ceb008b --- /dev/null +++ b/ext/test_fonts_mac.mm @@ -0,0 +1,48 @@ +// Copyright 2019 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "skia/ext/test_fonts.h" + +#include +#include + +#include "base/files/file_path.h" +#include "base/logging.h" +#include "base/mac/foundation_util.h" + +namespace skia { + +void InitializeSkFontMgrForTest() { + // Load font files in the resource folder. + static const char* const kFontFileNames[] = {"Ahem.ttf", + "ChromiumAATTest.ttf"}; + + NSMutableArray* font_urls = [NSMutableArray array]; + for (auto* font_file_name : kFontFileNames) { + NSURL* font_url = base::mac::FilePathToNSURL( + base::mac::PathForFrameworkBundleResource(font_file_name)); + [font_urls addObject:font_url.absoluteURL]; + } + + if (@available(macOS 10.15, *)) { + CTFontManagerRegisterFontURLs( + base::mac::NSToCFCast(font_urls), kCTFontManagerScopeProcess, + /*enabled=*/true, ^bool(CFArrayRef errors, bool done) { + if (CFArrayGetCount(errors)) { + DLOG(FATAL) << "Failed to activate fonts."; + } + return true; + }); + } else { + CFArrayRef errors = nullptr; + if (!CTFontManagerRegisterFontsForURLs(base::mac::NSToCFCast(font_urls), + kCTFontManagerScopeProcess, + &errors)) { + DLOG(FATAL) << "Failed to activate fonts."; + CFRelease(errors); + } + } +} + +} // namespace skia diff --git a/features.gni b/features.gni new file mode 100644 index 000000000000..b1f20ed9ccf4 --- /dev/null +++ b/features.gni @@ -0,0 +1,42 @@ +# Copyright 2019 The Chromium Authors +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +import("//build/config/features.gni") +import("//printing/buildflags/buildflags.gni") + +declare_args() { + # Enable experimental Skia Graphite backend. + enable_skia_graphite = false + + # Enable gtests using SkiaRenderer on Skia Graphite. + # TODO(rivr): Remove this and enable the tests by default once a software + # path for D3D12 is available. + enable_skia_graphite_gtests = false +} + +# Skia only needs to support GPU rasterization if we use the full Chromium +# rendering stack i.e. |use_blink| is true. +# TODO(crbug.com/1431198): Split out into separate enable_skia_ganesh flag. +skia_support_gpu = use_blink + +# Skia Ganesh GL backend is always enabled on all platforms - applies only when +# GPU rasterization is enabled. +skia_use_gl = true + +# Dawn is used with Skia Graphite by default. +skia_use_dawn = enable_skia_graphite + +# Metal is only used with Skia Graphite on Mac and iOS blink developer builds. +skia_use_metal = + enable_skia_graphite && is_apple && use_blink && !is_official_build + +skia_support_pdf = !is_ios && enable_printing + +# Skottie is not used on Android. To keep apk size small the skottie library is +# excluded from the binary. At the time this comment was written, it adds ~200KB +# to the APK. +skia_support_skottie = !is_android + +# Skia needs XMP support for gainmap HDR image decoding in blink. +skia_support_xmp = use_blink diff --git a/public/mojom/BUILD.gn b/public/mojom/BUILD.gn new file mode 100644 index 000000000000..fed282fe7893 --- /dev/null +++ b/public/mojom/BUILD.gn @@ -0,0 +1,162 @@ +# Copyright 2015 The Chromium Authors +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +import("//mojo/public/tools/bindings/mojom.gni") + +# Normally typemap traits sources should be build directly into mojom targets +# via the typemap file. This target is for typemapped mojo_base types whose +# traits are shared between chromium and blink variants. +component("shared_typemap_traits") { + output_name = "skia_shared_typemap_traits" + + sources = [ + "bitmap_skbitmap_mojom_traits.cc", + "bitmap_skbitmap_mojom_traits.h", + "image_info_mojom_traits.cc", + "image_info_mojom_traits.h", + "surface_origin_mojom_traits.h", + ] + + defines = [ "IS_SKIA_SHARED_TRAITS_IMPL" ] + + public_deps = [ + "//base", + "//mojo/public/cpp/base:shared_typemap_traits", + "//skia", + "//skia:skcms", + "//skia/public/mojom:mojom_shared", + ] +} + +mojom("mojom") { + generate_java = true + sources = [ + "bitmap.mojom", + "image_info.mojom", + "skcolor.mojom", + "skcolor4f.mojom", + "skcolorspace_primaries.mojom", + "surface_origin.mojom", + "tile_mode.mojom", + ] + + public_deps = [ "//mojo/public/mojom/base" ] + + shared_skia_cpp_typemaps = [ + { + types = [ + { + mojom = "skia.mojom.TileMode" + cpp = "::SkTileMode" + }, + ] + traits_headers = [ "tile_mode_mojom_traits.h" ] + traits_public_deps = [ "//skia" ] + }, + { + types = [ + { + mojom = "skia.mojom.BitmapN32" + cpp = "::SkBitmap" + nullable_is_same_type = true + }, + { + mojom = "skia.mojom.BitmapWithArbitraryBpp" + cpp = "::SkBitmap" + nullable_is_same_type = true + }, + { + mojom = "skia.mojom.BitmapWithArbitraryBpp" + cpp = "::SkBitmap" + nullable_is_same_type = true + }, + { + mojom = "skia.mojom.BitmapMappedFromTrustedProcess" + cpp = "::SkBitmap" + nullable_is_same_type = true + }, + { + mojom = "skia.mojom.InlineBitmap" + cpp = "::SkBitmap" + nullable_is_same_type = true + }, + ] + traits_headers = [ "bitmap_skbitmap_mojom_traits.h" ] + traits_public_deps = [ + ":shared_typemap_traits", + "//skia", + ] + }, + { + types = [ + { + mojom = "skia.mojom.SkColor" + cpp = "::SkColor" + copyable_pass_by_value = true + }, + ] + traits_headers = [ "skcolor_mojom_traits.h" ] + traits_public_deps = [ "//skia" ] + }, + { + types = [ + { + mojom = "skia.mojom.SkColor4f" + cpp = "::SkColor4f" + }, + ] + traits_headers = [ "skcolor4f_mojom_traits.h" ] + traits_public_deps = [ "//skia" ] + }, + { + types = [ + { + mojom = "skia.mojom.SkColorSpacePrimaries" + cpp = "::SkColorSpacePrimaries" + }, + ] + traits_headers = [ "skcolorspace_primaries_mojom_traits.h" ] + traits_public_deps = [ "//skia" ] + }, + { + types = [ + { + mojom = "skia.mojom.AlphaType" + cpp = "::SkAlphaType" + }, + { + mojom = "skia.mojom.ColorType" + cpp = "::SkColorType" + }, + { + mojom = "skia.mojom.ImageInfo" + cpp = "::SkImageInfo" + }, + { + mojom = "skia.mojom.BitmapN32ImageInfo" + cpp = "::SkImageInfo" + }, + ] + traits_headers = [ "image_info_mojom_traits.h" ] + traits_public_deps = [ + ":shared_typemap_traits", + "//skia", + ] + }, + { + types = [ + { + mojom = "skia.mojom.SurfaceOrigin" + cpp = "::GrSurfaceOrigin" + }, + ] + traits_headers = [ "surface_origin_mojom_traits.h" ] + traits_public_deps = [ ":shared_typemap_traits" ] + }, + ] + + cpp_typemaps = shared_skia_cpp_typemaps + blink_cpp_typemaps = shared_skia_cpp_typemaps + webui_module_path = "chrome://resources/mojo/skia/public/mojom/" +} diff --git a/public/mojom/OWNERS b/public/mojom/OWNERS new file mode 100644 index 000000000000..a534d61c4b17 --- /dev/null +++ b/public/mojom/OWNERS @@ -0,0 +1,5 @@ +per-file *_mojom_traits*.*=set noparent +per-file *_mojom_traits*.*=file://ipc/SECURITY_OWNERS + +per-file *.mojom=set noparent +per-file *.mojom=file://ipc/SECURITY_OWNERS diff --git a/public/mojom/bitmap.mojom b/public/mojom/bitmap.mojom new file mode 100644 index 000000000000..d23424740214 --- /dev/null +++ b/public/mojom/bitmap.mojom @@ -0,0 +1,60 @@ +// Copyright 2015 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// This file contains structures used to represent SkBitmaps in Mojo. +module skia.mojom; + +import "mojo/public/mojom/base/big_buffer.mojom"; +import "skia/public/mojom/image_info.mojom"; + +// The most common way to transfer an SkBitmap over IPC. This struct enforces +// that the bitmap is 32bpp to prevent buffer-overflow problems when reading/ +// writing the pixel buffer. +struct BitmapN32 { + BitmapN32ImageInfo image_info; + mojo_base.mojom.BigBuffer pixel_data; +}; + +// Marked stable as this is used in the crosapi. This struct should be +// avoided whenever possible. If used, extreme care must be taken when +// manipulating the pixels in the bitmap, either only using SkBitmap methods to +// read and write to them, or taking care to not assume that there are 32 bits- +// per-pixel. +[Stable, RenamedFrom="skia.mojom.Bitmap"] +struct BitmapWithArbitraryBpp { + ImageInfo image_info; + uint64 UNUSED_row_bytes; + mojo_base.mojom.BigBuffer pixel_data; +}; + +// Similar to above, but the generated bindings avoid copying pixel data on the +// receiving side of an IPC message. That can be a valuable optimization for +// large bitmaps. However, this is DANGEROUS as it leaves open the possibility +// for the sender to continue to modify the pixel data, which could lead to +// TOCTOU issues. Use this type *only* when the sender is fully trusted (and +// a compromise there would already mean system compromise), such as from the +// browser process. +// +// NOTE: It is important that the fields of this struct exactly match the +// fields of the Bitmap struct. This enables stable interfaces to freely +// migrate between these two types in a compatible fashion. +[Stable, RenamedFrom="skia.mojom.UnsafeBitmap"] +struct BitmapMappedFromTrustedProcess { + ImageInfo image_info; + uint64 UNUSED_row_bytes; + mojo_base.mojom.BigBuffer pixel_data; +}; + +// Encode an N32 SkBitmap for transport without relying on shared memory. +// Normally, it is preferable to use shared memory and this mojom type should +// NOT be used for IPC. +// +// This type is useful, however, for de/serialization to a string (via +// skia::mojom::InlineBitmap::Serialize() and Deserialize()) since it will not +// attempt to use a shared memory handle and will encode the actual pixel +// content always. +struct InlineBitmap { + BitmapN32ImageInfo image_info; + array pixel_data; +}; diff --git a/public/mojom/bitmap_skbitmap_mojom_traits.cc b/public/mojom/bitmap_skbitmap_mojom_traits.cc new file mode 100644 index 000000000000..a9df2eeeeddf --- /dev/null +++ b/public/mojom/bitmap_skbitmap_mojom_traits.cc @@ -0,0 +1,200 @@ +// Copyright 2016 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "skia/public/mojom/bitmap_skbitmap_mojom_traits.h" + +#include "base/ranges/algorithm.h" +#include "third_party/skia/include/core/SkPixelRef.h" + +namespace mojo { +namespace { + +// Maximum reasonable width and height. We don't try to deserialize bitmaps +// bigger than these dimensions. +// These limits are fairly large to accommodate images from the largest possible +// canvas. +constexpr int kMaxWidth = 64 * 1024; +constexpr int kMaxHeight = 64 * 1024; + +// A custom SkPixelRef subclass to wrap a BigBuffer storing the pixel data. +class BigBufferPixelRef final : public SkPixelRef { + public: + BigBufferPixelRef(mojo_base::BigBuffer buffer, + int width, + int height, + int row_bytes) + : SkPixelRef(width, height, buffer.data(), row_bytes), + buffer_(std::move(buffer)) {} + ~BigBufferPixelRef() override = default; + + private: + mojo_base::BigBuffer buffer_; +}; + +bool CreateSkBitmapForPixelData(SkBitmap* b, + const SkImageInfo& image_info, + base::span pixel_data) { + // Ensure width and height are reasonable. + if (image_info.width() > kMaxWidth || image_info.height() > kMaxHeight) + return false; + + // We require incoming bitmaps to be tightly packed by specifying the + // rowBytes() as minRowBytes(). Then we compare the number of bytes against + // `pixel_data.size()` later to verify the actual data is tightly packed. + if (!b->tryAllocPixels(image_info, image_info.minRowBytes())) + return false; + + // If the image is empty, return success after setting the image info. + if (image_info.width() == 0 || image_info.height() == 0) + return true; + + // If these don't match then the number of bytes sent does not match what the + // rest of the mojom said there should be. + if (pixel_data.size() != b->computeByteSize()) + return false; + + // Implementation note: This copy is important from a security perspective as + // it provides the recipient of the SkBitmap with a stable copy of the data. + // The sender could otherwise continue modifying the shared memory buffer + // underlying the BigBuffer instance. + base::ranges::copy(pixel_data, static_cast(b->getPixels())); + b->notifyPixelsChanged(); + return true; +} + +} // namespace + +// static +mojo_base::BigBufferView StructTraits::pixel_data(const SkBitmap& b) { + CHECK_EQ(b.rowBytes(), b.info().minRowBytes()); + return mojo_base::BigBufferView(base::make_span( + static_cast(b.getPixels()), b.computeByteSize())); +} + +// static +bool StructTraits::Read( + skia::mojom::BitmapN32DataView data, + SkBitmap* b) { + SkImageInfo image_info; + if (!data.ReadImageInfo(&image_info)) + return false; + + mojo_base::BigBufferView pixel_data_view; + if (!data.ReadPixelData(&pixel_data_view)) + return false; + + return CreateSkBitmapForPixelData(b, std::move(image_info), + pixel_data_view.data()); +} + +// static +mojo_base::BigBufferView +StructTraits::pixel_data( + const SkBitmap& b) { + CHECK_EQ(b.rowBytes(), b.info().minRowBytes()); + return mojo_base::BigBufferView(base::make_span( + static_cast(b.getPixels()), b.computeByteSize())); +} + +// static +bool StructTraits::Read( + skia::mojom::BitmapWithArbitraryBppDataView data, + SkBitmap* b) { + SkImageInfo image_info; + if (!data.ReadImageInfo(&image_info)) + return false; + + mojo_base::BigBufferView pixel_data_view; + if (!data.ReadPixelData(&pixel_data_view)) + return false; + + return CreateSkBitmapForPixelData(b, std::move(image_info), + pixel_data_view.data()); +} + +// static +mojo_base::BigBufferView +StructTraits::pixel_data(const SkBitmap& b) { + CHECK_EQ(b.rowBytes(), b.info().minRowBytes()); + return mojo_base::BigBufferView(base::make_span( + static_cast(b.getPixels()), b.computeByteSize())); +} + +// static +bool StructTraits< + skia::mojom::BitmapMappedFromTrustedProcessDataView, + SkBitmap>::Read(skia::mojom::BitmapMappedFromTrustedProcessDataView data, + SkBitmap* b) { + SkImageInfo image_info; + if (!data.ReadImageInfo(&image_info)) + return false; + + // Ensure width and height are reasonable. + if (image_info.width() > kMaxWidth || image_info.height() > kMaxHeight) + return false; + + // If the image is empty, return success after setting the image info. + if (image_info.width() == 0 || image_info.height() == 0) + return b->tryAllocPixels(image_info); + + // Otherwise, set a custom PixelRef to retain the BigBuffer. This avoids + // making another copy of the pixel data. + + mojo_base::BigBufferView pixel_data_view; + if (!data.ReadPixelData(&pixel_data_view)) + return false; + + // We require incoming bitmaps to be tightly packed by specifying the + // rowBytes() as minRowBytes(). Then we compare the number of bytes against + // `pixel_data_view.data().size()` later to verify the actual data is tightly + // packed. + if (!b->setInfo(image_info, image_info.minRowBytes())) + return false; + + // If these don't match then the number of bytes sent does not match what the + // rest of the mojom said there should be. + if (b->computeByteSize() != pixel_data_view.data().size()) + return false; + + // Allow the resultant SkBitmap to refer to the given BigBuffer. Note, the + // sender could continue modifying the pixels of the buffer, which could be a + // security concern for some applications. The trade-off is performance. + b->setPixelRef( + sk_make_sp( + mojo_base::BigBufferView::ToBigBuffer(std::move(pixel_data_view)), + image_info.width(), image_info.height(), image_info.minRowBytes()), + 0, 0); + return true; +} + +// static +base::span +StructTraits::pixel_data( + const SkBitmap& b) { + CHECK_EQ(b.rowBytes(), b.info().minRowBytes()); + return base::make_span(static_cast(b.getPixels()), + b.computeByteSize()); +} + +// static +bool StructTraits::Read( + skia::mojom::InlineBitmapDataView data, + SkBitmap* b) { + SkImageInfo image_info; + if (!data.ReadImageInfo(&image_info)) + return false; + + mojo::ArrayDataView pixel_data_view; + data.GetPixelDataDataView(&pixel_data_view); + + base::span pixel_data_bytes(pixel_data_view.data(), + pixel_data_view.size()); + + return CreateSkBitmapForPixelData(b, std::move(image_info), + std::move(pixel_data_bytes)); +} + +} // namespace mojo diff --git a/public/mojom/bitmap_skbitmap_mojom_traits.h b/public/mojom/bitmap_skbitmap_mojom_traits.h new file mode 100644 index 000000000000..2b5ad6a983b2 --- /dev/null +++ b/public/mojom/bitmap_skbitmap_mojom_traits.h @@ -0,0 +1,76 @@ +// Copyright 2016 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef SKIA_PUBLIC_MOJOM_BITMAP_SKBITMAP_MOJOM_TRAITS_H_ +#define SKIA_PUBLIC_MOJOM_BITMAP_SKBITMAP_MOJOM_TRAITS_H_ + +#include "base/component_export.h" +#include "base/containers/span.h" +#include "mojo/public/cpp/base/big_buffer.h" +#include "mojo/public/cpp/base/big_buffer_mojom_traits.h" +#include "mojo/public/cpp/bindings/array_traits.h" +#include "skia/public/mojom/bitmap.mojom-shared.h" +#include "skia/public/mojom/image_info_mojom_traits.h" +#include "third_party/skia/include/core/SkBitmap.h" + +namespace mojo { + +// Struct traits to convert between SkBitmap and mojom types. + +template <> +struct COMPONENT_EXPORT(SKIA_SHARED_TRAITS) + StructTraits { + static bool IsNull(const SkBitmap& b) { return b.isNull(); } + static void SetToNull(SkBitmap* b) { b->reset(); } + + static const SkImageInfo& image_info(const SkBitmap& b) { return b.info(); } + static mojo_base::BigBufferView pixel_data(const SkBitmap& b); + + static bool Read(skia::mojom::BitmapN32DataView data, SkBitmap* b); +}; + +template <> +struct COMPONENT_EXPORT(SKIA_SHARED_TRAITS) + StructTraits { + static bool IsNull(const SkBitmap& b) { return b.isNull(); } + static void SetToNull(SkBitmap* b) { b->reset(); } + + static const SkImageInfo& image_info(const SkBitmap& b) { return b.info(); } + static uint64_t UNUSED_row_bytes(const SkBitmap& b) { return 0; } + static mojo_base::BigBufferView pixel_data(const SkBitmap& b); + + static bool Read(skia::mojom::BitmapWithArbitraryBppDataView data, + SkBitmap* b); +}; + +template <> +struct COMPONENT_EXPORT(SKIA_SHARED_TRAITS) + StructTraits { + static bool IsNull(const SkBitmap& b) { return b.isNull(); } + static void SetToNull(SkBitmap* b) { b->reset(); } + + static const SkImageInfo& image_info(const SkBitmap& b) { return b.info(); } + static uint64_t UNUSED_row_bytes(const SkBitmap& b) { return 0; } + static mojo_base::BigBufferView pixel_data(const SkBitmap& b); + + static bool Read(skia::mojom::BitmapMappedFromTrustedProcessDataView data, + SkBitmap* b); +}; + +template <> +struct COMPONENT_EXPORT(SKIA_SHARED_TRAITS) + StructTraits { + static bool IsNull(const SkBitmap& b) { return b.isNull(); } + static void SetToNull(SkBitmap* b) { b->reset(); } + + static const SkImageInfo& image_info(const SkBitmap& b) { return b.info(); } + static base::span pixel_data(const SkBitmap& b); + + static bool Read(skia::mojom::InlineBitmapDataView data, SkBitmap* b); +}; + +} // namespace mojo + +#endif // SKIA_PUBLIC_MOJOM_BITMAP_SKBITMAP_MOJOM_TRAITS_H_ diff --git a/public/mojom/image_info.mojom b/public/mojom/image_info.mojom new file mode 100644 index 000000000000..4cd30793891b --- /dev/null +++ b/public/mojom/image_info.mojom @@ -0,0 +1,79 @@ +// Copyright 2015 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +module skia.mojom; + +// Mirror of SkColorType. Unsupported values and `UNKNOWN` will raise +// serialization and deserialization errors. +[Stable, Extensible] +enum ColorType { + [Default] UNKNOWN, + ALPHA_8, + RGB_565, + ARGB_4444, + RGBA_8888, + BGRA_8888, + // Note: this will fail traits deserialization. + DEPRECATED_INDEX_8, + GRAY_8, +}; + +// Mirror of SkAlphaType. Unsupported values and `UNKNOWN` will raise +// serialization and deserialization errors. +[Stable, Extensible] +enum AlphaType { + [Default] UNKNOWN, + ALPHA_TYPE_OPAQUE, + PREMUL, + UNPREMUL, +}; + +// Mirror of SkImageInfo. +[Stable] +struct ImageInfo { + ColorType color_type; + AlphaType alpha_type; + uint32 width; + uint32 height; + + // Note that both `color_transfer_function` and `color_to_xyz_matrix` must + // either both be set or both be unset. Unfortunately, this struct is marked + // Stable so it's no longer possible to fix this... + + // Color transfer function mapping encoded values to linear values, + // represented by this 7-parameter piecewise function: + // linear = sign(encoded) * (c*|encoded| + f) , 0 <= |encoded| < d + // = sign(encoded) * ((a*|encoded| + b)^g + e), d <= |encoded| + // (A simple gamma transfer function sets g to gamma and a to 1.) + // See SkColorSpace and skcms_TransferFunction. Null if the image has no + // explicit color space. Parameters are serialized as: g, a, b, c, d, e, f. + array? color_transfer_function; + + // Color transformation matrix to convert colors to XYZ D50, represented as + // a row-major 3x3 matrix. See SkColorSpace::MakeRGB(). Null if the image has + // no explicit color space. + array? color_to_xyz_matrix; +}; + +// Similar to ImageInfo, but is used when only N32 ColorType is allowed. As such +// the ColorType is not transmitted over the wire at all. +struct BitmapN32ImageInfo { + AlphaType alpha_type; + uint32 width; + uint32 height; + + // Color transfer function mapping encoded values to linear values, + // represented by this 7-parameter piecewise function: + // linear = sign(encoded) * (c*|encoded| + f) , 0 <= |encoded| < d + // = sign(encoded) * ((a*|encoded| + b)^g + e), d <= |encoded| + // (A simple gamma transfer function sets g to gamma and a to 1.) + // See SkColorSpace and skcms_TransferFunction. Null if the image has no + // explicit color space. Parameters are serialized as: g, a, b, c, d, e, f. + array? color_transfer_function; + + // Color transformation matrix to convert colors to XYZ D50, represented as + // a row-major 3x3 matrix. See SkColorSpace::MakeRGB(). Null if the image has + // no explicit color space. + array? color_to_xyz_matrix; +}; diff --git a/public/mojom/image_info_mojom_traits.cc b/public/mojom/image_info_mojom_traits.cc new file mode 100644 index 000000000000..4c9fe2894730 --- /dev/null +++ b/public/mojom/image_info_mojom_traits.cc @@ -0,0 +1,244 @@ +// Copyright 2018 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "skia/public/mojom/image_info_mojom_traits.h" + +#include "base/numerics/checked_math.h" +#include "base/numerics/safe_conversions.h" +#include "mojo/public/cpp/bindings/array_data_view.h" +#include "third_party/abseil-cpp/absl/types/optional.h" +#include "third_party/skia/include/core/SkColorSpace.h" +#include "third_party/skia/modules/skcms/skcms.h" + +namespace mojo { + +namespace { + +SkImageInfo MakeSkImageInfo(SkColorType color_type, + SkAlphaType alpha_type, + int width, + int height, + mojo::ArrayDataView color_transfer_function, + mojo::ArrayDataView color_to_xyz_matrix) { + CHECK_GE(width, 0); + CHECK_GE(height, 0); + sk_sp color_space; + if (!color_transfer_function.is_null() && !color_to_xyz_matrix.is_null()) { + const float* data = color_transfer_function.data(); + skcms_TransferFunction transfer_function; + CHECK_EQ(7u, color_transfer_function.size()); + transfer_function.g = data[0]; + transfer_function.a = data[1]; + transfer_function.b = data[2]; + transfer_function.c = data[3]; + transfer_function.d = data[4]; + transfer_function.e = data[5]; + transfer_function.f = data[6]; + + skcms_Matrix3x3 to_xyz_matrix; + CHECK_EQ(9u, color_to_xyz_matrix.size()); + memcpy(to_xyz_matrix.vals, color_to_xyz_matrix.data(), 9 * sizeof(float)); + color_space = SkColorSpace::MakeRGB(transfer_function, to_xyz_matrix); + } + + return SkImageInfo::Make(width, height, color_type, alpha_type, + std::move(color_space)); +} + +} // namespace + +// static +skia::mojom::AlphaType EnumTraits::ToMojom( + SkAlphaType type) { + switch (type) { + case kOpaque_SkAlphaType: + return skia::mojom::AlphaType::ALPHA_TYPE_OPAQUE; + case kPremul_SkAlphaType: + return skia::mojom::AlphaType::PREMUL; + case kUnpremul_SkAlphaType: + return skia::mojom::AlphaType::UNPREMUL; + case kUnknown_SkAlphaType: + // Unknown types should not be sent over mojo. + break; + } + CHECK(false); + return skia::mojom::AlphaType::UNKNOWN; +} + +// static +bool EnumTraits::FromMojom( + skia::mojom::AlphaType in, + SkAlphaType* out) { + switch (in) { + case skia::mojom::AlphaType::ALPHA_TYPE_OPAQUE: + *out = kOpaque_SkAlphaType; + return true; + case skia::mojom::AlphaType::PREMUL: + *out = kPremul_SkAlphaType; + return true; + case skia::mojom::AlphaType::UNPREMUL: + *out = kUnpremul_SkAlphaType; + return true; + case skia::mojom::AlphaType::UNKNOWN: + // Unknown types should not be sent over mojo. + return false; + } + return false; +} + +// static +skia::mojom::ColorType EnumTraits::ToMojom( + SkColorType type) { + switch (type) { + case kAlpha_8_SkColorType: + return skia::mojom::ColorType::ALPHA_8; + case kRGB_565_SkColorType: + return skia::mojom::ColorType::RGB_565; + case kARGB_4444_SkColorType: + return skia::mojom::ColorType::ARGB_4444; + case kRGBA_8888_SkColorType: + return skia::mojom::ColorType::RGBA_8888; + case kBGRA_8888_SkColorType: + return skia::mojom::ColorType::BGRA_8888; + case kGray_8_SkColorType: + return skia::mojom::ColorType::GRAY_8; + case kUnknown_SkColorType: + // Fall through as unknown values should not be sent over the wire. + default: + // Skia has color types not used by Chrome. + break; + } + CHECK(false); + return skia::mojom::ColorType::UNKNOWN; +} + +// static +bool EnumTraits::FromMojom( + skia::mojom::ColorType in, + SkColorType* out) { + switch (in) { + case skia::mojom::ColorType::ALPHA_8: + *out = kAlpha_8_SkColorType; + return true; + case skia::mojom::ColorType::RGB_565: + *out = kRGB_565_SkColorType; + return true; + case skia::mojom::ColorType::ARGB_4444: + *out = kARGB_4444_SkColorType; + return true; + case skia::mojom::ColorType::RGBA_8888: + *out = kRGBA_8888_SkColorType; + return true; + case skia::mojom::ColorType::BGRA_8888: + *out = kBGRA_8888_SkColorType; + return true; + case skia::mojom::ColorType::GRAY_8: + *out = kGray_8_SkColorType; + return true; + case skia::mojom::ColorType::DEPRECATED_INDEX_8: + case skia::mojom::ColorType::UNKNOWN: + // UNKNOWN or unsupported values should not be sent over mojo. + break; + } + return false; +} + +// static +uint32_t StructTraits::width( + const SkImageInfo& info) { + // Negative width images are invalid. + return base::checked_cast(info.width()); +} + +// static +uint32_t StructTraits::height( + const SkImageInfo& info) { + // Negative height images are invalid. + return base::checked_cast(info.height()); +} + +// static +absl::optional> +StructTraits::color_transfer_function(const SkImageInfo& info) { + SkColorSpace* color_space = info.colorSpace(); + if (!color_space) + return absl::nullopt; + skcms_TransferFunction fn; + color_space->transferFn(&fn); + return std::vector({fn.g, fn.a, fn.b, fn.c, fn.d, fn.e, fn.f}); +} + +// static +absl::optional> +StructTraits::color_to_xyz_matrix( + const SkImageInfo& info) { + SkColorSpace* color_space = info.colorSpace(); + if (!color_space) + return absl::nullopt; + skcms_Matrix3x3 to_xyz_matrix; + CHECK(color_space->toXYZD50(&to_xyz_matrix)); + + // C-style arrays-of-arrays are tightly packed, so directly copy into vector. + static_assert(sizeof(to_xyz_matrix.vals) == sizeof(float) * 9, + "matrix must be 3x3 floats"); + float* values = &to_xyz_matrix.vals[0][0]; + return std::vector(values, values + 9); +} + +// static +bool StructTraits::Read( + skia::mojom::ImageInfoDataView data, + SkImageInfo* info) { + SkColorType color_type; + SkAlphaType alpha_type; + + if (!data.ReadColorType(&color_type) || !data.ReadAlphaType(&alpha_type)) + return false; + + mojo::ArrayDataView color_transfer_function; + data.GetColorTransferFunctionDataView(&color_transfer_function); + mojo::ArrayDataView color_to_xyz_matrix; + data.GetColorToXyzMatrixDataView(&color_to_xyz_matrix); + + // The ImageInfo wire types are uint32_t, but the Skia type uses int, and the + // values can't be negative. + auto width = base::MakeCheckedNum(data.width()).Cast(); + auto height = base::MakeCheckedNum(data.height()).Cast(); + if (!width.IsValid() || !height.IsValid()) + return false; + + *info = MakeSkImageInfo( + color_type, alpha_type, width.ValueOrDie(), height.ValueOrDie(), + std::move(color_transfer_function), std::move(color_to_xyz_matrix)); + return true; +} + +// static +bool StructTraits::Read( + skia::mojom::BitmapN32ImageInfoDataView data, + SkImageInfo* info) { + SkAlphaType alpha_type; + if (!data.ReadAlphaType(&alpha_type)) + return false; + + mojo::ArrayDataView color_transfer_function; + data.GetColorTransferFunctionDataView(&color_transfer_function); + mojo::ArrayDataView color_to_xyz_matrix; + data.GetColorToXyzMatrixDataView(&color_to_xyz_matrix); + + // The ImageInfo wire types are uint32_t, but the Skia type uses int, and the + // values can't be negative. + auto width = base::MakeCheckedNum(data.width()).Cast(); + auto height = base::MakeCheckedNum(data.height()).Cast(); + if (!width.IsValid() || !height.IsValid()) + return false; + + *info = MakeSkImageInfo( + kN32_SkColorType, alpha_type, width.ValueOrDie(), height.ValueOrDie(), + std::move(color_transfer_function), std::move(color_to_xyz_matrix)); + return true; +} + +} // namespace mojo diff --git a/public/mojom/image_info_mojom_traits.h b/public/mojom/image_info_mojom_traits.h new file mode 100644 index 000000000000..25f3f3a58281 --- /dev/null +++ b/public/mojom/image_info_mojom_traits.h @@ -0,0 +1,83 @@ +// Copyright 2018 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef SKIA_PUBLIC_MOJOM_IMAGE_INFO_MOJOM_TRAITS_H_ +#define SKIA_PUBLIC_MOJOM_IMAGE_INFO_MOJOM_TRAITS_H_ + +#include + +#include "base/component_export.h" +#include "skia/public/mojom/image_info.mojom-shared.h" +#include "third_party/abseil-cpp/absl/types/optional.h" +#include "third_party/skia/include/core/SkImageInfo.h" + +namespace mojo { + +template <> +struct COMPONENT_EXPORT(SKIA_SHARED_TRAITS) + EnumTraits { + static skia::mojom::AlphaType ToMojom(SkAlphaType type); + static bool FromMojom(skia::mojom::AlphaType in, SkAlphaType* out); +}; + +template <> +struct COMPONENT_EXPORT(SKIA_SHARED_TRAITS) + EnumTraits { + static skia::mojom::ColorType ToMojom(SkColorType type); + static bool FromMojom(skia::mojom::ColorType in, SkColorType* out); +}; + +template <> +struct COMPONENT_EXPORT(SKIA_SHARED_TRAITS) + StructTraits { + static SkColorType color_type(const SkImageInfo& info) { + return info.colorType(); + } + static SkAlphaType alpha_type(const SkImageInfo& info) { + return info.alphaType(); + } + static uint32_t width(const SkImageInfo& info); + static uint32_t height(const SkImageInfo& info); + static absl::optional> color_transfer_function( + const SkImageInfo& info); + static absl::optional> color_to_xyz_matrix( + const SkImageInfo& info); + + static bool Read(skia::mojom::ImageInfoDataView data, SkImageInfo* info); +}; + +template <> +struct COMPONENT_EXPORT(SKIA_SHARED_TRAITS) + StructTraits { + static SkAlphaType alpha_type(const SkImageInfo& info) { + // BitmapN32ImageInfo only allows N32 SkImageInfos. + CHECK_EQ(info.colorType(), kN32_SkColorType); + return info.alphaType(); + } + static uint32_t width(const SkImageInfo& info) { + return ImageInfoStructTraits::width(info); + } + static uint32_t height(const SkImageInfo& info) { + return ImageInfoStructTraits::height(info); + } + static absl::optional> color_transfer_function( + const SkImageInfo& info) { + return ImageInfoStructTraits::color_transfer_function(info); + } + static absl::optional> color_to_xyz_matrix( + const SkImageInfo& info) { + return ImageInfoStructTraits::color_to_xyz_matrix(info); + } + + static bool Read(skia::mojom::BitmapN32ImageInfoDataView data, + SkImageInfo* info); + + private: + using ImageInfoStructTraits = + StructTraits; +}; + +} // namespace mojo + +#endif // SKIA_PUBLIC_MOJOM_IMAGE_INFO_MOJOM_TRAITS_H_ diff --git a/public/mojom/skcolor.mojom b/public/mojom/skcolor.mojom new file mode 100644 index 000000000000..163f2521677c --- /dev/null +++ b/public/mojom/skcolor.mojom @@ -0,0 +1,11 @@ +// Copyright 2019 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +module skia.mojom; + +// Mirror of SkColor +[Stable] +struct SkColor { + uint32 value; +}; diff --git a/public/mojom/skcolor4f.mojom b/public/mojom/skcolor4f.mojom new file mode 100644 index 000000000000..2167504377c5 --- /dev/null +++ b/public/mojom/skcolor4f.mojom @@ -0,0 +1,14 @@ +// Copyright 2022 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +module skia.mojom; + +// Mirror of SkColor4f +[Stable] +struct SkColor4f { + float r; //!< red component + float g; //!< green component + float b; //!< blue component + float a; //!< alpha component +}; diff --git a/public/mojom/skcolor4f_mojom_traits.h b/public/mojom/skcolor4f_mojom_traits.h new file mode 100644 index 000000000000..122c76cc1c82 --- /dev/null +++ b/public/mojom/skcolor4f_mojom_traits.h @@ -0,0 +1,30 @@ +// Copyright 2022 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef SKIA_PUBLIC_MOJOM_SKCOLOR4F_MOJOM_TRAITS_H_ +#define SKIA_PUBLIC_MOJOM_SKCOLOR4F_MOJOM_TRAITS_H_ + +#include "skia/public/mojom/skcolor4f.mojom.h" +#include "third_party/skia/include/core/SkColor.h" + +namespace mojo { + +template <> +struct StructTraits { + static float r(::SkColor4f color) { return color.fR; } + static float g(::SkColor4f color) { return color.fG; } + static float b(::SkColor4f color) { return color.fB; } + static float a(::SkColor4f color) { return color.fA; } + static bool Read(skia::mojom::SkColor4fDataView data, ::SkColor4f* color) { + color->fR = data.r(); + color->fG = data.g(); + color->fB = data.b(); + color->fA = data.a(); + return true; + } +}; + +} // namespace mojo + +#endif // SKIA_PUBLIC_MOJOM_SKCOLOR4F_MOJOM_TRAITS_H_ diff --git a/public/mojom/skcolor_mojom_traits.h b/public/mojom/skcolor_mojom_traits.h new file mode 100644 index 000000000000..7ec649bc3849 --- /dev/null +++ b/public/mojom/skcolor_mojom_traits.h @@ -0,0 +1,24 @@ +// Copyright 2019 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef SKIA_PUBLIC_MOJOM_SKCOLOR_MOJOM_TRAITS_H_ +#define SKIA_PUBLIC_MOJOM_SKCOLOR_MOJOM_TRAITS_H_ + +#include "skia/public/mojom/skcolor.mojom.h" +#include "third_party/skia/include/core/SkColor.h" + +namespace mojo { + +template <> +struct StructTraits { + static uint32_t value(::SkColor color) { return color; } + static bool Read(skia::mojom::SkColorDataView data, ::SkColor* color) { + *color = data.value(); + return true; + } +}; + +} // namespace mojo + +#endif // SKIA_PUBLIC_MOJOM_SKCOLOR_MOJOM_TRAITS_H_ diff --git a/public/mojom/skcolorspace_primaries.mojom b/public/mojom/skcolorspace_primaries.mojom new file mode 100644 index 000000000000..0dea8e7c0118 --- /dev/null +++ b/public/mojom/skcolorspace_primaries.mojom @@ -0,0 +1,16 @@ +// Copyright 2022 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +module skia.mojom; + +struct SkColorSpacePrimaries { + float rX; + float rY; + float gX; + float gY; + float bX; + float bY; + float wX; + float wY; +}; diff --git a/public/mojom/skcolorspace_primaries_mojom_traits.h b/public/mojom/skcolorspace_primaries_mojom_traits.h new file mode 100644 index 000000000000..f6975a34fbac --- /dev/null +++ b/public/mojom/skcolorspace_primaries_mojom_traits.h @@ -0,0 +1,58 @@ +// Copyright 2022 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef SKIA_PUBLIC_MOJOM_SKCOLORSPACE_PRIMARIES_MOJOM_TRAITS_H_ +#define SKIA_PUBLIC_MOJOM_SKCOLORSPACE_PRIMARIES_MOJOM_TRAITS_H_ + +#include "skia/ext/skcolorspace_primaries.h" +#include "skia/public/mojom/skcolorspace_primaries.mojom.h" +#include "third_party/skia/include/core/SkColorSpace.h" + +namespace mojo { + +template <> +struct StructTraits { + static float rX(const ::SkColorSpacePrimaries& primaries) { + return primaries.fRX; + } + static float rY(const ::SkColorSpacePrimaries& primaries) { + return primaries.fRY; + } + static float gX(const ::SkColorSpacePrimaries& primaries) { + return primaries.fGX; + } + static float gY(const ::SkColorSpacePrimaries& primaries) { + return primaries.fGY; + } + static float bX(const ::SkColorSpacePrimaries& primaries) { + return primaries.fBX; + } + static float bY(const ::SkColorSpacePrimaries& primaries) { + return primaries.fBY; + } + static float wX(const ::SkColorSpacePrimaries& primaries) { + return primaries.fWX; + } + static float wY(const ::SkColorSpacePrimaries& primaries) { + return primaries.fWY; + } + + static bool Read(skia::mojom::SkColorSpacePrimariesDataView data, + ::SkColorSpacePrimaries* color) { + color->fRX = data.rX(); + color->fRY = data.rY(); + color->fGX = data.gX(); + color->fGY = data.gY(); + color->fBX = data.bX(); + color->fBY = data.bY(); + color->fWX = data.wX(); + color->fWY = data.wY(); + return true; + } +}; + +} // namespace mojo + +#endif // SKIA_PUBLIC_MOJOM_SKCOLOR4F_MOJOM_TRAITS_H_ diff --git a/public/mojom/surface_origin.mojom b/public/mojom/surface_origin.mojom new file mode 100644 index 000000000000..8618ffcdb4ce --- /dev/null +++ b/public/mojom/surface_origin.mojom @@ -0,0 +1,11 @@ +// Copyright 2021 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +module skia.mojom; + +// Maps to Skia's GrSurfaceOrigin in third_party/skia/include/gpu/GrTypes.h +enum SurfaceOrigin { + kTopLeft, + kBottomLeft, +}; diff --git a/public/mojom/surface_origin_mojom_traits.h b/public/mojom/surface_origin_mojom_traits.h new file mode 100644 index 000000000000..3e9780d357f9 --- /dev/null +++ b/public/mojom/surface_origin_mojom_traits.h @@ -0,0 +1,47 @@ +// Copyright 2021 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef SKIA_PUBLIC_MOJOM_SURFACE_ORIGIN_MOJOM_TRAITS_H_ +#define SKIA_PUBLIC_MOJOM_SURFACE_ORIGIN_MOJOM_TRAITS_H_ + +#include "base/notreached.h" +#include "mojo/public/cpp/bindings/enum_traits.h" +#include "skia/public/mojom/surface_origin.mojom-shared.h" +#include "third_party/skia/include/gpu/GrTypes.h" + +namespace mojo { + +template <> +struct EnumTraits { + static skia::mojom::SurfaceOrigin ToMojom(GrSurfaceOrigin origin) { + switch (origin) { + case kTopLeft_GrSurfaceOrigin: + return skia::mojom::SurfaceOrigin::kTopLeft; + case kBottomLeft_GrSurfaceOrigin: + return skia::mojom::SurfaceOrigin::kBottomLeft; + } + NOTREACHED(); + } + + static bool FromMojom(skia::mojom::SurfaceOrigin origin, + GrSurfaceOrigin* out_origin) { + switch (origin) { + case skia::mojom::SurfaceOrigin::kTopLeft: + *out_origin = kTopLeft_GrSurfaceOrigin; + return true; + case skia::mojom::SurfaceOrigin::kBottomLeft: + *out_origin = kBottomLeft_GrSurfaceOrigin; + return true; + } + + // Mojo has already validated that `origin` is a valid value, so it must be + // covered by one of the cases above. + NOTREACHED(); + return false; + } +}; + +} // namespace mojo + +#endif // SKIA_PUBLIC_MOJOM_SURFACE_ORIGIN_MOJOM_TRAITS_H_ diff --git a/public/mojom/test/OWNERS b/public/mojom/test/OWNERS new file mode 100644 index 000000000000..a40e0a36946b --- /dev/null +++ b/public/mojom/test/OWNERS @@ -0,0 +1,4 @@ +per-file mojom_traits_unittest.cc=file://ipc/SECURITY_OWNERS + +per-file *.mojom=set noparent +per-file *.mojom=file://ipc/SECURITY_OWNERS diff --git a/public/mojom/test/mojom_traits_unittest.cc b/public/mojom/test/mojom_traits_unittest.cc new file mode 100644 index 000000000000..d79d2a51058d --- /dev/null +++ b/public/mojom/test/mojom_traits_unittest.cc @@ -0,0 +1,491 @@ +// Copyright 2016 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include +#include "mojo/public/cpp/test_support/test_utils.h" +#include "skia/public/mojom/bitmap.mojom.h" +#include "skia/public/mojom/bitmap_skbitmap_mojom_traits.h" +#include "skia/public/mojom/image_info.mojom-shared.h" +#include "skia/public/mojom/image_info.mojom.h" +#include "skia/public/mojom/tile_mode.mojom.h" +#include "skia/public/mojom/tile_mode_mojom_traits.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "third_party/skia/include/core/SkColorFilter.h" +#include "third_party/skia/include/core/SkColorSpace.h" +#include "third_party/skia/include/core/SkImageInfo.h" +#include "third_party/skia/include/core/SkString.h" +#include "third_party/skia/include/core/SkTileMode.h" +#include "third_party/skia/modules/skcms/skcms.h" +#include "ui/gfx/skia_util.h" + +namespace skia { +namespace { + +// A helper to construct a skia.mojom.BitmapN32 without using StructTraits +// to bypass checks on the sending/serialization side. +mojo::StructPtr ConstructBitmapN32( + SkImageInfo info, + std::vector pixels) { + auto mojom_bitmap = skia::mojom::BitmapN32::New(); + mojom_bitmap->image_info = std::move(info); + mojom_bitmap->pixel_data = std::move(pixels); + return mojom_bitmap; +} + +// A helper to construct a skia.mojom.BitmapWithArbitraryBpp without using +// StructTraits to bypass checks on the sending/serialization side. +mojo::StructPtr +ConstructBitmapWithArbitraryBpp(SkImageInfo info, + int row_bytes, + std::vector pixels) { + auto mojom_bitmap = skia::mojom::BitmapWithArbitraryBpp::New(); + mojom_bitmap->image_info = std::move(info); + mojom_bitmap->UNUSED_row_bytes = row_bytes; + mojom_bitmap->pixel_data = std::move(pixels); + return mojom_bitmap; +} + +// A helper to construct a skia.mojom.BitmapMappedFromTrustedProcess without +// using StructTraits to bypass checks on the sending/serialization side. +mojo::StructPtr +ConstructBitmapMappedFromTrustedProcess(SkImageInfo info, + int row_bytes, + std::vector pixels) { + auto mojom_bitmap = skia::mojom::BitmapMappedFromTrustedProcess::New(); + mojom_bitmap->image_info = std::move(info); + mojom_bitmap->UNUSED_row_bytes = row_bytes; + mojom_bitmap->pixel_data = mojo_base::BigBuffer(std::move(pixels)); + return mojom_bitmap; +} + +// A helper to construct a skia.mojom.InlineBitmap without using StructTraits +// to bypass checks on the sending/serialization side. +mojo::StructPtr ConstructInlineBitmap( + SkImageInfo info, + std::vector pixels) { + DCHECK_EQ(info.colorType(), kN32_SkColorType); + auto mojom_bitmap = skia::mojom::InlineBitmap::New(); + mojom_bitmap->image_info = std::move(info); + mojom_bitmap->pixel_data = std::move(pixels); + return mojom_bitmap; +} + +// A helper to construct a skia.mojom.ImageInfo without using StructTraits +// to bypass checks on the sending/serialization side. +mojo::StructPtr ConstructImageInfo( + SkColorType color_type, + SkAlphaType alpha_type, + uint32_t width, + uint32_t height) { + auto mojom_info = skia::mojom::ImageInfo::New(); + mojom_info->color_type = color_type; + mojom_info->alpha_type = alpha_type; + mojom_info->width = width; + mojom_info->height = height; + return mojom_info; +} + +TEST(StructTraitsTest, ImageInfo) { + SkImageInfo input = SkImageInfo::Make( + 34, 56, SkColorType::kGray_8_SkColorType, + SkAlphaType::kUnpremul_SkAlphaType, + SkColorSpace::MakeRGB(SkNamedTransferFn::kSRGB, SkNamedGamut::kAdobeRGB)); + SkImageInfo output; + ASSERT_TRUE(mojo::test::SerializeAndDeserialize( + input, output)); + EXPECT_EQ(input, output); + + SkImageInfo another_input_with_null_color_space = + SkImageInfo::Make(54, 43, SkColorType::kRGBA_8888_SkColorType, + SkAlphaType::kPremul_SkAlphaType, nullptr); + ASSERT_TRUE(mojo::test::SerializeAndDeserialize( + another_input_with_null_color_space, output)); + EXPECT_FALSE(output.colorSpace()); + EXPECT_EQ(another_input_with_null_color_space, output); +} + +// We catch negative integers on the sending side and crash, when struct traits +// are used. +TEST(StructTraitsDeathTest, ImageInfoOverflowSizeWithStructTrait) { + SkImageInfo input = SkImageInfo::Make( + std::numeric_limits::max(), + std::numeric_limits::max(), SkColorType::kGray_8_SkColorType, + SkAlphaType::kUnpremul_SkAlphaType, + SkColorSpace::MakeRGB(SkNamedTransferFn::kSRGB, SkNamedGamut::kAdobeRGB)); + SkImageInfo output; + EXPECT_DEATH(skia::mojom::ImageInfo::SerializeAsMessage(&input), ""); +} + +// We must reject sizes that would cause integer overflow on the receiving side. +// The wire format is `uint32_t`, but Skia needs us to convert that to an `int` +// for the SkImageInfo type. +TEST(StructTraitsTest, ImageInfoOverflowSizeWithoutStructTrait) { + SkImageInfo output; + mojo::StructPtr input = ConstructImageInfo( + SkColorType::kGray_8_SkColorType, SkAlphaType::kUnpremul_SkAlphaType, + std::numeric_limits::max(), + std::numeric_limits::max()); + EXPECT_FALSE(mojo::test::SerializeAndDeserialize( + input, output)); +} + +TEST(StructTraitsTest, ImageInfoCustomColorSpace) { + skcms_TransferFunction transfer{0.1f, 0.2f, 0.3f, 0.4f, 0.5f, 0.6f, 0.7f}; + skcms_Matrix3x3 gamut{ + .vals = {{0.1f, 0.2f, 0.3f}, {0.4f, 0.5f, 0.6f}, {0.7f, 0.8f, 0.9f}}}; + sk_sp color_space = SkColorSpace::MakeRGB(transfer, gamut); + SkImageInfo input = + SkImageInfo::Make(12, 34, SkColorType::kRGBA_8888_SkColorType, + kUnpremul_SkAlphaType, color_space); + SkImageInfo output; + ASSERT_TRUE(mojo::test::SerializeAndDeserialize( + input, output)); + EXPECT_TRUE(output.colorSpace()); + EXPECT_EQ(input, output); +} + +TEST(StructTraitsTest, TileMode) { + SkTileMode input(SkTileMode::kClamp); + SkTileMode output; + ASSERT_TRUE(mojo::test::SerializeAndDeserialize( + input, output)); + EXPECT_EQ(input, output); + input = SkTileMode::kRepeat; + ASSERT_TRUE(mojo::test::SerializeAndDeserialize( + input, output)); + EXPECT_EQ(input, output); + input = SkTileMode::kMirror; + ASSERT_TRUE(mojo::test::SerializeAndDeserialize( + input, output)); + EXPECT_EQ(input, output); + input = SkTileMode::kDecal; + ASSERT_TRUE(mojo::test::SerializeAndDeserialize( + input, output)); + EXPECT_EQ(input, output); +} + +TEST(StructTraitsTest, Bitmap) { + SkBitmap input; + input.allocPixels(SkImageInfo::MakeN32Premul( + 10, 5, + SkColorSpace::MakeRGB(SkNamedTransferFn::kLinear, + SkNamedGamut::kRec2020))); + input.eraseColor(SK_ColorYELLOW); + input.erase(SK_ColorTRANSPARENT, SkIRect::MakeXYWH(0, 1, 2, 3)); + SkBitmap output; + + auto BitmapsEqual = [](const SkBitmap& input, const SkBitmap& output) { + EXPECT_EQ(input.info(), output.info()); + EXPECT_EQ(input.rowBytes(), output.rowBytes()); + EXPECT_TRUE(gfx::BitmapsAreEqual(input, output)); + }; + + { + ASSERT_TRUE(mojo::test::SerializeAndDeserialize( + input, output)); + BitmapsEqual(input, output); + } + { + ASSERT_TRUE(mojo::test::SerializeAndDeserialize< + skia::mojom::BitmapWithArbitraryBpp>(input, output)); + BitmapsEqual(input, output); + } + { + ASSERT_TRUE(mojo::test::SerializeAndDeserialize< + skia::mojom::BitmapMappedFromTrustedProcess>(input, output)); + BitmapsEqual(input, output); + } + { + ASSERT_TRUE(mojo::test::SerializeAndDeserialize( + input, output)); + BitmapsEqual(input, output); + } +} + +// Null input produces a default-initialized SkBitmap. +TEST(StructTraitsTest, BitmapNull) { + SkBitmap input; + input.setInfo(SkImageInfo::MakeN32Premul( + 10, 5, + SkColorSpace::MakeRGB(SkNamedTransferFn::kLinear, + SkNamedGamut::kRec2020))); + EXPECT_TRUE(input.isNull()); + + auto IsDefaultInit = [](const SkBitmap& output) { + EXPECT_EQ(output.info().alphaType(), kUnknown_SkAlphaType); + EXPECT_EQ(output.info().colorType(), kUnknown_SkColorType); + EXPECT_EQ(output.rowBytes(), 0u); + EXPECT_TRUE(output.isNull()); + }; + + SkBitmap output; + { + EXPECT_TRUE(mojo::test::SerializeAndDeserialize( + input, output)); + IsDefaultInit(output); + } + { + EXPECT_TRUE(mojo::test::SerializeAndDeserialize< + skia::mojom::BitmapWithArbitraryBpp>(input, output)); + IsDefaultInit(output); + } + { + EXPECT_TRUE(mojo::test::SerializeAndDeserialize< + skia::mojom::BitmapMappedFromTrustedProcess>(input, output)); + IsDefaultInit(output); + } + { + EXPECT_TRUE(mojo::test::SerializeAndDeserialize( + input, output)); + IsDefaultInit(output); + } +} + +// Serialize to string works, we only need this verify this for InlineBitmap, +// as the other Bitmap types should not be used for this purpose. +TEST(StructTraitsTest, InlineBitmapSerializeToString) { + SkBitmap input; + input.allocPixels(SkImageInfo::MakeN32Premul(10, 5)); + input.eraseColor(SK_ColorYELLOW); + + auto serialized = skia::mojom::InlineBitmap::Serialize(&input); + SkBitmap output; + ASSERT_TRUE( + skia::mojom::InlineBitmap::Deserialize(std::move(serialized), &output)); + EXPECT_EQ(input.info(), output.info()); + EXPECT_EQ(input.rowBytes(), output.rowBytes()); + EXPECT_TRUE(gfx::BitmapsAreEqual(input, output)); +} + +// Verify that we can manually construct a valid skia.mojom object and +// deserialize it successfully. +TEST(StructTraitsTest, VerifyMojomConstruction) { + SkBitmap output; + + { + mojo::StructPtr input = + ConstructBitmapN32(SkImageInfo::MakeN32Premul(1, 1), {1, 2, 3, 4}); + EXPECT_TRUE(mojo::test::SerializeAndDeserialize( + input, output)); + } + { + mojo::StructPtr input = + ConstructBitmapWithArbitraryBpp(SkImageInfo::MakeN32Premul(1, 1), 0, + {1, 2, 3, 4}); + EXPECT_TRUE(mojo::test::SerializeAndDeserialize< + skia::mojom::BitmapWithArbitraryBpp>(input, output)); + } + { + mojo::StructPtr input = + ConstructBitmapMappedFromTrustedProcess( + SkImageInfo::MakeN32Premul(1, 1), 0, {1, 2, 3, 4}); + EXPECT_TRUE(mojo::test::SerializeAndDeserialize< + skia::mojom::BitmapMappedFromTrustedProcess>(input, output)); + } + { + mojo::StructPtr input = + ConstructInlineBitmap(SkImageInfo::MakeN32Premul(1, 1), {1, 2, 3, 4}); + EXPECT_TRUE(mojo::test::SerializeAndDeserialize( + input, output)); + } +} + +// We only allow 64 * 1024 as the max width. +TEST(StructTraitsTest, BitmapTooWideToSerialize) { + constexpr int kTooWide = 64 * 1024 + 1; + SkBitmap input; + input.allocPixels( + SkImageInfo::MakeN32(kTooWide, 1, SkAlphaType::kUnpremul_SkAlphaType)); + input.eraseColor(SK_ColorYELLOW); + SkBitmap output; + { + EXPECT_FALSE(mojo::test::SerializeAndDeserialize( + input, output)); + } + { + EXPECT_FALSE(mojo::test::SerializeAndDeserialize< + skia::mojom::BitmapWithArbitraryBpp>(input, output)); + } + { + EXPECT_FALSE(mojo::test::SerializeAndDeserialize< + skia::mojom::BitmapMappedFromTrustedProcess>(input, output)); + } + { + EXPECT_FALSE(mojo::test::SerializeAndDeserialize( + input, output)); + } +} + +// We only allow 64 * 1024 as the max height. +TEST(StructTraitsTest, BitmapTooTallToSerialize) { + constexpr int kTooTall = 64 * 1024 + 1; + SkBitmap input; + input.allocPixels( + SkImageInfo::MakeN32(1, kTooTall, SkAlphaType::kUnpremul_SkAlphaType)); + input.eraseColor(SK_ColorYELLOW); + SkBitmap output; + { + EXPECT_FALSE(mojo::test::SerializeAndDeserialize( + input, output)); + } + { + EXPECT_FALSE(mojo::test::SerializeAndDeserialize< + skia::mojom::BitmapWithArbitraryBpp>(input, output)); + } + { + EXPECT_FALSE(mojo::test::SerializeAndDeserialize< + skia::mojom::BitmapMappedFromTrustedProcess>(input, output)); + } + { + EXPECT_FALSE(mojo::test::SerializeAndDeserialize( + input, output)); + } +} + +template +static void BadRowBytes() { + SkImageInfo info = + SkImageInfo::MakeN32(8, 5, kPremul_SkAlphaType, SkColorSpace::MakeSRGB()); + const size_t row_bytes = info.minRowBytes() + info.bytesPerPixel(); + SkBitmap input; + EXPECT_TRUE(input.tryAllocPixels(info, row_bytes)); + // This will crash. + EXPECT_DEATH(MojomType::SerializeAsMessage(&input), ""); +} + +// We do not allow sending rowBytes() other than the minRowBytes(). +TEST(StructTraitsTest, BitmapSerializeInvalidRowBytes_BitmapN32) { + BadRowBytes(); +} +TEST(StructTraitsTest, BitmapSerializeInvalidRowBytes_BitmapWithArbitraryBpp) { + BadRowBytes(); +} +TEST(StructTraitsTest, + BitmapSerializeInvalidRowBytes_BitmapMappedFromTrustedProcess) { + BadRowBytes(); +} +TEST(StructTraitsTest, BitmapSerializeInvalidRowBytes_InlineBitmap) { + BadRowBytes(); +} + +template +static void BadColor(bool expect_crash) { + SkImageInfo info = SkImageInfo::MakeA8(10, 5); + SkBitmap input; + EXPECT_TRUE(input.tryAllocPixels(info)); + if (expect_crash) { + // This will crash. + EXPECT_DEATH(MojomType::SerializeAsMessage(&input), ""); + } else { + // This won't as the mojom allows arbitrary color formats. + MojomType::SerializeAsMessage(&input); + } +} + +TEST(StructTraitsTest, BitmapSerializeInvalidColorType_BitmapN32) { + BadColor(/*expect_crash=*/true); +} +TEST(StructTraitsTest, BitmapSerializeInvalidColorType_BitmapWithArbitraryBpp) { + BadColor(/*expect_crash=*/false); +} +TEST(StructTraitsTest, + BitmapSerializeInvalidColorType_BitmapMappedFromTrustedProcess) { + BadColor(/*expect_crash=*/false); +} +TEST(StructTraitsTest, BitmapSerializeInvalidColorType_InlineBitmap) { + BadColor(/*expect_crash=*/true); +} + +// The row_bytes field is ignored, and the minRowBytes() is always used. +TEST(StructTraitsTest, BitmapDeserializeIgnoresRowBytes) { + SkBitmap output; + + size_t ignored_row_bytes = 8; + size_t expected_row_bytes = 4; + { + mojo::StructPtr input = + ConstructBitmapWithArbitraryBpp(SkImageInfo::MakeN32Premul(1, 1), + ignored_row_bytes, {1, 2, 3, 4}); + EXPECT_TRUE(mojo::test::SerializeAndDeserialize< + skia::mojom::BitmapWithArbitraryBpp>(input, output)); + EXPECT_EQ(expected_row_bytes, output.rowBytes()); + } + { + mojo::StructPtr input = + ConstructBitmapMappedFromTrustedProcess( + SkImageInfo::MakeN32Premul(1, 1), ignored_row_bytes, {1, 2, 3, 4}); + EXPECT_TRUE(mojo::test::SerializeAndDeserialize< + skia::mojom::BitmapMappedFromTrustedProcess>(input, output)); + EXPECT_EQ(expected_row_bytes, output.rowBytes()); + } + { + // Neither skia::mojom::BitmapN32 nor skia::mojom::InlineBitmap have a + // row_bytes field to test. + } +} + +// The SkImageInfo claims 8 bytes, but the pixel vector has 4. +TEST(StructTraitsTest, InlineBitmapDeserializeTooFewBytes) { + SkImageInfo info = SkImageInfo::MakeN32Premul(2, 1); + std::vector pixels = {1, 2, 3, 4}; + SkBitmap output; + { + mojo::StructPtr input = + ConstructBitmapN32(info, pixels); + EXPECT_FALSE(mojo::test::SerializeAndDeserialize( + input, output)); + } + { + mojo::StructPtr input = + ConstructBitmapWithArbitraryBpp(info, 0, pixels); + EXPECT_FALSE(mojo::test::SerializeAndDeserialize< + skia::mojom::BitmapWithArbitraryBpp>(input, output)); + } + { + mojo::StructPtr input = + ConstructBitmapMappedFromTrustedProcess(info, 0, pixels); + EXPECT_FALSE(mojo::test::SerializeAndDeserialize< + skia::mojom::BitmapMappedFromTrustedProcess>(input, output)); + } + { + mojo::StructPtr input = + ConstructInlineBitmap(info, pixels); + EXPECT_FALSE(mojo::test::SerializeAndDeserialize( + input, output)); + } +} + +// The SkImageInfo claims 4 bytes, but the pixel vector has 8. +TEST(StructTraitsTest, InlineBitmapDeserializeTooManyBytes) { + SkImageInfo info = SkImageInfo::MakeN32Premul(1, 1); + std::vector pixels = {1, 2, 3, 4, 5, 6, 7, 8}; + SkBitmap output; + { + mojo::StructPtr input = + ConstructBitmapN32(info, pixels); + EXPECT_FALSE(mojo::test::SerializeAndDeserialize( + input, output)); + } + { + mojo::StructPtr input = + ConstructBitmapWithArbitraryBpp(info, 0, pixels); + EXPECT_FALSE(mojo::test::SerializeAndDeserialize< + skia::mojom::BitmapWithArbitraryBpp>(input, output)); + } + { + mojo::StructPtr input = + ConstructBitmapMappedFromTrustedProcess(info, 0, pixels); + EXPECT_FALSE(mojo::test::SerializeAndDeserialize< + skia::mojom::BitmapMappedFromTrustedProcess>(input, output)); + } + { + mojo::StructPtr input = + ConstructInlineBitmap(info, pixels); + EXPECT_FALSE(mojo::test::SerializeAndDeserialize( + input, output)); + } +} + +} // namespace +} // namespace skia diff --git a/public/mojom/tile_mode.mojom b/public/mojom/tile_mode.mojom new file mode 100644 index 000000000000..63a7c6ee665e --- /dev/null +++ b/public/mojom/tile_mode.mojom @@ -0,0 +1,13 @@ +// Copyright 2017 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +module skia.mojom; + +// Mirror of SkTileMode. +enum TileMode { + CLAMP, + REPEAT, + MIRROR, + DECAL +}; diff --git a/public/mojom/tile_mode_mojom_traits.h b/public/mojom/tile_mode_mojom_traits.h new file mode 100644 index 000000000000..84037b7267e5 --- /dev/null +++ b/public/mojom/tile_mode_mojom_traits.h @@ -0,0 +1,52 @@ +// Copyright 2017 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef SKIA_PUBLIC_MOJOM_TILE_MODE_MOJOM_TRAITS_H_ +#define SKIA_PUBLIC_MOJOM_TILE_MODE_MOJOM_TRAITS_H_ + +#include "base/notreached.h" +#include "skia/public/mojom/tile_mode.mojom-shared.h" +#include "third_party/skia/include/core/SkTileMode.h" + +namespace mojo { + +template <> +struct EnumTraits { + static skia::mojom::TileMode ToMojom(SkTileMode tile_mode) { + switch (tile_mode) { + case SkTileMode::kClamp: + return skia::mojom::TileMode::CLAMP; + case SkTileMode::kRepeat: + return skia::mojom::TileMode::REPEAT; + case SkTileMode::kMirror: + return skia::mojom::TileMode::MIRROR; + case SkTileMode::kDecal: + return skia::mojom::TileMode::DECAL; + } + NOTREACHED(); + return skia::mojom::TileMode::DECAL; + } + + static bool FromMojom(skia::mojom::TileMode input, SkTileMode* out) { + switch (input) { + case skia::mojom::TileMode::CLAMP: + *out = SkTileMode::kClamp; + return true; + case skia::mojom::TileMode::REPEAT: + *out = SkTileMode::kRepeat; + return true; + case skia::mojom::TileMode::MIRROR: + *out = SkTileMode::kMirror; + return true; + case skia::mojom::TileMode::DECAL: + *out = SkTileMode::kDecal; + return true; + } + return false; + } +}; + +} // namespace mojo + +#endif // SKIA_PUBLIC_MOJOM_TILE_MODE_MOJOM_TRAITS_H_ diff --git a/skia_Prefix.pch b/skia_Prefix.pch new file mode 100644 index 000000000000..75449962d43b --- /dev/null +++ b/skia_Prefix.pch @@ -0,0 +1,12 @@ +// Copyright 2008 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// Prefix header for all source files in the 'Skia' framework. + +#ifdef __OBJC__ +#import +#endif + +// Include the Skia prefix file. +#include "precompiled.cc" diff --git a/skia_resources.grd b/skia_resources.grd new file mode 100644 index 000000000000..72f2bd8bdd42 --- /dev/null +++ b/skia_resources.grd @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tools/filter_fuzz_stub/filter_fuzz_stub.cc b/tools/filter_fuzz_stub/filter_fuzz_stub.cc new file mode 100644 index 000000000000..4553aaccde6a --- /dev/null +++ b/tools/filter_fuzz_stub/filter_fuzz_stub.cc @@ -0,0 +1,96 @@ +// Copyright 2013 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "base/files/file_util.h" +#include "base/logging.h" +#include "base/process/memory.h" +#include "base/test/test_discardable_memory_allocator.h" +#include "third_party/skia/include/core/SkBitmap.h" +#include "third_party/skia/include/core/SkCanvas.h" +#include "third_party/skia/include/core/SkImage.h" +#include "third_party/skia/include/core/SkImageFilter.h" + +namespace { + +static const int BitmapSize = 24; + +bool ReadTestCase(const char* filename, std::string* ipc_filter_message) { + base::FilePath filepath = base::FilePath::FromUTF8Unsafe(filename); + + if (!base::ReadFileToString(filepath, ipc_filter_message)) { + LOG(ERROR) << filename << ": couldn't read file."; + return false; + } + + return true; +} + +void RunTestCase(std::string& ipc_filter_message, SkBitmap& bitmap, + SkCanvas* canvas) { + // This call shouldn't crash or cause ASAN to flag any memory issues + // If nothing bad happens within this call, everything is fine + sk_sp flattenable = SkImageFilter::Deserialize( + ipc_filter_message.c_str(), ipc_filter_message.size()); + + // Adding some info, but the test passed if we got here without any trouble + if (flattenable != NULL) { + LOG(INFO) << "Valid stream detected."; + // Let's see if using the filters can cause any trouble... + SkPaint paint; + paint.setImageFilter(flattenable); + canvas->save(); + canvas->clipRect(SkRect::MakeXYWH( + 0, 0, SkIntToScalar(BitmapSize), SkIntToScalar(BitmapSize))); + + // This call shouldn't crash or cause ASAN to flag any memory issues + // If nothing bad happens within this call, everything is fine + canvas->drawImage(bitmap.asImage(), 0, 0, SkSamplingOptions(), &paint); + + LOG(INFO) << "Filter DAG rendered successfully"; + canvas->restore(); + } else { + LOG(INFO) << "Invalid stream detected."; + } +} + +bool ReadAndRunTestCase(const char* filename, SkBitmap& bitmap, + SkCanvas* canvas) { + std::string ipc_filter_message; + + LOG(INFO) << "Test case: " << filename; + + // ReadTestCase will print a useful error message if it fails. + if (!ReadTestCase(filename, &ipc_filter_message)) + return false; + + RunTestCase(ipc_filter_message, bitmap, canvas); + + return true; +} + +} + +int main(int argc, char** argv) { + int ret = 0; + + base::EnableTerminationOnOutOfMemory(); + base::TestDiscardableMemoryAllocator discardable_memory_allocator; + base::DiscardableMemoryAllocator::SetInstance(&discardable_memory_allocator); + + SkBitmap bitmap; + bitmap.allocN32Pixels(BitmapSize, BitmapSize); + SkCanvas canvas(bitmap, SkSurfaceProps{}); + canvas.clear(0x00000000); + + for (int i = 1; i < argc; i++) + if (!ReadAndRunTestCase(argv[i], bitmap, &canvas)) + ret = 2; + + // Cluster-Fuzz likes "#EOF" as the last line of output to help distinguish + // successful runs from crashes. + printf("#EOF\n"); + + return ret; +} + diff --git a/tools/fuzzers/BUILD.gn b/tools/fuzzers/BUILD.gn new file mode 100644 index 000000000000..48380e0eb3e1 --- /dev/null +++ b/tools/fuzzers/BUILD.gn @@ -0,0 +1,136 @@ +# Copyright 2018 The Chromium Authors +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +import("//testing/libfuzzer/fuzzer_test.gni") + +group("fuzzers") { +} + +# Build Skia fuzzers from OSS-Fuzz on Windows since Windows is not supported by +# OSS-Fuzz. +if (is_win && use_fuzzing_engine) { + source_set("skia_fuzzer_sources") { + testonly = true + sources = [ + "//skia/tools/fuzzers/fuzzer_environment.cc", + "//third_party/skia/fuzz/Fuzz.cpp", + "//third_party/skia/fuzz/FuzzCommon.cpp", + ] + + # Use public_deps and public_configs so each fuzzer_test doesn't need to + # explicitly depend on "//skia" and "//skia:skia_library_config". + public_deps = [ "//skia" ] + public_configs = [ "//skia:skia_library_config" ] + deps = [ "//base/test:test_support" ] + } + + static_library("skia_encoder_fuzzer_lib") { + sources = [ "//third_party/skia/fuzz/FuzzEncoders.cpp" ] + configs += [ "//skia:skia_library_config" ] + deps = [ "//skia" ] + } + + config("skia_test_fonts_includes_config") { + include_dirs = [ + "//third_party/skia/tools/fonts", + "//third_party/skia/tools/", + ] + } + + static_library("skia_test_fonts") { + sources = [ + "//third_party/skia/tools/fonts/TestFontMgr.cpp", + "//third_party/skia/tools/fonts/TestSVGTypeface.cpp", + "//third_party/skia/tools/fonts/TestTypeface.cpp", + ] + configs += [ "//skia:skia_library_config" ] + + public_configs = [ ":skia_test_fonts_includes_config" ] + deps = [ "//skia" ] + } + + # TODO(metzman): Enable the other fuzzers that cannot yet build in Chromium. + fuzzer_test("skia_region_deserialize_fuzzer") { + sources = [ "//third_party/skia/fuzz/oss_fuzz/FuzzRegionDeserialize.cpp" ] + defines = [ "IS_FUZZING_WITH_LIBFUZZER" ] + deps = [ ":skia_fuzzer_sources" ] + } + + fuzzer_test("skia_image_filter_deserialize_fuzzer") { + sources = + [ "//third_party/skia/fuzz/oss_fuzz/FuzzImageFilterDeserialize.cpp" ] + defines = [ "IS_FUZZING_WITH_LIBFUZZER" ] + deps = [ + ":skia_fuzzer_sources", + ":skia_test_fonts", + ] + } + + fuzzer_test("skia_region_set_path_fuzzer") { + sources = [ "//third_party/skia/fuzz/oss_fuzz/FuzzRegionSetPath.cpp" ] + defines = [ "IS_FUZZING_WITH_LIBFUZZER" ] + deps = [ ":skia_fuzzer_sources" ] + } + + fuzzer_test("skia_textblob_deserialize_fuzzer") { + sources = [ "//third_party/skia/fuzz/oss_fuzz/FuzzTextBlobDeserialize.cpp" ] + defines = [ "IS_FUZZING_WITH_LIBFUZZER" ] + deps = [ + ":skia_fuzzer_sources", + ":skia_test_fonts", + ] + } + + fuzzer_test("skia_path_deserialize_fuzzer") { + sources = [ "//third_party/skia/fuzz/oss_fuzz/FuzzPathDeserialize.cpp" ] + defines = [ "IS_FUZZING_WITH_LIBFUZZER" ] + deps = [ ":skia_fuzzer_sources" ] + } + + fuzzer_test("skia_image_decode_fuzzer") { + sources = [ "//third_party/skia/fuzz/oss_fuzz/FuzzImage.cpp" ] + defines = [ "IS_FUZZING_WITH_LIBFUZZER" ] + deps = [ ":skia_fuzzer_sources" ] + } + + fuzzer_test("skia_png_encoder_fuzzer") { + sources = [ "//third_party/skia/fuzz/oss_fuzz/FuzzPNGEncoder.cpp" ] + defines = [ "IS_FUZZING_WITH_LIBFUZZER" ] + deps = [ + ":skia_encoder_fuzzer_lib", + ":skia_fuzzer_sources", + ] + } + + fuzzer_test("skia_jpeg_encoder_fuzzer") { + sources = [ "//third_party/skia/fuzz/oss_fuzz/FuzzJPEGEncoder.cpp" ] + defines = [ "IS_FUZZING_WITH_LIBFUZZER" ] + deps = [ + ":skia_encoder_fuzzer_lib", + ":skia_fuzzer_sources", + ] + } + + fuzzer_test("skia_webp_encoder_fuzzer") { + sources = [ "//third_party/skia/fuzz/oss_fuzz/FuzzWEBPEncoder.cpp" ] + defines = [ "IS_FUZZING_WITH_LIBFUZZER" ] + deps = [ + ":skia_encoder_fuzzer_lib", + ":skia_fuzzer_sources", + ] + } + + fuzzer_test("skia_skjson_fuzzer") { + sources = [ "//third_party/skia/fuzz/oss_fuzz/FuzzJSON.cpp" ] + defines = [ "IS_FUZZING_WITH_LIBFUZZER" ] + deps = [ ":skia_fuzzer_sources" ] + check_includes = false + } + + fuzzer_test("skia_image_decode_incremental_fuzzer") { + sources = [ "//third_party/skia/fuzz/oss_fuzz/FuzzIncrementalImage.cpp" ] + defines = [ "IS_FUZZING_WITH_LIBFUZZER" ] + deps = [ ":skia_fuzzer_sources" ] + } +} diff --git a/tools/fuzzers/OWNERS b/tools/fuzzers/OWNERS new file mode 100644 index 000000000000..b85a0c9be2ea --- /dev/null +++ b/tools/fuzzers/OWNERS @@ -0,0 +1,2 @@ +metzman@chromium.org +kjlubick@chromium.org diff --git a/tools/fuzzers/fuzzer_environment.cc b/tools/fuzzers/fuzzer_environment.cc new file mode 100644 index 000000000000..c6c39a7ec8f5 --- /dev/null +++ b/tools/fuzzers/fuzzer_environment.cc @@ -0,0 +1,19 @@ +// Copyright 2018 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "base/test/test_discardable_memory_allocator.h" + +namespace { + +class Environment { + base::TestDiscardableMemoryAllocator test_memory_allocator_; + + public: + Environment() { + base::DiscardableMemoryAllocator::SetInstance(&test_memory_allocator_); + } +}; + +static Environment env; +} // namespace