summaryrefslogtreecommitdiffstats
path: root/chromium/chrome/browser/resources/pdf/elements/viewer-ink-host.js
diff options
context:
space:
mode:
Diffstat (limited to 'chromium/chrome/browser/resources/pdf/elements/viewer-ink-host.js')
-rw-r--r--chromium/chrome/browser/resources/pdf/elements/viewer-ink-host.js332
1 files changed, 332 insertions, 0 deletions
diff --git a/chromium/chrome/browser/resources/pdf/elements/viewer-ink-host.js b/chromium/chrome/browser/resources/pdf/elements/viewer-ink-host.js
new file mode 100644
index 00000000000..285edf8a35d
--- /dev/null
+++ b/chromium/chrome/browser/resources/pdf/elements/viewer-ink-host.js
@@ -0,0 +1,332 @@
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/** @enum {string} */
+const State = {
+ LOADING: 'loading',
+ ACTIVE: 'active',
+ IDLE: 'idle'
+};
+
+const BACKGROUND_COLOR = '#525659';
+
+/**
+ * Hosts the Ink component which is responsible for both PDF rendering and
+ * annotation when in annotation mode.
+ */
+Polymer({
+ is: 'viewer-ink-host',
+
+ /** @private {InkAPI} */
+ ink_: null,
+
+ /** @private {?string} */
+ fileName_: null,
+
+ /** @private {ArrayBuffer} */
+ buffer_: null,
+
+ /** @private {State} */
+ state_: State.IDLE,
+
+ /** @private {PointerEvent} */
+ activePointer_: null,
+
+ /** @private {?number} */
+ lastZoom_: null,
+
+ /**
+ * Used to conditionally allow a 'touchstart' event to cause
+ * a gesture. If we receive a 'touchstart' with this timestamp
+ * we will skip calling `preventDefault()`.
+ * @private {?number}
+ */
+ allowTouchStartTimeStamp_: null,
+
+ /** @private {boolean} */
+ penMode_: false,
+
+ /** @type {?Viewport} */
+ viewport: null,
+
+ /** @type {?AnnotationTool} */
+ tool_: null,
+
+ /**
+ * Whether we should suppress pointer events due to a gesture,
+ * eg. pinch-zoom.
+ *
+ * @private {boolean}
+ */
+ pointerGesture_: false,
+
+ listeners: {
+ pointerdown: 'onPointerDown_',
+ pointerup: 'onPointerUpOrCancel_',
+ pointermove: 'onPointerMove_',
+ pointercancel: 'onPointerUpOrCancel_',
+ pointerleave: 'onPointerLeave_',
+ touchstart: 'onTouchStart_',
+ },
+
+ /** Turns off pen mode if it is active. */
+ resetPenMode() {
+ this.penMode_ = false;
+ },
+
+ /** @param {AnnotationTool} tool */
+ setAnnotationTool(tool) {
+ this.tool_ = tool;
+ if (this.state_ == State.ACTIVE) {
+ this.ink_.setAnnotationTool(tool);
+ }
+ },
+
+ /** @param {PointerEvent} e */
+ isActivePointer_: function(e) {
+ return this.activePointer_ && this.activePointer_.pointerId == e.pointerId;
+ },
+
+ /**
+ * Dispatches a pointer event to Ink.
+ *
+ * @param {PointerEvent} e
+ */
+ dispatchPointerEvent_: function(e) {
+ // TODO(dstockwell) come up with a solution to propagate e.timeStamp.
+ this.ink_.dispatchPointerEvent(e.type, {
+ pointerId: e.pointerId,
+ pointerType: e.pointerType,
+ clientX: e.clientX,
+ clientY: e.clientY,
+ pressure: e.pressure,
+ buttons: e.buttons,
+ });
+ },
+
+ /** @param {TouchEvent} e */
+ onTouchStart_: function(e) {
+ if (e.timeStamp !== this.allowTouchStartTimeStamp_) {
+ e.preventDefault();
+ }
+ this.allowTouchStartTimeStamp_ = null;
+ },
+
+ /** @param {PointerEvent} e */
+ onPointerDown_: function(e) {
+ if (e.pointerType == 'mouse' && e.buttons != 1 || this.pointerGesture_) {
+ return;
+ }
+
+ if (e.pointerType == 'pen') {
+ this.penMode_ = true;
+ }
+
+ if (this.activePointer_) {
+ if (this.activePointer_.pointerType == 'touch' &&
+ e.pointerType == 'touch') {
+ // A multi-touch gesture has started with the active pointer. Cancel
+ // the active pointer and suppress further events until it is released.
+ this.pointerGesture_ = true;
+ this.ink_.dispatchPointerEvent('pointercancel', {
+ pointerId: this.activePointer_.pointerId,
+ pointerType: this.activePointer_.pointerType,
+ });
+ }
+ return;
+ }
+
+ if (!this.viewport.isPointInsidePage({x: e.clientX, y: e.clientY}) &&
+ (e.pointerType == 'touch' || e.pointerType == 'pen')) {
+ // If a touch or pen is outside the page, we allow pan gestures to start.
+ this.allowTouchStartTimeStamp_ = e.timeStamp;
+ return;
+ }
+
+ if (e.pointerType == 'touch' && this.penMode_) {
+ // If we see a touch after having seen a pen, we allow touches to start
+ // pan gestures anywhere and suppress all touches from drawing.
+ this.allowTouchStartTimeStamp_ = e.timeStamp;
+ return;
+ }
+
+ this.activePointer_ = e;
+ this.dispatchPointerEvent_(e);
+ },
+
+ /** @param {PointerEvent} e */
+ onPointerLeave_: function(e) {
+ if (e.pointerType != 'mouse' || !this.isActivePointer_(e)) {
+ return;
+ }
+ this.onPointerUpOrCancel_(new PointerEvent('pointerup', e));
+ },
+
+ /** @param {PointerEvent} e */
+ onPointerUpOrCancel_: function(e) {
+ if (!this.isActivePointer_(e)) {
+ return;
+ }
+ this.activePointer_ = null;
+ if (!this.pointerGesture_) {
+ this.dispatchPointerEvent_(e);
+ // If the stroke was not cancelled (type == pointercanel),
+ // notify about mutation and record metrics.
+ if (e.type == 'pointerup') {
+ this.dispatchEvent(new CustomEvent('stroke-added'));
+ if (e.pointerType == 'mouse') {
+ PDFMetrics.record(PDFMetrics.UserAction.ANNOTATE_STROKE_DEVICE_MOUSE);
+ } else if (e.pointerType == 'pen') {
+ PDFMetrics.record(PDFMetrics.UserAction.ANNOTATE_STROKE_DEVICE_PEN);
+ } else if (e.pointerType == 'touch') {
+ PDFMetrics.record(PDFMetrics.UserAction.ANNOTATE_STROKE_DEVICE_TOUCH);
+ }
+ if (this.tool_.tool == 'eraser') {
+ PDFMetrics.record(PDFMetrics.UserAction.ANNOTATE_STROKE_TOOL_ERASER);
+ } else if (this.tool_.tool == 'pen') {
+ PDFMetrics.record(PDFMetrics.UserAction.ANNOTATE_STROKE_TOOL_PEN);
+ } else if (this.tool_.tool == 'highlighter') {
+ PDFMetrics.record(
+ PDFMetrics.UserAction.ANNOTATE_STROKE_TOOL_HIGHLIGHTER);
+ }
+ }
+ }
+ this.pointerGesture_ = false;
+ },
+
+ /** @param {PointerEvent} e */
+ onPointerMove_: function(e) {
+ if (!this.isActivePointer_(e) || this.pointerGesture_) {
+ return;
+ }
+
+ let events = e.getCoalescedEvents();
+ if (events.length == 0) {
+ events = [e];
+ }
+ for (const event of events) {
+ this.dispatchPointerEvent_(event);
+ }
+ },
+
+ /**
+ * Begins annotation mode with the document represented by `data`.
+ * When the return value resolves the Ink component will be ready
+ * to render immediately.
+ *
+ * @param {string} fileName The name of the PDF file.
+ * @param {!ArrayBuffer} data The contents of the PDF document.
+ * @return {!Promise} void value.
+ */
+ load: async function(fileName, data) {
+ this.fileName_ = fileName;
+ this.state_ = State.LOADING;
+ this.$.frame.src = 'ink/index.html';
+ await new Promise(resolve => this.$.frame.onload = resolve);
+ this.ink_ = await this.$.frame.contentWindow.initInk();
+ this.ink_.addUndoStateListener(
+ e => this.dispatchEvent(
+ new CustomEvent('undo-state-changed', {detail: e})));
+ this.ink_.setPDF(data);
+ this.state_ = State.ACTIVE;
+ this.viewportChanged();
+ // Wait for the next task to avoid a race where Ink drops the background
+ // color.
+ await new Promise(resolve => setTimeout(resolve));
+ this.ink_.setOutOfBoundsColor(BACKGROUND_COLOR);
+ const spacing = Viewport.PAGE_SHADOW.top + Viewport.PAGE_SHADOW.bottom;
+ this.ink_.setPageSpacing(spacing);
+ this.style.visibility = 'visible';
+ },
+
+ viewportChanged: function() {
+ if (this.state_ != State.ACTIVE) {
+ return;
+ }
+ const viewport = this.viewport;
+ const pos = viewport.position;
+ const size = viewport.size;
+ const zoom = viewport.getZoom();
+ const documentWidth = viewport.getDocumentDimensions().width * zoom;
+ // Adjust for page shadows.
+ const y = pos.y - Viewport.PAGE_SHADOW.top * zoom;
+ let x = pos.x - Viewport.PAGE_SHADOW.left * zoom;
+ // Center the document if the width is smaller than the viewport.
+ if (documentWidth < size.width) {
+ x += (documentWidth - size.width) / 2;
+ }
+ // Invert the Y-axis and convert Pixels to Points.
+ const pixelsToPoints = 72 / 96;
+ const scale = pixelsToPoints / zoom;
+ const camera = {
+ top: (-y) * scale,
+ left: (x) * scale,
+ right: (x + size.width) * scale,
+ bottom: (-y - size.height) * scale,
+ };
+ // Ink doesn't scale the shadow, so we must update it each time the zoom
+ // changes.
+ if (this.lastZoom_ !== zoom) {
+ this.lastZoom_ = zoom;
+ this.updateShadow_(zoom);
+ }
+ this.ink_.setCamera(camera);
+ },
+
+ /** Undo the last edit action. */
+ undo() {
+ this.ink_.undo();
+ },
+
+ /** Redo the last undone edit action. */
+ redo() {
+ this.ink_.redo();
+ },
+
+ /**
+ * @return {!Promise<{fileName: string, dataToSave: ArrayBuffer}>}
+ * The serialized PDF document including any annotations that were made.
+ */
+ saveDocument: async function() {
+ if (this.state_ == State.ACTIVE) {
+ this.buffer_ = await this.ink_.getPDFDestructive().buffer;
+ this.state_ = State.IDLE;
+ }
+ return {
+ fileName: /** @type {string} */ (this.fileName_),
+ dataToSave: this.buffer_,
+ };
+ },
+
+ /** @param {number} zoom */
+ updateShadow_(zoom) {
+ const boxWidth = (50 * zoom) |0;
+ const shadowWidth = (8 * zoom) |0;
+ const width = boxWidth + shadowWidth * 2 + 2;
+ const boxOffset = (width - boxWidth) / 2;
+
+ const canvas = document.createElement('canvas');
+ canvas.width = width;
+ canvas.height = width;
+
+ const ctx = canvas.getContext('2d');
+ ctx.fillStyle = 'black';
+ ctx.shadowColor = 'black';
+ ctx.shadowBlur = shadowWidth;
+ ctx.fillRect(boxOffset, boxOffset, boxWidth, boxWidth);
+ ctx.shadowBlur = 0;
+
+ // 9-piece markers
+ for (let i = 0; i < 4; i++) {
+ ctx.fillStyle = 'white';
+ ctx.fillRect(0, 0, width, 1);
+ ctx.fillStyle = 'black';
+ ctx.fillRect(shadowWidth + 1, 0, boxWidth, 1);
+ ctx.rotate(0.5 * Math.PI);
+ ctx.translate(0, -width);
+ }
+
+ this.ink_.setBorderImage(canvas.toDataURL());
+ }
+});