13 |
14 |
28 |
29 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/page/popup.js:
--------------------------------------------------------------------------------
1 | ;(function () {
2 | 'use strict'
3 |
4 | const rawText = `
5 | 1.41 [2025-02-06]:
6 | Stability update
7 | This version was originally planned as a major update, but the development of new features was delayed
8 | 1. Added a hotkey command for canvas mode
9 | 2. Added support for background images in pseudo elements
10 | 3. Improved scroll unlazy logic
11 | 4. Improved right click image size referencing logic
12 | 5. Other bug fixes and improvements
13 | P.S. Starting with this version, the extension will also be published on addons.mozilla.org for Firefox users
14 |
15 | 1.40 [2024-10-14]:
16 | 1. Added a new option to allow users to disable image unlazy on specific domains
17 | 2. Introduced a web demo, enabling users to try the feature before installation
18 | 3. Updated the options page and added a simple support page
19 | 4. Fixed a bug in the new view canvas feature that caused issues on sites like Notion
20 |
21 | 1.39.1 [2024-10-01]:
22 | Patch Update
23 | 1. Fixed a bug in the new view canvas features that caused issues with some sites like Google Sheets
24 |
25 | 1.39 [2024-09-29]:
26 | Major Update
27 | 1. Added an action to the icon context menu allowing users to view canvas elements
28 | // Note: This feature only supports snapshots, not GIF creation
29 | // May also be useful for cases where an image is visible but not accessible in normal mode
30 | // This could include an image drawn on a canvas element
31 | 2. Added support for local and blob images to mainstream reverse search
32 | 3. Fixed navigation, it will now correctly wait for images to be rendered on the screen
33 | 4. The space bar can now be used to send a middle click to an image (previously only "0" could be used)
34 | 5. Other bug fixes and improvements
35 |
36 | 1.38 [2024-09-09]:
37 | Stability update
38 | 1. Added support for data URL images to mainstream reverse search
39 | 2. Fixed a bug that could change the website's default layout
40 | 3. Fixed a bug that could toggle the website's default hotkeys (eg. page navigation)
41 | 4. Other bug fixes and improvements
42 |
43 | 1.37 [2024-08-11]:
44 | Performance and Stability update
45 | 1. The control panel will now auto hide after 1.5 seconds of mouse hover
46 | // move cursor over buttons will toggle the panel again
47 | // provides clearer view when using scroll to view image
48 | 2. Improved image viewer's logic for build/update image list
49 | 3. Refactored image collection logic to enhance stability of the image list
50 | 4. Rewritten auto scroll logic to ensure no images are skipped
51 | 5. Enhanced code quality
52 | 6. Other bug fixes and improvements
53 |
54 | 1.36 [2024-07-22]:
55 | Major Update
56 | 1. Added a hotkey for auto navigation (shift + arrow keys)
57 | 2. Added ton of code to support of custom element
58 | 3. Add sub-image check to improve image unlazy in url mode
59 | 4. Improve and refactor iframe image extraction logic
60 | 5. Improve CSS and layout of the image viewer
61 | 6. Refactor data structure for image info
62 | 7. Other bug fixes and improvements
63 |
64 | 1.35 [2024-07-03]:
65 | 1. Reduced zoom & rotate transition flash
66 | 2. Improved auto update logic
67 | 3. Enhanced ability to find larger size raw images
68 | 4. Reworked unlazy logic, no longer need to wait when reopening within a short time
69 | 5. Reworked iframe logic, can now handle iframe in iframe cases
70 | 6. Fixed a bug that changed current index after image list update
71 | 7. Other bug fixes and improvements
72 |
73 | 1.34 [2024-04-11]:
74 | Stability update
75 | 1. Prevented image loading flash in URL mode
76 | 2. Add smooth transition for image transform
77 | 3. Fixed a bug where AltGraph could not be used with Ctrl in hotkey combinations
78 | // related hotkey: image transformation and image reverse search
79 | 4. Improved code performance
80 | 5. Implemented error handling to minimize minor errors displayed to users
81 | 6. Added support for new type of unlazy (simulate mouse hover)
82 | 7. Other bug fixes and improvements
83 |
84 | 1.33 [2024-01-15]:
85 | Functional Update
86 | 1. Added a new default fit mode option: "Original size (does not exceed window)"
87 | 2. Added a maximum size limit (3x) for other fit modes to prevent enlarging small images too much
88 | 3. Added a new hotkey (Shift + B) for switching the background color: transparent -> black -> white
89 | 4. Added new hotkeys for image transformation:
90 | // Move: Ctrl + Alt + ↑↓←→ / WASD
91 | // Zoom: Alt + ↑↓ / WS
92 | // Rotate: Alt + ←→ / AD
93 | 5. Improved auto-scrolling
94 | 6. Added support for more edge cases
95 | 7. Other bug fixes and improvements
96 |
97 | 1.32 [2023-12-31]:
98 | 1. Improved accuracy of image middle click redirect
99 | 2. Enhanced size filter referencing of picking an image by right click
100 | 3. Solve CSS issues related to lazy images on some websites
101 | 4. Added support for the embed element
102 | 5. Refactored and improved code logic
103 | 6. Other bug fixes and improvements
104 |
105 | 1.31 [2023-10-22]:
106 | 1. Rotation now rotates around the center of the viewpoint
107 | 2. Auto scroll hotkey will toggle auto scroll instead of just starting it
108 | 3. Navigation with "WASD" is now supported
109 | 4. Support fast navigation by pressing the Ctrl key at the same time to activate it
110 | 5. Support memory of last image when restarting in page mode
111 | 6. Enhanced code quality
112 | 7. Other bug fixes and improvements
113 |
114 | 1.30 [2023-08-27]:
115 | Stability update
116 | 1. Improved SVG filtering
117 | 2. Added support for multiple layers unlazy
118 | 3. Enhanced logic for getting raw image URLs
119 | 4. Added support for more edge cases
120 | 5. Other bug fixes and improvements
121 |
122 | 1.29 [2023-08-08]:
123 | 1. Corrected code related to the service worker lifecycle
124 | 2. Enhanced unlazy logic to handle additional cases
125 | 3. Improved logic for updating the size filter when there are images of the same kind as the picked image
126 | 4. Enhanced the user experience on auto scroll
127 | 5. Numerous bug fixes and minor improvements
128 |
129 | 1.28 [2023-07-10]:
130 | Major Update
131 | 1. Added a hotkey to manually enable auto scroll
132 | 2. Added a hotkey to download images collected by image viewer
133 | // Note: This extension is not a resource downloader
134 | // Download functionality is limited to basic features
135 | // eg. selecting a download range and packaging in a zip file
136 | 3. Improved first display time of image viewer
137 | 4. Improved middle-click redirect to open the original image's hyperlink
138 | 5. Improved correctness of right click image pickup
139 | 6. Other bug fixes and improvements
140 |
141 | 1.27 [2023-07-06]:
142 | 1. Improved image selection, decrease the priority of image placeholder and image sprite
143 | 2. Improved border display after using moveTo
144 | 3. Improved auto scrolling and auto update
145 | 4. Some bug fixes
146 |
147 | 1.26 [2023-06-17]:
148 | 1. Improved the logic of using middle click to open the link of current image
149 | 2. Fixed a bug that caused jumping in viewer index
150 | 3. Fixed a bug that prevented the image viewer from automatically starting for image URLs
151 | 4. Other bug fixes and improvements
152 |
153 | 1.25 [2023-06-04]:
154 | 1. AltGraph key now functions the same as Alt key in hotkey
155 | 2. More intuitive zoom, where zooming now occurs at the screen center instead of the image center
156 | 3. Fixed the incorrect position of the border display after the moveTo operation
157 | 4. Fixed a bug that caused a conflict in the scroll function
158 | 5. Added a check for iframes to handle a bug in Chrome
159 | 6. Removed code that caused extra rendering time
160 | 7. Added caching to enhance performance
161 | 8. Improved performance on right click image pickup
162 |
163 | 1.24 [2023-06-01]:
164 | 1. Introduces method for old style lazy image
165 | 2. Enhance the moveTo function and label border
166 | 3. Improve stability of the extension
167 | 4. Fix bugs and improve performance
168 |
169 | 1.23 [2023-05-29]:
170 | 1. Add temporary image list storage
171 | 2. Significantly reduced startup time by approximately 3-10 times
172 | 3. Refine UI
173 | 4. Improve code logic
174 |
175 | 1.22 [2023-05-28]:
176 | 1. Support deeper-layer iframes
177 | 2. Enhance the moveTo function
178 | 3. Revamp border display following moveTo
179 | 4. Support additional edge cases
180 | 5. Improve performance and fix bugs
181 |
182 | 1.21 [2023-05-27]:
183 | 1. Fixed a bug when getting the image list, so it won't repeat the same image with different sizes
184 | 2. Fixed the "moveTo" button, now it functions correctly on websites like Instagram and Twitter
185 | 3. Fixed the image update, so it won't jump back to the first image when updating
186 | 4. Fixed a bug related to image looping, now it will wait for an image update when it reaches the end
187 |
188 | 1.20 [2023-05-26]:
189 | 1. Improved auto update and auto scroll
190 | 2. More stability on image file URLs
191 | 3. Added support for more iframe images
192 | 4. Improved performance and fixed bugs
193 |
194 | 1.19 [2023-05-14]:
195 | 1. Improve the stability of auto scroll
196 | 2. Improve the code logic for better performance
197 | 3. Fix a lot of bugs
198 |
199 | 1.18 [2023-05-04]:
200 | 1. Support auto scroll
201 | 2. Add options to enable auto scroll and disable hover check
202 | 3. Refactor code for better readability
203 | 4. Fix bug related to hover check and other minor bugs
204 |
205 | 1.17 [2023-04-30]:
206 | Stability update
207 | 1. Add some code to increase the stability
208 | 2. Add handle to more edge cases
209 | 3. Fix bugs
210 |
211 | 1.16 [2023-04-10]:
212 | 1. Image viewer now collects images after website adding new content
213 | // usually website update is toggled by scroll to the end of the page
214 | // you can archive it by scrolling on the scrollbar or press "End" key on keyboard
215 | // you may also use other "next page" script/extension
216 | 2. Fix issues for youtube thumbnail
217 | 3. Fix bugs related to last update
218 | 4. Refactor code to improving program structure
219 |
220 | 1.15 [2023-04-05]:
221 | Large Update
222 | 1. Add support on update image in the viewer
223 | 2. Solve the problem for image viewer can't be open on some websites
224 | 3. Fix CORS issues for iframe images
225 | 4. Fix other issues in rare situations
226 | 5. Improve performance and fix some bugs
227 |
228 | 1.14 [2023-04-01]:
229 | 1. Improve CSS of image viewer
230 | 2. Improve performance of right click image pickup
231 | 3. Add an icon image pre-check before unlazy image to improve performance
232 | 4. Enhance the method of getting image wrapper size
233 | 5. Bug fixes
234 |
235 | 1.13 [2023-03-18]:
236 | 1. Improve right click image pickup performance
237 | 2. Improve stability on image unlazy
238 | 3. Extend the loading time limit for images inside image viewer
239 | 4. Fix lot of typos and bugs
240 |
241 | 1.12 [2023-02-14]:
242 | 1. Add this popup page to show release notes when install or update
243 | 2. Improve stability
244 | 3. Add domain white list for image unlazy
245 | // create issues on github if you want to add domain to the list
246 | // may move to option page or just hide in source code
247 |
248 | 1.11 [2023-02-11]:
249 | 1. Images are now order by its real location
250 | 3. No longer use dataURL, ObjectURL is faster and better for the browser to render images
251 | 2. Min size filter will also considers wrapper of the selected image
252 | 4. Some website that disabled right click menu. Add "view last right click" in icon menu to handle it
253 |
254 | 1.10 [2023-02-11]:
255 | 1. Add MoveTo support for iframe images
256 | 2. Improve right click image pickup
257 | 3. Improve image check size method
258 |
259 | 1.9 [2023-01-13]:
260 | 1. Support image pickup using right click
261 | 2. Delay execution of worker script to improve performance
262 |
263 | 1.8 [2022-10-30]:
264 | 1. Improve the support of viewing images inside iframe
265 | 2. Refactor code to tidy up code related to iframe
266 |
267 | 1.7 [2022-10-04]:
268 | 1. Improve support on iframe images
269 | 2. Improve simpleUnlazyImage()
270 | 3. Add more keyboard shortcuts and svg filter in option
271 |
272 | 1.6 [2022-09-03]:
273 | 1. Support images inside iframe
274 | 2. Improve data transfer between content script and background
275 |
276 | 1.5 [2022-08-22]:
277 | 1. Renew simpleUnlazyImage()
278 | 2. Improve image-viewer.js
279 | 3. Support hotkey for reverse search image
280 |
281 | 1.4 [2022-08-10]:
282 | 1. Improve simpleUnlazyImage()
283 | 2. Support video element
284 | 3. Improve MoveTo button logic
285 | 4. Prevent input leak out from image viewer
286 | 5. Improve simpleUnlazyImage()
287 | 6. Add utility.js to separate utility function
288 |
289 | 1.3 [2022-07-01]:
290 | 1. Delay loading of image-viewer.js to improve performance
291 | 2. Add command support
292 | 3. Improve image unlazy
293 | 4. Renew activate image method to increase readability
294 |
295 | 1.2 [2022-07-01]:
296 | 1. Add simpleUnlazyImage() to unlazy image before getting image list
297 | 2. Change CSS to pin image viewer counter
298 |
299 | 1.1 [2022-07-01]:
300 | 1. Support mirror effect
301 | 2. Replace old transform method with matrix to improve performance
302 |
303 | 1.0 [2022-06-29]:
304 | First release on github
305 | `
306 | function createNotes() {
307 | const data = rawText.split('\n\n').map(t => t.trim().split('\n'))
308 |
309 | const noteContainerGroup = document.createElement('div')
310 | noteContainerGroup.classList.add('note-container-group')
311 | for (const textList of data) {
312 | const noteContainer = document.createElement('div')
313 | noteContainer.classList.add('note-container')
314 |
315 | const bar = document.createElement('button')
316 | bar.classList.add('bar')
317 | bar.type = 'button'
318 | bar.textContent = textList.shift()
319 |
320 | const noteText = document.createElement('div')
321 | noteText.classList.add('noteText')
322 | for (const line of textList) {
323 | const p = document.createElement('p')
324 | p.textContent = line
325 | noteText.appendChild(p)
326 | }
327 |
328 | bar.onclick = () => {
329 | if (noteContainer.classList.contains('active')) {
330 | noteContainer.classList.remove('active')
331 | noteText.style.maxHeight = null
332 | } else {
333 | noteContainer.classList.add('active')
334 | noteText.style.maxHeight = noteText.scrollHeight + 'px'
335 | }
336 | }
337 |
338 | noteContainer.appendChild(bar)
339 | noteContainer.appendChild(noteText)
340 | noteContainerGroup.appendChild(noteContainer)
341 | }
342 | document.body.appendChild(noteContainerGroup)
343 | }
344 |
345 | function toggleFirstNote() {
346 | const firstNote = document.querySelector('div.note-container-group > div:nth-child(1) > button')
347 | firstNote.nextElementSibling.style.transitionDuration = '0s'
348 | firstNote.click()
349 | setTimeout(() => (firstNote.nextElementSibling.style.transitionDuration = ''), 100)
350 | }
351 |
352 | function i18n() {
353 | chrome.i18n.getAcceptLanguages(languages => {
354 | const exist = ['en', 'ja', 'zh_CN', 'zh_TW']
355 | let displayLanguages = 'en'
356 | for (const lang of languages) {
357 | if (exist.includes(lang.replace('-', '_'))) {
358 | displayLanguages = lang
359 | break
360 | }
361 | if (exist.includes(lang.slice(0, 2))) {
362 | displayLanguages = lang.slice(0, 2)
363 | break
364 | }
365 | }
366 | document.documentElement.setAttribute('lang', displayLanguages)
367 | })
368 |
369 | for (const el of document.querySelectorAll('[data-i18n]')) {
370 | const tag = el.getAttribute('data-i18n')
371 | const message = chrome.i18n.getMessage(tag)
372 | if (!message) continue
373 | el.textContent = message
374 | if (el.value !== '') el.value = message
375 | }
376 | }
377 |
378 | function init() {
379 | createNotes()
380 | toggleFirstNote()
381 | i18n()
382 | }
383 |
384 | init()
385 | })()
386 |
--------------------------------------------------------------------------------
/page/support.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Support - Image Viewer
7 |
8 |
9 |
10 |
11 |
Image Viewer is a manifest V3 Chrome extension that improves your image viewing experience.
5 |
6 | If you like this extension, you can [buy me a coffee](https://ko-fi.com/tonymilktea)
7 |
8 | ## Features
9 |
10 | 1. Collect and view all images on the page.
11 | 2. Support video posters, canvas element and images in iframes.
12 | 3. Auto replace lazy loaded or resized images with original image.
13 | 4. Redirect middle click to original image to open link you want.
14 | 5. Go to original image on the page.
15 | 6. Fit, zoom, rotate and mirror the image.
16 | 7. Hotkey for image reverse search.
17 | 8. Download collected images.
18 | 9. Easy to use.
19 | 10. And more...
20 |
21 | ## Installation
22 |
23 | [Web Demo](https://hospotho.github.io/Image-Viewer/) (Does not include some extension-only features)
24 |
25 | You can install release version from [Chrome Web Store](https://chrome.google.com/webstore/detail/image-viewer/ghdcoodfcolpdebbdhbgkbodbjololfl) or [Firefox Add-ons](https://addons.mozilla.org/firefox/addon/syrup-image-viewer/) development version follow steps below:
26 |
27 | 1. Download the source code and place it anywhere you want.
28 | 2. Open your browser and go to `chrome://extensions`.
29 | 3. Enable Developer Mode.
30 | 4. Click the "Load Unpacked" button and select the folder with the source code.
31 |
32 | Note: Any tabs opened before the installation require a reload.
33 |
34 | ## How to use
35 |
36 | After adding this extension to your browser, it is recommended to pin it to the toolbar.
37 |
38 | For image tabs, Image Viewer will be activate automatically.
39 |
40 | For normal websites, you can activate Image Viewer by choose this extension from the right-click menu, click its icon on the toolbar or use keyboard hotkey (default Alt+1).
41 |
42 | For additional options, right-click the extension icon on the toolbar. You can start the Image Viewer with disabled size filter or start with the last picked image (use it when the right-click menu is disabled by the website).
43 |
44 |
45 |
46 |
Action
47 |
Controls
48 |
49 |
50 |
Pick image (size filter will use this image as reference)
51 |
right-click on the image
52 |
53 |
54 |
View previous/next image
55 |
↑↓←→wasd
56 |
57 |
58 |
Scroll on the control bar
59 |
60 |
61 |
Scroll on the close button
62 |
63 |
64 |
Fast navigation (10 images, no throttle)
65 |
Ctrl+↑↓←→
66 |
67 |
68 |
Auto navigation / Slideshow (until end or user interrupt)
69 |
Shift+↑↓←→
70 |
71 |
72 |
Go to original image on page
73 |
Enter
74 |
75 |
76 |
click "Move To" button on the control bar
77 |
78 |
79 |
middle-click the original image (Open tab for post, video, etc.)
80 |
middle-click on the image
81 |
82 |
83 |
space or 0 (both number row and numeric keypad)
84 |
85 |
86 |
Fitting image
87 |
Click fitting buttons on the control bar
88 |
89 |
90 |
Move image
91 |
click and drag
92 |
93 |
94 |
Ctrl+Alt+↑↓←→wasd
95 |
96 |
97 |
Reset image
98 |
double-click anywhere
99 |
100 |
101 |
Zoom image
102 |
Scroll on the image
103 |
104 |
105 |
Alt+↑↓ws
106 |
107 |
108 |
Rotate image
109 |
Hold Alt and scroll
110 |
111 |
112 |
Alt+←→ad
113 |
114 |
115 |
Mirror image
116 |
Hold Alt and click
117 |
118 |
119 |
Change background color (loop: transparent -> black -> white)
120 |
Shift+b
121 |
122 |
123 |
Download current image
124 |
Ctrl+Shift+d
125 |
126 |
127 |
Image reverse search
128 |
Press the hotkeys defined in setting
129 |
130 |
131 |
Download collected images
132 |
Shift+d (default)
133 |
134 |
135 |
Enable auto scroll
136 |
Shift+r (default)
137 |
138 |
139 |
Close Image Viewer
140 |
ESC or NumpadAdd
141 |
142 |
143 |
Click the close button
144 |
145 |
146 |
Close current tab
147 |
right-click the close button
148 |
149 |
150 |
151 | ## Browser support
152 |
153 | The entire project was written in Vanilla JavaScript with extension API supported by Chromium-based browsers. It should also work on Firefox, but it has not been tested yet.
154 |
155 | The standalone `image-viewer.js` should work on all modern browsers, and you can integrated into your own website with own collect image script.
156 |
157 | You may also use `image-viewer.js` with your own script with Tampermonkey or other alternatives.
158 |
159 | ## ToDo
160 |
161 | 1. `image-viewer.min.js`
162 | 2. support more image display mode
163 |
164 | ## History
165 |
166 | The prototype of this project was created by Eky Kwan under the MIT License, and the author of the translations in `_locales` is unknown. The first release v0.1 was launched on 2012-07-05, and the last release v0.1.6 was on 2012-08-12. However, the license file was either lost or not included in the Chrome Web Store version.
167 |
168 | Since I started using this extension, many new features have been added to the project. You can find the oldest version [here](https://github.com/hospotho/Image-Viewer-Legacy), some mirroring websites may still have the raw version of v0.1.6.
169 |
170 | The old version was hard to extend, and I felt tired of it in June 2022. Therefore, I decided to clean up all the old-style, messy jQuery code and rewrite the project completely. The rewrite is now complete and has also been upgraded to manifest V3.
171 |
172 | This project is currently developed and maintained by me.
173 |
174 | ## License
175 |
176 | MIT license
--------------------------------------------------------------------------------
/scripts/action-canvas.js:
--------------------------------------------------------------------------------
1 | ;(async function () {
2 | 'use strict'
3 |
4 | const safeSendMessage = function (...args) {
5 | if (chrome.runtime?.id) {
6 | return chrome.runtime.sendMessage(...args)
7 | }
8 | }
9 |
10 | if (typeof ImageViewerUtils !== 'object') {
11 | await safeSendMessage('load_utility')
12 | }
13 |
14 | if (document.body.classList.contains('iv-attached')) {
15 | ImageViewer('close_image_viewer')
16 | return
17 | }
18 |
19 | // init
20 | const options = window.ImageViewerOption
21 | options.closeButton = true
22 | options.canvasMode = true
23 | window.ImageViewerLastDom = null
24 |
25 | const orderedCanvasList = await ImageViewerUtils.getOrderedCanvasList(options)
26 | if (orderedCanvasList.length === 0) {
27 | alert('No canvas found')
28 | return
29 | }
30 |
31 | // build image viewer
32 | ImageViewer(orderedCanvasList, options)
33 | })()
34 |
--------------------------------------------------------------------------------
/scripts/action-folder.js:
--------------------------------------------------------------------------------
1 | ;(async function () {
2 | 'use strict'
3 |
4 | const safeSendMessage = function (...args) {
5 | if (chrome.runtime?.id) {
6 | return chrome.runtime.sendMessage(...args)
7 | }
8 | }
9 |
10 | if (typeof ImageViewerUtils !== 'object') {
11 | await safeSendMessage('load_utility')
12 | }
13 |
14 | if (document.body.classList.contains('iv-attached')) {
15 | ImageViewer('close_image_viewer')
16 | return
17 | }
18 |
19 | // init
20 | const options = window.ImageViewerOption
21 | options.closeButton = true
22 |
23 | const anchorList = [...document.getElementsByTagName('a')].filter(a => !a.href.endsWith('/'))
24 | const isImageList = await safeSendMessage({msg: 'is_file_image', urlList: anchorList.map(a => a.href)})
25 | const sizeList = await Promise.all(anchorList.map((a, i) => isImageList[i] && ImageViewerUtils.getImageRealSize(a.href)))
26 |
27 | const imageDataList = []
28 | const minSize = Math.min(options.minWidth, options.minHeight)
29 | for (let i = 0; i < anchorList.length; i++) {
30 | if (sizeList[i] >= minSize) imageDataList.push({src: anchorList[i].href, dom: anchorList[i]})
31 | }
32 |
33 | // build image viewer
34 | ImageViewer(imageDataList, options)
35 | })()
36 |
--------------------------------------------------------------------------------
/scripts/action-image.js:
--------------------------------------------------------------------------------
1 | ;(async function () {
2 | 'use strict'
3 |
4 | const safeSendMessage = function (...args) {
5 | if (chrome.runtime?.id) {
6 | return chrome.runtime.sendMessage(...args)
7 | }
8 | }
9 |
10 | if (typeof ImageViewerUtils !== 'object') {
11 | await safeSendMessage('load_utility')
12 | }
13 |
14 | if (document.body.classList.contains('iv-attached')) return
15 |
16 | // init
17 | const options = window.ImageViewerOption
18 | options.closeButton = true
19 |
20 | // update image size filter
21 | const nodeInfo = await safeSendMessage('get_info')
22 | const [srcUrl, nodeSize] = nodeInfo
23 | if (nodeSize) {
24 | options.minWidth = Math.min(nodeSize, options.minWidth)
25 | options.minHeight = Math.min(nodeSize, options.minHeight)
26 | }
27 |
28 | for (let i = 0; i < 10; i++) {
29 | if (window.ImageViewerLastDom !== undefined) break
30 | await new Promise(resolve => setTimeout(resolve, 20))
31 | }
32 | const dom = window.ImageViewerLastDom
33 | const domRect = dom?.getBoundingClientRect()
34 | const domSize = domRect ? [domRect.width, domRect.height] : [0, 0]
35 | ImageViewerUtils.updateWrapperSize(dom, domSize, options)
36 |
37 | const orderedImageList = await ImageViewerUtils.getOrderedImageList(options)
38 | const combinedImageList = ImageViewerUtils.combineImageList(orderedImageList, window.backupImageList)
39 | window.backupImageList = Array.from(combinedImageList)
40 |
41 | // find image index
42 | options.index = ImageViewerUtils.searchImageInfoIndex({src: srcUrl, dom: dom}, window.backupImageList)
43 | if (dom && options.index === -1) {
44 | options.index = 0
45 | window.backupImageList.unshift({src: srcUrl, dom: dom})
46 | console.log('Unshift image to list')
47 | }
48 |
49 | // build image viewer
50 | ImageViewer(window.backupImageList, options)
51 |
52 | // auto update
53 | let initComplete = false
54 | const initPeriod = 200
55 |
56 | let updateRelease = () => {}
57 | let updatePeriod = 500
58 | const multiplier = 1.2
59 |
60 | const initObserver = new MutationObserver(mutationList => {
61 | initComplete = mutationList.every(mutation => mutation.addedNodes.length === 0)
62 | })
63 | initObserver.observe(document.body, {childList: true, subtree: true})
64 |
65 | const container = ImageViewerUtils.getMainContainer()
66 | const updateObserver = new MutationObserver(async () => {
67 | let currentScrollX = container.scrollLeft
68 | let currentScrollY = container.scrollTop
69 | await new Promise(resolve => setTimeout(resolve, 50))
70 | // check scroll complete
71 | while (currentScrollX !== container.scrollLeft || currentScrollY !== container.scrollTop) {
72 | currentScrollX = container.scrollLeft
73 | currentScrollY = container.scrollTop
74 | await new Promise(resolve => setTimeout(resolve, 200))
75 | }
76 | updatePeriod = 500
77 | updateRelease()
78 | })
79 | updateObserver.observe(document.body, {childList: true, subtree: true})
80 |
81 | const unlazyObserver = new MutationObserver(mutationList => {
82 | const unlazyUpdate = mutationList.some(mutation => mutation.attributeName === 'iv-checking' && !mutation.target.hasAttribute('iv-checking'))
83 | if (unlazyUpdate || !document.body.classList.contains('iv-attached')) {
84 | updatePeriod = 500
85 | updateRelease()
86 | }
87 | })
88 | unlazyObserver.observe(document.body, {childList: true, subtree: true, attributeFilter: ['iv-checking']})
89 |
90 | while (document.body.classList.contains('iv-attached')) {
91 | // wait website init
92 | while (!initComplete) {
93 | initComplete = true
94 | await new Promise(resolve => setTimeout(resolve, initPeriod))
95 | }
96 | if (!document.body.classList.contains('iv-attached')) return
97 |
98 | // update image viewer
99 | if (dom?.tagName === 'IMG') {
100 | ImageViewerUtils.updateWrapperSize(dom, domSize, options)
101 | }
102 | const orderedImageList = await ImageViewerUtils.getOrderedImageList(options)
103 | const combinedImageList = ImageViewerUtils.combineImageList(orderedImageList, window.backupImageList)
104 | const currentImageList = ImageViewer('get_image_list')
105 |
106 | if (!document.body.classList.contains('iv-attached')) return
107 | if (combinedImageList.length > currentImageList.length || !ImageViewerUtils.isStrLengthEqual(combinedImageList, currentImageList)) {
108 | updatePeriod = 100
109 | window.backupImageList = Array.from(combinedImageList)
110 | ImageViewer(combinedImageList, options)
111 | }
112 |
113 | // wait website update
114 | await new Promise(resolve => {
115 | setTimeout(resolve, updatePeriod)
116 | updateRelease = resolve
117 | updatePeriod *= multiplier
118 | })
119 |
120 | // wait visible
121 | while (document.visibilityState !== 'visible') {
122 | await new Promise(resolve => setTimeout(resolve, 100))
123 | }
124 | }
125 | initObserver.disconnect()
126 | updateObserver.disconnect()
127 | unlazyObserver.disconnect()
128 | })()
129 |
--------------------------------------------------------------------------------
/scripts/action-page.js:
--------------------------------------------------------------------------------
1 | ;(async function () {
2 | 'use strict'
3 |
4 | const safeSendMessage = function (...args) {
5 | if (chrome.runtime?.id) {
6 | return chrome.runtime.sendMessage(...args)
7 | }
8 | }
9 |
10 | if (typeof ImageViewerUtils !== 'object') {
11 | await safeSendMessage('load_utility')
12 | }
13 |
14 | if (document.body.classList.contains('iv-attached')) {
15 | ImageViewer('close_image_viewer')
16 | return
17 | }
18 |
19 | // init
20 | const options = window.ImageViewerOption
21 | options.closeButton = true
22 | window.ImageViewerLastDom = null
23 |
24 | const orderedImageList = await ImageViewerUtils.getOrderedImageList(options)
25 | const combinedImageList = ImageViewerUtils.combineImageList(orderedImageList, window.backupImageList)
26 | window.backupImageList = Array.from(combinedImageList)
27 |
28 | // build image viewer
29 | ImageViewer(window.backupImageList, options)
30 |
31 | // auto update
32 | let initComplete = false
33 | const initPeriod = 200
34 |
35 | let updateRelease = () => {}
36 | let updatePeriod = 500
37 | const multiplier = 1.2
38 |
39 | const initObserver = new MutationObserver(mutationList => {
40 | initComplete = mutationList.every(mutation => mutation.addedNodes.length === 0)
41 | })
42 | initObserver.observe(document.body, {childList: true, subtree: true})
43 |
44 | const container = ImageViewerUtils.getMainContainer()
45 | const updateObserver = new MutationObserver(async () => {
46 | let currentScrollX = container.scrollLeft
47 | let currentScrollY = container.scrollTop
48 | await new Promise(resolve => setTimeout(resolve, 50))
49 | // check scroll complete
50 | while (currentScrollX !== container.scrollLeft || currentScrollY !== container.scrollTop) {
51 | currentScrollX = container.scrollLeft
52 | currentScrollY = container.scrollTop
53 | await new Promise(resolve => setTimeout(resolve, 200))
54 | }
55 | updatePeriod = 500
56 | updateRelease()
57 | })
58 | updateObserver.observe(document.body, {childList: true, subtree: true})
59 |
60 | const unlazyObserver = new MutationObserver(mutationList => {
61 | const unlazyUpdate = mutationList.some(mutation => mutation.attributeName === 'iv-checking' && !mutation.target.hasAttribute('iv-checking'))
62 | if (unlazyUpdate || !document.body.classList.contains('iv-attached')) {
63 | updatePeriod = 500
64 | updateRelease()
65 | }
66 | })
67 | unlazyObserver.observe(document.body, {childList: true, subtree: true, attributeFilter: ['iv-checking']})
68 |
69 | while (document.body.classList.contains('iv-attached')) {
70 | // wait website init
71 | while (!initComplete) {
72 | initComplete = true
73 | await new Promise(resolve => setTimeout(resolve, initPeriod))
74 | }
75 | if (!document.body.classList.contains('iv-attached')) return
76 |
77 | // update image viewer
78 | const orderedImageList = await ImageViewerUtils.getOrderedImageList(options)
79 | const combinedImageList = ImageViewerUtils.combineImageList(orderedImageList, window.backupImageList)
80 | const currentImageList = ImageViewer('get_image_list')
81 |
82 | if (!document.body.classList.contains('iv-attached')) return
83 | if (combinedImageList.length > currentImageList.length || !ImageViewerUtils.isStrLengthEqual(combinedImageList, currentImageList)) {
84 | updatePeriod = 100
85 | window.backupImageList = Array.from(combinedImageList)
86 | ImageViewer(combinedImageList, options)
87 | }
88 |
89 | // wait website update
90 | await new Promise(resolve => {
91 | setTimeout(resolve, updatePeriod)
92 | updateRelease = resolve
93 | updatePeriod *= multiplier
94 | })
95 |
96 | // wait visible
97 | while (document.visibilityState !== 'visible') {
98 | await new Promise(resolve => setTimeout(resolve, 100))
99 | }
100 | }
101 | initObserver.disconnect()
102 | updateObserver.disconnect()
103 | unlazyObserver.disconnect()
104 | })()
105 |
--------------------------------------------------------------------------------
/scripts/activate-url.js:
--------------------------------------------------------------------------------
1 | ;(function () {
2 | 'use strict'
3 |
4 | const safeSendMessage = function (...args) {
5 | if (chrome.runtime?.id) {
6 | return chrome.runtime.sendMessage(...args)
7 | }
8 | }
9 |
10 | // image url mode
11 | function isImageContained(small, large) {
12 | const canvas1 = document.createElement('canvas')
13 | const canvas2 = document.createElement('canvas')
14 | const ctx1 = canvas1.getContext('2d')
15 | const ctx2 = canvas2.getContext('2d', {willReadFrequently: true})
16 |
17 | // pooling
18 | const poolingWidth = 256
19 | const smallRatio = small.width / small.height
20 | const largeRatio = large.width / large.height
21 |
22 | canvas1.width = poolingWidth
23 | canvas1.height = poolingWidth / smallRatio
24 | ctx1.drawImage(small, 0, 0, small.width, small.height, 0, 0, poolingWidth, canvas1.height)
25 |
26 | canvas2.width = poolingWidth
27 | canvas2.height = poolingWidth / largeRatio
28 | ctx2.drawImage(large, 0, 0, large.width, large.height, 0, 0, poolingWidth, canvas2.height)
29 |
30 | // compare pixels
31 | const threshold = 0.5
32 | const base = ctx1.getImageData(0, 0, poolingWidth, canvas1.height).data
33 | const pixelCount = base.length / 4
34 |
35 | // check if center
36 | let diffCount = 0
37 | const center = ctx2.getImageData(0, (canvas2.height - canvas1.height) / 2, poolingWidth, canvas1.height).data
38 | for (let i = 0; i < base.length; i += 4) {
39 | if (Math.abs(base[i] - center[i]) + Math.abs(base[i + 1] - center[i + 1]) + Math.abs(base[i + 2] - center[i + 2]) > 24) {
40 | diffCount++
41 | }
42 | }
43 | if (diffCount / pixelCount < threshold) return true
44 |
45 | // check if topmost
46 | diffCount = 0
47 | const top = ctx2.getImageData(0, 0, poolingWidth, canvas1.height).data
48 | for (let i = 0; i < base.length; i += 4) {
49 | if (Math.abs(base[i] - top[i]) + Math.abs(base[i + 1] - top[i + 1]) + Math.abs(base[i + 2] - top[i + 2]) > 24) {
50 | diffCount++
51 | }
52 | }
53 | if (diffCount / pixelCount < threshold) return true
54 |
55 | return false
56 | }
57 |
58 | const argsRegex = /(.*?[=.](?:jpeg|jpg|png|gif|webp|bmp|tiff|avif))(?!\/)/i
59 | function getRawUrl(src) {
60 | if (src.startsWith('data') || src.startsWith('blob')) return src
61 |
62 | const filenameMatch = src.replace(/[-_]\d{3,4}x(?:\d{3,4})?\./, '.')
63 | if (filenameMatch !== src) return filenameMatch
64 |
65 | try {
66 | // protocol-relative URL
67 | const url = new URL(src, document.baseURI)
68 | const baseURI = url.origin + url.pathname
69 |
70 | const searchList = url.search
71 | .slice(1)
72 | .split('&')
73 | .filter(t => t.match(argsRegex))
74 | .join('&')
75 | const imgSearch = searchList ? '?' + searchList : ''
76 | const rawSearch = baseURI + imgSearch
77 |
78 | const argsMatch = rawSearch.match(argsRegex)
79 | if (argsMatch) {
80 | const rawUrl = argsMatch[1]
81 | if (rawUrl !== src) return rawUrl
82 | }
83 | } catch (error) {}
84 |
85 | const argsMatch = src.match(argsRegex)
86 | if (argsMatch) {
87 | const rawUrl = argsMatch[1]
88 | if (rawUrl !== src) return rawUrl
89 | }
90 | return src
91 | }
92 | function getImage(rawUrl) {
93 | return new Promise(resolve => {
94 | const img = new Image()
95 | img.onload = () => resolve(img)
96 | img.onerror = () => resolve(img)
97 | img.src = rawUrl
98 | })
99 | }
100 | function getUnlazyAttrList(img) {
101 | const src = img.currentSrc
102 | const rawUrl = getRawUrl(src)
103 | const attrList = []
104 | attrList.push({name: 'raw url', value: rawUrl})
105 | try {
106 | const url = new URL(src, document.baseURI)
107 | const pathname = url.pathname
108 | const search = url.search
109 | if (pathname.match(/[-_]thumb(?=nail)?\./)) {
110 | const nonThumbnailPath = pathname.replace(/[-_]thumb(?=nail)?\./, '.')
111 | const nonThumbnail = src.replace(pathname, nonThumbnailPath)
112 | attrList.push({name: 'non thumbnail path', value: nonThumbnail})
113 | }
114 |
115 | if (!src.includes('?')) throw new Error()
116 |
117 | if (!pathname.includes('.')) {
118 | const extMatch = search.match(/jpeg|jpg|png|gif|webp|bmp|tiff|avif/)
119 | if (extMatch) {
120 | const filenameWithExt = pathname + '.' + extMatch[0]
121 | const rawExtension = src.replace(pathname + search, filenameWithExt)
122 | attrList.push({name: 'raw extension', value: rawExtension})
123 | }
124 | }
125 | if (search.includes('width=') || search.includes('height=')) {
126 | const noSizeQuery = search.replace(/&?width=\d+|&?height=\d+/g, '')
127 | const rawQuery = src.replace(search, noSizeQuery)
128 | attrList.push({name: 'no size query', value: rawQuery})
129 | }
130 | const noQuery = src.replace(pathname + search, pathname)
131 | attrList.push({name: 'no query', value: noQuery})
132 | } catch (error) {}
133 | return attrList.filter(attr => attr.value !== src)
134 | }
135 |
136 | async function initImageViewer(image) {
137 | console.log('Start image mode')
138 |
139 | const options = window.ImageViewerOption
140 | options.closeButton = false
141 | options.minWidth = 0
142 | options.minHeight = 0
143 |
144 | await safeSendMessage('load_script')
145 | const imageDate = {src: image.src, dom: image}
146 | ImageViewer([imageDate], options)
147 | if (image.src.startsWith('data')) return
148 |
149 | const attrList = getUnlazyAttrList(image)
150 | for (const attr of attrList) {
151 | const rawImage = await getImage(attr.value)
152 | const rawSize = [rawImage.naturalWidth, rawImage.naturalHeight]
153 | if (image.naturalWidth > rawSize[0]) continue
154 | const rawRatio = rawSize[0] / rawSize[1]
155 | const currRatio = image.naturalWidth / image.naturalHeight
156 | // non trivial size or with proper ratio
157 | const nonTrivialSize = rawSize[0] % 10 || rawSize[1] % 10
158 | const properRatio = currRatio === 1 || Math.abs(rawRatio - currRatio) < 0.01 || rawRatio > 3 || rawRatio < 1 / 3
159 | const isRawCandidate = nonTrivialSize || properRatio
160 | if (isRawCandidate) {
161 | console.log(`Unlazy img with ${attr.name}`)
162 | const rawData = {src: attr.value, dom: image}
163 | ImageViewer([rawData], options)
164 | break
165 | }
166 | // sub image
167 | if (image.naturalWidth >= 256 && rawRatio < currRatio && isImageContained(image, rawImage)) {
168 | console.log(`Unlazy img with ${attr.name}`)
169 | const rawData = {src: attr.value, dom: image}
170 | ImageViewer([rawData], options)
171 | break
172 | }
173 | }
174 | }
175 |
176 | async function init() {
177 | // safe to send message in iframe
178 | if (window.top !== window.self) {
179 | safeSendMessage('load_extractor')
180 | return
181 | }
182 |
183 | await safeSendMessage('get_options')
184 | // Chrome may terminated service worker
185 | while (!window.ImageViewerOption) {
186 | await new Promise(resolve => setTimeout(resolve, 50))
187 | await safeSendMessage('get_options')
188 | }
189 |
190 | try {
191 | const image = document.querySelector(`img[src='${location.href}']`)
192 | image ? initImageViewer(image) : safeSendMessage('load_worker')
193 | } catch (error) {}
194 | }
195 |
196 | if (document.visibilityState === 'visible') {
197 | init()
198 | } else {
199 | const handleEvent = () => {
200 | document.removeEventListener('visibilitychange', handleEvent)
201 | window.removeEventListener('focus', handleEvent)
202 | init()
203 | }
204 | document.addEventListener('visibilitychange', handleEvent)
205 | window.addEventListener('focus', handleEvent)
206 | }
207 | })()
208 |
--------------------------------------------------------------------------------
/scripts/activate-worker.js:
--------------------------------------------------------------------------------
1 | ;(async function () {
2 | 'use strict'
3 |
4 | const safeSendMessage = function (...args) {
5 | if (chrome.runtime?.id) {
6 | return chrome.runtime.sendMessage(...args)
7 | }
8 | }
9 |
10 | // init
11 | const options = window.ImageViewerOption
12 | const domainList = []
13 | const regexList = []
14 | for (const str of options.hoverCheckDisableList) {
15 | if (str[0] === '/' && str[str.length - 1] === '/') {
16 | regexList.push(str)
17 | } else {
18 | domainList.push(str)
19 | }
20 | }
21 | let disableHoverCheck = domainList.includes(location.hostname)
22 | disableHoverCheck ||= regexList.some(regex => regex.test(location.href))
23 |
24 | if (window.top === window.self && !disableHoverCheck) {
25 | const styles = 'html.iv-worker-checking img {pointer-events: auto !important;} .disable-hover {pointer-events: none !important;}'
26 | const styleSheet = document.createElement('style')
27 | styleSheet.textContent = styles
28 | document.head.appendChild(styleSheet)
29 | }
30 |
31 | // image size
32 | const srcBitSizeMap = new Map()
33 | const srcRealSizeMap = new Map()
34 | const corsHostSet = new Set()
35 | const argsRegex = /(.*?[=.](?:jpeg|jpg|png|gif|webp|bmp|tiff|avif))(?!\/)/i
36 |
37 | async function fetchBitSize(url) {
38 | if (corsHostSet.has(url.hostname)) return 0
39 | try {
40 | const res = await fetch(url.href, {method: 'HEAD', signal: AbortSignal.timeout(5000)})
41 | if (!res.ok) return 0
42 | if (res.redirected) return -1
43 | const type = res.headers.get('Content-Type')
44 | const length = res.headers.get('Content-Length')
45 | if (type?.startsWith('image') || (type === 'application/octet-stream' && url.href.match(argsRegex))) {
46 | const size = Number(length)
47 | return size
48 | }
49 | return 0
50 | } catch (error) {
51 | if (error.name !== 'TimeoutError') corsHostSet.add(url.hostname)
52 | return 0
53 | }
54 | }
55 | function getImageBitSize(src) {
56 | if (!src || src === 'about:blank' || src.startsWith('data')) return 0
57 |
58 | const cache = srcBitSizeMap.get(src)
59 | if (cache !== undefined) return cache
60 |
61 | const promise = new Promise(_resolve => {
62 | const resolve = size => {
63 | srcBitSizeMap.set(src, size)
64 | _resolve(size)
65 | }
66 |
67 | let waiting = false
68 | const updateSize = size => {
69 | if (size) resolve(size)
70 | else if (waiting) waiting = false
71 | else if (src.startsWith('blob')) return resolve(Number.MAX_SAFE_INTEGER)
72 | else resolve(0)
73 | }
74 |
75 | // protocol-relative URL
76 | const url = new URL(src, document.baseURI)
77 | const href = url.href
78 | if (url.hostname !== location.hostname) {
79 | waiting = true
80 | safeSendMessage({msg: 'get_size', url: href}).then(updateSize)
81 | }
82 | fetchBitSize(url).then(updateSize)
83 | })
84 |
85 | srcBitSizeMap.set(src, promise)
86 | return promise
87 | }
88 | async function getImageRealSize(src) {
89 | const cache = srcRealSizeMap.get(src)
90 | if (cache !== undefined) return cache
91 |
92 | const promise = new Promise(_resolve => {
93 | const resolve = size => {
94 | srcRealSizeMap.set(src, size)
95 | _resolve(size)
96 | }
97 |
98 | const img = new Image()
99 | img.onload = () => resolve(Math.min(img.naturalWidth, img.naturalHeight))
100 | img.onerror = () => resolve(0)
101 | setTimeout(() => img.complete || resolve(0), 10000)
102 | img.src = src
103 | })
104 |
105 | srcRealSizeMap.set(src, promise)
106 | return promise
107 | }
108 |
109 | // image info
110 | const domSearcher = (function () {
111 | // searchImageFromTree
112 | function checkZIndex(e1, e2) {
113 | const e1zIndex = Number(window.getComputedStyle(e1).zIndex)
114 | const e2zIndex = Number(window.getComputedStyle(e2).zIndex)
115 |
116 | if (Number.isNaN(e1zIndex) || Number.isNaN(e2zIndex)) return 0
117 | if (e1zIndex > e2zIndex) {
118 | return -1
119 | } else if (e1zIndex < e2zIndex) {
120 | return 1
121 | } else {
122 | return 0
123 | }
124 | }
125 | function checkPosition(e1, e2) {
126 | const e1Rect = e1.getBoundingClientRect()
127 | const e2Rect = e2.getBoundingClientRect()
128 |
129 | const commonParent = e1.offsetParent || e1.parentNode
130 | const parentPosition = commonParent.getBoundingClientRect()
131 |
132 | const e1ActualPositionX = e1Rect.x - parentPosition.x
133 | const e1ActualPositionY = e1Rect.y - parentPosition.y
134 | const e2ActualPositionX = e2Rect.x - parentPosition.x
135 | const e2ActualPositionY = e2Rect.y - parentPosition.y
136 |
137 | if (e1ActualPositionY < e2ActualPositionY) {
138 | return -1
139 | } else if (e1ActualPositionY > e2ActualPositionY) {
140 | return 1
141 | } else if (e1ActualPositionX < e2ActualPositionX) {
142 | return -1
143 | } else {
144 | return 1
145 | }
146 | }
147 | function getTopElement(e1, e2) {
148 | // e1 -1, e2 1, same 0
149 | if (e1 === e2) return 0
150 |
151 | let result = checkZIndex(e1, e2)
152 | if (result !== 0) return result
153 |
154 | const e1Position = window.getComputedStyle(e1).position
155 | const e2Position = window.getComputedStyle(e2).position
156 | if (e1Position === 'absolute' || e2Position === 'absolute') {
157 | result = checkPosition(e1, e2)
158 | } else {
159 | result = e1.compareDocumentPosition(e2) & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1
160 | }
161 | return result
162 | }
163 |
164 | function getAllChildElements(node) {
165 | const result = []
166 | const stack = [node]
167 | while (stack.length) {
168 | const current = stack.pop()
169 | for (const node of current.querySelectorAll('*')) {
170 | result.push(node)
171 | if (node.shadowRoot) {
172 | stack.push(node.shadowRoot)
173 | }
174 | }
175 | }
176 | return result
177 | }
178 |
179 | async function searchImageFromTree(dom, mouseX, mouseY) {
180 | if (!dom) return null
181 |
182 | let root = dom
183 | let prevSibling = root.previousElementSibling
184 | let nextSibling = root.nextElementSibling
185 |
186 | let rootClassList = root.classList.toString()
187 | let prevClassList = prevSibling && prevSibling.classList.toString()
188 | let nextClassList = nextSibling && nextSibling.classList.toString()
189 |
190 | let hasSameKindSibling = false
191 | hasSameKindSibling ||= prevSibling ? prevClassList === rootClassList || prevSibling.tagName === root.tagName : false
192 | hasSameKindSibling ||= nextSibling ? nextClassList === rootClassList || nextSibling.tagName === root.tagName : false
193 | while (!hasSameKindSibling && root.parentElement) {
194 | root = root.parentElement
195 | prevSibling = root.previousElementSibling
196 | nextSibling = root.nextElementSibling
197 |
198 | rootClassList = root.classList.toString()
199 | prevClassList = prevSibling && prevSibling.classList.toString()
200 | nextClassList = nextSibling && nextSibling.classList.toString()
201 |
202 | hasSameKindSibling ||= prevSibling ? prevClassList === rootClassList || prevSibling.tagName === root.tagName : false
203 | hasSameKindSibling ||= nextSibling ? nextClassList === rootClassList || nextSibling.tagName === root.tagName : false
204 | }
205 |
206 | const relatedDomList = []
207 | const childList = getAllChildElements(root)
208 | for (const dom of childList) {
209 | const hidden = dom.offsetParent === null && dom.style.position !== 'fixed'
210 | if (hidden) {
211 | relatedDomList.push(dom)
212 | continue
213 | }
214 | const rect = dom.getBoundingClientRect()
215 | const inside = rect.left <= mouseX && rect.right >= mouseX && rect.top <= mouseY && rect.bottom >= mouseY
216 | if (inside) relatedDomList.push(dom)
217 | }
218 |
219 | const imageInfoList = []
220 | for (const dom of relatedDomList) {
221 | const imageInfo = await extractImageInfo(dom)
222 | if (isImageInfoValid(imageInfo)) imageInfoList.push(imageInfo)
223 | }
224 | if (imageInfoList.length === 0) {
225 | return childList.length < 5 ? searchImageFromTree(root.parentElement, mouseX, mouseY) : null
226 | }
227 | if (imageInfoList.length === 1) return imageInfoList[0]
228 | const filteredImageInfoList = imageInfoList.filter(info => info[2].tagName === 'IMG')
229 | if (filteredImageInfoList.length === 1) return filteredImageInfoList[0]
230 |
231 | imageInfoList.sort((a, b) => {
232 | if (a[2].tagName === 'IMG' && b[2].tagName === 'IMG') return a[2].compareDocumentPosition(b[2]) & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1
233 | if (a[2].tagName !== 'IMG' && b[2].tagName !== 'IMG') return getTopElement(a[2], b[2])
234 | if (a[2].tagName === 'IMG') return -1
235 | if (b[2].tagName === 'IMG') return 1
236 | return 0
237 | })
238 | const first = imageInfoList[0]
239 | const second = imageInfoList[1]
240 | const check = await isNewImageInfoBetter(first, second, mouseX, mouseY)
241 | return check ? first : second
242 | }
243 |
244 | // utility
245 | function extractNodeStyle(nodeStyle) {
246 | const backgroundImage = nodeStyle.backgroundImage
247 | if (backgroundImage === 'none') return null
248 | const bgList = backgroundImage.split(', ').filter(bg => bg.startsWith('url') && !bg.endsWith('.svg")'))
249 | if (bgList.length === 0) return null
250 | return bgList[0].slice(5, -2)
251 | }
252 | function getBackgroundURL(dom) {
253 | const nodeURL = extractNodeStyle(window.getComputedStyle(dom))
254 | if (nodeURL) return nodeURL
255 | const beforeURL = extractNodeStyle(window.getComputedStyle(dom, '::before'))
256 | if (beforeURL) return beforeURL
257 | const afterURL = extractNodeStyle(window.getComputedStyle(dom, '::after'))
258 | if (afterURL) return afterURL
259 | return null
260 | }
261 | async function extractBackgroundInfo(dom, minSize) {
262 | const bgUrl = getBackgroundURL(dom)
263 | if (!bgUrl) return null
264 | const realMinSize = Math.min(minSize, await getImageRealSize(bgUrl))
265 | return [bgUrl, realMinSize, dom]
266 | }
267 | async function extractImageInfo(dom) {
268 | const {width, height} = dom.getBoundingClientRect()
269 | if (dom.tagName === 'IMG') {
270 | // real time size and rendered size
271 | const sizeList = [width, height, dom.clientWidth, dom.clientHeight, dom.naturalWidth, dom.naturalHeight]
272 | const minSize = Math.min(...sizeList.filter(Boolean))
273 | return [dom.currentSrc, minSize, dom]
274 | }
275 | const minSize = Math.min(width, height, dom.clientWidth, dom.clientHeight)
276 | if (dom.tagName === 'VIDEO' && dom.hasAttribute('poster')) {
277 | return [dom.poster, minSize, dom]
278 | }
279 | const bgInfo = extractBackgroundInfo(dom, minSize)
280 | return bgInfo
281 | }
282 | async function extractImageInfoFromTree(dom) {
283 | const domInfo = await extractImageInfo(dom)
284 | if (domInfo) return domInfo
285 |
286 | const allChildren = dom.querySelectorAll('*')
287 | if (allChildren.length < 5) {
288 | for (const children of allChildren) {
289 | const info = await extractImageInfo(children)
290 | if (info) return info
291 | }
292 | }
293 | return null
294 | }
295 |
296 | const isImageInfoValid = imageInfo => imageInfo !== null && imageInfo[0] !== '' && imageInfo[0] !== 'about:blank'
297 | const isNewImageInfoBetter = async (newInfo, oldInfo, mouseX, mouseY) => {
298 | if (oldInfo === null) return true
299 | // data url
300 | const newUrl = newInfo[0]
301 | const oldUrl = oldInfo[0]
302 | if (newUrl.startsWith('data')) return false
303 | if (oldUrl.startsWith('data')) return true
304 | // svg image
305 | const newIsSvg = newUrl.startsWith('data:image/svg') || newUrl.includes('.svg')
306 | const oldIsSvg = oldUrl.startsWith('data:image/svg') || oldUrl.includes('.svg')
307 | if (oldIsSvg && !newIsSvg) return true
308 | // element type
309 | const oldIsImage = oldInfo[2].tagName === 'IMG' || oldInfo[2].tagName === 'VIDEO'
310 | const newIsImage = newInfo[2].tagName === 'IMG' || newInfo[2].tagName === 'VIDEO'
311 | // placeholder
312 | const oldIsPlaceholder = oldInfo[1] < 10
313 | if (oldIsImage && !newIsImage && !oldIsPlaceholder) return false
314 | // partial background
315 | if (!oldIsImage && newIsImage && !oldIsPlaceholder) {
316 | const bgPos = window.getComputedStyle(oldInfo[2]).backgroundPosition
317 | const isPartialBackground = bgPos.split('px').map(Number).some(Boolean)
318 | return isPartialBackground ? newInfo[1] >= oldInfo[1] : true
319 | }
320 | // mouse position
321 | const newRect = newInfo[2].getBoundingClientRect()
322 | const oldRect = oldInfo[2].getBoundingClientRect()
323 | const newOffset = [mouseX - newRect.left - newRect.width / 2, mouseY - newRect.top - newRect.height / 2]
324 | const oldOffset = [mouseX - oldRect.left - oldRect.width / 2, mouseY - oldRect.top - oldRect.height / 2]
325 | const newDist = Math.sqrt(newOffset[0] ** 2 + newOffset[1] ** 2)
326 | const oldDist = Math.sqrt(oldOffset[0] ** 2 + oldOffset[1] ** 2)
327 | if (newDist > oldDist + 50) return false
328 | // size check
329 | const asyncList = [[newUrl, oldUrl].map(getImageBitSize), [newUrl, oldUrl].map(getImageRealSize)].flat()
330 | const [newBitSize, oldBitSize, newRealSize, oldRealSize] = await Promise.all(asyncList)
331 | if (newBitSize * oldBitSize !== 0) {
332 | return newBitSize / newRealSize > oldBitSize / oldRealSize
333 | }
334 | return newRealSize > oldRealSize
335 | }
336 |
337 | return {
338 | searchDomByPosition: async function (elementList, mouseX, mouseY) {
339 | let firstVisibleDom = null
340 | let imageInfoFromPoint = null
341 | let imageDomLayer = 0
342 |
343 | let hiddenImageInfoFromPoint = null
344 | let hiddenDomLayer = 0
345 |
346 | const maxTry = Math.min(20, elementList.length)
347 | let index = 0
348 | let tryCount = 0
349 | while (tryCount < maxTry) {
350 | const dom = elementList[index]
351 | const visible = dom.offsetParent !== null || dom.style.position === 'fixed'
352 | firstVisibleDom ??= visible ? dom : null
353 | const imageInfo = await (!imageInfoFromPoint ? extractImageInfoFromTree(dom) : extractImageInfo(dom))
354 | const valid = isImageInfoValid(imageInfo)
355 | if (!valid) {
356 | index++
357 | tryCount++
358 | continue
359 | }
360 | if (!visible) {
361 | hiddenImageInfoFromPoint = imageInfo
362 | hiddenDomLayer = index++
363 | tryCount = Math.max(maxTry - 5, ++tryCount)
364 | continue
365 | }
366 | const better = await isNewImageInfoBetter(imageInfo, imageInfoFromPoint, mouseX, mouseY)
367 | if (better) {
368 | imageInfoFromPoint = imageInfo
369 | imageDomLayer = index
370 | const url = imageInfoFromPoint[0]
371 | const svgImg = url.startsWith('data:image/svg') || url.includes('.svg')
372 | if (!svgImg) tryCount = Math.max(maxTry - 5, tryCount)
373 | }
374 | index++
375 | tryCount++
376 | }
377 |
378 | if (imageInfoFromPoint) {
379 | console.log(`Image node found, layer ${imageDomLayer}`)
380 | return imageInfoFromPoint
381 | }
382 |
383 | if (hiddenImageInfoFromPoint) {
384 | console.log(`Hidden image node found, layer ${hiddenDomLayer}`)
385 | return hiddenImageInfoFromPoint
386 | }
387 |
388 | const imageInfoFromTree = await searchImageFromTree(firstVisibleDom, mouseX, mouseY)
389 | if (isImageInfoValid(imageInfoFromTree)) {
390 | console.log('Image node found, hide under dom tree')
391 | return imageInfoFromTree
392 | }
393 |
394 | return null
395 | }
396 | }
397 | })()
398 |
399 | function deepGetElementFromPoint(x, y) {
400 | function createTravelTask(root) {
401 | // lazy evaluation
402 | return () => {
403 | const queue = []
404 | const elementList = root.elementsFromPoint(x, y)
405 | for (const element of elementList) {
406 | if (visited.has(element) || visited.has(element.shadowRoot)) continue
407 | if (element.shadowRoot) {
408 | visited.add(element.shadowRoot)
409 | queue.push(createTravelTask(element.shadowRoot))
410 | } else {
411 | visited.add(element)
412 | queue.push(element)
413 | }
414 | }
415 | return queue
416 | }
417 | }
418 |
419 | const result = []
420 | const visited = new Set()
421 | const queue = [createTravelTask(document)]
422 | while (queue.length) {
423 | const current = queue.shift()
424 | if (typeof current === 'function') {
425 | queue.unshift(...current())
426 | } else {
427 | result.push(current)
428 | }
429 | }
430 | return result
431 | }
432 | const getOrderedElement = (function () {
433 | return disableHoverCheck
434 | ? (mouseX, mouseY) => {
435 | // lock pointer event back to auto
436 | document.documentElement.classList.add('iv-worker-checking')
437 | // get all elements include hover
438 | const elementsBeforeDisableHover = deepGetElementFromPoint(mouseX, mouseY)
439 | // reset pointer event as default
440 | document.documentElement.classList.remove('iv-worker-checking')
441 | return elementsBeforeDisableHover
442 | }
443 | : async (mouseX, mouseY) => {
444 | // lock pointer event back to auto
445 | document.documentElement.classList.add('iv-worker-checking')
446 | // get all elements include hover
447 | const elementsBeforeDisableHover = deepGetElementFromPoint(mouseX, mouseY)
448 | // reset pointer event as default
449 | document.documentElement.classList.remove('iv-worker-checking')
450 |
451 | // disable hover
452 | const mouseLeaveEvent = new Event('mouseleave')
453 | for (const element of elementsBeforeDisableHover) {
454 | element.classList.add('disable-hover')
455 | element.dispatchEvent(mouseLeaveEvent)
456 | }
457 | // release priority and allow other script clear up hover element
458 | await new Promise(resolve => setTimeout(resolve, 10))
459 | // clean up css
460 | for (const element of elementsBeforeDisableHover) {
461 | element.classList.remove('disable-hover')
462 | }
463 | // get all non hover elements
464 | const elementsAfterDisableHover = deepGetElementFromPoint(mouseX, mouseY)
465 |
466 | const stableElements = []
467 | const unstableElements = []
468 | for (const elem of elementsBeforeDisableHover) {
469 | if (elementsAfterDisableHover.includes(elem)) {
470 | stableElements.push(elem)
471 | } else {
472 | unstableElements.push(elem)
473 | }
474 | }
475 | const orderedElements = stableElements.concat(unstableElements)
476 | return orderedElements
477 | }
478 | })()
479 | async function getImageNodeInfo(mouseX, mouseY) {
480 | if (document.body.classList.contains('iv-attached')) return null
481 | const orderedElements = await getOrderedElement(mouseX, mouseY)
482 | const imageNodeInfo = await domSearcher.searchDomByPosition(orderedElements, mouseX, mouseY)
483 | return imageNodeInfo
484 | }
485 |
486 | const markingDom = (function () {
487 | return window.top === window.self
488 | ? dom => {
489 | window.ImageViewerLastDom = dom
490 | }
491 | : () => safeSendMessage('reset_dom')
492 | })()
493 |
494 | document.addEventListener(
495 | 'contextmenu',
496 | async e => {
497 | window.ImageViewerLastDom = undefined
498 |
499 | // release priority and allow contextmenu work properly
500 | await new Promise(resolve => setTimeout(resolve, 0))
501 | const imageNodeInfo = await getImageNodeInfo(e.clientX, e.clientY)
502 | if (imageNodeInfo === null) {
503 | markingDom(null)
504 | return
505 | }
506 | markingDom(imageNodeInfo[2])
507 |
508 | // display image dom
509 | console.log(imageNodeInfo.pop())
510 |
511 | safeSendMessage({msg: 'update_info', data: imageNodeInfo})
512 | },
513 | true
514 | )
515 | })()
516 |
--------------------------------------------------------------------------------
/scripts/download-images.js:
--------------------------------------------------------------------------------
1 | ;(function () {
2 | 'use strict'
3 |
4 | const safeSendMessage = function (...args) {
5 | if (chrome.runtime?.id) {
6 | return chrome.runtime.sendMessage(...args)
7 | }
8 | }
9 |
10 | // zip
11 | function generateCRCTable() {
12 | const crcTable = new Uint32Array(256)
13 | const polynomial = 0xedb88320 // CRC-32 polynomial
14 |
15 | for (let i = 0; i < 256; i++) {
16 | let crc = i
17 | crc = crc & 1 ? (crc >>> 1) ^ polynomial : crc >>> 1
18 | crc = crc & 1 ? (crc >>> 1) ^ polynomial : crc >>> 1
19 | crc = crc & 1 ? (crc >>> 1) ^ polynomial : crc >>> 1
20 | crc = crc & 1 ? (crc >>> 1) ^ polynomial : crc >>> 1
21 | crc = crc & 1 ? (crc >>> 1) ^ polynomial : crc >>> 1
22 | crc = crc & 1 ? (crc >>> 1) ^ polynomial : crc >>> 1
23 | crc = crc & 1 ? (crc >>> 1) ^ polynomial : crc >>> 1
24 | crc = crc & 1 ? (crc >>> 1) ^ polynomial : crc >>> 1
25 | crcTable[i] = crc
26 | }
27 | return crcTable
28 | }
29 | function calculateCRC32(data) {
30 | const crcTable = generateCRCTable()
31 | let crc = 0xffffffff // Initial CRC value
32 |
33 | for (let i = 0; i < data.length; i++) {
34 | const byte = data[i]
35 | crc = (crc >>> 8) ^ crcTable[(crc ^ byte) & 0xff]
36 | }
37 |
38 | crc ^= 0xffffffff // Final XOR value
39 | const crcBytes = new Uint8Array(4)
40 | crcBytes[0] = (crc >> 24) & 0xff
41 | crcBytes[1] = (crc >> 16) & 0xff
42 | crcBytes[2] = (crc >> 8) & 0xff
43 | crcBytes[3] = crc & 0xff
44 |
45 | return crcBytes
46 | }
47 |
48 | function buildLocalFileHeader(filename, data) {
49 | const crc32 = calculateCRC32(data)
50 | const compressedSize = data.length
51 | const encoder = new TextEncoder()
52 | const filenameBytes = encoder.encode(filename)
53 | const filenameLength = filenameBytes.length
54 |
55 | // Construct the local file header
56 | const localFileHeader = new Uint8Array(30 + filenameLength + compressedSize)
57 | const view = new DataView(localFileHeader.buffer)
58 |
59 | // Little-endian byte order
60 | view.setUint32(0, 0x04034b50, true) // Local file header signature
61 | view.setUint16(4, 20, true) // Version needed to extract (minimum)
62 | view.setUint16(6, 0, true) // No special flags
63 | view.setUint16(8, 0, true) // No compression
64 | view.setUint16(10, 0, true) // Placeholder for modification time (not used)
65 | view.setUint16(12, 0, true) // Placeholder for modification date (not used)
66 | view.setUint32(14, crc32, true) // CRC-32 of uncompressed data
67 | view.setUint32(18, compressedSize, true) // Compressed size
68 | view.setUint32(22, compressedSize, true) // Uncompressed size
69 | view.setUint16(26, filenameLength, true) // File name length
70 | view.setUint16(28, 0, true) // No extra fields
71 | localFileHeader.set(filenameBytes, 30) // File name
72 | localFileHeader.set(data, 30 + filenameLength) // File data
73 |
74 | return localFileHeader
75 | }
76 | function buildCentralDirectory(localFileHeader, offset) {
77 | const headerView = new DataView(localFileHeader.buffer)
78 | const filenameLength = headerView.getUint16(26, true)
79 | const headerData = localFileHeader.subarray(4, 30)
80 | const fileName = localFileHeader.subarray(30, 30 + filenameLength)
81 |
82 | // Construct the central directory entry
83 | const centralDirectoryEntry = new Uint8Array(46 + filenameLength)
84 | const view = new DataView(centralDirectoryEntry.buffer)
85 |
86 | // Little-endian byte order
87 | view.setUint32(0, 0x02014b50, true) // Central directory file header signature
88 | view.setUint16(4, 20, true) // Version made by
89 | centralDirectoryEntry.set(headerData, 6) // CDE 6-32 = LFH 4-30
90 | view.setUint16(32, 0, true) // No file comment
91 | view.setUint16(34, 0, true) // Disk number start
92 | view.setUint16(36, 0, true) // Internal file attributes
93 | view.setUint32(38, 0, true) // External file attributes
94 | view.setUint32(42, offset, true) // Relative offset of local file header
95 | centralDirectoryEntry.set(fileName, 46) // File name
96 |
97 | return centralDirectoryEntry
98 | }
99 |
100 | function buildZip(localFileHeaderList) {
101 | const centralDirectoryList = []
102 | let centralOffset = 0
103 |
104 | // Build central directory entries
105 | for (let i = 0; i < localFileHeaderList.length; i++) {
106 | const localFileHeader = localFileHeaderList[i]
107 | const centralDirectoryEntry = buildCentralDirectory(localFileHeader, centralOffset)
108 | centralDirectoryList.push(centralDirectoryEntry)
109 | centralOffset += localFileHeader.length
110 | }
111 |
112 | // Calculate the size of the central directory
113 | const centralDirectorySize = centralDirectoryList.reduce((total, entry) => total + entry.length, 0)
114 |
115 | // Build the end of central directory record
116 | const endOfCentralDirectoryRecord = new Uint8Array(22)
117 | const view = new DataView(endOfCentralDirectoryRecord.buffer)
118 |
119 | view.setUint32(0, 0x06054b50, true) // End of central directory signature
120 | view.setUint16(4, 0, true) // Number of this disk
121 | view.setUint16(6, 0, true) // Disk where central directory starts
122 | view.setUint16(8, localFileHeaderList.length, true) // Number of central directory records on this disk
123 | view.setUint16(10, localFileHeaderList.length, true) // Total number of central directory records
124 | view.setUint32(12, centralDirectorySize, true) // Size of central directory
125 | view.setUint32(16, centralOffset, true) // Offset of start of central directory
126 | view.setUint16(20, 0, true) // No comment
127 |
128 | // Combine all the components into the final zip file
129 | const zipSize = centralOffset + centralDirectorySize + endOfCentralDirectoryRecord.length
130 | const zipFile = new Uint8Array(zipSize)
131 |
132 | let offset = 0
133 | for (const localFileHeader of localFileHeaderList) {
134 | zipFile.set(localFileHeader, offset)
135 | offset += localFileHeader.length
136 | }
137 |
138 | for (const centralDirectoryEntry of centralDirectoryList) {
139 | zipFile.set(centralDirectoryEntry, offset)
140 | offset += centralDirectoryEntry.length
141 | }
142 |
143 | zipFile.set(endOfCentralDirectoryRecord, offset)
144 |
145 | return zipFile
146 | }
147 |
148 | // utility
149 | function getUserSelection(length) {
150 | if (length === 1) return [true]
151 |
152 | const userSelection = prompt("Images to Download: eg. '1-5, 8, 11-13'", `1-${length}`)
153 | if (!userSelection) return null
154 |
155 | const input = userSelection.replaceAll(' ', '')
156 | const regex = /^\d+(?:-\d+)?(?:,\d+(?:-\d+)?)*$/
157 | if (!regex.test(input)) {
158 | alert('Invalid selection.')
159 | return null
160 | }
161 |
162 | const result = new Array(length).fill(false)
163 | const processedInput = input.split(',')
164 | for (const part of processedInput) {
165 | if (part.includes('-')) {
166 | const [start, end] = part
167 | .split('-')
168 | .map(n => Math.min(length, Math.max(1, Number(n))) - 1)
169 | .sort((a, b) => a - b)
170 | for (let i = start; i <= end; i++) {
171 | result[i] = true
172 | }
173 | } else {
174 | const index = Math.min(length, Math.max(1, Number(part))) - 1
175 | result[index] = true
176 | }
177 | }
178 |
179 | return result
180 | }
181 | function getImageBinary(url) {
182 | return fetch(url)
183 | .then(response => response.arrayBuffer())
184 | .then(arrayBuffer => new Uint8Array(arrayBuffer))
185 | .catch(async () => {
186 | const [dataUrl] = await safeSendMessage({msg: 'request_cors_url', url: url})
187 | const res = await fetch(dataUrl)
188 | const rawArray = await res.arrayBuffer()
189 | return new Uint8Array(rawArray)
190 | })
191 | }
192 |
193 | // main
194 | async function main() {
195 | const imageList = ImageViewer('get_image_list')
196 | if (imageList.length === 0) return
197 |
198 | const imageUrlList = imageList.map(img => img.src)
199 | const selectionRange = getUserSelection(imageUrlList.length)
200 | if (selectionRange === null) return
201 |
202 | const selectedUrlList = imageUrlList.map((v, i) => [v, i]).filter(item => selectionRange[item[1]])
203 | if (selectedUrlList.length === 0) return
204 |
205 | const imageBinaryList = await Promise.all(selectedUrlList.map(async item => [await getImageBinary(item[0]), item[1]]))
206 |
207 | const localFileHeaderList = []
208 | for (const [data, index] of imageBinaryList) {
209 | const indexString = ('0000' + (index + 1)).slice(-5)
210 | const url = imageUrlList[index]
211 | const name = url.startsWith('data') ? '' : '_' + url.split('?')[0].split('/').at(-1)
212 | const extension = name.includes('.') ? '' : '.jpg'
213 | const filename = indexString + name + extension
214 |
215 | const localFileHeader = buildLocalFileHeader(filename, data)
216 | localFileHeaderList.push(localFileHeader)
217 | }
218 | const zip = buildZip(localFileHeaderList)
219 | const blob = new Blob([zip.buffer])
220 |
221 | const a = document.createElement('a')
222 | a.href = URL.createObjectURL(blob, 'application/zip')
223 | a.download = `ImageViewer_${Date.now()}_${document.title}.zip`
224 | a.click()
225 | URL.revokeObjectURL(a.href)
226 | }
227 |
228 | main()
229 | })()
230 |
--------------------------------------------------------------------------------
/scripts/extract-iframe.js:
--------------------------------------------------------------------------------
1 | window.ImageViewerExtractor = (function () {
2 | 'use strict'
3 |
4 | const safeSendMessage = function (...args) {
5 | if (chrome.runtime?.id) {
6 | return chrome.runtime.sendMessage(...args)
7 | }
8 | }
9 |
10 | async function getSubFrameRedirectedHref() {
11 | const subFrame = document.getElementsByTagName('iframe')
12 | const subFrameHref = [...subFrame].map(iframe => iframe.src)
13 | const subFrameRedirectedHref = subFrameHref.length ? await safeSendMessage({msg: 'get_redirect', data: subFrameHref}) : []
14 | return subFrameRedirectedHref
15 | }
16 |
17 | function isNodeSizeEnough(node, minWidth, minHeight) {
18 | const widthAttr = node.getAttribute('iv-width')
19 | const heightAttr = node.getAttribute('iv-height')
20 | if (widthAttr && heightAttr) {
21 | const width = Number(widthAttr)
22 | const height = Number(heightAttr)
23 | return width >= minWidth && height >= minHeight
24 | }
25 | const {width, height} = node.getBoundingClientRect()
26 | if (width === 0 || height === 0) {
27 | node.setAttribute('no-bg', '')
28 | return false
29 | }
30 | node.setAttribute('iv-width', width)
31 | node.setAttribute('iv-height', height)
32 | return width >= minWidth && height >= minHeight
33 | }
34 | function deepQuerySelectorAll(target, selector) {
35 | const result = []
36 | const stack = [target]
37 | const visited = []
38 | while (stack.length) {
39 | const current = stack.pop()
40 | // check shadowRoot
41 | for (const node of current.querySelectorAll('*:not([no-shadow])')) {
42 | if (node.shadowRoot) {
43 | stack.push(node.shadowRoot)
44 | } else {
45 | visited.push(node)
46 | }
47 | }
48 | result.push(...current.querySelectorAll(selector))
49 | }
50 | for (const node of visited) {
51 | node.setAttribute('no-shadow', '')
52 | }
53 | return result
54 | }
55 | function getImageList(options) {
56 | const minWidth = options.minWidth || 0
57 | const minHeight = options.minHeight || 0
58 | const imageList = []
59 |
60 | const rawImageList = deepQuerySelectorAll(document.body, 'img')
61 | for (const img of rawImageList) {
62 | // only client size should be checked in order to bypass large icon or hidden image
63 | const {width, height} = img.getBoundingClientRect()
64 | if ((width >= minWidth && height >= minHeight) || img === window.ImageViewerLastDom) {
65 | // currentSrc might be empty during unlazy or update
66 | const imgSrc = img.currentSrc || img.src
67 | imageList.push(imgSrc)
68 | }
69 | }
70 |
71 | const videoList = deepQuerySelectorAll(document.body, 'video[poster]')
72 | for (const video of videoList) {
73 | const {width, height} = video.getBoundingClientRect()
74 | if (width >= minWidth && height >= minHeight) {
75 | imageList.push(video.poster)
76 | }
77 | }
78 |
79 | const uncheckedNodeList = deepQuerySelectorAll(document.body, '*:not([no-bg])')
80 | if (!document.body.hasAttribute('no-bg')) uncheckedNodeList.push(document.body)
81 | for (const node of uncheckedNodeList) {
82 | if (!isNodeSizeEnough(node, minWidth, minHeight)) continue
83 | const attrUrl = node.getAttribute('iv-bg')
84 | if (attrUrl !== null) {
85 | imageList.push(attrUrl)
86 | continue
87 | }
88 | const nodeStyle = window.getComputedStyle(node)
89 | const backgroundImage = nodeStyle.backgroundImage
90 | if (backgroundImage === 'none') {
91 | node.setAttribute('no-bg', '')
92 | continue
93 | }
94 | const bgList = backgroundImage.split(', ').filter(bg => bg.startsWith('url') && !bg.endsWith('.svg")'))
95 | if (bgList.length === 0) {
96 | node.setAttribute('no-bg', '')
97 | continue
98 | }
99 | const url = bgList[0].slice(5, -2)
100 | node.setAttribute('iv-bg', url)
101 | imageList.push(url)
102 | }
103 |
104 | return options.svgFilter
105 | ? [...new Set(imageList)].filter(url => url !== '' && url !== 'about:blank' && !url.includes('.svg'))
106 | : [...new Set(imageList)].filter(url => url !== '' && url !== 'about:blank')
107 | }
108 | function getCanvasList(options) {
109 | const minWidth = options.minWidth || 0
110 | const minHeight = options.minHeight || 0
111 | const canvasList = []
112 |
113 | const rawCanvasList = deepQuerySelectorAll(document.body, 'canvas')
114 | for (const canvas of rawCanvasList) {
115 | const {width, height} = canvas.getBoundingClientRect()
116 | if (width >= minWidth && height >= minHeight) {
117 | const dataUrl = canvas.toDataURL()
118 | if (dataUrl === 'data:,') continue
119 | canvasList.push(dataUrl)
120 | }
121 | }
122 | return canvasList
123 | }
124 |
125 | return {
126 | extractImage: async function (options) {
127 | const subFrameRedirectedHref = await getSubFrameRedirectedHref()
128 | const imageList = options.canvasMode ? getCanvasList(options) : getImageList(options)
129 | return [location.href, subFrameRedirectedHref, imageList]
130 | }
131 | }
132 | })()
133 |
--------------------------------------------------------------------------------
/scripts/hook.js:
--------------------------------------------------------------------------------
1 | ;(function () {
2 | 'use strict'
3 |
4 | const safeSendMessage = function (...args) {
5 | if (chrome.runtime?.id) {
6 | return chrome.runtime.sendMessage(...args)
7 | }
8 | }
9 |
10 | // prevent image blob revoked
11 | const isImageUrlMap = new Map()
12 | const realCreate = URL.createObjectURL
13 | const realRevoke = URL.revokeObjectURL
14 | URL.createObjectURL = function (obj) {
15 | const url = realCreate(obj)
16 |
17 | if (!(obj instanceof Blob)) {
18 | isImageUrlMap.set(url, false)
19 | return url
20 | }
21 | if (obj.type.startsWith('image/')) {
22 | isImageUrlMap.set(url, true)
23 | return url
24 | }
25 | if (obj.size > 1024 * 1024 * 5 || (obj.type !== '' && obj.type !== 'application/octet-stream')) {
26 | isImageUrlMap.set(url, false)
27 | return url
28 | }
29 | const promise = new Promise(_resolve => {
30 | const resolve = result => {
31 | _resolve(result)
32 | isImageUrlMap.set(url, result)
33 | }
34 | const img = new Image()
35 | img.onload = () => resolve(true)
36 | img.onerror = () => resolve(false)
37 | img.src = url
38 | })
39 | isImageUrlMap.set(url, promise)
40 | return url
41 | }
42 | URL.revokeObjectURL = async function (url) {
43 | const isImage = await isImageUrlMap.get(url)
44 | if (!isImage) realRevoke(url)
45 | }
46 |
47 | // prevent canvas tainted
48 | async function getImageBase64(image) {
49 | try {
50 | const res = await fetch(image.src)
51 | if (res.ok) {
52 | const blob = await res.blob()
53 | const reader = new FileReader()
54 | const dataUrl = await new Promise(resolve => {
55 | reader.onload = () => resolve(reader.result)
56 | reader.readAsDataURL(blob)
57 | })
58 | return dataUrl
59 | }
60 | } catch (error) {}
61 | // wake up background
62 | while (true) {
63 | if (await safeSendMessage({msg: 'ping'})) break
64 | await new Promise(resolve => setTimeout(resolve, 50))
65 | }
66 | const [dataUrl] = await safeSendMessage({msg: 'request_cors_url', url: image.src})
67 | return dataUrl
68 | }
69 | async function getBase64Image(image) {
70 | const dataUrl = await getImageBase64(image)
71 | const dataImage = new Image()
72 | const result = await new Promise(resolve => {
73 | dataImage.onload = resolve(true)
74 | dataImage.onerror = resolve(false)
75 | dataImage.src = dataUrl
76 | })
77 | // return empty image on failure
78 | return result ? dataImage : new Image()
79 | }
80 | function checkCORS(image) {
81 | if (image.crossOrigin === 'anonymous') return false
82 | const canvas = document.createElement('canvas')
83 | canvas.width = 100
84 | canvas.height = 100
85 | const ctx = canvas.getContext('2d')
86 | realDrawImage.apply(ctx, [image, 0, 0])
87 | try {
88 | canvas.toDataURL('image/png')
89 | return false
90 | } catch (error) {}
91 | return true
92 | }
93 |
94 | const realDrawImage = CanvasRenderingContext2D.prototype.drawImage
95 | CanvasRenderingContext2D.prototype.drawImage = async function (...args) {
96 | if (args[0] instanceof HTMLImageElement) {
97 | this.canvas.cors = this.canvas.cors || checkCORS(args[0])
98 | if (this.canvas.cors) {
99 | args[0] = await getBase64Image(args[0])
100 | }
101 | }
102 | return realDrawImage.apply(this, args)
103 | }
104 | })()
105 |
--------------------------------------------------------------------------------
/version.txt:
--------------------------------------------------------------------------------
1 | 1.41 [2025-02-06]:
2 | Stability update
3 | This version was originally planned as a major update, but the development of new features was delayed
4 | 1. Added a hotkey command for canvas mode
5 | 2. Added support for background images in pseudo elements
6 | 3. Improved scroll unlazy logic
7 | 4. Improved right click image size referencing logic
8 | 5. Other bug fixes and improvements
9 | P.S. Starting with this version, the extension will also be published on addons.mozilla.org for Firefox users
10 |
11 | 1.40 [2024-10-14]:
12 | 1. Added a new option to allow users to disable image unlazy on specific domains
13 | 2. Introduced a web demo, enabling users to try the feature before installation
14 | 3. Updated the options page and added a simple support page
15 | 4. Fixed a bug in the new view canvas feature that caused issues on sites like Notion
16 |
17 | 1.39.1 [2024-10-01]:
18 | Patch Update
19 | 1. Fixed a bug in the new view canvas features that caused issues with some sites like Google Sheets
20 |
21 | 1.39 [2024-09-29]:
22 | Major Update
23 | 1. Added an action to the icon context menu allowing users to view canvas elements
24 | // Note: This feature only supports snapshots, not GIF creation
25 | // May also be useful for cases where an image is visible but not accessible in normal mode
26 | // This could include an image drawn on a canvas element
27 | 2. Added support for local and blob images to mainstream reverse search
28 | 3. Fixed navigation, it will now correctly wait for images to be rendered on the screen
29 | 4. The space bar can now be used to send a middle click to an image (previously only "0" could be used)
30 | 5. Other bug fixes and improvements
31 |
32 | 1.38 [2024-09-09]:
33 | Stability update
34 | 1. Added support for data URL images to mainstream reverse search
35 | 2. Fixed a bug that could change the website's default layout
36 | 3. Fixed a bug that could toggle the website's default hotkeys (eg. page navigation)
37 | 4. Other bug fixes and improvements
38 |
39 | 1.37 [2024-08-11]:
40 | Performance and Stability update
41 | 1. The control panel will now auto hide after 1.5 seconds of mouse hover
42 | // move cursor over buttons will toggle the panel again
43 | // provides clearer view when using scroll to view image
44 | 2. Improved image viewer's logic for build/update image list
45 | 3. Refactored image collection logic to enhance stability of the image list
46 | 4. Rewritten auto scroll logic to ensure no images are skipped
47 | 5. Enhanced code quality
48 | 6. Other bug fixes and improvements
49 |
50 | 1.36 [2024-07-22]:
51 | Major Update
52 | 1. Added a hotkey for auto navigation (shift + arrow keys)
53 | 2. Added ton of code to support of custom element
54 | 3. Add sub-image check to improve image unlazy in url mode
55 | 4. Improve and refactor iframe image extraction logic
56 | 5. Improve CSS and layout of the image viewer
57 | 6. Refactor data structure for image info
58 | 7. Other bug fixes and improvements
59 |
60 | 1.35 [2024-07-03]:
61 | 1. Reduced zoom & rotate transition flash
62 | 2. Improved auto update logic
63 | 3. Enhanced ability to find larger size raw images
64 | 4. Reworked unlazy logic, no longer need to wait when reopening within a short time
65 | 5. Reworked iframe logic, can now handle iframe in iframe cases
66 | 6. Fixed a bug that changed current index after image list update
67 | 7. Other bug fixes and improvements
68 |
69 | 1.34 [2024-04-11]:
70 | Stability update
71 | 1. Prevented image loading flash in URL mode
72 | 2. Add smooth transition for image transform
73 | 3. Fixed a bug where AltGraph could not be used with Ctrl in hotkey combinations
74 | // related hotkey: image transformation and image reverse search
75 | 4. Improved code performance
76 | 5. Implemented error handling to minimize minor errors displayed to users
77 | 6. Added support for new type of unlazy (simulate mouse hover)
78 | 7. Other bug fixes and improvements
79 |
80 | 1.33 [2024-01-15]:
81 | Functional Update
82 | 1. Added a new default fit mode option: "Original size (does not exceed window)"
83 | 2. Added a maximum size limit (3x) for other fit modes to prevent enlarging small images too much
84 | 3. Added a new hotkey (Shift + B) for switching the background color: transparent -> black -> white
85 | 4. Added new hotkeys for image transformation:
86 | - Move: Ctrl + Alt + ↑↓←→ / WASD
87 | - Zoom: Alt + ↑↓ / WS
88 | - Rotate: Alt + ←→ / AD
89 | 5. Improved auto-scrolling
90 | 6. Added support for more edge cases
91 | 7. Other bug fixes and improvements
92 |
93 | 1.32 [2023-12-31]:
94 | 1. Improved accuracy of image middle click redirect
95 | 2. Enhanced size filter referencing of picking an image by right click
96 | 3. Solve CSS issues related to lazy images on some websites
97 | 4. Added support for the embed element
98 | 5. Refactored and improved code logic
99 | 6. Other bug fixes and improvements
100 |
101 | 1.31 [2023-10-22]:
102 | 1. Rotation now rotates around the center of the viewpoint
103 | 2. Auto scroll hotkey will toggle auto scroll instead of just starting it
104 | 3. Navigation with "WASD" is now supported
105 | 4. Support fast navigation by pressing the Ctrl key at the same time to activate it
106 | 5. Support memory of last image when restarting in page mode
107 | 6. Enhanced code quality
108 | 7. Other bug fixes and improvements
109 |
110 | 1.30 [2023-08-27]:
111 | Stability update
112 | 1. Improved SVG filtering
113 | 2. Added support for multiple layers unlazy
114 | 3. Enhanced logic for getting raw image URL
115 | 4. Added support for more edge cases
116 | 5. Other bug fixes and improvements
117 |
118 | 1.29 [2023-08-08]:
119 | 1. Corrected code related to the service worker lifecycle
120 | 2. Enhanced unlazy logic to handle additional cases
121 | 3. Improved logic for updating the size filter when there are images of the same kind as the picked image
122 | 4. Enhanced the user experience on auto scroll
123 | 5. Numerous bug fixes and minor improvements
124 |
125 | 1.28 [2023-07-10]:
126 | Major Update
127 | 1. Added a hotkey to manually enable auto scroll
128 | 2. Added a hotkey to download images collected by image viewer
129 | // Note: This extension is not a resource downloader
130 | // Download functionality is limited to basic features
131 | // eg. selecting a download range and packaging in a zip file
132 | 3. Improved first display time of image viewer
133 | 4. Improved middle-click redirect to open the original image's hyperlink
134 | 5. Improved correctness of right click image pickup
135 | 6. Other bug fixes and improvements
136 |
137 | 1.27 [2023-07-06]:
138 | 1. Improved image selection, decrease the priority of image placeholder and image sprite
139 | 2. Improved border display after using moveTo
140 | 3. Improved auto scrolling and auto update
141 | 4. Some bug fixes
142 |
143 | 1.26 [2023-06-17]:
144 | 1. Improved the logic of using middle click to open the link of current image
145 | 2. Fixed a bug that caused jumping in viewer index
146 | 3. Fixed a bug that prevented the image viewer from automatically starting for image URLs
147 | 4. Other bug fixes and improvements
148 |
149 | 1.25 [2023-06-04]:
150 | 1. AltGraph key now functions the same as Alt key in hotkey
151 | 2. More intuitive zoom, where zooming now occurs at the screen center instead of the image center
152 | 3. Fixed the incorrect position of the border display after the moveTo operation
153 | 4. Fixed a bug that caused a conflict in the scroll function
154 | 5. Added a check for iframes to handle a bug in Chrome
155 | 6. Removed code that caused extra rendering time
156 | 7. Added caching to enhance performance
157 | 8. Improved performance on right click image pickup
158 |
159 | 1.24 [2023-06-01]:
160 | 1. Introduces method for old style lazy image
161 | 2. Enhance the moveTo function and label border
162 | 3. Improve stability of the extension
163 | 4. Fix bugs and improve performance
164 |
165 | 1.23 [2023-05-29]:
166 | 1. Add temporary image list storage
167 | 2. Significantly reduced startup time by approximately 3-10 times
168 | 3. Refine UI
169 | 4. Improve code logic
170 |
171 | 1.22 [2023-05-28]:
172 | 1. Support deeper-layer iframes
173 | 2. Enhance the moveTo function
174 | 3. Revamp border display following moveTo
175 | 4. Support additional edge cases
176 | 5. Improve performance and fix bugs
177 |
178 | 1.21 [2023-05-27]:
179 | 1. Fixed a bug when getting the image list, so it won't repeat the same image with different sizes
180 | 2. Fixed the "moveTo" button, now it functions correctly on websites like Instagram and Twitter
181 | 3. Fixed the image update, so it won't jump back to the first image when updating
182 | 4. Fixed a bug related to image looping, now it will wait for an image update when it reaches the end
183 |
184 | 1.20 [2023-05-26]:
185 | 1. Improved auto update and auto scroll
186 | 2. More stability on image file URLs
187 | 3. Added support for more iframe images
188 | 4. Improved performance and fixed bugs
189 |
190 | 1.19 [2023-05-14]:
191 | 1. Improve the stability of auto scroll
192 | 2. Improve the code logic for better performance
193 | 3. Fix a lot of bugs
194 |
195 | 1.18 [2023-05-04]:
196 | 1. Support auto scroll
197 | 2. Add options to enable auto scroll and disable hover check
198 | 3. Refactor code for better readability
199 | 4. Fix bug related to hover check and other minor bugs
200 |
201 | 1.17 [2023-04-30]:
202 | Stability update
203 | 1. Add some code to increase the stability
204 | 2. Add handle to more edge cases
205 | 3. Fix bugs
206 |
207 | 1.16 [2023-04-10]:
208 | 1. Image viewer now collects images after website adding new content
209 | // usually website update is toggled by scroll to the end of the page
210 | // you can archive it by scrolling on the scrollbar or press "End" key on keyboard
211 | // you may also use other "next page" script/extension
212 | 2. Fix issues for youtube thumbnail
213 | 3. Fix bugs related to last update
214 | 4. Refactor code to improving program structure
215 |
216 | 1.15 [2023-04-05]:
217 | Large Update
218 | 1. Add support on update image in the viewer
219 | 2. Solve the problem for image viewer can't be open on some websites
220 | 3. Fix CORS issues for iframe images
221 | 4. Fix other issues in rare situations
222 | 5. Improve performance and fix some bugs
223 |
224 | 1.14 [2023-04-01]:
225 | 1. Improve CSS of image viewer
226 | 2. Improve performance of right click image pickup
227 | 3. Add an icon image pre-check before unlazy image to improve performance
228 | 4. Enhance the method of getting image wrapper size
229 | 5. Bug fixes
230 |
231 | 1.13 [2023-03-18]:
232 | 1. Improve right click image pickup performance
233 | 2. Improve stability on image unlazy
234 | 3. Extend the loading time limit for images inside image viewer
235 | 4. Fix lot of typos and bugs
236 |
237 | 1.12 [2023-02-14]:
238 | 1. Add this popup page to show release notes when install or update
239 | 2. Improve stability
240 | 3. Add domain white list for image unlazy
241 | // create issues on github if you want to add domain to the list
242 | // may move to option page or just hide in source code
243 |
244 | 1.11 [2023-02-11]:
245 | 1. Images are now order by its real location
246 | 3. No longer use dataURL, ObjectURL is faster and better for the browser to render images
247 | 2. Min size filter will also considers wrapper of the selected image
248 | 4. Some website that disabled right click menu. Add "view last right click" in icon menu to handle it
249 |
250 | 1.10 [2023-02-11]:
251 | 1. Add MoveTo support for iframe images
252 | 2. Improve right click image pickup
253 | 3. Improve image check size method
254 |
255 | 1.9 [2023-01-13]:
256 | 1. Support image pickup using right click
257 | 2. Delay execution of worker script to improve performance
258 |
259 | 1.8 [2022-10-30]:
260 | 1. Improve the support of viewing images inside iframe
261 | 2. Refactor code to tidy up code related to iframe
262 |
263 | 1.7 [2022-10-04]:
264 | 1. Improve support on iframe images
265 | 2. Improve simpleUnlazyImage()
266 | 3. Add more keyboard shortcuts and svg filter in option
267 |
268 | 1.6 [2022-09-03]:
269 | 1. Support images inside iframe
270 | 2. Improve data transfer between content script and background
271 |
272 | 1.5 [2022-08-22]:
273 | 1. Renew simpleUnlazyImage()
274 | 2. Improve image-viewer.js
275 | 3. Support hotkey for reverse search image
276 |
277 | 1.4 [2022-08-10]:
278 | 1. Improve simpleUnlazyImage()
279 | 2. Support video element
280 | 3. Improve MoveTo button logic
281 | 4. Prevent input leak out from image viewer
282 | 5. Improve simpleUnlazyImage()
283 | 6. Add utility.js to separate utility function
284 |
285 | 1.3 [2022-07-01]:
286 | 1. Delay loading of image-viewer.js to improve performance
287 | 2. Add command support
288 | 3. Improve image unlazy
289 | 4. Renew activate image method to increase readability
290 |
291 | 1.2 [2022-07-01]:
292 | 1. Add simpleUnlazyImage() to unlazy image before getting image list
293 | 2. Change CSS to pin image viewer counter
294 |
295 | 1.1 [2022-07-01]:
296 | 1. Support mirror effect
297 | 2. Replace old transform method with matrix to improve performance
298 |
299 | 1.0 [2022-06-29]:
300 | First release on github
--------------------------------------------------------------------------------