jupyterlab_advanced_image_viewer_extension 1.0.7 → 1.0.9

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 CHANGED
@@ -21,6 +21,7 @@ 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
24
25
 
25
26
  ## Requirements
26
27
 
package/lib/index.js CHANGED
@@ -10,7 +10,8 @@ const CommandIDs = {
10
10
  zoomOut: 'advanced-image-viewer:zoom-out',
11
11
  resetFit: 'advanced-image-viewer:reset-fit',
12
12
  previous: 'advanced-image-viewer:previous-image',
13
- next: 'advanced-image-viewer:next-image'
13
+ next: 'advanced-image-viewer:next-image',
14
+ copyClipboard: 'advanced-image-viewer:copy-to-clipboard'
14
15
  };
15
16
  const IMAGE_EXTS = new Set([
16
17
  '.png',
@@ -21,6 +22,9 @@ const IMAGE_EXTS = new Set([
21
22
  '.svg',
22
23
  '.webp'
23
24
  ]);
25
+ // Raster formats that can be drawn to a canvas and copied as PNG. SVG is
26
+ // excluded - the drawio/SVG extension already adds its own "Copy as PNG".
27
+ const RASTER_EXTS = new Set(['.png', '.jpg', '.jpeg', '.gif', '.bmp', '.webp']);
24
28
  const defaults = {
25
29
  navEnabled: true,
26
30
  zoomStep: 0.1
@@ -131,6 +135,59 @@ const plugin = {
131
135
  isEnabled: () => currentController() !== null,
132
136
  execute: () => { var _a; return (_a = currentController()) === null || _a === void 0 ? void 0 : _a.reset(); }
133
137
  });
138
+ // Resolve the image widget under the context-menu target. Right-click does
139
+ // not activate the widget, so hit-test the clicked node rather than rely on
140
+ // tracker.currentWidget.
141
+ const contextImageWidget = () => {
142
+ var _a;
143
+ const node = app.contextMenuHitTest(n => n.classList.contains('jp-ImageViewer'));
144
+ if (!node) {
145
+ return null;
146
+ }
147
+ return (_a = tracker.find(widget => widget.content.node === node)) !== null && _a !== void 0 ? _a : null;
148
+ };
149
+ app.commands.addCommand(CommandIDs.copyClipboard, {
150
+ label: 'Copy to Clipboard',
151
+ // Raster only - SVG already has "Copy as PNG" from the drawio extension.
152
+ isVisible: () => {
153
+ const widget = contextImageWidget();
154
+ return (widget !== null &&
155
+ RASTER_EXTS.has(PathExt.extname(widget.context.path).toLowerCase()));
156
+ },
157
+ execute: async () => {
158
+ var _a;
159
+ const img = (_a = contextImageWidget()) === null || _a === void 0 ? void 0 : _a.content.node.querySelector('img');
160
+ if (!img) {
161
+ return;
162
+ }
163
+ // Copy the source pixels, not the on-screen zoom/pan/rotate view.
164
+ const canvas = document.createElement('canvas');
165
+ canvas.width = img.naturalWidth;
166
+ canvas.height = img.naturalHeight;
167
+ const ctx = canvas.getContext('2d');
168
+ if (!ctx) {
169
+ return;
170
+ }
171
+ ctx.drawImage(img, 0, 0);
172
+ const blob = await new Promise(resolve => canvas.toBlob(resolve, 'image/png'));
173
+ if (!blob) {
174
+ return;
175
+ }
176
+ try {
177
+ await navigator.clipboard.write([
178
+ new ClipboardItem({ 'image/png': blob })
179
+ ]);
180
+ }
181
+ catch (err) {
182
+ console.error('Advanced image viewer: clipboard copy failed', err);
183
+ }
184
+ }
185
+ });
186
+ app.contextMenu.addItem({
187
+ command: CommandIDs.copyClipboard,
188
+ selector: '.jp-ImageViewer',
189
+ rank: 0
190
+ });
134
191
  const navigate = async (delta) => {
135
192
  const widget = tracker.currentWidget;
136
193
  if (!widget) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jupyterlab_advanced_image_viewer_extension",
3
- "version": "1.0.7",
3
+ "version": "1.0.9",
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",
package/src/index.ts CHANGED
@@ -31,7 +31,8 @@ const CommandIDs = {
31
31
  zoomOut: 'advanced-image-viewer:zoom-out',
32
32
  resetFit: 'advanced-image-viewer:reset-fit',
33
33
  previous: 'advanced-image-viewer:previous-image',
34
- next: 'advanced-image-viewer:next-image'
34
+ next: 'advanced-image-viewer:next-image',
35
+ copyClipboard: 'advanced-image-viewer:copy-to-clipboard'
35
36
  };
36
37
 
37
38
  const IMAGE_EXTS = new Set([
@@ -44,6 +45,10 @@ const IMAGE_EXTS = new Set([
44
45
  '.webp'
45
46
  ]);
46
47
 
48
+ // Raster formats that can be drawn to a canvas and copied as PNG. SVG is
49
+ // excluded - the drawio/SVG extension already adds its own "Copy as PNG".
50
+ const RASTER_EXTS = new Set(['.png', '.jpg', '.jpeg', '.gif', '.bmp', '.webp']);
51
+
47
52
  interface ISettingsState {
48
53
  navEnabled: boolean;
49
54
  zoomStep: number;
@@ -186,6 +191,65 @@ const plugin: JupyterFrontEndPlugin<void> = {
186
191
  execute: () => currentController()?.reset()
187
192
  });
188
193
 
194
+ // Resolve the image widget under the context-menu target. Right-click does
195
+ // not activate the widget, so hit-test the clicked node rather than rely on
196
+ // tracker.currentWidget.
197
+ const contextImageWidget = (): IDocumentWidget<ImageViewer> | null => {
198
+ const node = app.contextMenuHitTest(n =>
199
+ n.classList.contains('jp-ImageViewer')
200
+ );
201
+ if (!node) {
202
+ return null;
203
+ }
204
+ return tracker.find(widget => widget.content.node === node) ?? null;
205
+ };
206
+
207
+ app.commands.addCommand(CommandIDs.copyClipboard, {
208
+ label: 'Copy to Clipboard',
209
+ // Raster only - SVG already has "Copy as PNG" from the drawio extension.
210
+ isVisible: () => {
211
+ const widget = contextImageWidget();
212
+ return (
213
+ widget !== null &&
214
+ RASTER_EXTS.has(PathExt.extname(widget.context.path).toLowerCase())
215
+ );
216
+ },
217
+ execute: async () => {
218
+ const img = contextImageWidget()?.content.node.querySelector('img');
219
+ if (!img) {
220
+ return;
221
+ }
222
+ // Copy the source pixels, not the on-screen zoom/pan/rotate view.
223
+ const canvas = document.createElement('canvas');
224
+ canvas.width = img.naturalWidth;
225
+ canvas.height = img.naturalHeight;
226
+ const ctx = canvas.getContext('2d');
227
+ if (!ctx) {
228
+ return;
229
+ }
230
+ ctx.drawImage(img, 0, 0);
231
+ const blob = await new Promise<Blob | null>(resolve =>
232
+ canvas.toBlob(resolve, 'image/png')
233
+ );
234
+ if (!blob) {
235
+ return;
236
+ }
237
+ try {
238
+ await navigator.clipboard.write([
239
+ new ClipboardItem({ 'image/png': blob })
240
+ ]);
241
+ } catch (err) {
242
+ console.error('Advanced image viewer: clipboard copy failed', err);
243
+ }
244
+ }
245
+ });
246
+
247
+ app.contextMenu.addItem({
248
+ command: CommandIDs.copyClipboard,
249
+ selector: '.jp-ImageViewer',
250
+ rank: 0
251
+ });
252
+
189
253
  const navigate = async (delta: number): Promise<void> => {
190
254
  const widget = tracker.currentWidget;
191
255
  if (!widget) {