blob: 95d6213a952eb5c0658d1a62e7039bac46af55ff [file] [log] [blame]
// Copyright 2014 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_ts.js';
import {FittingType, NamedDestinationMessageData, Point} from './constants.js';
import {Size} from './viewport.js';
export interface OpenPdfParams {
url?: string;
zoom?: number;
view?: FittingType;
viewPosition?: number;
position?: Point;
page?: number;
}
type GetNamedDestinationCallback = (name: string) =>
Promise<NamedDestinationMessageData>;
// Parses the open pdf parameters passed in the url to set initial viewport
// settings for opening the pdf.
export class OpenPdfParamsParser {
private getNamedDestinationCallback_: GetNamedDestinationCallback;
private viewportDimensions_?: Size;
/**
* @param getNamedDestinationCallback Function called to fetch information for
* a named destination.
*/
constructor(getNamedDestinationCallback: GetNamedDestinationCallback) {
this.getNamedDestinationCallback_ = getNamedDestinationCallback;
}
/**
* Calculate the zoom level needed for making viewport focus on a rectangular
* area in the PDF document.
* @param size The dimensions of the rectangular area to be focused on.
* @return The zoom level needed for focusing on the rectangular area. A zoom
* level of 0 indicates that the zoom level cannot be calculated with the
* given information.
*/
private calculateRectZoomLevel_(size: Size): number {
if (size.height === 0 || size.width === 0) {
return 0;
}
assert(this.viewportDimensions_);
return Math.min(
this.viewportDimensions_.height / size.height,
this.viewportDimensions_.width / size.width);
}
/**
* Parse zoom parameter of open PDF parameters. The PDF should be opened at
* the specified zoom level.
* @return Map with zoom parameters (zoom and position).
*/
private parseZoomParam_(paramValue: string): OpenPdfParams {
const paramValueSplit = paramValue.split(',');
if (paramValueSplit.length !== 1 && paramValueSplit.length !== 3) {
return {};
}
// User scale of 100 means zoom value of 100% i.e. zoom factor of 1.0.
const zoomFactor = parseFloat(paramValueSplit[0]) / 100;
if (Number.isNaN(zoomFactor)) {
return {};
}
// Handle #zoom=scale.
if (paramValueSplit.length === 1) {
return {'zoom': zoomFactor};
}
// Handle #zoom=scale,left,top.
const position = {
x: parseFloat(paramValueSplit[1]),
y: parseFloat(paramValueSplit[2]),
};
return {'position': position, 'zoom': zoomFactor};
}
/**
* Parse view parameter of open PDF parameters. The PDF should be opened at
* the specified fitting type mode and position.
* @return Map with view parameters (view and viewPosition).
*/
private parseViewParam_(paramValue: string): OpenPdfParams {
const viewModeComponents = paramValue.toLowerCase().split(',');
if (viewModeComponents.length < 1) {
return {};
}
const params: OpenPdfParams = {};
const viewMode = viewModeComponents[0];
let acceptsPositionParam;
if (viewMode === 'fit') {
params['view'] = FittingType.FIT_TO_PAGE;
acceptsPositionParam = false;
} else if (viewMode === 'fith') {
params['view'] = FittingType.FIT_TO_WIDTH;
acceptsPositionParam = true;
} else if (viewMode === 'fitv') {
params['view'] = FittingType.FIT_TO_HEIGHT;
acceptsPositionParam = true;
}
if (!acceptsPositionParam || viewModeComponents.length < 2) {
return params;
}
const position = parseFloat(viewModeComponents[1]);
if (!Number.isNaN(position)) {
params['viewPosition'] = position;
}
return params;
}
/**
* Parse view parameters which come from nameddest.
* @return Map with view parameters.
*/
private parseNameddestViewParam_(paramValue: string): OpenPdfParams {
const viewModeComponents = paramValue.toLowerCase().split(',');
const viewMode = viewModeComponents[0];
const params: OpenPdfParams = {};
if (viewMode === 'xyz' && viewModeComponents.length === 4) {
const x = parseFloat(viewModeComponents[1]);
const y = parseFloat(viewModeComponents[2]);
const zoom = parseFloat(viewModeComponents[3]);
// If zoom is originally 0 for the XYZ view, it is guaranteed to be
// transformed into "null" by the backend.
assert(zoom !== 0);
if (!Number.isNaN(zoom)) {
params['zoom'] = zoom;
}
if (!Number.isNaN(x) || !Number.isNaN(y)) {
params['position'] = {x: x, y: y};
}
return params;
}
if (viewMode === 'fitr' && viewModeComponents.length === 5) {
assert(this.viewportDimensions_ !== undefined);
let x1 = parseFloat(viewModeComponents[1]);
let y1 = parseFloat(viewModeComponents[2]);
let x2 = parseFloat(viewModeComponents[3]);
let y2 = parseFloat(viewModeComponents[4]);
if (!Number.isNaN(x1) && !Number.isNaN(y1) && !Number.isNaN(x2) &&
!Number.isNaN(y2)) {
if (x1 > x2) {
[x1, x2] = [x2, x1];
}
if (y1 > y2) {
[y1, y2] = [y2, y1];
}
const rectSize = {width: x2 - x1, height: y2 - y1};
params['position'] = {x: x1, y: y1};
const zoom = this.calculateRectZoomLevel_(rectSize);
if (zoom !== 0) {
params['zoom'] = zoom;
}
}
return params;
}
return this.parseViewParam_(paramValue);
}
/** Parse the parameters encoded in the fragment of a URL. */
private parseUrlParams_(url: string): URLSearchParams {
const hash = new URL(url).hash;
const params = new URLSearchParams(hash.substring(1));
// Handle the case of http://foo.com/bar#NAMEDDEST. This is not
// explicitly mentioned except by example in the Adobe
// "PDF Open Parameters" document.
if (Array.from(params).length === 1) {
const key = Array.from(params.keys())[0];
if (params.get(key) === '') {
params.append('nameddest', key);
params.delete(key);
}
}
return params;
}
/** Store current viewport's dimensions. */
setViewportDimensions(dimensions: Size) {
this.viewportDimensions_ = dimensions;
}
/**
* @param url that needs to be parsed.
* @return Whether the toolbar UI element should be shown.
*/
shouldShowToolbar(url: string): boolean {
const urlParams = this.parseUrlParams_(url);
const navpanes = urlParams.get('navpanes');
const toolbar = urlParams.get('toolbar');
// If navpanes is set to '1', then the toolbar must be shown, regardless of
// the value of toolbar.
return navpanes === '1' || toolbar !== '0';
}
/**
* @param url that needs to be parsed.
* @param sidenavCollapsed the default sidenav state if there are no
* overriding open parameters.
* @return Whether the sidenav UI element should be shown.
*/
shouldShowSidenav(url: string, sidenavCollapsed: boolean): boolean {
const urlParams = this.parseUrlParams_(url);
const navpanes = urlParams.get('navpanes');
const toolbar = urlParams.get('toolbar');
// If there are no relevant open parameters, default to the original value.
if (navpanes === null && toolbar === null) {
return !sidenavCollapsed;
}
return navpanes === '1';
}
/**
* Parse PDF url parameters. These parameters are mentioned in the url
* and specify actions to be performed when opening pdf files.
* See http://www.adobe.com/content/dam/Adobe/en/devnet/acrobat/
* pdfs/pdf_open_parameters.pdf for details.
* @param url that needs to be parsed.
*/
async getViewportFromUrlParams(url: string): Promise<OpenPdfParams> {
const params: OpenPdfParams = {url};
const urlParams = this.parseUrlParams_(url);
if (urlParams.has('page')) {
// |pageNumber| is 1-based, but goToPage() take a zero-based page index.
const pageNumber = parseInt(urlParams.get('page')!, 10);
if (!Number.isNaN(pageNumber) && pageNumber > 0) {
params['page'] = pageNumber - 1;
}
}
if (urlParams.has('view')) {
Object.assign(params, this.parseViewParam_(urlParams.get('view')!));
}
if (urlParams.has('zoom')) {
Object.assign(params, this.parseZoomParam_(urlParams.get('zoom')!));
}
if (params.page === undefined && urlParams.has('nameddest')) {
const data =
await this.getNamedDestinationCallback_(urlParams.get('nameddest')!);
if (data.pageNumber !== -1) {
params.page = data.pageNumber;
}
if (data.namedDestinationView) {
Object.assign(
params, this.parseNameddestViewParam_(data.namedDestinationView));
}
return params;
}
return Promise.resolve(params);
}
}