openseadragon-capture 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +137 -0
- package/dist/cjs/capture.js +147 -0
- package/dist/cjs/index.js +6 -0
- package/dist/esm/capture.js +142 -0
- package/dist/esm/index.js +1 -0
- package/dist/types/capture.d.ts +26 -0
- package/dist/types/index.d.ts +2 -0
- package/package.json +60 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Ravi Shankar Saini
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# openseadragon-capture
|
|
2
|
+
|
|
3
|
+
Capture high-quality screenshots from OpenSeadragon viewers with optional overlay layers.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install openseadragon-capture
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import OpenSeadragon from 'openseadragon';
|
|
15
|
+
import { createScreenshot } from 'openseadragon-capture';
|
|
16
|
+
|
|
17
|
+
// Initialize OpenSeadragon viewer with CORS support
|
|
18
|
+
const viewer = OpenSeadragon({
|
|
19
|
+
id: 'viewer',
|
|
20
|
+
tileSources: 'path/to/your/image.dzi',
|
|
21
|
+
crossOriginPolicy: 'Anonymous' // Required for screenshot export
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
// Wait for viewer to open
|
|
25
|
+
viewer.addHandler('open', async () => {
|
|
26
|
+
// Create screenshot instance
|
|
27
|
+
const screenshot = createScreenshot(viewer);
|
|
28
|
+
|
|
29
|
+
// Capture as Blob (recommended - memory efficient)
|
|
30
|
+
const blob = await screenshot.toBlob({
|
|
31
|
+
format: 'png',
|
|
32
|
+
quality: 0.9,
|
|
33
|
+
scale: 2
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// Or capture as data URL
|
|
37
|
+
const dataUrl = await screenshot.capture({
|
|
38
|
+
format: 'png',
|
|
39
|
+
quality: 0.9,
|
|
40
|
+
overlays: [overlayCanvas]
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// Or download directly
|
|
44
|
+
await screenshot.download('screenshot.png', {
|
|
45
|
+
overlays: [overlayCanvas]
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## API
|
|
51
|
+
|
|
52
|
+
### `createScreenshot(viewer: OpenSeadragon.Viewer): OpenSeadragonScreenshot`
|
|
53
|
+
|
|
54
|
+
Creates a screenshot instance for the given viewer.
|
|
55
|
+
|
|
56
|
+
### `toBlob(options?: ScreenshotOptions): Promise<Blob>`
|
|
57
|
+
|
|
58
|
+
Captures the viewer as a Blob. Recommended for memory efficiency.
|
|
59
|
+
|
|
60
|
+
**Throws:** Error if viewer is not open or canvas is unavailable.
|
|
61
|
+
|
|
62
|
+
### `capture(options?: ScreenshotOptions): Promise<string>`
|
|
63
|
+
|
|
64
|
+
Captures the viewer as a data URL.
|
|
65
|
+
|
|
66
|
+
**Throws:** Error if viewer is not open or canvas is unavailable.
|
|
67
|
+
|
|
68
|
+
### `download(filename: string, options?: ScreenshotOptions): Promise<void>`
|
|
69
|
+
|
|
70
|
+
Captures and downloads the screenshot with the specified filename.
|
|
71
|
+
|
|
72
|
+
### ScreenshotOptions
|
|
73
|
+
|
|
74
|
+
```typescript
|
|
75
|
+
export type ScreenshotFormat = 'png' | 'jpeg' | 'webp';
|
|
76
|
+
|
|
77
|
+
interface ScreenshotOptions {
|
|
78
|
+
/** Output image format. @default 'png' */
|
|
79
|
+
format?: ScreenshotFormat;
|
|
80
|
+
|
|
81
|
+
/** Image quality (0-1). @default 0.9 */
|
|
82
|
+
quality?: number;
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Upscaling factor applied via canvas interpolation.
|
|
86
|
+
* Does NOT fetch higher-resolution tiles.
|
|
87
|
+
* @default 1
|
|
88
|
+
*/
|
|
89
|
+
scale?: number;
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Overlay canvases to composite.
|
|
93
|
+
* Must already be in viewer pixel space with transforms applied.
|
|
94
|
+
*/
|
|
95
|
+
overlays?: HTMLCanvasElement[];
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* If true, temporarily fits entire image to viewport before capture.
|
|
99
|
+
* Causes momentary viewport change.
|
|
100
|
+
* @default true
|
|
101
|
+
*/
|
|
102
|
+
fitImageToViewport?: boolean;
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Index of the tiled image to capture (for multi-image viewers).
|
|
106
|
+
* @default 0
|
|
107
|
+
*/
|
|
108
|
+
imageIndex?: number;
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Important Limitations
|
|
113
|
+
|
|
114
|
+
⚠️ **CORS Requirement**: Images must be served with CORS headers. Set `crossOriginPolicy: 'Anonymous'` in viewer config. CORS errors will cause capture to fail.
|
|
115
|
+
|
|
116
|
+
⚠️ **Scale Behavior**: The `scale` parameter upscales via canvas interpolation. It does NOT fetch higher-resolution tiles. For true high-resolution exports, tiles must be loaded at the target resolution.
|
|
117
|
+
|
|
118
|
+
⚠️ **Viewport Changes**: `fitImageToViewport: true` temporarily changes the viewport to fit the entire image, which may cause visual flicker.
|
|
119
|
+
|
|
120
|
+
⚠️ **Overlay Requirements**: Overlay canvases must already be in viewer pixel space with transforms applied. Overlays with mismatched dimensions will be stretched.
|
|
121
|
+
|
|
122
|
+
⚠️ **Timing**: Capture waits for tiles to load using heuristic-based timing. May fail if tiles load slowly.
|
|
123
|
+
|
|
124
|
+
⚠️ **Memory**: Large scale factors can create very large canvases (warning shown if exceeding 16MP).
|
|
125
|
+
|
|
126
|
+
## Production Considerations
|
|
127
|
+
|
|
128
|
+
- Always handle promise rejections (CORS errors are common)
|
|
129
|
+
- Use `toBlob()` instead of `capture()` for better memory management
|
|
130
|
+
- Wait for viewer 'open' event before capturing
|
|
131
|
+
- Test with your specific tile sources and CORS configuration
|
|
132
|
+
- Validate overlay canvas dimensions match viewer canvas
|
|
133
|
+
- Be cautious with large scale factors to avoid memory issues
|
|
134
|
+
|
|
135
|
+
## License
|
|
136
|
+
|
|
137
|
+
MIT
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.OpenSeadragonScreenshot = void 0;
|
|
4
|
+
exports.createScreenshot = createScreenshot;
|
|
5
|
+
class OpenSeadragonScreenshot {
|
|
6
|
+
constructor(viewer) {
|
|
7
|
+
this.viewer = viewer;
|
|
8
|
+
}
|
|
9
|
+
async capture(options = {}) {
|
|
10
|
+
const blob = await this.toBlob(options);
|
|
11
|
+
return new Promise((resolve, reject) => {
|
|
12
|
+
const reader = new FileReader();
|
|
13
|
+
reader.onloadend = () => resolve(reader.result);
|
|
14
|
+
reader.onerror = () => reject(new Error('Failed to convert blob to data URL'));
|
|
15
|
+
reader.readAsDataURL(blob);
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
async toBlob(options = {}) {
|
|
19
|
+
this.ensureViewerReady();
|
|
20
|
+
const { format = 'png', quality = 0.9, scale = 1, overlays = [], fitImageToViewport = true, imageIndex = 0 } = options;
|
|
21
|
+
const stage = await this.prepareCapture(scale, overlays, fitImageToViewport, imageIndex);
|
|
22
|
+
return this.renderStage(stage, format, quality);
|
|
23
|
+
}
|
|
24
|
+
ensureViewerReady() {
|
|
25
|
+
if (!this.viewer.isOpen()) {
|
|
26
|
+
throw new Error('[OpenSeadragon Capture] Viewer is not open. Wait for the "open" event before capturing.');
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
async prepareCapture(scale, overlays, fitImageToViewport, imageIndex) {
|
|
30
|
+
await this.waitForDraw();
|
|
31
|
+
if (fitImageToViewport) {
|
|
32
|
+
const canvas = await this.captureFullImage(imageIndex);
|
|
33
|
+
return { canvas, overlays, scale };
|
|
34
|
+
}
|
|
35
|
+
const canvas = this.getCanvas();
|
|
36
|
+
return { canvas, overlays, scale };
|
|
37
|
+
}
|
|
38
|
+
getCanvas() {
|
|
39
|
+
const canvas = this.viewer.drawer?.canvas;
|
|
40
|
+
if (!canvas) {
|
|
41
|
+
throw new Error('[OpenSeadragon Capture] Canvas not available. Ensure viewer is fully initialized.');
|
|
42
|
+
}
|
|
43
|
+
return canvas;
|
|
44
|
+
}
|
|
45
|
+
waitForDraw() {
|
|
46
|
+
return new Promise((resolve) => {
|
|
47
|
+
const timeout = setTimeout(() => {
|
|
48
|
+
requestAnimationFrame(() => resolve());
|
|
49
|
+
}, 100);
|
|
50
|
+
this.viewer.addOnceHandler('animation-finish', () => {
|
|
51
|
+
clearTimeout(timeout);
|
|
52
|
+
requestAnimationFrame(() => resolve());
|
|
53
|
+
});
|
|
54
|
+
this.viewer.forceRedraw();
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
async captureFullImage(imageIndex) {
|
|
58
|
+
const tiledImage = this.viewer.world.getItemAt(imageIndex);
|
|
59
|
+
if (!tiledImage) {
|
|
60
|
+
throw new Error(`[OpenSeadragon Capture] No image at index ${imageIndex}`);
|
|
61
|
+
}
|
|
62
|
+
const currentBounds = this.viewer.viewport.getBounds();
|
|
63
|
+
const bounds = tiledImage.getBounds();
|
|
64
|
+
try {
|
|
65
|
+
this.viewer.viewport.fitBounds(bounds, true);
|
|
66
|
+
await this.waitForFullLoad(tiledImage);
|
|
67
|
+
return this.getCanvas();
|
|
68
|
+
}
|
|
69
|
+
finally {
|
|
70
|
+
this.viewer.viewport.fitBounds(currentBounds, true);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
waitForFullLoad(tiledImage) {
|
|
74
|
+
if (tiledImage.getFullyLoaded()) {
|
|
75
|
+
return this.waitForDraw();
|
|
76
|
+
}
|
|
77
|
+
return new Promise((resolve) => {
|
|
78
|
+
tiledImage.addOnceHandler('fully-loaded-change', () => {
|
|
79
|
+
this.waitForDraw().then(resolve);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
validateOverlays(canvas, overlays) {
|
|
84
|
+
for (const overlay of overlays) {
|
|
85
|
+
if (overlay.width !== canvas.width || overlay.height !== canvas.height) {
|
|
86
|
+
console.warn(`[OpenSeadragon Capture] Overlay dimensions (${overlay.width}x${overlay.height}) ` +
|
|
87
|
+
`do not match viewer canvas (${canvas.width}x${canvas.height}). ` +
|
|
88
|
+
`Overlay will be stretched.`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
renderStage(stage, format, quality) {
|
|
93
|
+
return new Promise((resolve, reject) => {
|
|
94
|
+
try {
|
|
95
|
+
const outputCanvas = document.createElement('canvas');
|
|
96
|
+
const outputCtx = outputCanvas.getContext('2d', { alpha: format === 'png' });
|
|
97
|
+
if (!outputCtx) {
|
|
98
|
+
reject(new Error('[OpenSeadragon Capture] Failed to get canvas context'));
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
outputCanvas.width = stage.canvas.width * stage.scale;
|
|
102
|
+
outputCanvas.height = stage.canvas.height * stage.scale;
|
|
103
|
+
const maxPixels = 16777216;
|
|
104
|
+
if (outputCanvas.width * outputCanvas.height > maxPixels) {
|
|
105
|
+
console.warn(`[OpenSeadragon Capture] Output canvas is very large (${outputCanvas.width}x${outputCanvas.height}). ` +
|
|
106
|
+
`This may cause memory issues.`);
|
|
107
|
+
}
|
|
108
|
+
this.validateOverlays(stage.canvas, stage.overlays);
|
|
109
|
+
outputCtx.imageSmoothingEnabled = true;
|
|
110
|
+
outputCtx.imageSmoothingQuality = 'high';
|
|
111
|
+
outputCtx.drawImage(stage.canvas, 0, 0, outputCanvas.width, outputCanvas.height);
|
|
112
|
+
for (const overlay of stage.overlays) {
|
|
113
|
+
if (overlay.width > 0 && overlay.height > 0) {
|
|
114
|
+
outputCtx.drawImage(overlay, 0, 0, outputCanvas.width, outputCanvas.height);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
outputCanvas.toBlob((blob) => blob ? resolve(blob) : reject(new Error('[OpenSeadragon Capture] Failed to create blob')), `image/${format}`, quality);
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
121
|
+
reject(new Error(`[OpenSeadragon Capture] ${message}. Check CORS policy if using remote images.`));
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
async download(filename, options = {}) {
|
|
126
|
+
const blob = await this.toBlob(options);
|
|
127
|
+
const url = URL.createObjectURL(blob);
|
|
128
|
+
try {
|
|
129
|
+
const link = document.createElement('a');
|
|
130
|
+
link.download = filename;
|
|
131
|
+
link.href = url;
|
|
132
|
+
link.click();
|
|
133
|
+
}
|
|
134
|
+
finally {
|
|
135
|
+
if (typeof requestIdleCallback === 'undefined') {
|
|
136
|
+
setTimeout(() => URL.revokeObjectURL(url), 100);
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
requestIdleCallback(() => URL.revokeObjectURL(url), { timeout: 1000 });
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
exports.OpenSeadragonScreenshot = OpenSeadragonScreenshot;
|
|
145
|
+
function createScreenshot(viewer) {
|
|
146
|
+
return new OpenSeadragonScreenshot(viewer);
|
|
147
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createScreenshot = exports.OpenSeadragonScreenshot = void 0;
|
|
4
|
+
var capture_1 = require("./capture");
|
|
5
|
+
Object.defineProperty(exports, "OpenSeadragonScreenshot", { enumerable: true, get: function () { return capture_1.OpenSeadragonScreenshot; } });
|
|
6
|
+
Object.defineProperty(exports, "createScreenshot", { enumerable: true, get: function () { return capture_1.createScreenshot; } });
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
export class OpenSeadragonScreenshot {
|
|
2
|
+
constructor(viewer) {
|
|
3
|
+
this.viewer = viewer;
|
|
4
|
+
}
|
|
5
|
+
async capture(options = {}) {
|
|
6
|
+
const blob = await this.toBlob(options);
|
|
7
|
+
return new Promise((resolve, reject) => {
|
|
8
|
+
const reader = new FileReader();
|
|
9
|
+
reader.onloadend = () => resolve(reader.result);
|
|
10
|
+
reader.onerror = () => reject(new Error('Failed to convert blob to data URL'));
|
|
11
|
+
reader.readAsDataURL(blob);
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
async toBlob(options = {}) {
|
|
15
|
+
this.ensureViewerReady();
|
|
16
|
+
const { format = 'png', quality = 0.9, scale = 1, overlays = [], fitImageToViewport = true, imageIndex = 0 } = options;
|
|
17
|
+
const stage = await this.prepareCapture(scale, overlays, fitImageToViewport, imageIndex);
|
|
18
|
+
return this.renderStage(stage, format, quality);
|
|
19
|
+
}
|
|
20
|
+
ensureViewerReady() {
|
|
21
|
+
if (!this.viewer.isOpen()) {
|
|
22
|
+
throw new Error('[OpenSeadragon Capture] Viewer is not open. Wait for the "open" event before capturing.');
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
async prepareCapture(scale, overlays, fitImageToViewport, imageIndex) {
|
|
26
|
+
await this.waitForDraw();
|
|
27
|
+
if (fitImageToViewport) {
|
|
28
|
+
const canvas = await this.captureFullImage(imageIndex);
|
|
29
|
+
return { canvas, overlays, scale };
|
|
30
|
+
}
|
|
31
|
+
const canvas = this.getCanvas();
|
|
32
|
+
return { canvas, overlays, scale };
|
|
33
|
+
}
|
|
34
|
+
getCanvas() {
|
|
35
|
+
const canvas = this.viewer.drawer?.canvas;
|
|
36
|
+
if (!canvas) {
|
|
37
|
+
throw new Error('[OpenSeadragon Capture] Canvas not available. Ensure viewer is fully initialized.');
|
|
38
|
+
}
|
|
39
|
+
return canvas;
|
|
40
|
+
}
|
|
41
|
+
waitForDraw() {
|
|
42
|
+
return new Promise((resolve) => {
|
|
43
|
+
const timeout = setTimeout(() => {
|
|
44
|
+
requestAnimationFrame(() => resolve());
|
|
45
|
+
}, 100);
|
|
46
|
+
this.viewer.addOnceHandler('animation-finish', () => {
|
|
47
|
+
clearTimeout(timeout);
|
|
48
|
+
requestAnimationFrame(() => resolve());
|
|
49
|
+
});
|
|
50
|
+
this.viewer.forceRedraw();
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
async captureFullImage(imageIndex) {
|
|
54
|
+
const tiledImage = this.viewer.world.getItemAt(imageIndex);
|
|
55
|
+
if (!tiledImage) {
|
|
56
|
+
throw new Error(`[OpenSeadragon Capture] No image at index ${imageIndex}`);
|
|
57
|
+
}
|
|
58
|
+
const currentBounds = this.viewer.viewport.getBounds();
|
|
59
|
+
const bounds = tiledImage.getBounds();
|
|
60
|
+
try {
|
|
61
|
+
this.viewer.viewport.fitBounds(bounds, true);
|
|
62
|
+
await this.waitForFullLoad(tiledImage);
|
|
63
|
+
return this.getCanvas();
|
|
64
|
+
}
|
|
65
|
+
finally {
|
|
66
|
+
this.viewer.viewport.fitBounds(currentBounds, true);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
waitForFullLoad(tiledImage) {
|
|
70
|
+
if (tiledImage.getFullyLoaded()) {
|
|
71
|
+
return this.waitForDraw();
|
|
72
|
+
}
|
|
73
|
+
return new Promise((resolve) => {
|
|
74
|
+
tiledImage.addOnceHandler('fully-loaded-change', () => {
|
|
75
|
+
this.waitForDraw().then(resolve);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
validateOverlays(canvas, overlays) {
|
|
80
|
+
for (const overlay of overlays) {
|
|
81
|
+
if (overlay.width !== canvas.width || overlay.height !== canvas.height) {
|
|
82
|
+
console.warn(`[OpenSeadragon Capture] Overlay dimensions (${overlay.width}x${overlay.height}) ` +
|
|
83
|
+
`do not match viewer canvas (${canvas.width}x${canvas.height}). ` +
|
|
84
|
+
`Overlay will be stretched.`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
renderStage(stage, format, quality) {
|
|
89
|
+
return new Promise((resolve, reject) => {
|
|
90
|
+
try {
|
|
91
|
+
const outputCanvas = document.createElement('canvas');
|
|
92
|
+
const outputCtx = outputCanvas.getContext('2d', { alpha: format === 'png' });
|
|
93
|
+
if (!outputCtx) {
|
|
94
|
+
reject(new Error('[OpenSeadragon Capture] Failed to get canvas context'));
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
outputCanvas.width = stage.canvas.width * stage.scale;
|
|
98
|
+
outputCanvas.height = stage.canvas.height * stage.scale;
|
|
99
|
+
const maxPixels = 16777216;
|
|
100
|
+
if (outputCanvas.width * outputCanvas.height > maxPixels) {
|
|
101
|
+
console.warn(`[OpenSeadragon Capture] Output canvas is very large (${outputCanvas.width}x${outputCanvas.height}). ` +
|
|
102
|
+
`This may cause memory issues.`);
|
|
103
|
+
}
|
|
104
|
+
this.validateOverlays(stage.canvas, stage.overlays);
|
|
105
|
+
outputCtx.imageSmoothingEnabled = true;
|
|
106
|
+
outputCtx.imageSmoothingQuality = 'high';
|
|
107
|
+
outputCtx.drawImage(stage.canvas, 0, 0, outputCanvas.width, outputCanvas.height);
|
|
108
|
+
for (const overlay of stage.overlays) {
|
|
109
|
+
if (overlay.width > 0 && overlay.height > 0) {
|
|
110
|
+
outputCtx.drawImage(overlay, 0, 0, outputCanvas.width, outputCanvas.height);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
outputCanvas.toBlob((blob) => blob ? resolve(blob) : reject(new Error('[OpenSeadragon Capture] Failed to create blob')), `image/${format}`, quality);
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
117
|
+
reject(new Error(`[OpenSeadragon Capture] ${message}. Check CORS policy if using remote images.`));
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
async download(filename, options = {}) {
|
|
122
|
+
const blob = await this.toBlob(options);
|
|
123
|
+
const url = URL.createObjectURL(blob);
|
|
124
|
+
try {
|
|
125
|
+
const link = document.createElement('a');
|
|
126
|
+
link.download = filename;
|
|
127
|
+
link.href = url;
|
|
128
|
+
link.click();
|
|
129
|
+
}
|
|
130
|
+
finally {
|
|
131
|
+
if (typeof requestIdleCallback === 'undefined') {
|
|
132
|
+
setTimeout(() => URL.revokeObjectURL(url), 100);
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
requestIdleCallback(() => URL.revokeObjectURL(url), { timeout: 1000 });
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
export function createScreenshot(viewer) {
|
|
141
|
+
return new OpenSeadragonScreenshot(viewer);
|
|
142
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { OpenSeadragonScreenshot, createScreenshot } from './capture';
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import OpenSeadragon from 'openseadragon';
|
|
2
|
+
export type ScreenshotFormat = 'png' | 'jpeg' | 'webp';
|
|
3
|
+
export interface ScreenshotOptions {
|
|
4
|
+
format?: ScreenshotFormat;
|
|
5
|
+
quality?: number;
|
|
6
|
+
scale?: number;
|
|
7
|
+
overlays?: HTMLCanvasElement[];
|
|
8
|
+
fitImageToViewport?: boolean;
|
|
9
|
+
imageIndex?: number;
|
|
10
|
+
}
|
|
11
|
+
export declare class OpenSeadragonScreenshot {
|
|
12
|
+
private readonly viewer;
|
|
13
|
+
constructor(viewer: OpenSeadragon.Viewer);
|
|
14
|
+
capture(options?: ScreenshotOptions): Promise<string>;
|
|
15
|
+
toBlob(options?: ScreenshotOptions): Promise<Blob>;
|
|
16
|
+
private ensureViewerReady;
|
|
17
|
+
private prepareCapture;
|
|
18
|
+
private getCanvas;
|
|
19
|
+
private waitForDraw;
|
|
20
|
+
private captureFullImage;
|
|
21
|
+
private waitForFullLoad;
|
|
22
|
+
private validateOverlays;
|
|
23
|
+
private renderStage;
|
|
24
|
+
download(filename: string, options?: ScreenshotOptions): Promise<void>;
|
|
25
|
+
}
|
|
26
|
+
export declare function createScreenshot(viewer: OpenSeadragon.Viewer): OpenSeadragonScreenshot;
|
package/package.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "openseadragon-capture",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Capture high-quality screenshots from OpenSeadragon viewers with optional overlay layers.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/cjs/index.js",
|
|
7
|
+
"module": "dist/esm/index.js",
|
|
8
|
+
"types": "dist/types/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"import": "./dist/esm/index.js",
|
|
12
|
+
"require": "./dist/cjs/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=20"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"dist",
|
|
20
|
+
"README.md",
|
|
21
|
+
"LICENSE"
|
|
22
|
+
],
|
|
23
|
+
"workspaces": [
|
|
24
|
+
"site",
|
|
25
|
+
"tests"
|
|
26
|
+
],
|
|
27
|
+
"scripts": {
|
|
28
|
+
"clean": "node ./scripts/clean.mjs",
|
|
29
|
+
"build": "npm run build:dual",
|
|
30
|
+
"build:dual": "npm run clean && tsc -p tsconfig.esm.json && tsc -p tsconfig.cjs.json",
|
|
31
|
+
"dev": "npm run -w site dev",
|
|
32
|
+
"build:demo": "npm run build && npm run -w site build",
|
|
33
|
+
"deploy:demo": "npm run build && npm run build:demo && git subtree push --prefix site/dist origin gh-pages",
|
|
34
|
+
"test": "npm run -w tests test",
|
|
35
|
+
"test:run": "npm run -w tests test:run",
|
|
36
|
+
"prepare": "npm run build"
|
|
37
|
+
},
|
|
38
|
+
"repository": {
|
|
39
|
+
"type": "git",
|
|
40
|
+
"url": "git+https://github.com/rssaini01/openseadragon-capture.git"
|
|
41
|
+
},
|
|
42
|
+
"keywords": [
|
|
43
|
+
"openseadragon",
|
|
44
|
+
"canvas",
|
|
45
|
+
"overlay",
|
|
46
|
+
"screenshot",
|
|
47
|
+
"export"
|
|
48
|
+
],
|
|
49
|
+
"author": "Ravi Shankar Saini",
|
|
50
|
+
"license": "MIT",
|
|
51
|
+
"bugs": {
|
|
52
|
+
"url": "https://github.com/rssaini01/openseadragon-capture/issues"
|
|
53
|
+
},
|
|
54
|
+
"homepage": "https://github.com/rssaini01/openseadragon-capture/",
|
|
55
|
+
"devDependencies": {
|
|
56
|
+
"@types/openseadragon": "^5.0.1",
|
|
57
|
+
"openseadragon": "^5.0.1",
|
|
58
|
+
"typescript": "^5.9.3"
|
|
59
|
+
}
|
|
60
|
+
}
|