X Split Image Merger (Pro)_EN

Automatically merge 4-part split images on X (Twitter) in strict 1-2-3-4 order, bypassing CORS issues.

Du musst eine Erweiterung wie Tampermonkey, Greasemonkey oder Violentmonkey installieren, um dieses Skript zu installieren.

Um dieses Skript zu installieren, müssen Sie eine Erweiterung wie Tampermonkey oder Violentmonkey installieren.

Um dieses Skript zu installieren, müssen Sie eine Erweiterung wie Tampermonkey oder Violentmonkey installieren.

Um dieses Skript zu installieren, müssen Sie eine Erweiterung wie Tampermonkey oder Userscripts installieren.

Um dieses Skript zu installieren, müssen Sie eine Erweiterung wie Tampermonkey installieren.

Sie müssten eine Skript Manager Erweiterung installieren damit sie dieses Skript installieren können

(Ich habe schon ein Skript Manager, Lass mich es installieren!)

Um dieses Design zu installieren, müssen Sie eine Erweiterung wie Stylus installieren.

Um dieses Design zu installieren, müssen Sie eine Erweiterung wie Stylus installieren.

Um dieses Design zu installieren, müssen Sie eine Erweiterung wie Stylus installieren.

Um diesen Stil zu installieren, müssen Sie eine Erweiterung für den Benutzerstil Verwaltung installieren.

Um diesen Stil zu installieren, müssen Sie eine Erweiterung für den Benutzerstil Verwaltung installieren.

Um diesen Stil zu installieren, müssen Sie eine Erweiterung für den Benutzerstil Verwaltung installieren.

(Ich habe bereits einen Benutzerstil Verwaltung, ich möchte ihn installieren!)

// ==UserScript==
// @name         X Split Image Merger (Pro)_EN
// @namespace    http://tampermonkey.net/
// @version      1.0
// @description  Automatically merge 4-part split images on X (Twitter) in strict 1-2-3-4 order, bypassing CORS issues.
// @author       https://x.com/Jiangshi_i
// @match        *://*.x.com/*
// @match        *://*.twitter.com/*
// @grant        GM_xmlhttpRequest
// @connect      pbs.twimg.com
// @icon         https://abs.twimg.com/favicons/twitter.3.ico
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    // 1. Use MutationObserver instead of setInterval for better performance
    const observer = new MutationObserver(checkForImages);
    observer.observe(document.body, { childList: true, subtree: true });

    function checkForImages() {
        // Accurately locate the main tweet content to avoid grabbing profile pictures
        const posts = document.querySelectorAll('article[data-testid="tweet"]');

        posts.forEach(post => {
            // Must be an img inside tweetPhoto to ensure it's a media image in the post
            const images = post.querySelectorAll('[data-testid="tweetPhoto"] img');

            // If there are exactly 4 images and no button has been added yet
            if (images.length === 4 && !post.querySelector('.merge-btn-pro')) {
                addMergeButton(post, images);
            }
        });
    }

    function addMergeButton(post, images) {
        const buttonGroup = post.querySelector('[role="group"]'); // Usually the row with Retweet/Like buttons
        const button = document.createElement('button');
        button.textContent = 'Merge Images ⬇️';
        button.className = 'merge-btn-pro';
        // Simple UI styling
        button.style.cssText = `
            margin: 10px 0;
            padding: 8px 16px;
            background-color: #1d9bf0;
            color: white;
            border: none;
            border-radius: 9999px;
            font-weight: bold;
            cursor: pointer;
            font-size: 14px;
        `;

        button.onclick = (e) => {
            e.stopPropagation(); // Prevent accidentally clicking into the tweet
            button.textContent = 'Processing...';
            button.style.backgroundColor = '#536471';
            mergeImages(images, button);
        };

        // Insert the button below the images, above the interaction buttons
        if (buttonGroup) {
            buttonGroup.parentElement.insertBefore(button, buttonGroup);
        } else {
            post.appendChild(button);
        }
    }

    // Bypass CORS restrictions using Tampermonkey's built-in request to get Blob data
    function fetchImageAsBase64(url) {
        return new Promise((resolve, reject) => {
            // Replace the image quality parameter with 'orig' to get the highest resolution
            const highResUrl = url.replace(/&name=[^&]+/, '&name=orig');

            GM_xmlhttpRequest({
                method: 'GET',
                url: highResUrl,
                responseType: 'blob',
                onload: function(response) {
                    const reader = new FileReader();
                    reader.onloadend = () => resolve(reader.result);
                    reader.readAsDataURL(response.response);
                },
                onerror: reject
            });
        });
    }

    async function mergeImages(imageNodes, btnElement) {
        try {
            // Convert NodeList to an array. The order here is strictly: 0(Top-Left), 1(Top-Right), 2(Bottom-Left), 3(Bottom-Right)
            const imgUrls = Array.from(imageNodes).map(img => img.src);

            // 1. Fetch all image Base64 data concurrently (Promise.all ensures the returned array order matches imgUrls)
            const base64List = await Promise.all(imgUrls.map(fetchImageAsBase64));

            // 2. Convert Base64 to Image objects for Canvas drawing
            const loadedImages = await Promise.all(base64List.map(base64 => {
                return new Promise(resolve => {
                    const img = new Image();
                    img.onload = () => resolve(img);
                    img.src = base64;
                });
            }));

            // 3. Calculate canvas dimensions
            // Assuming all images have the same width, use the first image's width as the baseline
            const targetWidth = loadedImages[0].naturalWidth;
            let totalHeight = 0;

            // Calculate the proportional height for each segment based on the target width and accumulate total height
            loadedImages.forEach(img => {
                const scale = targetWidth / img.naturalWidth;
                totalHeight += img.naturalHeight * scale;
            });

            // 4. Create canvas and draw in strict order
            const canvas = document.createElement('canvas');
            const ctx = canvas.getContext('2d');
            canvas.width = targetWidth;
            canvas.height = totalHeight;

            let currentY = 0;
            // Since loadedImages is in 1-2-3-4 order, iterating and drawing directly results in top-to-bottom order
            loadedImages.forEach(img => {
                const scale = targetWidth / img.naturalWidth;
                const drawHeight = img.naturalHeight * scale;
                ctx.drawImage(img, 0, currentY, targetWidth, drawHeight);
                currentY += drawHeight; // Move Y coordinate downwards
            });

            // 5. Trigger download
            downloadCanvas(canvas);

            // Restore button state
            btnElement.textContent = 'Merged ✅';
            btnElement.style.backgroundColor = '#00ba7c';
            setTimeout(() => {
                btnElement.textContent = 'Merge Images ⬇️';
                btnElement.style.backgroundColor = '#1d9bf0';
            }, 3000);

        } catch (error) {
            console.error("Failed to merge images:", error);
            btnElement.textContent = 'Failed ❌';
            btnElement.style.backgroundColor = '#f4212e';
            alert('Image download failed. This might be due to network issues or CORS restrictions. Press F12 to check the console for errors.');
        }
    }

    function downloadCanvas(canvas) {
        const link = document.createElement('a');
        link.download = `X_Merged_Image_${new Date().getTime()}.png`;
        link.href = canvas.toDataURL('image/png', 1.0);
        link.click();
    }
})();