jupyterlab_advanced_image_viewer_extension 1.0.7 → 1.0.11
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/README.md +2 -0
- package/lib/index.js +70 -1
- package/package.json +2 -1
- package/src/index.ts +81 -1
package/README.md
CHANGED
|
@@ -21,6 +21,8 @@ The viewer toolbar adds zoom in / out / fit controls and a `help` link that list
|
|
|
21
21
|
- **Drag to pan** - hold and drag to move the image at any zoom (canvas-style); the grab cursor is always available and Fit recenters
|
|
22
22
|
- **Toolbar controls** - zoom in, zoom out, and reset-to-fit buttons on the viewer toolbar
|
|
23
23
|
- **Arrow-key navigation** - Left and Right open the previous and next image in the same folder, advancing within one viewer instead of opening new tabs
|
|
24
|
+
- **Copy to clipboard** - right-click a raster image (PNG/JPG/…) and choose "Copy to Clipboard" to copy it as PNG to the system clipboard
|
|
25
|
+
- **Refresh** - a toolbar refresh button reloads the image from disk, the same control the HTML viewer provides
|
|
24
26
|
|
|
25
27
|
## Requirements
|
|
26
28
|
|
package/lib/index.js
CHANGED
|
@@ -2,6 +2,7 @@ import { Dialog, showDialog, Toolbar, ToolbarButton } from '@jupyterlab/apputils
|
|
|
2
2
|
import { PathExt } from '@jupyterlab/coreutils';
|
|
3
3
|
import { IImageTracker } from '@jupyterlab/imageviewer';
|
|
4
4
|
import { ISettingRegistry } from '@jupyterlab/settingregistry';
|
|
5
|
+
import { refreshIcon } from '@jupyterlab/ui-components';
|
|
5
6
|
import { Widget } from '@lumino/widgets';
|
|
6
7
|
import { ViewerController } from './controller';
|
|
7
8
|
const PLUGIN_ID = 'jupyterlab_advanced_image_viewer_extension:plugin';
|
|
@@ -10,7 +11,8 @@ const CommandIDs = {
|
|
|
10
11
|
zoomOut: 'advanced-image-viewer:zoom-out',
|
|
11
12
|
resetFit: 'advanced-image-viewer:reset-fit',
|
|
12
13
|
previous: 'advanced-image-viewer:previous-image',
|
|
13
|
-
next: 'advanced-image-viewer:next-image'
|
|
14
|
+
next: 'advanced-image-viewer:next-image',
|
|
15
|
+
copyClipboard: 'advanced-image-viewer:copy-to-clipboard'
|
|
14
16
|
};
|
|
15
17
|
const IMAGE_EXTS = new Set([
|
|
16
18
|
'.png',
|
|
@@ -21,6 +23,9 @@ const IMAGE_EXTS = new Set([
|
|
|
21
23
|
'.svg',
|
|
22
24
|
'.webp'
|
|
23
25
|
]);
|
|
26
|
+
// Raster formats that can be drawn to a canvas and copied as PNG. SVG is
|
|
27
|
+
// excluded - the drawio/SVG extension already adds its own "Copy as PNG".
|
|
28
|
+
const RASTER_EXTS = new Set(['.png', '.jpg', '.jpeg', '.gif', '.bmp', '.webp']);
|
|
24
29
|
const defaults = {
|
|
25
30
|
navEnabled: true,
|
|
26
31
|
zoomStep: 0.1
|
|
@@ -86,6 +91,17 @@ const plugin = {
|
|
|
86
91
|
controller.dispose();
|
|
87
92
|
controllers.delete(viewer);
|
|
88
93
|
});
|
|
94
|
+
// Reload from disk, like the HTML viewer's refresh button. That button
|
|
95
|
+
// skips the reload while the model is dirty; an image model is never
|
|
96
|
+
// edited, so there is nothing here to discard.
|
|
97
|
+
widget.toolbar.addItem('advanced-refresh', new ToolbarButton({
|
|
98
|
+
icon: refreshIcon,
|
|
99
|
+
tooltip: 'Reload image from disk',
|
|
100
|
+
onClick: async () => {
|
|
101
|
+
await widget.context.revert();
|
|
102
|
+
widget.update();
|
|
103
|
+
}
|
|
104
|
+
}));
|
|
89
105
|
widget.toolbar.addItem('advanced-zoom-out', new ToolbarButton({
|
|
90
106
|
label: '-',
|
|
91
107
|
tooltip: 'Zoom out',
|
|
@@ -131,6 +147,59 @@ const plugin = {
|
|
|
131
147
|
isEnabled: () => currentController() !== null,
|
|
132
148
|
execute: () => { var _a; return (_a = currentController()) === null || _a === void 0 ? void 0 : _a.reset(); }
|
|
133
149
|
});
|
|
150
|
+
// Resolve the image widget under the context-menu target. Right-click does
|
|
151
|
+
// not activate the widget, so hit-test the clicked node rather than rely on
|
|
152
|
+
// tracker.currentWidget.
|
|
153
|
+
const contextImageWidget = () => {
|
|
154
|
+
var _a;
|
|
155
|
+
const node = app.contextMenuHitTest(n => n.classList.contains('jp-ImageViewer'));
|
|
156
|
+
if (!node) {
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
return (_a = tracker.find(widget => widget.content.node === node)) !== null && _a !== void 0 ? _a : null;
|
|
160
|
+
};
|
|
161
|
+
app.commands.addCommand(CommandIDs.copyClipboard, {
|
|
162
|
+
label: 'Copy to Clipboard',
|
|
163
|
+
// Raster only - SVG already has "Copy as PNG" from the drawio extension.
|
|
164
|
+
isVisible: () => {
|
|
165
|
+
const widget = contextImageWidget();
|
|
166
|
+
return (widget !== null &&
|
|
167
|
+
RASTER_EXTS.has(PathExt.extname(widget.context.path).toLowerCase()));
|
|
168
|
+
},
|
|
169
|
+
execute: async () => {
|
|
170
|
+
var _a;
|
|
171
|
+
const img = (_a = contextImageWidget()) === null || _a === void 0 ? void 0 : _a.content.node.querySelector('img');
|
|
172
|
+
if (!img) {
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
// Copy the source pixels, not the on-screen zoom/pan/rotate view.
|
|
176
|
+
const canvas = document.createElement('canvas');
|
|
177
|
+
canvas.width = img.naturalWidth;
|
|
178
|
+
canvas.height = img.naturalHeight;
|
|
179
|
+
const ctx = canvas.getContext('2d');
|
|
180
|
+
if (!ctx) {
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
ctx.drawImage(img, 0, 0);
|
|
184
|
+
const blob = await new Promise(resolve => canvas.toBlob(resolve, 'image/png'));
|
|
185
|
+
if (!blob) {
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
try {
|
|
189
|
+
await navigator.clipboard.write([
|
|
190
|
+
new ClipboardItem({ 'image/png': blob })
|
|
191
|
+
]);
|
|
192
|
+
}
|
|
193
|
+
catch (err) {
|
|
194
|
+
console.error('Advanced image viewer: clipboard copy failed', err);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
app.contextMenu.addItem({
|
|
199
|
+
command: CommandIDs.copyClipboard,
|
|
200
|
+
selector: '.jp-ImageViewer',
|
|
201
|
+
rank: 0
|
|
202
|
+
});
|
|
134
203
|
const navigate = async (delta) => {
|
|
135
204
|
const widget = tracker.currentWidget;
|
|
136
205
|
if (!widget) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jupyterlab_advanced_image_viewer_extension",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.11",
|
|
4
4
|
"description": "JupyterLab extension for advanced image viewing: fit-to-screen, cursor-anchored wheel zoom, drag-to-pan, and Left/Right arrow keys to move to the previous and next image in a folder",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"jupyter",
|
|
@@ -62,6 +62,7 @@
|
|
|
62
62
|
"@jupyterlab/docregistry": "^4.0.0",
|
|
63
63
|
"@jupyterlab/imageviewer": "^4.0.0",
|
|
64
64
|
"@jupyterlab/settingregistry": "^4.0.0",
|
|
65
|
+
"@jupyterlab/ui-components": "^4.0.0",
|
|
65
66
|
"@lumino/disposable": "^2.0.0",
|
|
66
67
|
"@lumino/widgets": "^2.0.0"
|
|
67
68
|
},
|
package/src/index.ts
CHANGED
|
@@ -18,6 +18,8 @@ import { IImageTracker, ImageViewer } from '@jupyterlab/imageviewer';
|
|
|
18
18
|
|
|
19
19
|
import { ISettingRegistry } from '@jupyterlab/settingregistry';
|
|
20
20
|
|
|
21
|
+
import { refreshIcon } from '@jupyterlab/ui-components';
|
|
22
|
+
|
|
21
23
|
import { IDisposable } from '@lumino/disposable';
|
|
22
24
|
|
|
23
25
|
import { Widget } from '@lumino/widgets';
|
|
@@ -31,7 +33,8 @@ const CommandIDs = {
|
|
|
31
33
|
zoomOut: 'advanced-image-viewer:zoom-out',
|
|
32
34
|
resetFit: 'advanced-image-viewer:reset-fit',
|
|
33
35
|
previous: 'advanced-image-viewer:previous-image',
|
|
34
|
-
next: 'advanced-image-viewer:next-image'
|
|
36
|
+
next: 'advanced-image-viewer:next-image',
|
|
37
|
+
copyClipboard: 'advanced-image-viewer:copy-to-clipboard'
|
|
35
38
|
};
|
|
36
39
|
|
|
37
40
|
const IMAGE_EXTS = new Set([
|
|
@@ -44,6 +47,10 @@ const IMAGE_EXTS = new Set([
|
|
|
44
47
|
'.webp'
|
|
45
48
|
]);
|
|
46
49
|
|
|
50
|
+
// Raster formats that can be drawn to a canvas and copied as PNG. SVG is
|
|
51
|
+
// excluded - the drawio/SVG extension already adds its own "Copy as PNG".
|
|
52
|
+
const RASTER_EXTS = new Set(['.png', '.jpg', '.jpeg', '.gif', '.bmp', '.webp']);
|
|
53
|
+
|
|
47
54
|
interface ISettingsState {
|
|
48
55
|
navEnabled: boolean;
|
|
49
56
|
zoomStep: number;
|
|
@@ -129,6 +136,20 @@ const plugin: JupyterFrontEndPlugin<void> = {
|
|
|
129
136
|
controllers.delete(viewer);
|
|
130
137
|
});
|
|
131
138
|
|
|
139
|
+
// Reload from disk, like the HTML viewer's refresh button. That button
|
|
140
|
+
// skips the reload while the model is dirty; an image model is never
|
|
141
|
+
// edited, so there is nothing here to discard.
|
|
142
|
+
widget.toolbar.addItem(
|
|
143
|
+
'advanced-refresh',
|
|
144
|
+
new ToolbarButton({
|
|
145
|
+
icon: refreshIcon,
|
|
146
|
+
tooltip: 'Reload image from disk',
|
|
147
|
+
onClick: async () => {
|
|
148
|
+
await widget.context.revert();
|
|
149
|
+
widget.update();
|
|
150
|
+
}
|
|
151
|
+
})
|
|
152
|
+
);
|
|
132
153
|
widget.toolbar.addItem(
|
|
133
154
|
'advanced-zoom-out',
|
|
134
155
|
new ToolbarButton({
|
|
@@ -186,6 +207,65 @@ const plugin: JupyterFrontEndPlugin<void> = {
|
|
|
186
207
|
execute: () => currentController()?.reset()
|
|
187
208
|
});
|
|
188
209
|
|
|
210
|
+
// Resolve the image widget under the context-menu target. Right-click does
|
|
211
|
+
// not activate the widget, so hit-test the clicked node rather than rely on
|
|
212
|
+
// tracker.currentWidget.
|
|
213
|
+
const contextImageWidget = (): IDocumentWidget<ImageViewer> | null => {
|
|
214
|
+
const node = app.contextMenuHitTest(n =>
|
|
215
|
+
n.classList.contains('jp-ImageViewer')
|
|
216
|
+
);
|
|
217
|
+
if (!node) {
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
return tracker.find(widget => widget.content.node === node) ?? null;
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
app.commands.addCommand(CommandIDs.copyClipboard, {
|
|
224
|
+
label: 'Copy to Clipboard',
|
|
225
|
+
// Raster only - SVG already has "Copy as PNG" from the drawio extension.
|
|
226
|
+
isVisible: () => {
|
|
227
|
+
const widget = contextImageWidget();
|
|
228
|
+
return (
|
|
229
|
+
widget !== null &&
|
|
230
|
+
RASTER_EXTS.has(PathExt.extname(widget.context.path).toLowerCase())
|
|
231
|
+
);
|
|
232
|
+
},
|
|
233
|
+
execute: async () => {
|
|
234
|
+
const img = contextImageWidget()?.content.node.querySelector('img');
|
|
235
|
+
if (!img) {
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
// Copy the source pixels, not the on-screen zoom/pan/rotate view.
|
|
239
|
+
const canvas = document.createElement('canvas');
|
|
240
|
+
canvas.width = img.naturalWidth;
|
|
241
|
+
canvas.height = img.naturalHeight;
|
|
242
|
+
const ctx = canvas.getContext('2d');
|
|
243
|
+
if (!ctx) {
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
ctx.drawImage(img, 0, 0);
|
|
247
|
+
const blob = await new Promise<Blob | null>(resolve =>
|
|
248
|
+
canvas.toBlob(resolve, 'image/png')
|
|
249
|
+
);
|
|
250
|
+
if (!blob) {
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
try {
|
|
254
|
+
await navigator.clipboard.write([
|
|
255
|
+
new ClipboardItem({ 'image/png': blob })
|
|
256
|
+
]);
|
|
257
|
+
} catch (err) {
|
|
258
|
+
console.error('Advanced image viewer: clipboard copy failed', err);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
app.contextMenu.addItem({
|
|
264
|
+
command: CommandIDs.copyClipboard,
|
|
265
|
+
selector: '.jp-ImageViewer',
|
|
266
|
+
rank: 0
|
|
267
|
+
});
|
|
268
|
+
|
|
189
269
|
const navigate = async (delta: number): Promise<void> => {
|
|
190
270
|
const widget = tracker.currentWidget;
|
|
191
271
|
if (!widget) {
|