Image gallery as CBZ

Download all images from a gallery page as a CBZ (ZIP) file.

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

You will need to install an extension such as Tampermonkey to install this script.

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

Advertisement:

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

Advertisement:

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==UserScript==
// @name         Image gallery as CBZ
// @namespace    i2p.schimon.cbz-gallery
// @version      26.04.21
// @description  Download all images from a gallery page as a CBZ (ZIP) file.
// @tag          cbz
// @author       Schimon Jehudah
// @copyright    2026, Schimon Jehudah (http://schimon.i2p)
// @license      MIT; https://opensource.org/licenses/MIT
// @match        *://*/*
// @homepageURL  https://greasyfork.org/scripts/574852-image-gallery-as-cbz
// @supportURL   https://greasyfork.org/scripts/574852-image-gallery-as-cbz/feedback
// @grant        GM.registerMenuCommand
// @grant        GM.xmlHttpRequest
// @connect      *
// @require      https://cdn.jsdelivr.net/npm/[email protected]/umd/index.min.js
// @run-at       context-menu
// ==/UserScript==

const MIN_WIDTH = 100;
const MIN_HEIGHT = 100;
console.log('[GalleryDL] Script v1.5 initialized');

(async function() {
    'use strict';
    console.log('[GalleryDL] Starting download...');
    const imageInfos = getImages();
    if (!imageInfos.length) {
        alert('No images found.');
        return;
    }
    const files = {};
    const imageData = [];
    const seen = new Set();
    let ok = 0, fail = 0, totalSize = 0;
    await Promise.allSettled(imageInfos.map(async (info, i) => {
        try {
            const data = await fetchUint8(info.src);
            let fname = getFilename(info.src, i);
            if (seen.has(fname)) {
                const p = fname.split('.');
                const ext = p.pop();
                fname = `${p.join('.')}_${i}.${ext}`;
            }
            seen.add(fname);
            console.log(`[GalleryDL] Adding: ${fname}`);
            files[fname] = data;

            imageData.push({
                filename: fname,
                url: info.src,
                size: data.length
            });

            totalSize += data.length;
            ok++;
        } catch (e) {
            console.error(`[GalleryDL] Failed ${info.src}:`, e);
            fail++;
        }
    }));
    console.log(`[GalleryDL] Fetched: ${ok} ok, ${fail} failed`);
    if (!ok) {
        alert('All downloads failed. Check console.');
        return;
    }
    console.log('[GalleryDL] Generating metadata files...');
    const metadataText = createMetadataText(ok, totalSize);
    const imagesCsv = createImagesCsv(imageData);

    files['metadata.txt'] = new TextEncoder().encode(metadataText);
    files['images.csv'] = new TextEncoder().encode(imagesCsv);

    console.log('[GalleryDL] Generating ZIP via fflate.zipSync...');
    let zipped;
    try {
        zipped = fflate.zipSync(files);
        console.log(`[GalleryDL] ZIP generated: ${zipped.length} bytes`);
    } catch (e) {
        console.error('[GalleryDL] ZIP generation failed:', e);
        alert('ZIP generation failed. See console.');
        return;
    }
    const blob = new Blob([zipped], {type: 'application/zip'});
    console.log(`[GalleryDL] Final Blob created: ${blob.size} bytes`);
    const name = document.title.replace(/[^a-z0-9]/gi, '_').substring(0, 50) + '_images.cbz';
    downloadBlob(blob, name);
    console.log('[GalleryDL] Done');
})();

function getImages() {
    console.log('[GalleryDL] Scanning DOM...');
    const imgs = Array.from(document.querySelectorAll('img'))
        .filter(img => (img.naturalWidth || img.width) > MIN_WIDTH || (img.naturalHeight || img.height) > MIN_HEIGHT)
        .map(img => ({src: img.src, width: img.naturalWidth || img.width, height: img.naturalHeight || img.height}));
    const links = Array.from(document.querySelectorAll('a[href]'))
        .map(a => a.href)
        .filter(href => /\.(jpg|jpeg|png|gif|webp|bmp|svg)(\?.*)?$/i.test(href))
        .map(href => ({src: href, width: 0, height: 0}));
    const all = [...imgs, ...links].filter((item, idx, arr) =>
        item.src && !item.src.startsWith('data:') && arr.findIndex(i => i.src === item.src) === idx
    );
    console.log(`[GalleryDL] Found ${all.length} unique image URLs`);
    return all;
}
function getFilename(url, index) {
    try {
        const u = new URL(url);
        let name = u.pathname.split('/').pop() || `image_${index}`;
        let ext = name.split('.').pop();
        if (!ext || ext.length > 5) ext = 'jpg';
        name = name.replace(/[^a-zA-Z0-9._-]/g, '_');
        return name.length > 100 ? `image_${index}.${ext}` : name;
    } catch (e) {
        return `image_${index}.jpg`;
    }
}
function fetchUint8(url) {
    console.log(`[GalleryDL] Fetching: ${url}`);
    return new Promise((resolve, reject) => {
        GM.xmlHttpRequest({
            method: 'GET',
            url: url,
            responseType: 'arraybuffer',
            onload: function(r) {
                if (r.status !== 200) {
                    console.error(`[GalleryDL] HTTP ${r.status}: ${url}`);
                    return reject(new Error(`HTTP ${r.status}`));
                }
                const data = new Uint8Array(r.response);
                console.log(`[GalleryDL] OK (${data.length} bytes): ${url}`);
                resolve(data);
            },
            onerror: function(e) {
                console.error(`[GalleryDL] Network error: ${url}`, e);
                reject(e);
            }
        });
    });
}
function downloadBlob(blob, filename) {
    console.log(`[GalleryDL] Triggering download: ${filename} (${blob.size} bytes)`);
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = filename;
    document.body.appendChild(a);
    a.click();
    a.remove();
    URL.revokeObjectURL(url);
    console.log('[GalleryDL] Download triggered');
}
function createMetadataText(imageCount, totalSize) {
    const meta = [];
    meta.push(`Title: ${document.title}`);
    meta.push(`URL: ${window.location.href}`);
    meta.push(`Timestamp: ${new Date().toISOString()}`);
    meta.push(`Images Downloaded: ${imageCount}`);
    meta.push(`Total Size (bytes): ${totalSize}`);

    const desc = document.querySelector('meta[name="description"]');
    if (desc) meta.push(`Meta Description: ${desc.content}`);

    return meta.join('\n');
}
function createImagesCsv(imageData) {
    const lines = ['filename,source_url,size_bytes'];
    imageData.forEach(img => {
        lines.push(`${img.filename},${img.url},${img.size}`);
    });
    return lines.join('\n');
}