0

[PDF Ink Signatures] Transform PdfInkModule invalidation areas

The invalidation area provided by PdfInkModule to its client is supposed
to be in screen coordinates.  This is done correctly for in-progress
strokes, but completed/erased strokes have been providing areas whose
values are in canonical coordinates.

This issue becomes noticeable when drawing of completed strokes happens
automatically because they are part of the in-memory PDF document, and
no longer explicitly drawn by PdfInkModule.

Add a new helper to convert a finished stroke's envelope into the screen
coordinates that are needed by PdfInkModule's client.

Bug: 375445386
Change-Id: Ie0276db5225b4b9b0e1b7c79e0792d1437105d04
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/5960211
Code-Coverage: findit-for-me@appspot.gserviceaccount.com <findit-for-me@appspot.gserviceaccount.com>
Reviewed-by: Lei Zhang <thestig@chromium.org>
Commit-Queue: Alan Screen <awscreen@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1375995}
This commit is contained in:
Alan Screen
2024-10-31 00:46:33 +00:00
committed by Chromium LUCI CQ
parent e9fbad1437
commit 4773789b25
7 changed files with 244 additions and 24 deletions

@ -7,6 +7,7 @@
#include "base/time/time.h"
#include "third_party/ink/src/ink/brush/brush.h"
#include "third_party/ink/src/ink/color/color.h"
#include "third_party/ink/src/ink/geometry/point.h"
#include "third_party/ink/src/ink/strokes/input/stroke_input.h"
#include "ui/gfx/geometry/point_f.h"
@ -17,7 +18,7 @@ ink::StrokeInput CreateInkStrokeInput(ink::StrokeInput::ToolType tool_type,
base::TimeDelta elapsed_time) {
return {
.tool_type = tool_type,
.position = {position.x(), position.y()},
.position = InkPointFromGfxPoint(position),
.elapsed_time = ink::Duration32::Seconds(
static_cast<float>(elapsed_time.InSecondsF())),
};
@ -29,4 +30,8 @@ SkColor GetSkColorFromInkBrush(const ink::Brush& brush) {
return SkColorSetARGB(rgba.a, rgba.r, rgba.g, rgba.b);
}
ink::Point InkPointFromGfxPoint(const gfx::PointF& point) {
return ink::Point(point.x(), point.y());
}
} // namespace chrome_pdf

@ -6,6 +6,7 @@
#define PDF_PDF_INK_CONVERSIONS_H_
#include "base/time/time.h"
#include "third_party/ink/src/ink/geometry/point.h"
#include "third_party/ink/src/ink/strokes/input/stroke_input.h"
#include "third_party/skia/include/core/SkColor.h"
@ -25,6 +26,8 @@ ink::StrokeInput CreateInkStrokeInput(ink::StrokeInput::ToolType tool_type,
SkColor GetSkColorFromInkBrush(const ink::Brush& brush);
ink::Point InkPointFromGfxPoint(const gfx::PointF& point);
} // namespace chrome_pdf
#endif // PDF_PDF_INK_CONVERSIONS_H_

