2019-12-06 11:33:24 -08:00
|
|
|
/**
|
|
|
|
* Copyright 2019 Google Inc. All rights reserved.
|
|
|
|
* Modifications copyright (c) Microsoft Corporation.
|
|
|
|
*
|
|
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
* you may not use this file except in compliance with the License.
|
|
|
|
* You may obtain a copy of the License at
|
|
|
|
*
|
|
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
*
|
|
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
* See the License for the specific language governing permissions and
|
|
|
|
* limitations under the License.
|
|
|
|
*/
|
|
|
|
|
2022-04-06 13:57:14 -08:00
|
|
|
import type * as dom from './dom';
|
|
|
|
import type { Rect } from '../common/types';
|
2020-08-22 07:07:13 -07:00
|
|
|
import { helper } from './helper';
|
2022-04-06 13:57:14 -08:00
|
|
|
import type { Page } from './page';
|
|
|
|
import type { Frame } from './frames';
|
|
|
|
import type { ParsedSelector } from './common/selectorParser';
|
|
|
|
import type * as types from './types';
|
|
|
|
import type { Progress } from './progress';
|
2022-04-07 12:55:44 -08:00
|
|
|
import { assert } from '../utils';
|
2022-04-07 12:26:50 -08:00
|
|
|
import { MultiMap } from '../utils';
|
2019-12-06 11:33:24 -08:00
|
|
|
|
2022-02-03 22:44:23 -07:00
|
|
|
declare global {
|
|
|
|
interface Window {
|
|
|
|
__cleanupScreenshot?: () => void;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-02-18 22:34:56 -07:00
|
|
|
export type ScreenshotOptions = {
|
|
|
|
type?: 'png' | 'jpeg',
|
|
|
|
quality?: number,
|
|
|
|
omitBackground?: boolean,
|
2022-03-10 18:26:50 -07:00
|
|
|
animations?: 'disabled' | 'allow',
|
2022-02-15 08:05:05 -07:00
|
|
|
mask?: { frame: Frame, selector: string}[],
|
2022-02-18 22:34:56 -07:00
|
|
|
fullPage?: boolean,
|
|
|
|
clip?: Rect,
|
2022-04-01 12:28:40 -07:00
|
|
|
scale?: 'css' | 'device',
|
2022-03-10 17:54:36 -07:00
|
|
|
fonts?: 'ready' | 'nowait',
|
2022-03-29 18:48:13 -06:00
|
|
|
caret?: 'hide' | 'initial',
|
2022-02-15 08:05:05 -07:00
|
|
|
};
|
|
|
|
|
2019-12-06 11:33:24 -08:00
|
|
|
export class Screenshotter {
|
|
|
|
private _queue = new TaskQueue();
|
|
|
|
private _page: Page;
|
|
|
|
|
2019-12-11 12:36:42 -08:00
|
|
|
constructor(page: Page) {
|
2019-12-06 11:33:24 -08:00
|
|
|
this._page = page;
|
2020-06-18 10:52:08 -07:00
|
|
|
this._queue = new TaskQueue();
|
2019-12-06 11:33:24 -08:00
|
|
|
}
|
|
|
|
|
2021-04-07 07:01:38 +08:00
|
|
|
private async _originalViewportSize(progress: Progress): Promise<{ viewportSize: types.Size, originalViewportSize: types.Size | null }> {
|
2020-02-10 09:15:15 -08:00
|
|
|
const originalViewportSize = this._page.viewportSize();
|
|
|
|
let viewportSize = originalViewportSize;
|
2021-04-07 07:01:38 +08:00
|
|
|
if (!viewportSize)
|
2021-04-29 14:53:53 -07:00
|
|
|
viewportSize = await this._page.mainFrame().waitForFunctionValueInUtility(progress, () => ({ width: window.innerWidth, height: window.innerHeight }));
|
2020-02-10 09:15:15 -08:00
|
|
|
return { viewportSize, originalViewportSize };
|
|
|
|
}
|
|
|
|
|
2021-04-07 07:01:38 +08:00
|
|
|
private async _fullPageSize(progress: Progress): Promise<types.Size> {
|
2021-04-29 14:53:53 -07:00
|
|
|
const fullPageSize = await this._page.mainFrame().waitForFunctionValueInUtility(progress, () => {
|
2020-03-03 16:09:32 -08:00
|
|
|
if (!document.body || !document.documentElement)
|
|
|
|
return null;
|
|
|
|
return {
|
|
|
|
width: Math.max(
|
|
|
|
document.body.scrollWidth, document.documentElement.scrollWidth,
|
|
|
|
document.body.offsetWidth, document.documentElement.offsetWidth,
|
|
|
|
document.body.clientWidth, document.documentElement.clientWidth
|
|
|
|
),
|
|
|
|
height: Math.max(
|
|
|
|
document.body.scrollHeight, document.documentElement.scrollHeight,
|
|
|
|
document.body.offsetHeight, document.documentElement.offsetHeight,
|
|
|
|
document.body.clientHeight, document.documentElement.clientHeight
|
|
|
|
),
|
|
|
|
};
|
|
|
|
});
|
2021-04-07 07:01:38 +08:00
|
|
|
return fullPageSize!;
|
2020-03-03 16:09:32 -08:00
|
|
|
}
|
|
|
|
|
2022-02-18 22:34:56 -07:00
|
|
|
async screenshotPage(progress: Progress, options: ScreenshotOptions): Promise<Buffer> {
|
2020-03-24 13:08:53 -03:00
|
|
|
const format = validateScreenshotOptions(options);
|
2019-12-06 11:33:24 -08:00
|
|
|
return this._queue.postTask(async () => {
|
2022-03-11 23:40:28 -07:00
|
|
|
progress.log('taking page screenshot');
|
2021-11-30 14:11:15 -08:00
|
|
|
const { viewportSize } = await this._originalViewportSize(progress);
|
2022-03-29 18:48:13 -06:00
|
|
|
await this._preparePageForScreenshot(progress, options.caret !== 'initial', options.animations === 'disabled', options.fonts === 'ready');
|
2022-02-09 13:52:11 -07:00
|
|
|
progress.throwIfAborted(); // Avoid restoring after failure - should be done by cleanup.
|
2020-03-03 16:09:32 -08:00
|
|
|
|
|
|
|
if (options.fullPage) {
|
2021-04-07 07:01:38 +08:00
|
|
|
const fullPageSize = await this._fullPageSize(progress);
|
2020-03-03 16:09:32 -08:00
|
|
|
let documentRect = { x: 0, y: 0, width: fullPageSize.width, height: fullPageSize.height };
|
|
|
|
const fitsViewport = fullPageSize.width <= viewportSize.width && fullPageSize.height <= viewportSize.height;
|
|
|
|
if (options.clip)
|
|
|
|
documentRect = trimClipToSize(options.clip, documentRect);
|
2021-11-30 14:11:15 -08:00
|
|
|
const buffer = await this._screenshot(progress, format, documentRect, undefined, fitsViewport, options);
|
2020-06-24 10:16:54 -07:00
|
|
|
progress.throwIfAborted(); // Avoid restoring after failure - should be done by cleanup.
|
2022-02-09 13:52:11 -07:00
|
|
|
await this._restorePageAfterScreenshot();
|
2020-06-24 10:16:54 -07:00
|
|
|
return buffer;
|
2019-12-06 11:33:24 -08:00
|
|
|
}
|
|
|
|
|
2020-03-03 16:09:32 -08:00
|
|
|
const viewportRect = options.clip ? trimClipToSize(options.clip, viewportSize) : { x: 0, y: 0, ...viewportSize };
|
2022-02-09 13:52:11 -07:00
|
|
|
const buffer = await this._screenshot(progress, format, undefined, viewportRect, true, options);
|
|
|
|
progress.throwIfAborted(); // Avoid restoring after failure - should be done by cleanup.
|
|
|
|
await this._restorePageAfterScreenshot();
|
|
|
|
return buffer;
|
2021-04-07 07:01:38 +08:00
|
|
|
});
|
2019-12-06 11:33:24 -08:00
|
|
|
}
|
|
|
|
|
2022-02-18 22:34:56 -07:00
|
|
|
async screenshotElement(progress: Progress, handle: dom.ElementHandle, options: ScreenshotOptions): Promise<Buffer> {
|
2020-03-24 13:08:53 -03:00
|
|
|
const format = validateScreenshotOptions(options);
|
2019-12-06 11:33:24 -08:00
|
|
|
return this._queue.postTask(async () => {
|
2022-03-11 23:40:28 -07:00
|
|
|
progress.log('taking element screenshot');
|
2021-11-30 14:11:15 -08:00
|
|
|
const { viewportSize } = await this._originalViewportSize(progress);
|
2020-03-03 16:09:32 -08:00
|
|
|
|
2022-03-29 18:48:13 -06:00
|
|
|
await this._preparePageForScreenshot(progress, options.caret !== 'initial', options.animations === 'disabled', options.fonts === 'ready');
|
2022-02-09 13:52:11 -07:00
|
|
|
progress.throwIfAborted(); // Do not do extra work.
|
|
|
|
|
2020-06-24 10:16:54 -07:00
|
|
|
await handle._waitAndScrollIntoViewIfNeeded(progress);
|
2021-04-07 07:01:38 +08:00
|
|
|
|
|
|
|
progress.throwIfAborted(); // Do not do extra work.
|
2021-11-30 14:11:15 -08:00
|
|
|
const boundingBox = await handle.boundingBox();
|
2020-03-03 16:09:32 -08:00
|
|
|
assert(boundingBox, 'Node is either not visible or not an HTMLElement');
|
2019-12-06 11:33:24 -08:00
|
|
|
assert(boundingBox.width !== 0, 'Node has 0 width.');
|
|
|
|
assert(boundingBox.height !== 0, 'Node has 0 height.');
|
2020-02-10 09:15:15 -08:00
|
|
|
|
2020-03-03 16:09:32 -08:00
|
|
|
const fitsViewport = boundingBox.width <= viewportSize.width && boundingBox.height <= viewportSize.height;
|
2021-04-07 07:01:38 +08:00
|
|
|
progress.throwIfAborted(); // Avoid extra work.
|
2021-04-29 14:53:53 -07:00
|
|
|
const scrollOffset = await this._page.mainFrame().waitForFunctionValueInUtility(progress, () => ({ x: window.scrollX, y: window.scrollY }));
|
2020-03-03 16:09:32 -08:00
|
|
|
const documentRect = { ...boundingBox };
|
|
|
|
documentRect.x += scrollOffset.x;
|
|
|
|
documentRect.y += scrollOffset.y;
|
2021-11-30 14:11:15 -08:00
|
|
|
const buffer = await this._screenshot(progress, format, helper.enclosingIntRect(documentRect), undefined, fitsViewport, options);
|
2020-06-24 10:16:54 -07:00
|
|
|
progress.throwIfAborted(); // Avoid restoring after failure - should be done by cleanup.
|
2022-02-09 13:52:11 -07:00
|
|
|
await this._restorePageAfterScreenshot();
|
2020-06-24 10:16:54 -07:00
|
|
|
return buffer;
|
2021-04-07 07:01:38 +08:00
|
|
|
});
|
2019-12-06 11:33:24 -08:00
|
|
|
}
|
|
|
|
|
2022-03-29 18:48:13 -06:00
|
|
|
async _preparePageForScreenshot(progress: Progress, hideCaret: boolean, disableAnimations: boolean, waitForFonts: boolean) {
|
2022-03-11 23:40:28 -07:00
|
|
|
if (disableAnimations)
|
|
|
|
progress.log(' disabled all CSS animations');
|
|
|
|
if (waitForFonts)
|
|
|
|
progress.log(' waiting for fonts to load...');
|
2022-02-03 22:44:23 -07:00
|
|
|
await Promise.all(this._page.frames().map(async frame => {
|
2022-03-29 18:48:13 -06:00
|
|
|
await frame.nonStallingEvaluateInExistingContext('(' + (async function(hideCaret: boolean, disableAnimations: boolean, waitForFonts: boolean) {
|
2022-02-03 22:44:23 -07:00
|
|
|
const styleTag = document.createElement('style');
|
2022-03-29 18:48:13 -06:00
|
|
|
if (hideCaret) {
|
|
|
|
styleTag.textContent = `
|
|
|
|
*:not(#playwright-aaaaaaaaaa.playwright-bbbbbbbbbbb.playwright-cccccccccc.playwright-dddddddddd.playwright-eeeeeeeee) {
|
|
|
|
caret-color: transparent !important;
|
|
|
|
}
|
|
|
|
`;
|
|
|
|
document.documentElement.append(styleTag);
|
|
|
|
}
|
2022-02-09 13:52:11 -07:00
|
|
|
const infiniteAnimationsToResume: Set<Animation> = new Set();
|
|
|
|
const cleanupCallbacks: (() => void)[] = [];
|
|
|
|
|
|
|
|
if (disableAnimations) {
|
|
|
|
const collectRoots = (root: Document | ShadowRoot, roots: (Document|ShadowRoot)[] = []): (Document|ShadowRoot)[] => {
|
|
|
|
roots.push(root);
|
|
|
|
const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
|
|
|
|
do {
|
|
|
|
const node = walker.currentNode;
|
|
|
|
const shadowRoot = node instanceof Element ? node.shadowRoot : null;
|
|
|
|
if (shadowRoot)
|
|
|
|
collectRoots(shadowRoot, roots);
|
|
|
|
} while (walker.nextNode());
|
|
|
|
return roots;
|
|
|
|
};
|
|
|
|
const handleAnimations = (root: Document|ShadowRoot): void => {
|
|
|
|
for (const animation of root.getAnimations()) {
|
|
|
|
if (!animation.effect || animation.playbackRate === 0 || infiniteAnimationsToResume.has(animation))
|
|
|
|
continue;
|
|
|
|
const endTime = animation.effect.getComputedTiming().endTime;
|
|
|
|
if (Number.isFinite(endTime)) {
|
|
|
|
try {
|
|
|
|
animation.finish();
|
|
|
|
} catch (e) {
|
|
|
|
// animation.finish() should not throw for
|
|
|
|
// finite animations, but we'd like to be on the
|
|
|
|
// safe side.
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
try {
|
|
|
|
animation.cancel();
|
|
|
|
infiniteAnimationsToResume.add(animation);
|
|
|
|
} catch (e) {
|
|
|
|
// animation.cancel() should not throw for
|
|
|
|
// infinite animations, but we'd like to be on the
|
|
|
|
// safe side.
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
for (const root of collectRoots(document)) {
|
|
|
|
const handleRootAnimations: (() => void) = handleAnimations.bind(null, root);
|
|
|
|
handleRootAnimations();
|
|
|
|
root.addEventListener('transitionrun', handleRootAnimations);
|
|
|
|
root.addEventListener('animationstart', handleRootAnimations);
|
|
|
|
cleanupCallbacks.push(() => {
|
|
|
|
root.removeEventListener('transitionrun', handleRootAnimations);
|
|
|
|
root.removeEventListener('animationstart', handleRootAnimations);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-02-03 22:44:23 -07:00
|
|
|
window.__cleanupScreenshot = () => {
|
|
|
|
styleTag.remove();
|
2022-02-09 13:52:11 -07:00
|
|
|
for (const animation of infiniteAnimationsToResume) {
|
|
|
|
try {
|
|
|
|
animation.play();
|
|
|
|
} catch (e) {
|
|
|
|
// animation.play() should never throw, but
|
|
|
|
// we'd like to be on the safe side.
|
|
|
|
}
|
|
|
|
}
|
|
|
|
for (const cleanupCallback of cleanupCallbacks)
|
|
|
|
cleanupCallback();
|
2022-02-03 22:44:23 -07:00
|
|
|
delete window.__cleanupScreenshot;
|
|
|
|
};
|
2022-03-10 17:54:36 -07:00
|
|
|
|
|
|
|
if (waitForFonts)
|
|
|
|
await document.fonts.ready;
|
2022-03-29 18:48:13 -06:00
|
|
|
}).toString() + `)(${hideCaret}, ${disableAnimations}, ${waitForFonts})`, false, 'utility').catch(() => {});
|
2022-02-03 22:44:23 -07:00
|
|
|
}));
|
2022-03-11 23:40:28 -07:00
|
|
|
if (waitForFonts)
|
|
|
|
progress.log(' fonts in all frames are loaded');
|
2022-02-09 13:52:11 -07:00
|
|
|
progress.cleanupWhenAborted(() => this._restorePageAfterScreenshot());
|
|
|
|
}
|
|
|
|
|
|
|
|
async _restorePageAfterScreenshot() {
|
|
|
|
await Promise.all(this._page.frames().map(async frame => {
|
|
|
|
frame.nonStallingEvaluateInExistingContext('window.__cleanupScreenshot && window.__cleanupScreenshot()', false, 'utility').catch(() => {});
|
|
|
|
}));
|
|
|
|
}
|
|
|
|
|
2022-03-31 15:33:29 -07:00
|
|
|
async _maskElements(progress: Progress, options: ScreenshotOptions): Promise<() => Promise<void>> {
|
|
|
|
const framesToParsedSelectors: MultiMap<Frame, ParsedSelector> = new MultiMap();
|
|
|
|
|
|
|
|
const cleanup = async () => {
|
|
|
|
await Promise.all([...framesToParsedSelectors.keys()].map(async frame => {
|
|
|
|
await frame.hideHighlight();
|
|
|
|
}));
|
|
|
|
};
|
|
|
|
|
2022-03-15 14:13:45 -07:00
|
|
|
if (!options.mask || !options.mask.length)
|
2022-03-31 15:33:29 -07:00
|
|
|
return cleanup;
|
2022-03-15 14:13:45 -07:00
|
|
|
|
2022-02-15 08:05:05 -07:00
|
|
|
await Promise.all((options.mask || []).map(async ({ frame, selector }) => {
|
|
|
|
const pair = await frame.resolveFrameForSelectorNoWait(selector);
|
|
|
|
if (pair)
|
|
|
|
framesToParsedSelectors.set(pair.frame, pair.info.parsed);
|
|
|
|
}));
|
|
|
|
progress.throwIfAborted(); // Avoid extra work.
|
|
|
|
|
|
|
|
await Promise.all([...framesToParsedSelectors.keys()].map(async frame => {
|
|
|
|
await frame.maskSelectors(framesToParsedSelectors.get(frame));
|
|
|
|
}));
|
2022-03-31 15:33:29 -07:00
|
|
|
progress.cleanupWhenAborted(cleanup);
|
|
|
|
return cleanup;
|
2022-02-15 08:05:05 -07:00
|
|
|
}
|
|
|
|
|
2022-03-10 13:07:10 -08:00
|
|
|
private async _screenshot(progress: Progress, format: 'png' | 'jpeg', documentRect: types.Rect | undefined, viewportRect: types.Rect | undefined, fitsViewport: boolean, options: ScreenshotOptions): Promise<Buffer> {
|
2022-02-09 13:52:11 -07:00
|
|
|
if ((options as any).__testHookBeforeScreenshot)
|
|
|
|
await (options as any).__testHookBeforeScreenshot();
|
|
|
|
progress.throwIfAborted(); // Screenshotting is expensive - avoid extra work.
|
|
|
|
const shouldSetDefaultBackground = options.omitBackground && format === 'png';
|
|
|
|
if (shouldSetDefaultBackground) {
|
|
|
|
await this._page._delegate.setBackgroundColor({ r: 0, g: 0, b: 0, a: 0 });
|
|
|
|
progress.cleanupWhenAborted(() => this._page._delegate.setBackgroundColor());
|
|
|
|
}
|
2022-02-03 22:44:23 -07:00
|
|
|
progress.throwIfAborted(); // Avoid extra work.
|
|
|
|
|
2022-03-31 15:33:29 -07:00
|
|
|
const cleanupHighlight = await this._maskElements(progress, options);
|
2022-02-15 08:05:05 -07:00
|
|
|
progress.throwIfAborted(); // Avoid extra work.
|
|
|
|
|
2022-04-01 12:28:40 -07:00
|
|
|
const buffer = await this._page._delegate.takeScreenshot(progress, format, documentRect, viewportRect, options.quality, fitsViewport, options.scale || 'device');
|
2020-06-24 10:16:54 -07:00
|
|
|
progress.throwIfAborted(); // Avoid restoring after failure - should be done by cleanup.
|
2022-02-15 08:05:05 -07:00
|
|
|
|
2022-03-31 15:33:29 -07:00
|
|
|
await cleanupHighlight();
|
2022-02-15 08:05:05 -07:00
|
|
|
progress.throwIfAborted(); // Avoid restoring after failure - should be done by cleanup.
|
|
|
|
|
2019-12-06 11:33:24 -08:00
|
|
|
if (shouldSetDefaultBackground)
|
2019-12-11 12:36:42 -08:00
|
|
|
await this._page._delegate.setBackgroundColor();
|
2020-06-24 10:16:54 -07:00
|
|
|
progress.throwIfAborted(); // Avoid side effects.
|
|
|
|
if ((options as any).__testHookAfterScreenshot)
|
|
|
|
await (options as any).__testHookAfterScreenshot();
|
2019-12-06 11:33:24 -08:00
|
|
|
return buffer;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
class TaskQueue {
|
|
|
|
private _chain: Promise<any>;
|
|
|
|
|
|
|
|
constructor() {
|
|
|
|
this._chain = Promise.resolve();
|
|
|
|
}
|
|
|
|
|
|
|
|
postTask(task: () => any): Promise<any> {
|
|
|
|
const result = this._chain.then(task);
|
|
|
|
this._chain = result.catch(() => {});
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-03-03 16:09:32 -08:00
|
|
|
function trimClipToSize(clip: types.Rect, size: types.Size): types.Rect {
|
|
|
|
const p1 = {
|
|
|
|
x: Math.max(0, Math.min(clip.x, size.width)),
|
|
|
|
y: Math.max(0, Math.min(clip.y, size.height))
|
|
|
|
};
|
|
|
|
const p2 = {
|
|
|
|
x: Math.max(0, Math.min(clip.x + clip.width, size.width)),
|
|
|
|
y: Math.max(0, Math.min(clip.y + clip.height, size.height))
|
|
|
|
};
|
2019-12-06 11:33:24 -08:00
|
|
|
const result = { x: p1.x, y: p1.y, width: p2.x - p1.x, height: p2.y - p1.y };
|
2020-03-03 16:09:32 -08:00
|
|
|
assert(result.width && result.height, 'Clipped area is either empty or outside the resulting image');
|
2019-12-06 11:33:24 -08:00
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
2022-02-18 22:34:56 -07:00
|
|
|
function validateScreenshotOptions(options: ScreenshotOptions): 'png' | 'jpeg' {
|
2019-12-06 11:33:24 -08:00
|
|
|
let format: 'png' | 'jpeg' | null = null;
|
|
|
|
// options.type takes precedence over inferring the type from options.path
|
|
|
|
// because it may be a 0-length file with no extension created beforehand (i.e. as a temp file).
|
|
|
|
if (options.type) {
|
|
|
|
assert(options.type === 'png' || options.type === 'jpeg', 'Unknown options.type value: ' + options.type);
|
|
|
|
format = options.type;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!format)
|
|
|
|
format = 'png';
|
|
|
|
|
2020-12-23 09:53:14 -08:00
|
|
|
if (options.quality !== undefined) {
|
2019-12-06 11:33:24 -08:00
|
|
|
assert(format === 'jpeg', 'options.quality is unsupported for the ' + format + ' screenshots');
|
|
|
|
assert(typeof options.quality === 'number', 'Expected options.quality to be a number but found ' + (typeof options.quality));
|
|
|
|
assert(Number.isInteger(options.quality), 'Expected options.quality to be an integer');
|
|
|
|
assert(options.quality >= 0 && options.quality <= 100, 'Expected options.quality to be between 0 and 100 (inclusive), got ' + options.quality);
|
|
|
|
}
|
|
|
|
if (options.clip) {
|
|
|
|
assert(typeof options.clip.x === 'number', 'Expected options.clip.x to be a number but found ' + (typeof options.clip.x));
|
|
|
|
assert(typeof options.clip.y === 'number', 'Expected options.clip.y to be a number but found ' + (typeof options.clip.y));
|
|
|
|
assert(typeof options.clip.width === 'number', 'Expected options.clip.width to be a number but found ' + (typeof options.clip.width));
|
|
|
|
assert(typeof options.clip.height === 'number', 'Expected options.clip.height to be a number but found ' + (typeof options.clip.height));
|
|
|
|
assert(options.clip.width !== 0, 'Expected options.clip.width not to be 0.');
|
|
|
|
assert(options.clip.height !== 0, 'Expected options.clip.height not to be 0.');
|
|
|
|
}
|
|
|
|
return format;
|
|
|
|
}
|