/// <reference lib="dom" />
import puppeteer from 'puppeteer';
import type { Browser, Page } from 'puppeteer';
import * as fs from 'fs';
import * as path from 'path';
export interface ScreenshotOptions {
pageId?: string; // Page ID to capture (optional, uses current page if not specified)
nodeId?: string; // Specific node to zoom in on and capture
padding?: number; // Padding around node in pixels (default: 50)
width?: number; // Viewport width (default: 1920)
height?: number; // Viewport height (default: 1080)
scale?: number; // Device scale factor for higher resolution (default: 1)
format?: 'png' | 'jpeg'; // Image format (default: png)
quality?: number; // JPEG quality 0-100 (default: 90)
fullPage?: boolean; // Capture full canvas (default: false)
outputPath?: string; // Where to save (default: ./screenshots/)
}
export interface ScreenshotResult {
success: boolean;
filePath?: string;
base64?: string;
width?: number;
height?: number;
error?: string;
}
export class PaperScreenshot {
private browser: Browser | null = null;
private page: Page | null = null;
private cookies: string;
private debug: boolean;
constructor(cookies: string, debug = false) {
this.cookies = cookies;
this.debug = debug;
}
private log(message: string, data?: any) {
if (this.debug) {
console.error(`[Screenshot] ${message}`, data ? JSON.stringify(data, null, 2) : '');
}
}
private parseCookies(): Array<{ name: string; value: string; domain: string }> {
const cookiePairs = this.cookies.split(';').map(c => c.trim());
return cookiePairs.map(pair => {
const [name, ...valueParts] = pair.split('=');
return {
name: name.trim(),
value: valueParts.join('='),
domain: '.paper.design',
};
}).filter(c => c.name && c.value);
}
async initialize(): Promise<void> {
if (this.browser) return;
this.log('Launching browser...');
this.browser = await puppeteer.launch({
headless: false,
args: [
'--no-sandbox',
'--disable-setuid-sandbox',
'--disable-dev-shm-usage',
'--disable-accelerated-2d-canvas',
'--disable-gpu',
'--window-size=1920,1080',
],
});
this.page = await this.browser.newPage();
// Set cookies for authentication
const cookies = this.parseCookies();
this.log('Setting cookies:', { count: cookies.length });
await this.page.setCookie(...cookies);
// Set viewport
await this.page.setViewport({ width: 1920, height: 1080 });
this.log('Browser initialized');
}
private async zoomToNode(nodeId: string, padding: number = 50): Promise<void> {
if (!this.page) return;
// The node should be selected via URL parameter (node=nodeId)
// But if that doesn't work, we need to select it from the layers panel
this.log('Waiting for page to load with node param...');
// Wait for the page to fully load
await new Promise(r => setTimeout(r, 3000));
// Check if the node is selected by looking for selection indicators
// If not selected via URL, try to find and click the node in the layers panel
// Try to find the node in the layers panel by its data attribute or aria label
this.log('Looking for node in layers panel: ' + nodeId);
try {
// Look for a layer item that contains the node ID in its data attributes or href
const layerItem = await this.page.$(`[data-node-id="${nodeId}"], [href*="${nodeId}"], [data-id="${nodeId}"]`);
if (layerItem) {
this.log('Found node in layers panel, clicking it');
await layerItem.click();
await new Promise(r => setTimeout(r, 500));
} else {
this.log('Node not found by data attribute, trying text search in layers');
// The layers panel might show the node - try to find any clickable element
// that might select our node
}
} catch (e) {
this.log('Could not find node in layers panel: ' + e);
}
// Press Escape to ensure no dialogs are open
await this.page.keyboard.press('Escape');
await new Promise(r => setTimeout(r, 300));
// Click on canvas to ensure it has focus
this.log('Clicking on canvas to focus');
await this.page.mouse.click(800, 500);
await new Promise(r => setTimeout(r, 500));
// Use Shift+2 to zoom to the selected node
this.log('Pressing Shift+2 to zoom to selection');
await this.page.keyboard.down('Shift');
await this.page.keyboard.press('Digit2');
await this.page.keyboard.up('Shift');
// Wait for zoom animation to complete
await new Promise(r => setTimeout(r, 2500));
}
async takeScreenshot(
documentId: string,
options: ScreenshotOptions = {}
): Promise<ScreenshotResult> {
try {
await this.initialize();
if (!this.page) throw new Error('Page not initialized');
const {
pageId,
nodeId,
width = 1920,
height = 1080,
scale = 1,
format = 'png',
quality = 90,
fullPage = false,
outputPath,
} = options;
// Set viewport
await this.page.setViewport({ width, height, deviceScaleFactor: scale });
// Navigate to the document
// Paper URL format: https://app.paper.design/file/{documentId}?page={pageId}&node={nodeId}
let url = `https://app.paper.design/file/${documentId}`;
const params: string[] = [];
if (pageId) {
params.push(`page=${pageId}`);
}
if (nodeId) {
params.push(`node=${nodeId}`);
}
if (params.length > 0) {
url += '?' + params.join('&');
}
this.log('Navigating to:', url);
await this.page.goto(url, {
waitUntil: 'networkidle0',
timeout: 90000
});
// Wait for the page to fully render
this.log('Waiting for page to render...');
await new Promise(r => setTimeout(r, 5000));
// Check if we have any content loaded
const hasContent = await this.page.evaluate(() => {
return document.body.innerHTML.length > 1000;
});
if (!hasContent) {
this.log('Warning: Page may not be fully loaded');
}
// If nodeId is specified, zoom to fit the node
if (nodeId) {
this.log('Zooming to node:', nodeId);
await this.zoomToNode(nodeId, options.padding || 50);
}
// Take screenshot
const screenshotOptions: any = {
type: format,
fullPage: fullPage,
};
if (format === 'jpeg') {
screenshotOptions.quality = quality;
}
// Generate output path
const timestamp = Date.now();
const pageLabel = pageId ? `_${pageId.substring(0, 8)}` : '';
const filename = `paper_${documentId}${pageLabel}_${timestamp}.${format}`;
const screenshotDir = outputPath || './screenshots';
// Ensure directory exists
if (!fs.existsSync(screenshotDir)) {
fs.mkdirSync(screenshotDir, { recursive: true });
}
const filePath = path.join(screenshotDir, filename);
screenshotOptions.path = filePath;
this.log('Taking screenshot:', filePath);
const screenshotResult = await this.page.screenshot(screenshotOptions) as unknown;
// Handle both Buffer and Uint8Array returns
const buffer = Buffer.from(screenshotResult as Uint8Array);
const base64 = buffer.toString('base64');
return {
success: true,
filePath: path.resolve(filePath),
base64,
width,
height,
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
this.log('Screenshot failed:', errorMessage);
return {
success: false,
error: errorMessage,
};
}
}
async close(): Promise<void> {
if (this.browser) {
await this.browser.close();
this.browser = null;
this.page = null;
this.log('Browser closed');
}
}
}