@ -50,7 +50,6 @@
#include "third_party/skia/include/core/SkColor.h"
#include "ui/gfx/geometry/point_f.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/geometry/rect_conversions.h"
#include "ui/gfx/geometry/rect_f.h"
#include "ui/gfx/geometry/skia_conversions.h"
@ -92,11 +91,6 @@ ink::Rect GetEraserRect(const gfx::PointF& center, int distance_to_center) {
{center.x() + distance_to_center, center.y() + distance_to_center});
}
gfx::Rect InkRectToEnclosingGfxRect(const ink::Rect& rect) {
return gfx::ToEnclosingRect(
gfx::RectF(rect.XMin(), rect.YMin(), rect.Width(), rect.Height()));
}
SkRect GetDrawPageClipRect(const gfx::Rect& content_rect,
const gfx::Vector2dF& origin_offset) {
gfx::RectF clip_rect(content_rect);
@ -582,14 +576,14 @@ bool PdfInkModule::EraseHelper(const gfx::PointF& position, int page_index) {
CHECK(undo_redo_success);
}
const std::optional<ink::Rect>& invalidate_rect =
invalidate_envelope.AsRect();
if (!invalidate_rect.has_value()) {
if (invalidate_envelope.IsEmpty()) {
return false;
}
// If `invalidate_rect` has a value, then something got erased.
client_->Invalidate(InkRectToEnclosingGfxRect(invalidate_rect.value()));
// If `invalidate_envelope` isn't empty, then something got erased.
client_->Invalidate(CanonicalInkEnvelopeToInvalidationScreenRect(
invalidate_envelope, client_->GetOrientation(),
client_->GetPageContentsRect(page_index), client_->GetZoom()));
return true;
}
@ -837,10 +831,9 @@ void PdfInkModule::ApplyUndoRedoCommandsHelper(std::set<InkStrokeId> ids,
ids.erase(id);
}
const std::optional<ink::Rect>& invalidate_rect =
invalidate_envelope.AsRect();
CHECK(invalidate_rect.has_value());
client_->Invalidate(InkRectToEnclosingGfxRect(invalidate_rect.value()));
client_->Invalidate(CanonicalInkEnvelopeToInvalidationScreenRect(
invalidate_envelope, client_->GetOrientation(),
client_->GetPageContentsRect(page_index), client_->GetZoom()));
client_->UpdateThumbnail(page_index);
if (ids.empty()) {

@ -1240,7 +1240,7 @@ TEST_F(PdfInkModuleUndoRedoTest, UndoRedoInvalidationsBasic) {
// `ink::ModeledShape::Bounds()` looks like it is smaller than what would be
// expected given the inputs.
const gfx::Rect kInvalidationAreaUndoRedo(gfx::Point(8.0f, 13.0f),
gfx::Size(24.0f, 6.0f));
gfx::Size(25.0f, 7.0f));
EXPECT_THAT(
client().invalidations(),
ElementsAre(kInvalidationAreaMouseDown, kInvalidationAreaMouseMove,
@ -1277,11 +1277,8 @@ TEST_F(PdfInkModuleUndoRedoTest, UndoRedoInvalidationsScaledRotated90) {
// entire stroke makes sense. What is being returned by
// `ink::ModeledShape::Bounds()` looks like it is smaller than what would be
// expected given the inputs.
// TODO(crbug.com/375445386): Invalidation area for undo/redo should be
// similar to location & combined size of the invalidation areas that happened
// while stroking.
const gfx::Rect kInvalidationAreaUndoRedo(gfx::Point(6.0f, 43.0f),
gfx::Size(4.0f, 13.0f));
const gfx::Rect kInvalidationAreaUndoRedo(gfx::Point(7.0f, 12.0f),
gfx::Size(27.0f, 9.0f));
EXPECT_THAT(
client().invalidations(),
ElementsAre(kInvalidationAreaMouseDown, kInvalidationAreaMouseMove,

@ -4,12 +4,62 @@
#include "pdf/pdf_ink_transform.h"
#include <algorithm>
#include <optional>
#include "base/check_op.h"
#include "base/notreached.h"
#include "third_party/ink/src/ink/geometry/envelope.h"
#include "third_party/ink/src/ink/geometry/rect.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/geometry/rect_conversions.h"
#include "ui/gfx/geometry/rect_f.h"
#include "ui/gfx/geometry/vector2d_f.h"
namespace chrome_pdf {
namespace {
// Performs what is mostly an inverse operation of
// `EventPositionToCanonicalPosition()`, to convert from canonical coordinates
// to screen coordinates. This is not a strict inverse because it
// intentionally does not adjust for `page_content_rect` offset from origin.
// This is due to its usage for invalidation calculations, where clients account
// for that offset separately.
gfx::PointF CanonicalPositionToScreenPosition(
const gfx::PointF& canonical_position,
PageOrientation orientation,
const gfx::Rect& page_content_rect,
float scale_factor) {
CHECK_GT(scale_factor, 0.0f);
CHECK(!page_content_rect.IsEmpty());
gfx::PointF screen_position = canonical_position;
screen_position.Scale(scale_factor);
switch (orientation) {
case PageOrientation::kOriginal:
// No further modification needed.
break;
case PageOrientation::kClockwise90:
screen_position.SetPoint(
page_content_rect.width() - screen_position.y() - 1,
screen_position.x());
break;
case PageOrientation::kClockwise180:
screen_position.SetPoint(
page_content_rect.width() - screen_position.x() - 1,
page_content_rect.height() - screen_position.y() - 1);
break;
case PageOrientation::kClockwise270:
screen_position.SetPoint(
screen_position.y(),
page_content_rect.height() - screen_position.x() - 1);
break;
}
return screen_position;
}
} // namespace
gfx::PointF EventPositionToCanonicalPosition(const gfx::PointF& event_position,
PageOrientation orientation,
const gfx::Rect& page_content_rect,
@ -71,4 +121,28 @@ ink::AffineTransform GetInkRenderTransform(
NOTREACHED();
}
gfx::Rect CanonicalInkEnvelopeToInvalidationScreenRect(
const ink::Envelope& envelope,
PageOrientation orientation,
const gfx::Rect& page_content_rect,
float scale_factor) {
const std::optional<ink::Rect>& ink_rect = envelope.AsRect();
CHECK(ink_rect.has_value());
gfx::PointF p1 = CanonicalPositionToScreenPosition(
gfx::PointF(ink_rect->XMin(), ink_rect->YMin()), orientation,
page_content_rect, scale_factor);
gfx::PointF p2 = CanonicalPositionToScreenPosition(
gfx::PointF(ink_rect->XMax(), ink_rect->YMax()), orientation,
page_content_rect, scale_factor);
// Width and height get +1 since both of the points are to be included in the
// area; otherwise it would be an open rectangle on two edges.
float x = std::min(p1.x(), p2.x());
float y = std::min(p1.y(), p2.y());
float w = std::max(p1.x(), p2.x()) - x + 1;
float h = std::max(p1.y(), p2.y()) - y + 1;
return gfx::ToEnclosingRect(gfx::RectF(x, y, w, h));
}
} // namespace chrome_pdf

@ -9,14 +9,18 @@
#include "pdf/page_orientation.h"
#include "third_party/ink/src/ink/geometry/affine_transform.h"
#include "ui/gfx/geometry/point_f.h"
#include "ui/gfx/geometry/rect.h"
static_assert(BUILDFLAG(ENABLE_PDF_INK2), "ENABLE_PDF_INK2 not set to true");
namespace gfx {
class Rect;
class Vector2dF;
} // namespace gfx
namespace ink {
class Envelope;
} // namespace ink
namespace chrome_pdf {
// Converts a screen-based event input position into a page-based CSS pixels
@ -41,7 +45,7 @@ gfx::PointF EventPositionToCanonicalPosition(const gfx::PointF& event_position,
const gfx::Rect& page_content_rect,
float scale_factor);
// Generate the affine transformation for rendering a page's strokes to the
// Generates the affine transformation for rendering a page's strokes to the
// screen, based on the page and its position within the viewport. Parameters
// are the same as for `EventPositionToCanonicalPosition()`, with the addition
// of:
@ -95,6 +99,17 @@ ink::AffineTransform GetInkRenderTransform(
const gfx::Rect& page_content_rect,
float scale_factor);
// Converts `ink::Envelope` to screen coordinates as needed for invalidation.
// Uses the same `orientation`, `page_content_rect`, and `scale_factor`
// parameters as used in `EventPositionToCanonicalPosition()`. This function
// uses them in reverse, to convert canonical coordinates back to screen
// coordinates. The caller must provide a non-empty `envelope`.
gfx::Rect CanonicalInkEnvelopeToInvalidationScreenRect(
const ink::Envelope& envelope,
PageOrientation orientation,
const gfx::Rect& page_content_rect,
float scale_factor);
} // namespace chrome_pdf
#endif // PDF_PDF_INK_TRANSFORM_H_

@ -7,10 +7,12 @@
#include <array>
#include "pdf/page_orientation.h"
#include "pdf/pdf_ink_conversions.h"
#include "pdf/test/pdf_ink_test_helpers.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/ink/src/ink/geometry/affine_transform.h"
#include "third_party/ink/src/ink/geometry/envelope.h"
#include "ui/gfx/geometry/point_f.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/geometry/size.h"
@ -281,4 +283,135 @@ TEST(PdfInkTransformTest, RenderTransformOffsetZoomScrolledClockwise90) {
InkAffineTransformEq(0.0f, -2.0f, 137.0f, 2.0f, 0.0f, -4.0f));
}
TEST(PdfInkTransformTest,
CanonicalInkEnvelopeToInvalidationScreenRectIdentity) {
// Representation of page contents in screen coordinates without scale or
// rotation.
constexpr gfx::Rect kPageContentRect(kPageSizePortrait);
{
// Envelope that covers the entire page contents, in canonical coordinates.
ink::Envelope envelope(InkPointFromGfxPoint(kCanonicalPositionTopLeft));
envelope.Add(InkPointFromGfxPoint(kCanonicalPositionBottomRight));
// Invalidation rectangle for `envelope` should result in the same value as
// the entire page contents.
gfx::Rect screen_rect = CanonicalInkEnvelopeToInvalidationScreenRect(
envelope, PageOrientation::kOriginal, kPageContentRect, kScaleFactor1x);
EXPECT_EQ(screen_rect, kPageContentRect);
}
{
// Envelope that covers a portion of page contents, in canonical
// coordinates.
ink::Envelope envelope(InkPointFromGfxPoint({20.0f, 35.0f}));
envelope.Add(InkPointFromGfxPoint({40.0f, 45.0f}));
// Invalidation rectangle for `envelope` should result in save value as
// input.
gfx::Rect screen_rect = CanonicalInkEnvelopeToInvalidationScreenRect(
envelope, PageOrientation::kOriginal, kPageContentRect, kScaleFactor1x);
EXPECT_EQ(screen_rect, gfx::Rect(20.0f, 35.0f, 21.0f, 11.0f));
}
}
TEST(PdfInkTransformTest,
CanonicalInkEnvelopeToInvalidationScreenRectScaledAndRotated90) {
// Scaled and rotated representation of page contents in screen coordinates.
constexpr gfx::Rect kPageContentRect({0, 0}, kPageSizeLandscape2x);
{
// Envelope that covers the entire page contents, in canonical coordinates.
ink::Envelope envelope(InkPointFromGfxPoint(kCanonicalPositionTopLeft));
envelope.Add(InkPointFromGfxPoint(kCanonicalPositionBottomRight +
kCanonicalPositionHalf));
// Invalidation rectangle for `envelope` should be scaled and rotated,
// resulting in the same value as the entire page contents.
gfx::Rect screen_rect = CanonicalInkEnvelopeToInvalidationScreenRect(
envelope, PageOrientation::kClockwise90, kPageContentRect,
kScaleFactor2x);
EXPECT_EQ(screen_rect, kPageContentRect);
}
{
// Envelope that covers a portion of page contents, in canonical
// coordinates.
ink::Envelope envelope(InkPointFromGfxPoint({20.0f, 35.0f}));
envelope.Add(InkPointFromGfxPoint({40.0f, 45.0f}));
// Invalidation rectangle for `envelope` should be scaled and rotated.
gfx::Rect screen_rect = CanonicalInkEnvelopeToInvalidationScreenRect(
envelope, PageOrientation::kClockwise90, kPageContentRect,
kScaleFactor2x);
EXPECT_EQ(screen_rect, gfx::Rect(29.0f, 40.0f, 21.0f, 41.0f));
}
}
TEST(PdfInkTransformTest,
CanonicalInkEnvelopeToInvalidationScreenRectScaledAndRotated180) {
// Scaled and rotated representation of page contents in screen coordinates.
constexpr gfx::Rect kPageContentRect({0, 0}, kPageSizePortrait2x);
{
// Envelope that covers the entire page contents, in canonical coordinates.
ink::Envelope envelope(InkPointFromGfxPoint(kCanonicalPositionTopLeft));
envelope.Add(InkPointFromGfxPoint(kCanonicalPositionBottomRight +
kCanonicalPositionHalf));
// Invalidation rectangle for `envelope` should be scaled and rotated,
// resulting in the same value as the entire page contents.
gfx::Rect screen_rect = CanonicalInkEnvelopeToInvalidationScreenRect(
envelope, PageOrientation::kClockwise180, kPageContentRect,
kScaleFactor2x);
EXPECT_EQ(screen_rect, kPageContentRect);
}
{
// Envelope that covers a portion of page contents, in canonical
// coordinates.
ink::Envelope envelope(InkPointFromGfxPoint({20.0f, 35.0f}));
envelope.Add(InkPointFromGfxPoint({40.0f, 45.0f}));
// Invalidation rectangle for `envelope` should be scaled and rotated.
gfx::Rect screen_rect = CanonicalInkEnvelopeToInvalidationScreenRect(
envelope, PageOrientation::kClockwise180, kPageContentRect,
kScaleFactor2x);
EXPECT_EQ(screen_rect, gfx::Rect(19.0f, 29.0f, 41.0f, 21.0f));
}
}
TEST(PdfInkTransformTest,
CanonicalInkEnvelopeToInvalidationScreenRectScaledAndRotated270) {
// Scaled and rotated representation of page contents in screen coordinates.
constexpr gfx::Rect kPageContentRect({0, 0}, kPageSizeLandscape2x);
{
// Envelope that covers the entire page contents, in canonical coordinates.
ink::Envelope envelope(InkPointFromGfxPoint(kCanonicalPositionTopLeft));
envelope.Add(InkPointFromGfxPoint(kCanonicalPositionBottomRight +
kCanonicalPositionHalf));
// Invalidation rectangle for `envelope` should be scaled and rotated,
// resulting in the same value as the entire page contents.
gfx::Rect screen_rect = CanonicalInkEnvelopeToInvalidationScreenRect(
envelope, PageOrientation::kClockwise270, kPageContentRect,
kScaleFactor2x);
EXPECT_EQ(screen_rect, kPageContentRect);
}
{
// Envelope that covers a portion of page contents, in canonical
// coordinates.
ink::Envelope envelope(InkPointFromGfxPoint({20.0f, 35.0f}));
envelope.Add(InkPointFromGfxPoint({40.0f, 45.0f}));
// Invalidation rectangle for `envelope` should be scaled and rotated.
gfx::Rect screen_rect = CanonicalInkEnvelopeToInvalidationScreenRect(
envelope, PageOrientation::kClockwise270, kPageContentRect,
kScaleFactor2x);
EXPECT_EQ(screen_rect, gfx::Rect(70.0f, 19.0f, 21.0f, 41.0f));
}
}
} // namespace chrome_pdf