blob: 981edd54f6a323e65b9fec7ba0667e2d26d583e2 [file]
// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef PDF_PDF_INK_MODULE_H_
#define PDF_PDF_INK_MODULE_H_
#include <map>
#include <memory>
#include <optional>
#include <set>
#include <variant>
#include <vector>
#include "base/containers/flat_set.h"
#include "base/gtest_prod_util.h"
#include "base/memory/raw_ref.h"
#include "base/memory/weak_ptr.h"
#include "base/time/time.h"
#include "base/timer/timer.h"
#include "base/values.h"
#include "pdf/buildflags.h"
#include "pdf/page_orientation.h"
#include "pdf/pdf_ink_annotation_mode.h"
#include "pdf/pdf_ink_brush.h"
#include "pdf/pdf_ink_ids.h"
#include "pdf/pdf_ink_undo_redo_model.h"
#include "pdf/ui/thumbnail.h"
#include "third_party/ink/src/ink/geometry/affine_transform.h"
#include "third_party/ink/src/ink/geometry/partitioned_mesh.h"
#include "third_party/ink/src/ink/rendering/skia/native/skia_renderer.h"
#include "third_party/ink/src/ink/strokes/in_progress_stroke.h"
#include "third_party/ink/src/ink/strokes/input/stroke_input.h"
#include "third_party/ink/src/ink/strokes/input/stroke_input_batch.h"
#include "third_party/ink/src/ink/strokes/stroke.h"
#include "ui/gfx/geometry/point_f.h"
#include "ui/gfx/geometry/transform.h"
static_assert(BUILDFLAG(ENABLE_PDF_INK2), "ENABLE_PDF_INK2 not set to true");
class SkCanvas;
namespace blink {
class WebKeyboardEvent;
class WebInputEvent;
class WebMouseEvent;
class WebPointerProperties;
class WebTouchEvent;
} // namespace blink
namespace chrome_pdf {
class PdfInkModuleClient;
class PdfInkModule {
public:
explicit PdfInkModule(PdfInkModuleClient& client);
PdfInkModule(const PdfInkModule&) = delete;
PdfInkModule& operator=(const PdfInkModule&) = delete;
~PdfInkModule();
bool enabled() const { return mode_ != InkAnnotationMode::kOff; }
// Returns whether the text selection change event should be blocked to
// prevent modifying the clipboard content.
bool ShouldBlockTextSelectionChanged();
// Determines if there are any in-progress inputs to be drawn.
bool HasInputsToDraw() const;
// Draws any in-progress inputs into `canvas`. Must either be in text
// highlighting state or in drawing stroke state with non-empty
// `drawing_stroke_state().inputs`.
void Draw(SkCanvas& canvas);
// Generates a thumbnail of `thumbnail_size` for the page at `page_index`
// using DrawThumbnail(). Sends the result to the WebUI if successful.
// Otherwise, do not send anything to the WebUI.
// `thumbnail_size` must be non-empty.
void GenerateAndSendInkThumbnail(int page_index,
const gfx::Size& thumbnail_size);
// Returns whether the event was handled or not.
bool HandleInputEvent(const blink::WebInputEvent& event);
// Returns whether the message was handled or not.
bool OnMessage(const base::DictValue& message);
// Informs PdfInkModule that the plugin geometry changed.
void OnGeometryChanged();
// For testing only. Returns the current `PdfInkBrush` used to draw strokes,
// or nullptr if there is no brush because `PdfInkModule` is not in the
// drawing state.
const PdfInkBrush* GetPdfInkBrushForTesting() const;
private:
friend class PdfInkModuleStrokeTest;
FRIEND_TEST_ALL_PREFIXES(PdfInkModuleTest, HandleSetAnnotationModeMessage);
// A stroke that has been completed, its ID, and whether it should be drawn
// or not.
struct FinishedStrokeState {
FinishedStrokeState(ink::Stroke stroke, InkStrokeId id);
FinishedStrokeState(const FinishedStrokeState&) = delete;
FinishedStrokeState& operator=(const FinishedStrokeState&) = delete;
FinishedStrokeState(FinishedStrokeState&&) noexcept;
FinishedStrokeState& operator=(FinishedStrokeState&&) noexcept;
~FinishedStrokeState();
// Coordinates for each stroke are stored in a canonical format specified in
// pdf_ink_transform.h.
ink::Stroke stroke;
// A unique ID to identify this stroke.
InkStrokeId id;
bool should_draw = true;
};
// Each page of a document can have many strokes. Each stroke is restricted
// to just one page.
// The elements are stored with IDs in an increasing order.
using PageStrokes = std::vector<FinishedStrokeState>;
// Mapping of a 0-based page index to the strokes for that page.
using DocumentStrokesMap = std::map<int, PageStrokes>;
// A shape that was loaded from a "V2" path from the PDF itself, its ID, and
// whether it should be drawn or not.
struct LoadedV2ShapeState {
LoadedV2ShapeState(ink::PartitionedMesh shape, InkModeledShapeId id);
LoadedV2ShapeState(const LoadedV2ShapeState&) = delete;
LoadedV2ShapeState& operator=(const LoadedV2ShapeState&) = delete;
LoadedV2ShapeState(LoadedV2ShapeState&&) noexcept;
LoadedV2ShapeState& operator=(LoadedV2ShapeState&&) noexcept;
~LoadedV2ShapeState();
// Coordinates for each shape are stored in a canonical format specified in
// pdf_ink_transform.h.
ink::PartitionedMesh shape;
// A unique ID to identify this shape.
InkModeledShapeId id;
bool should_draw = true;
};
// Like PageStrokes, but for shapes created from "V2" paths in the PDF.
using PageV2InkPathShapes = std::vector<LoadedV2ShapeState>;
// Like DocumentStrokesMap, but for PageV2InkPathShapes.
using DocumentV2InkPathShapesMap = std::map<int, PageV2InkPathShapes>;
struct EventDetails {
// The event position. Coordinates match the screen-based position that
// are provided during stroking from `blink::WebMouseEvent` positions.
gfx::PointF position;
// The event time.
base::TimeTicks timestamp;
// The type of tool used to generate the input.
ink::StrokeInput::ToolType tool_type = ink::StrokeInput::ToolType::kUnknown;
};
struct DrawingStrokeState {
DrawingStrokeState();
DrawingStrokeState(const DrawingStrokeState&) = delete;
DrawingStrokeState& operator=(const DrawingStrokeState&) = delete;
~DrawingStrokeState();
// The current brush type to use for drawing strokes.
PdfInkBrush::Type brush_type;
std::optional<base::TimeTicks> start_time;
// The 0-based page index which is currently being stroked.
int page_index = -1;
// Details from the last input. Used after stroking has already started,
// for invalidation and for extrapolating where a stroke crosses the page
// boundary. Also used to compensate for missed events, when an end event
// was consumed by a different view and this is detected afterwards when
// PdfInkModule finally sees input events again.
std::optional<EventDetails> input_last_event;
// The points that make up the current stroke, divided into segments.
// A new segment will be necessary each time the input leaves the page
// during collection and then returns back into the original starting page.
// The coordinates added into each segment are stored in a canonical format
// specified in pdf_ink_transform.h.
std::vector<ink::StrokeInputBatch> inputs;
};
class StrokeIdGenerator {
public:
StrokeIdGenerator();
~StrokeIdGenerator();
// Returns an available ID and advance the next available ID internally.
InkStrokeId GetIdAndAdvance();
void ResetIdTo(InkStrokeId id);
private:
// The next available ID for use in FinishedStrokeState.
InkStrokeId next_stroke_id_ = InkStrokeId(0);
};
struct EraserState {
EraserState();
EraserState(const EraserState&) = delete;
EraserState& operator=(const EraserState&) = delete;
~EraserState();
bool erasing = false;
base::flat_set<int> page_indices_with_stroke_erasures;
base::flat_set<int> page_indices_with_partitioned_mesh_erasures;
// The event position for the last input, similar to what is stored in
// `DrawingStrokeState` for compensating for missed input events.
std::optional<gfx::PointF> input_last_event_position;
// The type of tool used to generate the input.
ink::StrokeInput::ToolType tool_type;
};
struct TextHighlightState {
TextHighlightState();
TextHighlightState(const TextHighlightState&) = delete;
TextHighlightState& operator=(const TextHighlightState&) = delete;
~TextHighlightState();
// Tracks whether the current text highlight has finished highlighting a
// multi-click text selection, but has not yet exited text highlight state.
// For example, the user may click text three times to select the line, but
// may not have performed mouseup nor touchend. The user should still be in
// text highlight state but should be unable to highlight any additional
// text.
bool finished_multi_click = false;
// A mapping of 0-based page indices to a list of strokes on pages that
// represent the user's highlighter text selections. Unlike drawing strokes
// which are limited to one page, text selection may cover multiple pages.
// For example, when the user has the highlighter brush selected, they may
// select text from page A to page B. Strokes will be drawn to cover any
// selected text and stored in the page index of the page they are on.
std::map<int, std::vector<ink::Stroke>> highlight_strokes;
// Details from the last input. Used to compensate for missed events, such
// as a missed move event, or an end event that was consumed by a different
// view and detected afterwards when PdfInkModule finally sees input events
// again. Not wrapped in an `std::optional` because this state is only
// active when the user is actively selecting text. The event time is
// unused.
EventDetails input_last_event;
// Whether the text highlight was initiated by a keyboard event.
bool initiated_by_keyboard = false;
};
// Drawing brush state changes that are pending the completion of an
// in-progress stroke.
struct PendingDrawingBrushState {
SkColor color;
float size;
PdfInkBrush::Type type;
};
// Data used to draw a text highlight stroke. If `first_point` equals
// `second_point`, then `second_point` is not used.
// `brush_size` should cover the stroke on the smaller dimension of the
// highlight rect.
// All values are based on canonical coordinates.
struct TextSelectionHighlightStrokeData {
gfx::PointF first_point;
gfx::PointF second_point;
float brush_size;
};
// The transform to and clip page rect needed to render strokes on screen.
struct TransformAndClipRect {
ink::AffineTransform transform;
SkRect clip_rect;
};
// Event handlers. Returns whether the event was handled or not.
bool OnKeyDown(const blink::WebKeyboardEvent& event);
bool OnMouseDown(const blink::WebMouseEvent& event);
bool OnMouseUp(const blink::WebMouseEvent& event);
bool OnMouseMove(const blink::WebMouseEvent& event);
bool OnTouchStart(const blink::WebTouchEvent& event);
bool OnTouchEnd(const blink::WebTouchEvent& event);
bool OnTouchMove(const blink::WebTouchEvent& event);
// Dedicated handlers for eraser tip events from stylus devices.
bool OnEraserTipTouchStart(const blink::WebTouchEvent& event);
bool OnEraserTipTouchEnd(const blink::WebTouchEvent& event);
bool OnEraserTipTouchMove(const blink::WebTouchEvent& event);
// Helper for event handlers above that deals with potentially missing events.
// Can only be called when is_drawing_stroke() returns true.
void MaybeFinishStrokeForMissingMouseUpEvent();
// Return values have the same semantics as On{Mouse,Touch}*() above.
bool StartStroke(const gfx::PointF& position,
base::TimeTicks timestamp,
ink::StrokeInput::ToolType tool_type,
const blink::WebPointerProperties* properties);
bool ContinueStroke(const gfx::PointF& position,
base::TimeTicks timestamp,
ink::StrokeInput::ToolType tool_type,
const blink::WebPointerProperties* properties);
bool FinishStroke(const gfx::PointF& position,
base::TimeTicks timestamp,
ink::StrokeInput::ToolType tool_type,
const blink::WebPointerProperties* properties);
// Return values have the same semantics as On{Mouse,Touch}*() above.
bool StartEraseStroke(const gfx::PointF& position,
ink::StrokeInput::ToolType tool_type);
bool ContinueEraseStroke(const gfx::PointF& position,
ink::StrokeInput::ToolType tool_type);
bool FinishEraseStroke(const gfx::PointF& position,
ink::StrokeInput::ToolType tool_type);
// Shared code for the Erase methods above.
void EraseHelper(const gfx::PointF& position, int page_index);
// Return values have the same semantics as On{Mouse,Touch}*() above.
bool StartTextHighlight(const gfx::PointF& position,
int click_count,
ink::StrokeInput::ToolType tool_type);
bool ContinueTextHighlight(const gfx::PointF& position);
bool FinishTextHighlight(const gfx::PointF& position,
bool is_multi_click,
ink::StrokeInput::ToolType tool_type);
// Returns a highlighter stroke that matches the position and size of
// `selection_rect`. `selection_rect` is in canonical coordinates.
// On failure, return std::nullopt.
std::optional<ink::Stroke> GetHighlightStrokeFromSelectionRect(
const gfx::RectF& selection_rect);
// Returns the data needed to create a text highlight stroke that covers
// `selection_rect`. `selection_rect` is in canonical coordinates.
TextSelectionHighlightStrokeData GetTextSelectionHighlightStrokeData(
const gfx::RectF& selection_rect);
// Converts PdfInkModuleClient's text selection to strokes and returns a
// mapping of 0-based page indices to a list of those strokes. See comments
// for `TextHighlightState::highlight_strokes`.
std::map<int, std::vector<ink::Stroke>> GetTextSelectionAsStrokes();
// Starts a timer for text selection multi-clicks that, when fired, will
// report text highlight metrics.
void StartTextSelectionMultiClickTimer(ink::StrokeInput::ToolType tool_type);
// Stops the timer from `StartTextSelectionMultiClickTimer()` without
// reporting any metrics.
void StopTextSelectionMultiClickTimer();
// Sets `using_stylus_instead_of_touch_` to true if `tool_type` is
// `ink::StrokeInput::ToolType::kStylus`. Otherwise do nothing.
void MaybeRecordPenInput(ink::StrokeInput::ToolType tool_type);
// Returns true if `using_stylus_instead_of_touch_` is set, and `tool_type` is
// `ink::StrokeInput::ToolType::kTouch`.
bool ShouldIgnoreTouchInput(ink::StrokeInput::ToolType tool_type);
void HandleAnnotationRedoMessage(const base::DictValue& message);
void HandleAnnotationUndoMessage(const base::DictValue& message);
void HandleEditTextAnnotationMessage(const base::DictValue& message);
void HandleFinishTextAnnotationMessage(const base::DictValue& message);
void HandleGetAllTextAnnotationsMessage(const base::DictValue& message);
void HandleGetAnnotationBrushMessage(const base::DictValue& message);
void HandleSetAnnotationBrushMessage(const base::DictValue& message);
void HandleSetAnnotationModeMessage(const base::DictValue& message);
bool is_drawing_stroke() const {
return std::holds_alternative<DrawingStrokeState>(current_tool_state_);
}
bool is_erasing_stroke() const {
return std::holds_alternative<EraserState>(current_tool_state_);
}
bool is_text_highlighting() const {
return std::holds_alternative<TextHighlightState>(current_tool_state_);
}
const DrawingStrokeState& drawing_stroke_state() const {
return std::get<DrawingStrokeState>(current_tool_state_);
}
DrawingStrokeState& drawing_stroke_state() {
return std::get<DrawingStrokeState>(current_tool_state_);
}
const EraserState& erasing_stroke_state() const {
return std::get<EraserState>(current_tool_state_);
}
EraserState& erasing_stroke_state() {
return std::get<EraserState>(current_tool_state_);
}
const TextHighlightState& text_highlight_state() const {
return std::get<TextHighlightState>(current_tool_state_);
}
TextHighlightState& text_highlight_state() {
return std::get<TextHighlightState>(current_tool_state_);
}
// Returns true when the user is using a highlighter over selectable text at
// `position`. Can only be called when is_drawing_stroke() returns true.
//
// - Only returns true when the text highlighting feature is enabled.
bool IsHighlightingTextAtPosition(const gfx::PointF& position) const;
// Returns the current brush. Must be in a drawing stroke state.
PdfInkBrush& GetDrawingBrush();
const PdfInkBrush& GetDrawingBrush() const;
// Returns the brush with type `brush_type`.
const PdfInkBrush& GetBrush(PdfInkBrush::Type brush_type) const;
// Converts `current_tool_state_` into segments of `ink::InProgressStroke`.
// Requires `current_tool_state_` to hold a `DrawingStrokeState`. If there is
// no `DrawingStrokeState`, or the state currently has no inputs, then the
// segments will be empty.
std::vector<ink::InProgressStroke> CreateInProgressStrokeSegmentsFromInputs()
const;
// Wrapper around GetEventToCanonicalTransform(). `page_index` is the page
// that the to-be-transformed position is on. The page must be visible.
gfx::Transform GetEventToCanonicalTransformForPage(int page_index);
// Inverse of GetEventToCanonicalTransformForPage(), for convenience.
gfx::Transform GetCanonicalToEventTransformForPage(int page_index);
// Helper to convert `position` to a canonical position and record it into
// `current_tool_state_` for the indicated `timestamp`, `tool_type`, and
// optional `properties`.
// Can only be called when drawing. Returns whether the operation succeeded or
// not.
bool RecordStrokePosition(const gfx::PointF& position,
base::TimeTicks timestamp,
ink::StrokeInput::ToolType tool_type,
const blink::WebPointerProperties* properties);
void ApplyUndoRedoCommands(const PdfInkUndoRedoModel::Commands& commands);
void ApplyUndoRedoCommandsHelper(std::set<PdfInkUndoRedoModel::IdType> ids,
bool should_draw);
void ApplyUndoRedoDiscards(
const PdfInkUndoRedoModel::DiscardedDrawCommands& discards);
// Sets the cursor to a drawing/erasing brush cursor when necessary.
void MaybeSetCursor();
// Handles setting the cursor only for mousemove events at `position`. This
// differs from `MaybeSetCursor()` in that it may also set the cursor to an
// I-beam for text highlighting.
void MaybeSetCursorOnMouseMove(const gfx::PointF& position);
// Returns whether the drawing brush was set or not.
bool MaybeSetDrawingBrush();
void DrawStrokeInRenderer(ink::SkiaRenderer& skia_renderer,
SkCanvas& canvas,
int page_index,
const ink::Stroke& stroke);
void DrawInProgressStrokeInRenderer(ink::SkiaRenderer& skia_renderer,
SkCanvas& canvas,
int page_index,
const ink::InProgressStroke& stroke);
// Returns the transform and the clip page rect needed to render strokes on
// page `page_index`.
TransformAndClipRect GetTransformAndClipRect(int page_index);
// Helper that calls GenerateAndSendInkThumbnail() without needing to specify
// the thumbnail size. This helper determines the size by asking
// PdfInkModuleClient.
void GenerateAndSendInkThumbnailInternal(int page_index);
// Draws `strokes_` for `page_index` into `canvas`. Here, `canvas` only covers
// the region for the page at `page_index`, so this only draws strokes for
// that page, regardless of page visibility.
bool DrawThumbnail(SkCanvas& canvas, int page_index);
// Updates the page indices in `ink_updates` using
// GenerateAndSendInkThumbnailInternal(), and updates the page indices in
// `pdf_updates` using PdfInkModuleClient::RequestThumbnail().
void RequestThumbnailUpdates(const base::flat_set<int>& ink_updates,
const base::flat_set<int>& pdf_updates);
// Handles the callback for PDF thumbnail generation requests. Sends
// `thumbnail` to the WebUI.
void OnGotThumbnail(int page_index, Thumbnail thumbnail);
const raw_ref<PdfInkModuleClient> client_;
InkAnnotationMode mode_ = InkAnnotationMode::kOff;
bool using_stylus_instead_of_touch_ = false;
bool loaded_data_from_pdf_ = false;
// Shapes loaded from the PDF.
DocumentV2InkPathShapesMap loaded_v2_shapes_;
// Generates IDs for use in FinishedStrokeState and PdfInkUndoRedoModel.
StrokeIdGenerator stroke_id_generator_;
// Store a PdfInkBrush for each brush type so that the brush parameters are
// saved when swapping between brushes. The PdfInkBrushes should not be
// modified in the middle of an in-progress stroke.
PdfInkBrush highlighter_brush_;
PdfInkBrush pen_brush_;
// The parameters that are to be applied to the drawing brushes when a new
// stroke is started. These can be modified at any time, including in the
// middle of an in-progress stroke.
std::optional<PendingDrawingBrushState> pending_drawing_brush_state_;
// The state of the current tool that is in use.
std::variant<DrawingStrokeState, EraserState, TextHighlightState>
current_tool_state_;
// Brush type to restore after eraser tip interaction ends.
std::optional<PdfInkBrush::Type> saved_brush_type_for_eraser_tip_;
// The state of the strokes that have been completed.
DocumentStrokesMap strokes_;
PdfInkUndoRedoModel undo_redo_model_;
// A timer used for reporting metrics during multi-click text selection.
base::OneShotTimer text_selection_click_timer_;
base::WeakPtrFactory<PdfInkModule> weak_factory_{this};
};
} // namespace chrome_pdf
#endif // PDF_PDF_INK_MODULE_H_