{"id":57,"date":"2026-03-13T03:13:21","date_gmt":"2026-03-13T03:13:21","guid":{"rendered":"https:\/\/tools.sanepo.com\/?post_type=tool&#038;p=57"},"modified":"2026-03-14T01:51:20","modified_gmt":"2026-03-14T01:51:20","slug":"compress-convert-images","status":"publish","type":"tool","link":"https:\/\/tools.sanepo.com\/zh-hans\/features\/compress-convert-images\/","title":{"rendered":"Compress Convert Images Unlimited Free"},"content":{"rendered":"\n<!-- \n  WP-NanoTech: Client-Side Dynamic Bulk Image Compressor\n  Author: WP-NanoTech Developer\n  Description: Modern, sleek in-browser image optimization tool. Enterprise Ready.\n-->\n<div id=\"wpnt-img-compressor\">\n    <style>\n        @import url('https:\/\/fonts.googleapis.com\/css2?family=Inter:wght@400;500;600;700&display=swap');\n\n        \/* Modern, Clean, Minimalist Scoped CSS *\/\n        #wpnt-img-compressor {\n            \/* Tool Specific Variables *\/\n            --wpnt-success: #10b981;\n            --wpnt-danger: #ef4444;\n            --wpnt-radius-lg: 16px;\n            --wpnt-radius-md: 12px;\n            --wpnt-radius-sm: 8px;\n            \n            width: 100%;\n            margin: 30px auto;\n            background: linear-gradient(135deg, var(--bg-primary) 0%, var(--bg-secondary) 100%);\n            border: 1px solid var(--border-color);\n            border-radius: var(--wpnt-radius-lg);\n            box-shadow: var(--card-shadow);\n            padding: 32px;\n            box-sizing: border-box;\n            line-height: 1.5;\n            backdrop-filter: blur(10px);\n            -webkit-backdrop-filter: blur(10px);\n        }\n\n        #wpnt-img-compressor * {\n            box-sizing: border-box;\n            transition: var(--transition);\n        }\n\n        \/* Header *\/\n        #wpnt-img-compressor .wpnt-header {\n            text-align: center;\n            margin-bottom: 24px;\n        }\n\n        #wpnt-img-compressor h3 {\n            margin: 0 0 8px 0;\n            font-size: 1.75rem;\n            font-weight: 700;\n            letter-spacing: -0.025em;\n        }\n\n        #wpnt-img-compressor p {\n            margin: 0;\n            font-size: 0.95rem;\n            opacity: 0.8;\n        }\n        \n        #wpnt-img-compressor .wpnt-privacy-badge {\n            display: inline-block;\n            background: rgba(16, 185, 129, 0.1);\n            color: #10b981; \/* Ensure good contrast *\/\n            border: 1px solid rgba(16, 185, 129, 0.2);\n            padding: 6px 12px;\n            border-radius: 20px;\n            font-size: 0.8rem;\n            font-weight: 600;\n            margin-top: 12px;\n        }\n\n        \/* Segmented Control (The Toggle) *\/\n        #wpnt-img-compressor .wpnt-toggle-container {\n            display: flex;\n            background: var(--glass-bg);\n            padding: 6px;\n            border-radius: var(--wpnt-radius-md);\n            border: 1px solid var(--border-color);\n            margin-bottom: 24px;\n            box-shadow: var(--card-shadow);\n        }\n\n        #wpnt-img-compressor .wpnt-toggle-item {\n            flex: 1;\n            position: relative;\n        }\n\n        #wpnt-img-compressor .wpnt-toggle-item input {\n            display: none;\n        }\n\n        #wpnt-img-compressor .wpnt-toggle-item label {\n            display: block;\n            text-align: center;\n            padding: 10px 16px;\n            border-radius: var(--wpnt-radius-sm);\n            cursor: pointer;\n            font-weight: 600;\n            font-size: 0.9rem;\n            opacity: 0.7;\n            z-index: 2;\n            position: relative;\n        }\n\n        #wpnt-img-compressor .wpnt-toggle-item input:checked + label {\n            background: var(--bg-secondary);\n            color: var(--accent-color);\n            opacity: 1;\n            box-shadow: 0 2px 8px rgba(0,0,0,0.05);\n        }\n\n        \/* Settings Grid *\/\n        #wpnt-img-compressor .wpnt-settings {\n            display: grid;\n            grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));\n            gap: 20px;\n            margin-bottom: 32px;\n        }\n\n        #wpnt-img-compressor .wpnt-setting-group {\n            background: var(--glass-bg);\n            padding: 16px 20px;\n            border-radius: var(--wpnt-radius-md);\n            border: 1px solid var(--border-color);\n            box-shadow: var(--card-shadow);\n        }\n\n        #wpnt-img-compressor .wpnt-label {\n            display: flex;\n            justify-content: space-between;\n            align-items: center;\n            font-weight: 600;\n            font-size: 0.85rem;\n            text-transform: uppercase;\n            letter-spacing: 0.05em;\n            opacity: 0.8;\n            margin-bottom: 12px;\n        }\n\n        #wpnt-img-compressor .wpnt-val {\n            color: var(--accent-color);\n            font-weight: 700;\n            opacity: 1;\n        }\n\n        \/* Modern Select *\/\n        #wpnt-img-compressor select {\n            width: 100%;\n            padding: 10px 12px;\n            border-radius: var(--wpnt-radius-sm);\n            border: 1px solid rgba(0,0,0,0.1);\n            background-color: var(--glass-bg);\n            font-family: inherit;\n            font-size: 0.95rem;\n            color: inherit;\n            outline: none;\n            cursor: pointer;\n            appearance: none;\n            background-image: url(\"data:image\/svg+xml;charset=UTF-8,%3csvg xmlns='http:\/\/www.w3.org\/2000\/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c\/polyline%3e%3c\/svg%3e\");\n            background-repeat: no-repeat;\n            background-position: right 12px center;\n            background-size: 16px;\n        }\n\n        #wpnt-img-compressor select:focus {\n            border-color: var(--accent-color);\n            box-shadow: 0 0 0 3px rgba(67, 97, 238, 0.15);\n        }\n\n        \/* Modern Range Slider *\/\n        #wpnt-img-compressor input[type=\"range\"] {\n            -webkit-appearance: none;\n            width: 100%;\n            background: transparent;\n            margin-top: 8px;\n        }\n        #wpnt-img-compressor input[type=\"range\"]:focus { outline: none; }\n        #wpnt-img-compressor input[type=\"range\"]::-webkit-slider-runnable-track {\n            width: 100%; height: 6px; cursor: pointer;\n            background: rgba(0,0,0,0.08); border-radius: 3px;\n        }\n        #wpnt-img-compressor input[type=\"range\"]::-webkit-slider-thumb {\n            height: 20px; width: 20px; border-radius: 50%;\n            background: var(--accent-color); cursor: pointer;\n            -webkit-appearance: none; margin-top: -7px;\n            border: 3px solid var(--bg-secondary);\n            box-shadow: 0 2px 6px rgba(0,0,0,0.15);\n        }\n        #wpnt-img-compressor input[type=\"range\"]:hover::-webkit-slider-thumb {\n            transform: scale(1.15);\n        }\n\n        \/* Dropzone *\/\n        #wpnt-img-compressor .wpnt-dropzone {\n            border: 2px dashed rgba(67, 97, 238, 0.3);\n            border-radius: var(--wpnt-radius-lg);\n            padding: 48px 24px;\n            text-align: center;\n            cursor: pointer;\n            background: var(--glass-bg);\n            position: relative;\n            overflow: hidden;\n            box-shadow: var(--card-shadow);\n        }\n\n        #wpnt-img-compressor .wpnt-dropzone:hover,\n        #wpnt-img-compressor .wpnt-dropzone.dragover {\n            border-color: var(--accent-color);\n            background: var(--bg-secondary);\n        }\n\n        #wpnt-img-compressor .wpnt-dropzone input[type=\"file\"] {\n            display: none;\n        }\n\n        #wpnt-img-compressor .wpnt-icon-upload {\n            color: var(--accent-color);\n            margin-bottom: 16px;\n        }\n\n        #wpnt-img-compressor .wpnt-btn {\n            background: var(--accent-color);\n            color: #ffffff;\n            border: none;\n            padding: 10px 24px;\n            border-radius: var(--wpnt-radius-sm);\n            cursor: pointer;\n            font-weight: 500;\n            font-size: 0.95rem;\n            display: inline-flex;\n            align-items: center;\n            justify-content: center;\n            text-decoration: none;\n            gap: 8px;\n        }\n\n        #wpnt-img-compressor .wpnt-btn:hover {\n            background: var(--accent-hover);\n            transform: translateY(-2px);\n            box-shadow: 0 4px 12px rgba(67, 97, 238, 0.25);\n        }\n\n        #wpnt-img-compressor .wpnt-btn-secondary {\n            background: transparent;\n            color: inherit;\n            border: 1px solid var(--border-color);\n            box-shadow: none;\n        }\n\n        #wpnt-img-compressor .wpnt-btn-secondary:hover {\n            background: rgba(0,0,0,0.05);\n            color: var(--wpnt-danger);\n            border-color: var(--wpnt-danger);\n            transform: translateY(0);\n            box-shadow: none;\n        }\n\n        \/* Action Bar *\/\n        #wpnt-img-compressor .wpnt-action-bar {\n            display: flex;\n            justify-content: space-between;\n            align-items: center;\n            background: var(--glass-bg);\n            padding: 16px 24px;\n            border-radius: var(--wpnt-radius-md);\n            border: 1px solid var(--border-color);\n            margin-top: 32px;\n            box-shadow: var(--card-shadow);\n            backdrop-filter: blur(5px);\n        }\n\n        \/* Results *\/\n        #wpnt-img-compressor .wpnt-results {\n            margin-top: 20px;\n            display: grid;\n            grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));\n            gap: 20px;\n        }\n\n        #wpnt-img-compressor .wpnt-card {\n            border: 1px solid var(--border-color);\n            border-radius: var(--wpnt-radius-md);\n            padding: 16px;\n            background: var(--glass-bg);\n            display: flex;\n            flex-direction: column;\n            box-shadow: var(--card-shadow);\n            backdrop-filter: blur(5px);\n            position: relative;\n        }\n\n        \/* Close Button Styles *\/\n        #wpnt-img-compressor .wpnt-btn-close {\n            position: absolute;\n            top: 8px;\n            right: 8px;\n            width: 26px;\n            height: 26px;\n            background: rgba(0, 0, 0, 0.4);\n            color: #ffffff;\n            border: none;\n            border-radius: 50%;\n            cursor: pointer;\n            display: flex;\n            align-items: center;\n            justify-content: center;\n            font-size: 18px;\n            line-height: 1;\n            z-index: 10;\n            opacity: 0;\n            transition: var(--transition);\n        }\n\n        #wpnt-img-compressor .wpnt-card:hover .wpnt-btn-close {\n            opacity: 1;\n        }\n\n        #wpnt-img-compressor .wpnt-btn-close:hover {\n            background: var(--wpnt-danger);\n            transform: scale(1.1);\n        }\n\n        #wpnt-img-compressor .wpnt-card img {\n            width: 100%;\n            height: 160px;\n            object-fit: cover;\n            border-radius: var(--wpnt-radius-sm);\n            margin-bottom: 16px;\n            background: var(--bg-primary);\n            border: 1px solid rgba(0,0,0,0.05);\n        }\n\n        #wpnt-img-compressor .wpnt-stats {\n            font-size: 0.85rem;\n            margin-bottom: 16px;\n            flex-grow: 1;\n        }\n\n        #wpnt-img-compressor .wpnt-filename {\n            font-weight: 600;\n            font-size: 0.95rem;\n            margin-bottom: 12px;\n            white-space: nowrap;\n            overflow: hidden;\n            text-overflow: ellipsis;\n            display: block;\n        }\n\n        #wpnt-img-compressor .wpnt-stat-row {\n            display: flex;\n            justify-content: space-between;\n            margin-bottom: 6px;\n            opacity: 0.8;\n        }\n\n        #wpnt-img-compressor .wpnt-stat-row.final {\n            font-weight: 600;\n            margin-bottom: 12px;\n            opacity: 1;\n        }\n\n        #wpnt-img-compressor .wpnt-badge {\n            display: inline-flex;\n            align-items: center;\n            gap: 4px;\n            padding: 4px 8px;\n            border-radius: 4px;\n            font-weight: 600;\n            font-size: 0.75rem;\n        }\n\n        #wpnt-img-compressor .wpnt-badge.success {\n            background: #d1fae5; color: #047857;\n        }\n        \n        #wpnt-img-compressor .wpnt-badge.danger {\n            background: #fee2e2; color: #b91c1c;\n        }\n        \n        #wpnt-img-compressor .wpnt-badge.neutral {\n            background: var(--glass-bg); opacity: 0.8; border: 1px solid var(--border-color);\n        }\n\n        #wpnt-img-compressor .wpnt-btn-download {\n            width: 100%;\n            text-align: center;\n            background: var(--glass-bg);\n            color: var(--accent-color);\n            border: 1px solid var(--accent-color);\n        }\n\n        #wpnt-img-compressor .wpnt-btn-download:hover {\n            background: var(--accent-color);\n            color: #ffffff;\n        }\n\n        \/* =========================================\n           MOBILE RESPONSIVENESS (SUPER IMPORTANT) \n           ========================================= *\/\n        @media (max-width: 600px) {\n            #wpnt-img-compressor {\n                padding: 20px 16px; \/* Kurangi padding agar tidak makan tempat *\/\n                margin: 20px auto;\n            }\n            #wpnt-img-compressor h3 {\n                font-size: 1.4rem;\n            }\n            #wpnt-img-compressor .wpnt-settings {\n                grid-template-columns: 1fr; \/* Paksa 1 kolom penuh di mobile *\/\n                gap: 16px;\n            }\n            #wpnt-img-compressor .wpnt-action-bar {\n                flex-direction: column;\n                gap: 16px;\n                text-align: center;\n            }\n            #wpnt-img-compressor .wpnt-action-bar > div {\n                display: flex;\n                width: 100%;\n                gap: 12px;\n            }\n            #wpnt-img-compressor .wpnt-action-bar .wpnt-btn {\n                flex: 1;\n                padding: 10px;\n                font-size: 0.85rem;\n            }\n            #wpnt-img-compressor .wpnt-btn-close {\n                opacity: 1; \/* Selalu muncul di mobile (karena tidak ada hover) *\/\n            }\n        }\n\n        @media (max-width: 480px) {\n            \/* Ubah Toggle Pill menjadi list vertikal agar teks tidak terpotong *\/\n            #wpnt-img-compressor .wpnt-toggle-container {\n                flex-direction: column;\n                gap: 4px;\n            }\n            #wpnt-img-compressor .wpnt-toggle-item label {\n                padding: 12px;\n                font-size: 0.95rem;\n            }\n            #wpnt-img-compressor .wpnt-privacy-badge {\n                font-size: 0.7rem;\n                white-space: normal;\n                line-height: 1.4;\n            }\n            #wpnt-img-compressor select {\n                font-size: 0.85rem; \/* Cegah dropdown terpotong *\/\n            }\n            #wpnt-img-compressor .wpnt-dropzone {\n                padding: 32px 16px;\n            }\n        }\n    <\/style>\n\n    <div class=\"wpnt-header\">\n        <h3>Image Optimizer Pro<\/h3>\n        <p>Zero-server-load processing. Fast, secure, and private.<\/p>\n        <div class=\"wpnt-privacy-badge\">\ud83d\udd12 Privacy Safe: EXIF\/GPS data automatically removed<\/div>\n    <\/div>\n\n    <!-- The Segmented Control Toggle -->\n    <div class=\"wpnt-toggle-container\">\n        <div class=\"wpnt-toggle-item\">\n            <input type=\"radio\" id=\"mode-compress\" name=\"wpnt-mode\" value=\"compress\">\n            <label for=\"mode-compress\">Compress Only<\/label>\n        <\/div>\n        <div class=\"wpnt-toggle-item\">\n            <input type=\"radio\" id=\"mode-convert\" name=\"wpnt-mode\" value=\"convert\">\n            <label for=\"mode-convert\">Convert Only<\/label>\n        <\/div>\n        <div class=\"wpnt-toggle-item\">\n            <input type=\"radio\" id=\"mode-both\" name=\"wpnt-mode\" value=\"both\" checked>\n            <label for=\"mode-both\">Convert &#038; Compress<\/label>\n        <\/div>\n    <\/div>\n\n    <div class=\"wpnt-settings\">\n        <div class=\"wpnt-setting-group\" id=\"group-resize\">\n            <div class=\"wpnt-label\">Max Resolution<\/div>\n            <select id=\"wpnt-resize\">\n                <option value=\"original\">Original (No Resize)<\/option>\n                <option value=\"1920\">Max 1920px (HD Web)<\/option>\n                <option value=\"1280\" selected>Max 1280px (Standard Web)<\/option>\n                <option value=\"800\">Max 800px (Blog Post)<\/option>\n            <\/select>\n        <\/div>\n        <div class=\"wpnt-setting-group\" id=\"group-format\">\n            <div class=\"wpnt-label\">Target Format<\/div>\n            <select id=\"wpnt-format\">\n                <option value=\"image\/webp\">WebP (Recommended for Web)<\/option>\n                <option value=\"image\/jpeg\">JPEG (Universal)<\/option>\n                <option value=\"image\/png\">PNG (Lossless)<\/option>\n            <\/select>\n            <div id=\"wpnt-png-warning\" style=\"color: var(--wpnt-danger); font-size: 0.75rem; margin-top: 8px; display: none;\">\n                * Note: Browser API cannot compress PNG quality. It will only resize it.\n            <\/div>\n        <\/div>\n        <div class=\"wpnt-setting-group\" id=\"group-quality\">\n            <div class=\"wpnt-label\">\n                Quality <span class=\"wpnt-val\" id=\"wpnt-quality-val\">60%<\/span>\n            <\/div>\n            <!-- DEFAULT VALUE DIUBAH KE 60% AGAR KOMPRESI LEBIH TERASA -->\n            <input type=\"range\" id=\"wpnt-quality\" min=\"10\" max=\"100\" value=\"60\">\n        <\/div>\n    <\/div>\n\n    <div class=\"wpnt-dropzone\" id=\"wpnt-dropzone\">\n        <svg class=\"wpnt-icon-upload\" width=\"48\" height=\"48\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n            <path d=\"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4\"><\/path>\n            <polyline points=\"17 8 12 3 7 8\"><\/polyline>\n            <line x1=\"12\" y1=\"3\" x2=\"12\" y2=\"15\"><\/line>\n        <\/svg>\n        <p style=\"font-weight: 500; font-size: 1.1rem; margin-bottom: 8px;\">Drop your images here<\/p>\n        <p style=\"margin-bottom: 24px; opacity: 0.8;\">Supports JPG, PNG, WEBP<\/p>\n        <button class=\"wpnt-btn\" onclick=\"document.getElementById('wpnt-file-input').click()\">Browse Files<\/button>\n        <input type=\"file\" id=\"wpnt-file-input\" multiple accept=\"image\/jpeg, image\/png, image\/webp\">\n    <\/div>\n\n    <!-- Action Bar (Hidden by default) -->\n    <div id=\"wpnt-action-bar\" class=\"wpnt-action-bar\" style=\"display: none;\">\n        <span><strong id=\"wpnt-result-count\">0<\/strong> <span style=\"opacity: 0.8;\">Images Processed<\/span><\/span>\n        <div style=\"display:flex; gap:12px;\">\n            <button id=\"wpnt-btn-clear\" class=\"wpnt-btn wpnt-btn-secondary\">Clear All<\/button>\n            <button id=\"wpnt-btn-dl-all\" class=\"wpnt-btn\">\n                <svg width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4\"><\/path><polyline points=\"7 10 12 15 17 10\"><\/polyline><line x1=\"12\" y1=\"15\" x2=\"12\" y2=\"3\"><\/line><\/svg>\n                Download All\n            <\/button>\n        <\/div>\n    <\/div>\n\n    <div class=\"wpnt-results\" id=\"wpnt-results\"><\/div>\n    \n<\/div>\n\n<script>\n    (() => {\n        const dropzone = document.getElementById('wpnt-dropzone');\n        const fileInput = document.getElementById('wpnt-file-input');\n        const resultsContainer = document.getElementById('wpnt-results');\n        const actionBar = document.getElementById('wpnt-action-bar');\n        const resultCount = document.getElementById('wpnt-result-count');\n        const btnClear = document.getElementById('wpnt-btn-clear');\n        const btnDownloadAll = document.getElementById('wpnt-btn-dl-all');\n        \n        \/\/ Settings Elements\n        const modeRadios = document.querySelectorAll('input[name=\"wpnt-mode\"]');\n        const groupFormat = document.getElementById('group-format');\n        const groupQuality = document.getElementById('group-quality');\n        const formatSelect = document.getElementById('wpnt-format');\n        const qualitySlider = document.getElementById('wpnt-quality');\n        const qualityVal = document.getElementById('wpnt-quality-val');\n        const resizeSelect = document.getElementById('wpnt-resize');\n        const pngWarning = document.getElementById('wpnt-png-warning');\n\n        \/\/ Logic to fade out irrelevant settings based on selected mode\n        function updateUIBasedOnMode() {\n            const selectedMode = document.querySelector('input[name=\"wpnt-mode\"]:checked').value;\n            \n            if (selectedMode === 'compress') {\n                groupFormat.style.opacity = '0.4';\n                groupFormat.style.pointerEvents = 'none';\n                groupQuality.style.opacity = '1';\n                groupQuality.style.pointerEvents = 'auto';\n                pngWarning.style.display = 'block'; \/\/ Show PNG warning\n            } else {\n                let isConvertOnly = false;\n                if (selectedMode === 'convert') {\n                    isConvertOnly = true;\n                }\n\n                if (isConvertOnly) {\n                    groupFormat.style.opacity = '1';\n                    groupFormat.style.pointerEvents = 'auto';\n                    groupQuality.style.opacity = '0.4';\n                    groupQuality.style.pointerEvents = 'none';\n                    pngWarning.style.display = 'none';\n                } else { \/\/ 'both'\n                    groupFormat.style.opacity = '1';\n                    groupFormat.style.pointerEvents = 'auto';\n                    groupQuality.style.opacity = '1';\n                    groupQuality.style.pointerEvents = 'auto';\n                    pngWarning.style.display = 'none';\n                }\n            }\n        }\n\n        function updateActionBarVisibility() {\n            const count = resultsContainer.children.length;\n            resultCount.textContent = count;\n            if (count > 0) {\n                actionBar.style.display = 'flex';\n            } else {\n                actionBar.style.display = 'none';\n            }\n        }\n\n        \/\/ Action Bar Event Listeners\n        btnClear.addEventListener('click', () => {\n            resultsContainer.innerHTML = '';\n            updateActionBarVisibility();\n        });\n\n        btnDownloadAll.addEventListener('click', () => {\n            const downloadLinks = resultsContainer.querySelectorAll('.wpnt-btn-download');\n            let delay = 0;\n            \n            \/\/ Staggered download to prevent browser blocking multiple popups\n            downloadLinks.forEach(link => {\n                setTimeout(() => {\n                    link.click();\n                }, delay);\n                delay = delay + 300; \/\/ 300ms delay between each download\n            });\n        });\n\n        \/\/ Attach event listeners to toggles\n        modeRadios.forEach(radio => radio.addEventListener('change', updateUIBasedOnMode));\n        \n        \/\/ Quality slider display update\n        qualitySlider.addEventListener('input', (e) => {\n            qualityVal.textContent = e.target.value + '%';\n        });\n\n        \/\/ Initialize UI state\n        updateUIBasedOnMode();\n\n        \/\/ Drag & Drop Handling\n        dropzone.addEventListener('dragenter', preventDef, false);\n        dropzone.addEventListener('dragover', preventDef, false);\n        dropzone.addEventListener('dragleave', preventDef, false);\n        dropzone.addEventListener('drop', preventDef, false);\n\n        function preventDef(e) {\n            e.preventDefault(); \n            e.stopPropagation();\n        }\n\n        dropzone.addEventListener('dragenter', () => dropzone.classList.add('dragover'), false);\n        dropzone.addEventListener('dragover', () => dropzone.classList.add('dragover'), false);\n        dropzone.addEventListener('dragleave', () => dropzone.classList.remove('dragover'), false);\n        dropzone.addEventListener('drop', () => dropzone.classList.remove('dragover'), false);\n\n        dropzone.addEventListener('drop', e => handleFiles(e.dataTransfer.files), false);\n        fileInput.addEventListener('change', e => handleFiles(e.target.files));\n\n        function handleFiles(files) {\n            [...files].forEach(processImage);\n        }\n\n        function formatBytes(bytes) {\n            if (bytes === 0) return '0 B';\n            const k = 1024;\n            const sizes = ['B', 'KB', 'MB'];\n            const i = Math.floor(Math.log(bytes) \/ Math.log(k));\n            return parseFloat((bytes \/ Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];\n        }\n\n        function getExtensionFromMime(mimeType) {\n            if (mimeType === 'image\/webp') return '.webp';\n            if (mimeType === 'image\/png') return '.png';\n            return '.jpg';\n        }\n\n        function processImage(file) {\n            if (!file.type.startsWith('image\/')) return;\n\n            const mode = document.querySelector('input[name=\"wpnt-mode\"]:checked').value;\n            \n            \/\/ Determine Processing Parameters based on Mode\n            let outputMime = formatSelect.value;\n            let quality = parseInt(qualitySlider.value) \/ 100;\n            const resizeValue = resizeSelect.value;\n\n            if (mode === 'compress') {\n                outputMime = file.type; \/\/ Keep original format strictly\n            } else {\n                let isConvertOnly = false;\n                if (mode === 'convert') {\n                    isConvertOnly = true;\n                }\n                if (isConvertOnly) {\n                    quality = 1.0; \/\/ Max quality for conversion only\n                }\n            }\n\n            const extension = getExtensionFromMime(outputMime);\n            let suffix = '-optimized';\n            if (mode === 'convert') {\n                suffix = '-converted';\n            }\n            const newFilename = file.name.replace(\/\\.[^\/.]+$\/, \"\") + suffix + extension;\n\n            const reader = new FileReader();\n            reader.readAsDataURL(file);\n            reader.onload = function(event) {\n                const img = new Image();\n                img.src = event.target.result;\n                \n                img.onload = function() {\n                    const canvas = document.createElement('canvas');\n                    const ctx = canvas.getContext('2d');\n                    \n                    \/\/ SMART RESIZE LOGIC\n                    let targetWidth = img.width;\n                    let targetHeight = img.height;\n                    \n                    let needsResize = false;\n                    if (resizeValue !== 'original') {\n                        const maxDim = parseInt(resizeValue);\n                        if (img.width > maxDim) {\n                            needsResize = true;\n                        }\n                        if (img.height > maxDim) {\n                            needsResize = true;\n                        }\n                        \n                        if (needsResize) {\n                            if (img.width > img.height) {\n                                targetWidth = maxDim;\n                                targetHeight = Math.round((img.height \/ img.width) * maxDim);\n                            } else {\n                                targetHeight = maxDim;\n                                targetWidth = Math.round((img.width \/ img.height) * maxDim);\n                            }\n                        }\n                    }\n\n                    canvas.width = targetWidth;\n                    canvas.height = targetHeight;\n                    ctx.drawImage(img, 0, 0, targetWidth, targetHeight);\n\n                    \/\/ Execute Canvas to Blob with STRICT absolute quality\n                    canvas.toBlob((blob) => {\n                        if (blob) {\n                            renderCard(file, blob, newFilename, img.src, mode, needsResize, outputMime);\n                        }\n                    }, outputMime, quality);\n                }\n            }\n        }\n\n        function renderCard(originalFile, newBlob, newFilename, previewSrc, mode, wasResized, outputMime) {\n            const origSize = originalFile.size;\n            let finalBlob = newBlob;\n            let finalSize = newBlob.size;\n            let finalFilename = newFilename;\n            let statusBadge = '';\n            \n            \/\/ Nested IF logic (No WP Mangling)\n            let isAlreadyOptimized = false;\n            if (mode === 'compress') {\n                if (finalSize >= origSize) {\n                    if (!wasResized) {\n                        isAlreadyOptimized = true;\n                    }\n                }\n            }\n            \n            \/\/ SMART FALLBACK\n            if (isAlreadyOptimized) {\n                finalBlob = originalFile;\n                finalSize = origSize;\n                finalFilename = originalFile.name; \/\/ Keep original extension\n                \n                \/\/ Beri tahu user kenapa file tidak bisa dikompres jika itu PNG\n                if (outputMime === 'image\/png') {\n                    statusBadge = '<span class=\"wpnt-badge neutral\" title=\"Browser API cannot lossy compress PNGs.\">PNG Limit: Kept Original<\/span>';\n                } else {\n                    statusBadge = '<span class=\"wpnt-badge neutral\">Already Optimized<\/span>';\n                }\n            } else {\n                const savingsPercent = ((origSize - finalSize) \/ origSize * 100).toFixed(1);\n                \n                let isJustConverted = false;\n                if (mode === 'convert') {\n                    if (origSize === finalSize) {\n                        if (!wasResized) {\n                            isJustConverted = true;\n                        }\n                    }\n                }\n\n                if (isJustConverted) {\n                    statusBadge = '<span class=\"wpnt-badge neutral\">Converted<\/span>';\n                } else if (finalSize < origSize) {\n                    statusBadge = '<span class=\"wpnt-badge success\">&darr; ' + savingsPercent + '% Saved<\/span>';\n                } else if (finalSize > origSize) {\n                    statusBadge = '<span class=\"wpnt-badge danger\">&uarr; Size Increased<\/span>';\n                } else {\n                    statusBadge = '<span class=\"wpnt-badge neutral\">Processed<\/span>';\n                }\n            }\n\n            const downloadUrl = URL.createObjectURL(finalBlob);\n\n            const card = document.createElement('div');\n            card.className = 'wpnt-card';\n            \n            \/\/ Safe string concatenation to prevent WordPress 'wpautop' issues\n            card.innerHTML = \n                '<button class=\"wpnt-btn-close\" title=\"Remove image\">&times;<\/button>' +\n                '<img decoding=\"async\" src=\"' + previewSrc + '\" alt=\"' + originalFile.name + '\">' +\n                '<div class=\"wpnt-stats\">' +\n                    '<span class=\"wpnt-filename\" title=\"' + finalFilename + '\">' + finalFilename + '<\/span>' +\n                    '<div class=\"wpnt-stat-row\">' +\n                        '<span>Original:<\/span> <span>' + formatBytes(origSize) + '<\/span>' +\n                    '<\/div>' +\n                    '<div class=\"wpnt-stat-row final\">' +\n                        '<span>New Size:<\/span> <span>' + formatBytes(finalSize) + '<\/span>' +\n                    '<\/div>' +\n                    '<div style=\"display:flex; align-items:center;\">' + statusBadge + '<\/div>' +\n                '<\/div>' +\n                '<a href=\"' + downloadUrl + '\" download=\"' + finalFilename + '\" class=\"wpnt-btn wpnt-btn-download\">Download<\/a>';\n\n            \/\/ Event listener untuk tombol Hapus (Close)\n            card.querySelector('.wpnt-btn-close').addEventListener('click', () => {\n                card.remove();\n                updateActionBarVisibility(); \/\/ Update action bar when item is removed\n            });\n\n            resultsContainer.prepend(card);\n            \n            \/\/ Show Action Bar after successfully rendering a card\n            updateActionBarVisibility();\n        }\n    })();\n<\/script>\n\n\n\n<p><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Image Optimizer Pro Zero-server-load processing. Fast,  [&hellip;]<\/p>\n","protected":false},"featured_media":60,"template":"","meta":[],"class_list":["post-57","tool","type-tool","status-publish","has-post-thumbnail","hentry"],"_links":{"self":[{"href":"https:\/\/tools.sanepo.com\/zh-hans\/wp-json\/wp\/v2\/tool\/57","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/tools.sanepo.com\/zh-hans\/wp-json\/wp\/v2\/tool"}],"about":[{"href":"https:\/\/tools.sanepo.com\/zh-hans\/wp-json\/wp\/v2\/types\/tool"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/tools.sanepo.com\/zh-hans\/wp-json\/wp\/v2\/media\/60"}],"wp:attachment":[{"href":"https:\/\/tools.sanepo.com\/zh-hans\/wp-json\/wp\/v2\/media?parent=57"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}