1212// @description:ja 画像を強力に閲覧できるツール。ポップアップ表示、拡大・縮小、回転、一括保存などの機能を自動で実行できます
1313// @description:pt-BR Poderosa ferramenta de visualização de imagens on-line, que pode pop-up/dimensionar/girar/salvar em lote imagens automaticamente
1414// @description:ru Мощный онлайн-инструмент для просмотра изображений, который может автоматически отображать/масштабировать/вращать/пакетно сохранять изображения
15- // @version 2025.12.19 .1
15+ // @version 2026.1.5 .1
1616// @icon data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAMAAADXqc3KAAAAV1BMVEUAAAD////29vbKysoqKioiIiKysrKhoaGTk5N9fX3z8/Pv7+/r6+vk5OTb29vOzs6Ojo5UVFQzMzMZGRkREREMDAy4uLisrKylpaV4eHhkZGRPT08/Pz/IfxjQAAAAgklEQVQoz53RRw7DIBBAUb5pxr2m3/+ckfDImwyJlL9DDzQgDIUMRu1vWOxTBdeM+onApENF0qHjpkOk2VTwLVEF40Kbfj1wK8AVu2pQA1aBBYDHJ1wy9Cf4cXD5chzNAvsAnc8TjoLAhIzsBao9w1rlVTIvkOYMd9nm6xPi168t9AYkbANdajpjcwAAAABJRU5ErkJggg==
1717// @namespace https://github.com/hoothin/UserScripts
1818// @homepage https://pv.hoothin.com/
@@ -12055,6 +12055,186 @@ ImgOps | https://imgops.com/#b#`;
1205512055 }
1205612056 } else GM_fetch = fetch;
1205712057
12058+ class ImageSizeFetcher {
12059+ constructor(gmXhr, config = {}) {
12060+ if (!gmXhr) throw new Error("GM_xmlhttpRequest is required");
12061+ this.gmXhr = gmXhr;
12062+ this.cache = new Map();
12063+ this.queue = [];
12064+ this.activeCount = 0;
12065+ this.concurrency = config.concurrency || 3;
12066+ this.timeout = config.timeout || 5000;
12067+ }
12068+
12069+ async getSize(url, options = {}) {
12070+ const { force = false, strategy = 'auto' } = options;
12071+ if (!url) return null;
12072+
12073+ if (!force && this.cache.has(url)) {
12074+ return this.cache.get(url);
12075+ }
12076+
12077+ if (url.startsWith('data:')) {
12078+ const size = this._calculateBase64Size(url);
12079+ this.cache.set(url, size);
12080+ return size;
12081+ }
12082+
12083+ if (url.startsWith('blob:')) {
12084+ return this._getBlobSize(url);
12085+ }
12086+
12087+ if (strategy === 'local-only' || strategy === 'auto') {
12088+ const perfSize = this._getFromPerformance(url);
12089+ if (perfSize !== null) {
12090+ this.cache.set(url, perfSize);
12091+ return perfSize;
12092+ }
12093+ if (strategy === 'local-only') return null;
12094+ }
12095+
12096+ return this._scheduleRequest(url);
12097+ }
12098+
12099+ _calculateBase64Size(dataUrl) {
12100+ const base64Str = dataUrl.split(',')[1];
12101+ if (!base64Str) return 0;
12102+ const len = base64Str.length;
12103+ const padding = (base64Str.match(/=/g) || []).length;
12104+ return (len * 3 / 4) - padding;
12105+ }
12106+
12107+ async _getBlobSize(blobUrl) {
12108+ try {
12109+ const response = await fetch(blobUrl);
12110+ const blob = await response.blob();
12111+ return blob.size;
12112+ } catch (e) {
12113+ console.error('Blob fetch failed', e);
12114+ return null;
12115+ }
12116+ }
12117+
12118+ _getFromPerformance(url) {
12119+ const entries = performance.getEntriesByName(url);
12120+ if (entries.length > 0) {
12121+ const entry = entries[entries.length - 1];
12122+ if (entry.decodedBodySize > 0) return entry.decodedBodySize;
12123+ if (entry.encodedBodySize > 0) return entry.encodedBodySize;
12124+ }
12125+ return null;
12126+ }
12127+
12128+ _scheduleRequest(url) {
12129+ return new Promise((resolve, reject) => {
12130+ this.queue.push({ url, resolve, reject });
12131+ this._processQueue();
12132+ });
12133+ }
12134+
12135+ _processQueue() {
12136+ if (this.activeCount >= this.concurrency || this.queue.length === 0) {
12137+ return;
12138+ }
12139+
12140+ this.activeCount++;
12141+ const { url, resolve, reject } = this.queue.shift();
12142+
12143+ this._fetchByHead(url)
12144+ .then(size => {
12145+ if (size !== null) this.cache.set(url, size);
12146+ resolve(size);
12147+ })
12148+ .catch(err => {
12149+ console.warn(`Fetch size failed for ${url}`, err);
12150+ resolve(null);
12151+ })
12152+ .finally(() => {
12153+ this.activeCount--;
12154+ this._processQueue();
12155+ });
12156+ }
12157+
12158+ _fetchByHead(url) {
12159+ return new Promise((resolve, reject) => {
12160+ this.gmXhr({
12161+ method: "HEAD",
12162+ url: url,
12163+ timeout: this.timeout,
12164+ onload: (response) => {
12165+ const lenStr = response.responseHeaders.match(/content-length:\s*(\d+)/i);
12166+ if (lenStr && lenStr[1]) {
12167+ resolve(parseInt(lenStr[1], 10));
12168+ } else {
12169+ resolve(null);
12170+ }
12171+ },
12172+ onerror: (err) => reject(err),
12173+ ontimeout: () => reject(new Error("Timeout"))
12174+ });
12175+ });
12176+ }
12177+ }
12178+
12179+ function formatBytes(bytes) {
12180+ if (bytes === null || typeof bytes === "undefined" || isNaN(bytes) || bytes < 0) return "";
12181+ const units = ["B", "KB", "MB", "GB", "TB"];
12182+ let size = bytes;
12183+ let unitIndex = 0;
12184+ while (size >= 1024 && unitIndex < units.length - 1) {
12185+ size /= 1024;
12186+ unitIndex++;
12187+ }
12188+ const precision = size >= 100 ? 0 : (size >= 10 ? 1 : 2);
12189+ const value = size.toFixed(precision).replace(/\.0+$|(\.[0-9]*[1-9])0+$/, "$1");
12190+ return value + " " + units[unitIndex];
12191+ }
12192+
12193+ function updateGalleryHeaderSize(gallery, img, strategy) {
12194+ if (!gallery || !gallery.eleMaps || !gallery.eleMaps['head-left-img-info-size']) return;
12195+ const sizeSpan = gallery.eleMaps['head-left-img-info-size'];
12196+ sizeSpan.textContent = "";
12197+ sizeSpan.title = "";
12198+ if (!img || img.nodeName !== 'IMG') return;
12199+ const sizeUrl = img.src;
12200+ if (!sizeUrl) return;
12201+ imageSizeFetcher.getSize(sizeUrl, { strategy: strategy || 'auto' }).then(size => {
12202+ if (size === null) return;
12203+ if (gallery.img !== img || img.src !== sizeUrl) return;
12204+ if (img.naturalWidth && img.naturalHeight) {
12205+ gallery.eleMaps['head-left-img-info-resolution'].textContent = img.naturalWidth + " x " + img.naturalHeight;
12206+ }
12207+ const sizeText = formatBytes(size);
12208+ sizeSpan.textContent = "(" + sizeText + ")";
12209+ sizeSpan.title = sizeText;
12210+ });
12211+ }
12212+
12213+ function updateViewmoreSizeLabel(labelNode, baseText, url, strategy, baseTextGetter) {
12214+ if (!labelNode || !url) return;
12215+ const initText = typeof baseTextGetter === "function" ? baseTextGetter() : baseText;
12216+ labelNode.textContent = initText;
12217+ imageSizeFetcher.getSize(url, { strategy: strategy || 'local-only' }).then(size => {
12218+ if (size === null) return;
12219+ const sizeText = formatBytes(size);
12220+ const currentBase = typeof baseTextGetter === "function" ? baseTextGetter() : baseText;
12221+ labelNode.textContent = currentBase + " | " + sizeText;
12222+ });
12223+ }
12224+
12225+ var viewmoreAutoSizeThrottle = { last: 0 };
12226+ function scheduleViewmoreAutoSize(labelNode, baseText, url, baseTextGetter) {
12227+ if (!labelNode || !url) return;
12228+ var now = Date.now();
12229+ var delay = Math.max(0, 800 - (now - viewmoreAutoSizeThrottle.last));
12230+ setTimeout(() => {
12231+ viewmoreAutoSizeThrottle.last = Date.now();
12232+ updateViewmoreSizeLabel(labelNode, baseText, url, 'auto', baseTextGetter);
12233+ }, delay);
12234+ }
12235+
12236+ var imageSizeFetcher = new ImageSizeFetcher(_GM_xmlhttpRequest, { concurrency: 3, timeout: 5000 });
12237+
1205812238 var canvas = document.createElement('CANVAS');
1205912239 if (document.body) {
1206012240 if (canvas.style) canvas.style.display = "none";
@@ -13939,6 +14119,7 @@ ImgOps | https://imgops.com/#b#`;
1393914119 '<span class="pv-gallery-head-float-left">'+
1394014120 '<span title="'+i18n("picInfo")+'" class="pv-gallery-head-left-img-info">'+
1394114121 '<span class="pv-gallery-head-left-img-info-resolution" title="'+i18n("resolution")+'">0 x 0</span>'+
14122+ '<span class="pv-gallery-head-left-img-info-size" title="Size"></span>'+
1394214123 '<span class="pv-gallery-head-left-img-info-count" title="'+i18n("picNum")+'">(1 / 1)</span>'+
1394314124 '<span class="pv-gallery-head-left-img-info-scaling" title="'+i18n("scaleRatio")+'">(100%)</span>'+
1394414125 '<span class="pv-gallery-vertical-align-helper"></span>'+
@@ -14325,6 +14506,7 @@ ImgOps | https://imgops.com/#b#`;
1432514506 'head-left-img-info',
1432614507 'head-left-img-info-description',
1432714508 'head-left-img-info-resolution',
14509+ 'head-left-img-info-size',
1432814510 'head-left-img-info-count',
1432914511 'head-left-img-info-scaling',
1433014512
@@ -16289,8 +16471,28 @@ ImgOps | https://imgops.com/#b#`;
1628916471 dlSpan.onclick=clickCb;
1629016472 var topP=document.createElement('p');
1629116473 topP.className="pv-top-banner";
16292- topP.innerHTML=createHTML(img.naturalWidth+' x '+img.naturalHeight);
16474+ var baseTextGetter = () => (img.naturalWidth + ' x ' + img.naturalHeight);
16475+ var baseText = baseTextGetter();
16476+ topP.textContent = baseText;
16477+ updateViewmoreSizeLabel(topP, baseText, curNode.dataset.src, 'local-only', baseTextGetter);
1629316478 topP.title=dlSpan.title;
16479+ let hoverTimer = null;
16480+ let autoFetched = false;
16481+ let onEnter = () => {
16482+ if (autoFetched || topP.textContent.indexOf("|") !== -1) return;
16483+ hoverTimer = setTimeout(() => {
16484+ autoFetched = true;
16485+ scheduleViewmoreAutoSize(topP, baseText, curNode.dataset.src, baseTextGetter);
16486+ }, 1000);
16487+ };
16488+ let onLeave = () => {
16489+ if (hoverTimer) {
16490+ clearTimeout(hoverTimer);
16491+ hoverTimer = null;
16492+ }
16493+ };
16494+ imgSpan.addEventListener("mouseenter", onEnter, true);
16495+ imgSpan.addEventListener("mouseleave", onLeave, true);
1629416496 var checkBox=document.createElement('input');
1629516497 checkBox.type="checkbox";
1629616498 let self=this;
@@ -17000,6 +17202,7 @@ ImgOps | https://imgops.com/#b#`;
1700017202 this.imgNaturalSize=imgNaturalSize;
1700117203
1700217204 this.eleMaps['head-left-img-info-resolution'].textContent=imgNaturalSize.w + ' x ' + imgNaturalSize.h;
17205+ updateGalleryHeaderSize(this, img, 'auto');
1700317206 var thumbnails=this.eleMaps['sidebar-thumbnails-container'].childNodes,i=0;
1700417207 thumbnails=Array.prototype.slice.call(thumbnails).filter(function(thumbnail){
1700517208 if(thumbnail.style.display=="none"){
@@ -17037,6 +17240,7 @@ ImgOps | https://imgops.com/#b#`;
1703717240 this.imgError=true;
1703817241 this.img.style.display='none';
1703917242 this.eleMaps['img_broken'].style.display='inline-block';
17243+ this.eleMaps['head-left-img-info-size'].textContent='';
1704017244 dataset(relatedThumb,'naturalSize',JSON.stringify({w: 0, h: 0}));
1704117245 }else{
1704217246 var srcs=dataset(relatedThumb, 'srcs');
@@ -17049,6 +17253,7 @@ ImgOps | https://imgops.com/#b#`;
1704917253
1705017254 self.imgNaturalSize=imgNaturalSize;
1705117255 self.eleMaps['head-left-img-info-resolution'].textContent=imgNaturalSize.w + ' x ' + imgNaturalSize.h;
17256+ updateGalleryHeaderSize(self, this, 'auto');
1705217257 dataset(relatedThumb,'naturalSize',JSON.stringify(imgNaturalSize));
1705317258 let key = imgNaturalSize.w + "x" + imgNaturalSize.h;
1705417259 self.sizeMap[key] = (self.sizeMap[key] || 0) + 1;
@@ -17584,6 +17789,7 @@ ImgOps | https://imgops.com/#b#`;
1758417789 //清空dom
1758517790 this.eleMaps['sidebar-thumbnails-container'].innerHTML=createHTML('');
1758617791 this.eleMaps['head-left-img-info-resolution'].textContent='0 x 0';
17792+ this.eleMaps['head-left-img-info-size'].textContent='';
1758717793 this.eleMaps['head-left-img-info-count'].textContent='(1 / 1)';
1758817794 this.eleMaps['head-left-img-info-scaling'].textContent='(100%)';
1758917795 //隐藏滚动条
@@ -18890,6 +19096,10 @@ ImgOps | https://imgops.com/#b#`;
1889019096 .pv-gallery-head-left-img-info{\
1889119097 cursor:help;\
1889219098 }\
19099+ .pv-gallery-head-left-img-info-size {\
19100+ margin-left: 6px;\
19101+ margin-right: 6px;\
19102+ }\
1889319103 .pv-gallery-head-left-img-info-description {\
1889419104 margin-left: 10px;\
1889519105 margin-right: 10px;\
@@ -20624,6 +20834,8 @@ ImgOps | https://imgops.com/#b#`;
2062420834 this.initPos = initPos || false;
2062520835 this.preview = !!preview;
2062620836 this.isImg = this.img.nodeName.toUpperCase() == 'IMG';
20837+ this.imgSizeBytes = null;
20838+ this.imgSizeText = "";
2062720839 this.init();
2062820840 if (data && this.isImg) {
2062920841 this.img.src = location.protocol == "https" ? data.src.replace(/^http:/,"https:") : data.src;
@@ -20838,6 +21050,8 @@ ImgOps | https://imgops.com/#b#`;
2083821050 img.onload = function(e) {
2083921051 if (self.removed) return;
2084021052 self.loaded = true;
21053+ self.imgSizeBytes = null;
21054+ self.imgSizeText = "";
2084121055 container.style.background='';
2084221056 if (self.preview && img.naturalHeight == 1 && img.naturalWidth == 1) {
2084321057 self.remove();
@@ -20849,6 +21063,7 @@ ImgOps | https://imgops.com/#b#`;
2084921063 w:img.naturalWidth,
2085021064 };
2085121065 self.setToolBadge('zoom',self.zoomLevel);
21066+ self.fetchImgSize(self.img.src, 'auto');
2085221067 if (self==uniqueImgWin && prefs.floatBar.globalkeys.previewFollowMouse) {
2085321068 self.followPos(uniqueImgWinInitX, uniqueImgWinInitY);
2085421069 } else {
@@ -20884,7 +21099,8 @@ ImgOps | https://imgops.com/#b#`;
2088421099 }
2088521100 if (imgNaturalSize.h && imgNaturalSize.w) {
2088621101 container.style.background='';
20887- setSearchState(`<strong>${img.naturalWidth} x ${img.naturalHeight}</strong>`, self.imgState);
21102+ self.updateImgState(1);
21103+ self.fetchImgSize(self.img.src, 'auto');
2088821104 }
2088921105 if (!this.isImg) {
2089021106 if (/^video$/i.test(img.nodeName)) {
@@ -22775,14 +22991,34 @@ ImgOps | https://imgops.com/#b#`;
2277522991 document.addEventListener('mousemove',moveHandler,true);
2277622992 document.addEventListener('mouseup',mouseupHandler,true);
2277722993 },
22994+ updateImgState:function(zoomValue){
22995+ if (!this.imgState || !this.img || !this.img.naturalWidth) return;
22996+ var sizeText = this.imgSizeText ? " (" + this.imgSizeText + ")" : "";
22997+ var zoomText = (typeof zoomValue === "number" && zoomValue !== 1) ? " (" + parseInt(zoomValue * 100) + "%)" : "";
22998+ var indexText = (this.curIndex >= 0 && this.data && this.data.all) ? " <b>[" + (this.curIndex + 1) + "/" + this.data.all.length + "]</b>" : "";
22999+ setSearchState("<strong>" + this.img.naturalWidth + " x " + this.img.naturalHeight + "</strong>" + sizeText + zoomText + indexText, this.imgState);
23000+ this.descriptionSpan && this.imgState.appendChild(this.descriptionSpan);
23001+ },
23002+ fetchImgSize:function(url, strategy){
23003+ if (!this.isImg) return;
23004+ var sizeUrl = url || (this.img ? this.img.src : "");
23005+ if (!sizeUrl) return;
23006+ var self = this;
23007+ imageSizeFetcher.getSize(sizeUrl, { strategy: strategy || 'auto' }).then(size => {
23008+ if (size === null) return;
23009+ if (!self.img || self.img.src !== sizeUrl) return;
23010+ self.imgSizeBytes = size;
23011+ self.imgSizeText = formatBytes(size);
23012+ self.updateImgState(self.zoomLevel || 1);
23013+ });
23014+ },
2277823015 setToolBadge:function(tool,content){
2277923016 var scale=0;
2278023017 switch(tool){
2278123018 case 'zoom':{
2278223019 scale=2;
2278323020 if (this.img.naturalWidth) {
22784- setSearchState(`<strong>${this.img.naturalWidth} x ${this.img.naturalHeight}</strong>` + (content !== 1 ? ` (${parseInt(content * 100)}%)` : "") + (this.curIndex >=0 ? ` <b>[${this.curIndex + 1}/${this.data.all.length}]</b>` : ""), this.imgState);
22785- this.descriptionSpan && this.imgState.appendChild(this.descriptionSpan);
23021+ this.updateImgState(content);
2278623022 }
2278723023 }break;
2278823024 case 'rotate':{
@@ -27778,4 +28014,4 @@ ImgOps | https://imgops.com/#b#`;
2777828014 init2();
2777928015 }
2778028016
27781- })(this,window,document,(typeof unsafeWindow=='undefined'? window : unsafeWindow));
28017+ })(this,window,document,(typeof unsafeWindow=='undefined'? window : unsafeWindow));
0 commit comments