// 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 {assert} from 'chrome://resources/js/assert.js';
import {CrLitElement} from 'chrome://resources/lit/v3_0/lit.rollup.js';
import type {PropertyValues} from 'chrome://resources/lit/v3_0/lit.rollup.js';
import {ChangePageOrigin} from './viewer_bookmark.js';
import {getCss} from './viewer_thumbnail.css.js';
import {getHtml} 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 CrLitElement {
static get is() {
return 'viewer-thumbnail';
}
static override get styles() {
return getCss();
}
override render() {
return getHtml.bind(this)();
}
static override get properties() {
return {
clockwiseRotations: {type: Number},
isActive: {
type: Boolean,
reflect: true,
},
pageNumber: {type: Number},
};
}
clockwiseRotations: number = 0;
isActive: boolean = true;
pageNumber: number = 0;
override updated(changedProperties: PropertyValues<this>) {
super.updated(changedProperties);
if (changedProperties.has('clockwiseRotations') && this.getCanvas_()) {
this.styleCanvas_();
}
if (changedProperties.has('isActive') && this.isActive) {
this.scrollIntoView({block: 'nearest'});
}
}
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 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 updated(). 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);
}
protected onClick_() {
this.fire(
'change-page',
{page: this.pageNumber - 1, origin: ChangePageOrigin.THUMBNAIL});
}
/**
* 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);