blob: 646f22f3ce3b63df2f88680b1024d2b9539b6bcf [file] [log] [blame]
// 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.
import './pdf-shared.css.js';
import {assert} from 'chrome://resources/js/assert_ts.js';
import {PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
import {ChangePageOrigin} from './viewer-bookmark.js';
import {getTemplate} from './viewer-thumbnail.html.js';
// The maximum widths of thumbnails for each layout (px).
// These constants should be kept in sync with `kMaxWidthPortraitPx` and
// `kMaxWidthLandscapePx` in pdf/thumbnail.cc.
const PORTRAIT_WIDTH: number = 108;
const LANDSCAPE_WIDTH: number = 140;
export const PAINTED_ATTRIBUTE: string = 'painted';
export interface ViewerThumbnailElement {
$: {
thumbnail: HTMLElement,
};
}
export class ViewerThumbnailElement extends PolymerElement {
static get is() {
return 'viewer-thumbnail';
}
static get template() {
return getTemplate();
}
static get properties() {
return {
clockwiseRotations: {
type: Number,
value: 0,
observer: 'clockwiseRotationsChanged_',
},
isActive: {
type: Boolean,
observer: 'isActiveChanged_',
reflectToAttribute: true,
},
pageNumber: Number,
};
}
clockwiseRotations: number;
isActive: boolean;
pageNumber: number;
set image(imageData: ImageData) {
let canvas = this.getCanvas_();
if (!canvas) {
canvas = document.createElement('canvas');
// Prevent copying or saving of the thumbnail image in case the document
// has restricted access rights.
canvas.oncontextmenu = e => e.preventDefault();
this.$.thumbnail.appendChild(canvas);
}
canvas.width = imageData.width;
canvas.height = imageData.height;
this.styleCanvas_();
const ctx = canvas.getContext('2d')!;
ctx.putImageData(imageData, 0, 0);
}
clearImage() {
if (!this.isPainted()) {
return;
}
// `canvas` can be `null` in tests because `image` is set only in response
// to the plugin.
const canvas = this.getCanvas_();
if (canvas) {
canvas.remove();
}
this.removeAttribute(PAINTED_ATTRIBUTE);
}
getClickTarget(): HTMLElement {
return this.$.thumbnail;
}
private clockwiseRotationsChanged_() {
if (this.getCanvas_()) {
this.styleCanvas_();
}
}
private getCanvas_(): HTMLCanvasElement|null {
return this.shadowRoot!.querySelector('canvas');
}
/**
* Calculates the CSS size of the thumbnail depending on the rotation, the
* dimensions of the image data, and the screen resolution. The plugin
* scales the thumbnail image data by the device to pixel ratio, so that
* scaling must be taken into account on the UI.
*/
private getThumbnailCssSize_(rotated: boolean):
{width: number, height: number} {
const canvas = this.getCanvas_()!;
const isPortrait = canvas.width < canvas.height !== rotated;
const orientedWidth = rotated ? canvas.height : canvas.width;
const orientedHeight = rotated ? canvas.width : canvas.height;
// Try scaling down such that the width of thumbnail is `PORTRAIT_WIDTH` or
// `LANDSCAPE_WIDTH`, but never scale up to retain the resolution of the
// thumbnail.
const cssWidth = Math.min(
isPortrait ? PORTRAIT_WIDTH : LANDSCAPE_WIDTH,
Math.trunc(orientedWidth / window.devicePixelRatio));
const scale = cssWidth / orientedWidth;
const cssHeight = Math.trunc(orientedHeight * scale);
return {width: cssWidth, height: cssHeight};
}
/**
* Focuses and scrolls the element into view.
* The default scroll behavior of focus() acts differently than
* scrollIntoView(), which is called in isActiveChanged_(). This method
* unifies the behavior.
*/
focusAndScroll() {
this.scrollIntoView({block: 'nearest'});
this.focus({preventScroll: true});
}
isPainted(): boolean {
return this.hasAttribute(PAINTED_ATTRIBUTE);
}
setPainted() {
this.toggleAttribute(PAINTED_ATTRIBUTE, true);
}
private isActiveChanged_() {
if (this.isActive) {
this.scrollIntoView({block: 'nearest'});
}
}
private onClick_() {
this.dispatchEvent(new CustomEvent('change-page', {
detail: {page: this.pageNumber - 1, origin: ChangePageOrigin.THUMBNAIL},
bubbles: true,
composed: true,
}));
}
/**
* Sets the canvas CSS size to maintain the resolution of the thumbnail at any
* rotation.
*/
private styleCanvas_() {
assert(this.clockwiseRotations >= 0 && this.clockwiseRotations < 4);
const canvas = this.getCanvas_()!;
const div = this.shadowRoot!.querySelector<HTMLElement>('#thumbnail')!;
const degreesRotated = this.clockwiseRotations * 90;
canvas.style.transform = `rotate(${degreesRotated}deg)`;
// For the purposes of determining the dimensions, a rotation of 180deg is
// not rotated.
const rotated = this.clockwiseRotations % 2 !== 0;
const cssSize = this.getThumbnailCssSize_(rotated);
div.style.width = `${cssSize.width}px`;
div.style.height = `${cssSize.height}px`;
// When rotated, the canvas's height becomes the parent div's width and vice
// versa.
canvas.style.width = `${rotated ? cssSize.height : cssSize.width}px`;
canvas.style.height = `${rotated ? cssSize.width : cssSize.height}px`;
}
}
declare global {
interface HTMLElementTagNameMap {
'viewer-thumbnail': ViewerThumbnailElement;
}
}
customElements.define(ViewerThumbnailElement.is, ViewerThumbnailElement);