Screenshot.cpp 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109
  1. /*
  2. * Copyright (c) 2022, Tim Flynn <trflynn89@serenityos.org>
  3. *
  4. * SPDX-License-Identifier: BSD-2-Clause
  5. */
  6. #include <AK/Optional.h>
  7. #include <LibGfx/Bitmap.h>
  8. #include <LibGfx/Rect.h>
  9. #include <LibWeb/DOM/Document.h>
  10. #include <LibWeb/DOM/ElementFactory.h>
  11. #include <LibWeb/HTML/AnimationFrameCallbackDriver.h>
  12. #include <LibWeb/HTML/BrowsingContext.h>
  13. #include <LibWeb/HTML/HTMLCanvasElement.h>
  14. #include <LibWeb/HTML/TagNames.h>
  15. #include <LibWeb/HTML/TraversableNavigable.h>
  16. #include <LibWeb/HTML/Window.h>
  17. #include <LibWeb/Namespace.h>
  18. #include <LibWeb/Page/Page.h>
  19. #include <LibWeb/Platform/EventLoopPlugin.h>
  20. #include <LibWeb/WebDriver/Error.h>
  21. #include <LibWeb/WebDriver/Screenshot.h>
  22. namespace Web::WebDriver {
  23. // https://w3c.github.io/webdriver/#dfn-encoding-a-canvas-as-base64
  24. static Response encode_canvas_element(HTML::HTMLCanvasElement& canvas)
  25. {
  26. // FIXME: 1. If the canvas element’s bitmap’s origin-clean flag is set to false, return error with error code unable to capture screen.
  27. // 2. If the canvas element’s bitmap has no pixels (i.e. either its horizontal dimension or vertical dimension is zero) then return error with error code unable to capture screen.
  28. if (canvas.bitmap()->width() == 0 || canvas.bitmap()->height() == 0)
  29. return Error::from_code(ErrorCode::UnableToCaptureScreen, "Captured screenshot is empty"sv);
  30. // 3. Let file be a serialization of the canvas element’s bitmap as a file, using "image/png" as an argument.
  31. // 4. Let data url be a data: URL representing file. [RFC2397]
  32. auto data_url = canvas.to_data_url("image/png"sv, {});
  33. // 5. Let index be the index of "," in data url.
  34. auto index = data_url.find_byte_offset(',');
  35. VERIFY(index.has_value());
  36. // 6. Let encoded string be a substring of data url using (index + 1) as the start argument.
  37. auto encoded_string = MUST(data_url.substring_from_byte_offset(*index + 1));
  38. // 7. Return success with data encoded string.
  39. return JsonValue { move(encoded_string) };
  40. }
  41. // Common animation callback steps between:
  42. // https://w3c.github.io/webdriver/#take-screenshot
  43. // https://w3c.github.io/webdriver/#take-element-screenshot
  44. Response capture_element_screenshot(Painter const& painter, Page& page, DOM::Element& element, Gfx::IntRect& rect)
  45. {
  46. Optional<Response> encoded_string_or_error;
  47. // https://w3c.github.io/webdriver/#dfn-draw-a-bounding-box-from-the-framebuffer
  48. auto draw_bounding_box_from_the_framebuffer = [&]() -> ErrorOr<JS::NonnullGCPtr<HTML::HTMLCanvasElement>, WebDriver::Error> {
  49. // 1. If either the initial viewport's width or height is 0 CSS pixels, return error with error code unable to capture screen.
  50. auto viewport_rect = page.top_level_traversable()->viewport_rect();
  51. if (viewport_rect.is_empty())
  52. return Error::from_code(ErrorCode::UnableToCaptureScreen, "Viewport is empty"sv);
  53. auto viewport_device_rect = page.enclosing_device_rect(viewport_rect).to_type<int>();
  54. // 2. Let paint width be the initial viewport's width – min(rectangle x coordinate, rectangle x coordinate + rectangle width dimension).
  55. auto paint_width = viewport_device_rect.width() - min(rect.x(), rect.x() + rect.width());
  56. // 3. Let paint height be the initial viewport's height – min(rectangle y coordinate, rectangle y coordinate + rectangle height dimension).
  57. auto paint_height = viewport_device_rect.height() - min(rect.y(), rect.y() + rect.height());
  58. // 4. Let canvas be a new canvas element, and set its width and height to paint width and paint height, respectively.
  59. auto canvas_element = DOM::create_element(element.document(), HTML::TagNames::canvas, Namespace::HTML).release_value_but_fixme_should_propagate_errors();
  60. auto& canvas = verify_cast<HTML::HTMLCanvasElement>(*canvas_element);
  61. // FIXME: Handle DevicePixelRatio in HiDPI mode.
  62. MUST(canvas.set_width(paint_width));
  63. MUST(canvas.set_height(paint_height));
  64. // FIXME: 5. Let context, a canvas context mode, be the result of invoking the 2D context creation algorithm given canvas as the target.
  65. if (!canvas.create_bitmap(paint_width, paint_height))
  66. return Error::from_code(ErrorCode::UnableToCaptureScreen, "Unable to create a screenshot bitmap"sv);
  67. // 6. Complete implementation specific steps equivalent to drawing the region of the framebuffer specified by the following coordinates onto context:
  68. // - X coordinate: rectangle x coordinate
  69. // - Y coordinate: rectangle y coordinate
  70. // - Width: paint width
  71. // - Height: paint height
  72. Gfx::IntRect paint_rect { rect.x(), rect.y(), paint_width, paint_height };
  73. painter(paint_rect, *canvas.bitmap());
  74. // 7. Return success with canvas.
  75. return canvas;
  76. };
  77. (void)element.document().window()->animation_frame_callback_driver().add([&](auto) {
  78. auto canvas_or_error = draw_bounding_box_from_the_framebuffer();
  79. if (canvas_or_error.is_error()) {
  80. encoded_string_or_error = canvas_or_error.release_error();
  81. return;
  82. }
  83. encoded_string_or_error = encode_canvas_element(canvas_or_error.release_value());
  84. });
  85. Platform::EventLoopPlugin::the().spin_until([&]() { return encoded_string_or_error.has_value(); });
  86. return encoded_string_or_error.release_value();
  87. }
  88. }