{"id":71,"date":"2026-03-16T07:39:51","date_gmt":"2026-03-16T07:39:51","guid":{"rendered":"https:\/\/tools.sanepo.com\/?post_type=tool&#038;p=71"},"modified":"2026-03-16T07:39:53","modified_gmt":"2026-03-16T07:39:53","slug":"online-photo-cropper","status":"publish","type":"tool","link":"https:\/\/tools.sanepo.com\/hi\/features\/online-photo-cropper\/","title":{"rendered":"Free Online Photo Cropper \u2013 Secure Client-Side Image Resizer"},"content":{"rendered":"\n<div id=\"wpnt-photo-cropper\">\n    <div class=\"wpnt-header\">\n        <div class=\"wpnt-header-top\">\n            <h3 class=\"wpnt-title\">Free Online Photo Cropper<\/h3>\n        <\/div>\n        <p class=\"wpnt-subtitle\">Advanced precision image cutting by Sanepo Tools.<\/p>\n        <div class=\"wpnt-privacy-badge\">\n            <svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"12\" height=\"12\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" style=\"display:inline; margin-right:4px; vertical-align:middle;\"><rect width=\"18\" height=\"11\" x=\"3\" y=\"11\" rx=\"2\" ry=\"2\"\/><path d=\"M7 11V7a5 5 0 0 1 10 0v4\"\/><\/svg>\n            100% Secure: Local Browser Processing\n        <\/div>\n    <\/div>\n\n    <!-- Area Upload -->\n    <div class=\"wpnt-dropzone\" id=\"wpnt-dropzone\">\n        <div class=\"wpnt-upload-circle\">\n            <svg class=\"wpnt-icon-upload\" 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\">\n                <path d=\"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4\"\/>\n                <polyline points=\"17 8 12 3 7 8\"\/>\n                <line x1=\"12\" y1=\"3\" x2=\"12\" y2=\"15\"\/>\n            <\/svg>\n        <\/div>\n        <p class=\"wpnt-upload-text\">Drag &#038; Drop your image here<\/p>\n        <p class=\"wpnt-upload-subtext\">or click to browse from device<\/p>\n        <input type=\"file\" id=\"wpnt-file-input\" accept=\"image\/jpeg, image\/png, image\/webp\" \/>\n    <\/div>\n\n    <!-- Area Workspace -->\n    <div id=\"wpnt-workspace\" style=\"display: none;\">\n        \n        <div class=\"wpnt-settings\">\n            <div class=\"wpnt-setting-group\">\n                <div class=\"wpnt-label\">Aspect Ratio<\/div>\n                <select id=\"wpnt-ratio\">\n                    <option value=\"free\">Free \/ Custom Form<\/option>\n                    <option value=\"1\">1:1 (Square &#8211; Profile)<\/option>\n                    <option value=\"0.8\">4:5 (Insta Portrait)<\/option>\n                    <option value=\"0.75\">3:4 (Standard Portrait)<\/option>\n                    <option value=\"1.777\">16:9 (Widescreen)<\/option>\n                    <option value=\"1.333\">4:3 (Landscape)<\/option>\n                <\/select>\n            <\/div>\n            <div class=\"wpnt-setting-group\">\n                <div class=\"wpnt-label\">Export Format<\/div>\n                <select id=\"wpnt-format\">\n                    <option value=\"image\/jpeg\">JPG (Best for Photos)<\/option>\n                    <option value=\"image\/png\">PNG (Best for Graphics)<\/option>\n                    <option value=\"image\/webp\">WebP (Modern Web)<\/option>\n                <\/select>\n            <\/div>\n            <div class=\"wpnt-setting-group\" id=\"wpnt-quality-group\">\n                <div class=\"wpnt-label\">Quality <span id=\"wpnt-quality-val\" class=\"wpnt-badge-val\">90%<\/span><\/div>\n                <input type=\"range\" id=\"wpnt-quality\" class=\"wpnt-range\" min=\"10\" max=\"100\" value=\"90\">\n            <\/div>\n        <\/div>\n\n        <!-- Toolbar Tindakan -->\n        <div class=\"wpnt-toolbar\">\n            <div class=\"wpnt-tools-left\">\n                <button class=\"wpnt-icon-btn\" id=\"wpnt-btn-rot-l\" title=\"Rotate Left\">\n                    <svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8\"\/><path d=\"M3 3v5h5\"\/><\/svg>\n                <\/button>\n                <button class=\"wpnt-icon-btn\" id=\"wpnt-btn-flip-h\" title=\"Flip Horizontal\">\n                    <svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M8 3H5a2 2 0 0 0-2 2v14c0 1.1.9 2 2 2h3\"\/><path d=\"M16 3h3a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-3\"\/><path d=\"M12 20v2\"\/><path d=\"M12 14v2\"\/><path d=\"M12 8v2\"\/><path d=\"M12 2v2\"\/><\/svg>\n                <\/button>\n                <button class=\"wpnt-icon-btn\" id=\"wpnt-btn-flip-v\" title=\"Flip Vertical\">\n                    <svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M21 8V5a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v3\"\/><path d=\"M21 16v3a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-3\"\/><path d=\"M4 12H2\"\/><path d=\"M10 12H8\"\/><path d=\"M16 12h-2\"\/><path d=\"M22 12h-2\"\/><\/svg>\n                <\/button>\n                <div class=\"wpnt-divider\"><\/div>\n                <button class=\"wpnt-icon-btn\" id=\"wpnt-btn-reset\" title=\"Reset Image\">\n                    <svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8\"\/><path d=\"M3 3v5h5\"\/><\/svg>\n                <\/button>\n            <\/div>\n            \n            <div class=\"wpnt-tools-right\">\n                <div class=\"wpnt-dimension-badge\" id=\"wpnt-dimension-display\">0 x 0 px<\/div>\n                <button class=\"wpnt-icon-btn wpnt-preview-btn\" id=\"wpnt-btn-preview\" title=\"Hold to Preview Crop\">\n                    <svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z\"\/><circle cx=\"12\" cy=\"12\" r=\"3\"\/><\/svg>\n                <\/button>\n            <\/div>\n        <\/div>\n\n        <!-- Editor Container -->\n        <div class=\"wpnt-editor-container\" id=\"wpnt-editor-container\">\n            <img decoding=\"async\" id=\"wpnt-source-image\" src=\"\" alt=\"Source\" \/>\n            <div class=\"wpnt-crop-box\" id=\"wpnt-crop-box\">\n                <div class=\"wpnt-grid\">\n                    <div class=\"wpnt-grid-line wpnt-grid-h1\"><\/div>\n                    <div class=\"wpnt-grid-line wpnt-grid-h2\"><\/div>\n                    <div class=\"wpnt-grid-line wpnt-grid-v1\"><\/div>\n                    <div class=\"wpnt-grid-line wpnt-grid-v2\"><\/div>\n                <\/div>\n                <div class=\"wpnt-resizer wpnt-resizer-nw\" data-dir=\"nw\"><\/div>\n                <div class=\"wpnt-resizer wpnt-resizer-ne\" data-dir=\"ne\"><\/div>\n                <div class=\"wpnt-resizer wpnt-resizer-sw\" data-dir=\"sw\"><\/div>\n                <div class=\"wpnt-resizer wpnt-resizer-se\" data-dir=\"se\"><\/div>\n            <\/div>\n        <\/div>\n\n        <div class=\"wpnt-actions\">\n            <button class=\"wpnt-btn wpnt-btn-secondary\" id=\"wpnt-btn-new\">\n                <svg xmlns=\"http:\/\/www.w3.org\/2000\/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 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8\"\/><path d=\"M21 3v5h-5\"\/><\/svg>\n                Upload New\n            <\/button>\n            <button class=\"wpnt-btn\" id=\"wpnt-btn-download\">\n                <svg xmlns=\"http:\/\/www.w3.org\/2000\/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\"\/><polyline points=\"7 10 12 15 17 10\"\/><line x1=\"12\" y1=\"15\" x2=\"12\" y2=\"3\"\/><\/svg>\n                Crop &#038; Download\n            <\/button>\n        <\/div>\n    <\/div>\n<\/div>\n\n<style>\n    @import url('https:\/\/fonts.googleapis.com\/css2?family=Inter:wght@400;500;600;700&display=swap');\n\n    \/* TEMA MANUAL BERBASIS CLASS - BEBAS BENTROK *\/\n    #wpnt-photo-cropper {\n        --accent-color: #4361ee;\n        --accent-hover: #3a56d4;\n        --wpnt-success: #10b981;\n        --wpnt-radius-lg: 16px;\n        --wpnt-radius-md: 12px;\n        --wpnt-radius-sm: 8px;\n        --transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);\n        \n        font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;\n        border-radius: var(--wpnt-radius-lg);\n        padding: 32px;\n        box-sizing: border-box;\n        width: 100%;\n        margin: 30px auto;\n        transition: var(--transition);\n        position: relative;\n        overflow: hidden;\n        \n        \/* Default: Light Mode Variables *\/\n        --bg-main: #ffffff;\n        --bg-panel: #f8fafc;\n        --bg-input: #ffffff;\n        --border-color: #e2e8f0;\n        --text-main: #1e293b;\n        --text-muted: #64748b;\n        --shadow-sm: 0 1px 2px 0 rgb(0 0 0 \/ 0.05);\n        --shadow-md: 0 4px 6px -1px rgb(0 0 0 \/ 0.1), 0 2px 4px -2px rgb(0 0 0 \/ 0.1);\n        --shadow-lg: 0 10px 15px -3px rgb(0 0 0 \/ 0.1), 0 4px 6px -4px rgb(0 0 0 \/ 0.1);\n        --grid-color: rgba(226, 232, 240, 0.8);\n        \n        background: var(--bg-main);\n        border: 1px solid var(--border-color);\n        box-shadow: var(--shadow-lg);\n    }\n\n    \/* Varian Dark Mode - Diaktifkan oleh JS Sensor Otomatis *\/\n    #wpnt-photo-cropper.wpnt-dark-theme {\n        --bg-main: #0f172a;\n        --bg-panel: #1e293b;\n        --bg-input: #0f172a;\n        --border-color: #334155;\n        --text-main: #f1f5f9;\n        --text-muted: #94a3b8;\n        --shadow-sm: 0 1px 2px 0 rgb(0 0 0 \/ 0.3);\n        --shadow-md: 0 4px 6px -1px rgb(0 0 0 \/ 0.4);\n        --shadow-lg: 0 10px 25px -5px rgb(0 0 0 \/ 0.5);\n        --grid-color: rgba(51, 65, 85, 0.8);\n    }\n\n    \/* Reset Global untuk isolasi *\/\n    #wpnt-photo-cropper * { box-sizing: border-box; color: var(--text-main); margin: 0; padding: 0; }\n    #wpnt-photo-cropper p { line-height: 1.5; }\n\n    \/* Header *\/\n    #wpnt-photo-cropper .wpnt-header { text-align: center; margin-bottom: 28px; }\n    #wpnt-photo-cropper .wpnt-header-top { display: flex; justify-content: center; align-items: center; gap: 12px; position: relative; margin-bottom: 8px;}\n    \n    #wpnt-photo-cropper .wpnt-title { font-size: 1.75rem; font-weight: 700; letter-spacing: -0.025em; }\n    #wpnt-photo-cropper .wpnt-subtitle { font-size: 0.95rem; color: var(--text-muted); }\n    #wpnt-photo-cropper .wpnt-privacy-badge { display: inline-flex; align-items: center; background: rgba(16, 185, 129, 0.1); color: var(--wpnt-success); border: 1px solid rgba(16, 185, 129, 0.2); padding: 4px 12px; border-radius: 20px; font-size: 0.75rem; font-weight: 600; margin-top: 12px; }\n\n    \/* Dropzone Terpoles *\/\n    #wpnt-photo-cropper .wpnt-dropzone { border: 2px dashed var(--border-color); border-radius: var(--wpnt-radius-lg); padding: 48px 24px; text-align: center; cursor: pointer; background: var(--bg-panel); position: relative; transition: var(--transition); }\n    #wpnt-photo-cropper .wpnt-dropzone:hover, #wpnt-photo-cropper .wpnt-dropzone.dragover { border-color: var(--accent-color); background: rgba(67, 97, 238, 0.05); }\n    #wpnt-photo-cropper .wpnt-dropzone input[type=\"file\"] { display: none; }\n    #wpnt-photo-cropper .wpnt-upload-circle { width: 64px; height: 64px; background: var(--bg-main); border-radius: 50%; display: flex; align-items: center; justify-content: center; margin: 0 auto 16px auto; box-shadow: var(--shadow-sm); border: 1px solid var(--border-color); transition: var(--transition); }\n    #wpnt-photo-cropper .wpnt-dropzone:hover .wpnt-upload-circle { transform: translateY(-4px) scale(1.05); box-shadow: var(--shadow-md); border-color: var(--accent-color); }\n    #wpnt-photo-cropper .wpnt-icon-upload { color: var(--accent-color); width: 28px; height: 28px; }\n    #wpnt-photo-cropper .wpnt-upload-text { font-weight: 600; font-size: 1.1rem; margin-bottom: 4px; }\n    #wpnt-photo-cropper .wpnt-upload-subtext { color: var(--text-muted); font-size: 0.85rem; }\n\n    \/* Settings Grid *\/\n    #wpnt-photo-cropper .wpnt-settings { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 16px; margin-bottom: 16px; }\n    #wpnt-photo-cropper .wpnt-setting-group { background: var(--bg-panel); padding: 12px 16px; border-radius: var(--wpnt-radius-md); border: 1px solid var(--border-color); }\n    #wpnt-photo-cropper .wpnt-label { display: flex; justify-content: space-between; align-items: center; font-weight: 600; font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.05em; color: var(--text-muted); margin-bottom: 10px; }\n    #wpnt-photo-cropper .wpnt-badge-val { background: var(--accent-color); color: white; padding: 2px 6px; border-radius: 4px; font-size: 0.7rem; font-weight: 700; }\n    \n    \/* Custom Inputs *\/\n    #wpnt-photo-cropper select { width: 100%; padding: 8px 12px; border-radius: var(--wpnt-radius-sm); border: 1px solid var(--border-color); background-color: var(--bg-input); font-family: inherit; font-size: 0.9rem; color: var(--text-main); outline: none; cursor: pointer; appearance: none; 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='%234361ee' 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\"); background-repeat: no-repeat; background-position: right 12px center; background-size: 16px; transition: border-color 0.2s; }\n    #wpnt-photo-cropper select:focus { border-color: var(--accent-color); box-shadow: 0 0 0 3px rgba(67, 97, 238, 0.15); }\n    \n    \/* Range Slider *\/\n    #wpnt-photo-cropper .wpnt-range { -webkit-appearance: none; width: 100%; background: transparent; height: 24px; outline: none; margin-top: 4px; }\n    #wpnt-photo-cropper .wpnt-range::-webkit-slider-runnable-track { width: 100%; height: 6px; background: var(--border-color); border-radius: 3px; }\n    #wpnt-photo-cropper .wpnt-range::-webkit-slider-thumb { -webkit-appearance: none; height: 16px; width: 16px; border-radius: 50%; background: var(--accent-color); margin-top: -5px; cursor: pointer; box-shadow: var(--shadow-sm); transition: transform 0.1s; }\n    #wpnt-photo-cropper .wpnt-range::-webkit-slider-thumb:hover { transform: scale(1.2); }\n    #wpnt-photo-cropper .wpnt-range:focus::-webkit-slider-thumb { box-shadow: 0 0 0 3px rgba(67, 97, 238, 0.2); }\n\n    \/* Toolbar Elegan *\/\n    #wpnt-photo-cropper .wpnt-toolbar { display: flex; justify-content: space-between; align-items: center; background: var(--bg-panel); padding: 8px 12px; border-radius: var(--wpnt-radius-md); border: 1px solid var(--border-color); margin-bottom: 16px; flex-wrap: wrap; gap: 10px; }\n    #wpnt-photo-cropper .wpnt-tools-left, #wpnt-photo-cropper .wpnt-tools-right { display: flex; align-items: center; gap: 8px; }\n    #wpnt-photo-cropper .wpnt-divider { width: 1px; height: 20px; background: var(--border-color); margin: 0 4px; }\n    \n    #wpnt-photo-cropper .wpnt-icon-btn { background: var(--bg-main); border: 1px solid var(--border-color); color: var(--text-main); width: 36px; height: 36px; border-radius: var(--wpnt-radius-sm); display: flex; justify-content: center; align-items: center; cursor: pointer; transition: all 0.2s; box-shadow: var(--shadow-sm); }\n    #wpnt-photo-cropper .wpnt-icon-btn:hover { background: var(--bg-panel); border-color: var(--accent-color); color: var(--accent-color); transform: translateY(-2px); box-shadow: var(--shadow-md); }\n    #wpnt-photo-cropper .wpnt-dimension-badge { font-family: monospace; font-size: 0.8rem; font-weight: 600; background: var(--bg-main); padding: 6px 12px; border-radius: 20px; color: var(--text-main); border: 1px solid var(--border-color); }\n    #wpnt-photo-cropper .wpnt-preview-btn { border-color: var(--accent-color); color: var(--accent-color); }\n    #wpnt-photo-cropper .wpnt-preview-btn:active { background: var(--accent-color); color: white; transform: scale(0.95); }\n\n    \/* Custom Cropper Workspace *\/\n    #wpnt-photo-cropper .wpnt-editor-container { position: relative; width: 100%; height: 55vh; min-height: 320px; background: repeating-conic-gradient(var(--grid-color) 0% 25%, transparent 0% 50%) 50% \/ 20px 20px; border-radius: var(--wpnt-radius-md); overflow: hidden; display: flex; justify-content: center; align-items: center; touch-action: none; user-select: none; border: 1px solid var(--border-color); box-shadow: inset var(--shadow-sm); }\n    #wpnt-photo-cropper #wpnt-source-image { position: absolute; max-width: 100%; max-height: 100%; object-fit: contain; pointer-events: none; }\n    \n    #wpnt-photo-cropper .wpnt-crop-box { position: absolute; border: 2px dashed #ffffff; box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.65); cursor: move; touch-action: none; z-index: 10; display: flex; flex-direction: column; transition: box-shadow 0.2s; }\n    \n    \/* Preview Mode *\/\n    #wpnt-photo-cropper .wpnt-editor-container.preview-mode .wpnt-crop-box { box-shadow: 0 0 0 9999px var(--bg-main); border-color: transparent; }\n    #wpnt-photo-cropper .wpnt-editor-container.preview-mode .wpnt-grid, \n    #wpnt-photo-cropper .wpnt-editor-container.preview-mode .wpnt-resizer { display: none; }\n\n    \/* Rule of Thirds *\/\n    #wpnt-photo-cropper .wpnt-grid { position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; opacity: 0; transition: opacity 0.2s ease; }\n    #wpnt-photo-cropper .wpnt-crop-box.active .wpnt-grid { opacity: 1; }\n    #wpnt-photo-cropper .wpnt-grid-line { position: absolute; background: rgba(255, 255, 255, 0.7); box-shadow: 0 0 2px rgba(0,0,0,0.3); }\n    #wpnt-photo-cropper .wpnt-grid-h1 { top: 33.33%; left: 0; width: 100%; height: 1px; }\n    #wpnt-photo-cropper .wpnt-grid-h2 { top: 66.66%; left: 0; width: 100%; height: 1px; }\n    #wpnt-photo-cropper .wpnt-grid-v1 { left: 33.33%; top: 0; height: 100%; width: 1px; }\n    #wpnt-photo-cropper .wpnt-grid-v2 { left: 66.66%; top: 0; height: 100%; width: 1px; }\n\n    \/* Handles *\/\n    #wpnt-photo-cropper .wpnt-resizer { position: absolute; width: 18px; height: 18px; background: var(--accent-color); border: 2px solid #fff; border-radius: 50%; z-index: 20; box-shadow: var(--shadow-md); transition: transform 0.1s; }\n    #wpnt-photo-cropper .wpnt-resizer:hover { transform: scale(1.3); }\n    #wpnt-photo-cropper .wpnt-resizer-nw { top: -9px; left: -9px; cursor: nwse-resize; }\n    #wpnt-photo-cropper .wpnt-resizer-ne { top: -9px; right: -9px; cursor: nesw-resize; }\n    #wpnt-photo-cropper .wpnt-resizer-sw { bottom: -9px; left: -9px; cursor: nesw-resize; }\n    #wpnt-photo-cropper .wpnt-resizer-se { bottom: -9px; right: -9px; cursor: nwse-resize; }\n\n    \/* Action Buttons *\/\n    #wpnt-photo-cropper .wpnt-actions { display: grid; grid-template-columns: 1fr 2fr; gap: 16px; margin-top: 24px; }\n    #wpnt-photo-cropper .wpnt-btn { background: var(--accent-color); color: #ffffff; border: none; padding: 14px 24px; border-radius: var(--wpnt-radius-sm); cursor: pointer; font-weight: 600; font-size: 1rem; display: inline-flex; align-items: center; justify-content: center; gap: 8px; width: 100%; transition: all 0.2s; box-shadow: var(--shadow-sm); }\n    #wpnt-photo-cropper .wpnt-btn:hover:not(:disabled) { background: var(--accent-hover); transform: translateY(-2px); box-shadow: var(--shadow-md); }\n    #wpnt-photo-cropper .wpnt-btn-secondary { background: var(--bg-panel); color: var(--text-main); border: 1px solid var(--border-color); }\n    #wpnt-photo-cropper .wpnt-btn-secondary:hover:not(:disabled) { background: var(--border-color); box-shadow: none; color: var(--text-main); }\n\n    @media (max-width: 600px) {\n        #wpnt-photo-cropper { padding: 20px 16px; margin: 15px auto; border-radius: var(--wpnt-radius-md); }\n        #wpnt-photo-cropper .wpnt-title { font-size: 1.4rem; }\n        #wpnt-photo-cropper .wpnt-header-top { justify-content: flex-start; }\n        #wpnt-photo-cropper .wpnt-settings { grid-template-columns: 1fr; gap: 12px; }\n        #wpnt-photo-cropper .wpnt-editor-container { height: 50vh; }\n        #wpnt-photo-cropper .wpnt-actions { grid-template-columns: 1fr; gap: 12px; }\n    }\n<\/style>\n\n<script>\n    (function() {\n        \/\/ Elemen DOM\n        const container = document.getElementById('wpnt-photo-cropper');\n        const dropzone = document.getElementById('wpnt-dropzone');\n        const fileInput = document.getElementById('wpnt-file-input');\n        const workspace = document.getElementById('wpnt-workspace');\n        const sourceImage = document.getElementById('wpnt-source-image');\n        const editorContainer = document.getElementById('wpnt-editor-container');\n        const cropBox = document.getElementById('wpnt-crop-box');\n        \n        \/\/ Input & Controls\n        const ratioSelect = document.getElementById('wpnt-ratio');\n        const formatSelect = document.getElementById('wpnt-format');\n        const qualityInput = document.getElementById('wpnt-quality');\n        const qualityVal = document.getElementById('wpnt-quality-val');\n        const qualityGroup = document.getElementById('wpnt-quality-group');\n        \n        \/\/ Buttons\n        const btnDownload = document.getElementById('wpnt-btn-download');\n        const btnNew = document.getElementById('wpnt-btn-new');\n        const btnRotL = document.getElementById('wpnt-btn-rot-l');\n        const btnFlipH = document.getElementById('wpnt-btn-flip-h');\n        const btnFlipV = document.getElementById('wpnt-btn-flip-v');\n        const btnReset = document.getElementById('wpnt-btn-reset');\n        const btnPreview = document.getElementById('wpnt-btn-preview');\n        const dimDisplay = document.getElementById('wpnt-dimension-display');\n\n        let originalImage = new Image();\n        let flattenedImage = new Image(); \n        let tfState = { rot: 0, flipH: 1, flipV: 1 };\n        let boxState = { x: 0, y: 0, w: 100, h: 100 };\n        let imgState = { x: 0, y: 0, w: 0, h: 0, natW: 0, natH: 0 };\n        let action = null; \n        let startX, startY, startBox;\n\n        \/\/ --- SMART THEME DETECTOR (WP THEME SYNC) ---\n        function setTheme(isDark) {\n            if(isDark) {\n                container.classList.add('wpnt-dark-theme');\n            } else {\n                container.classList.remove('wpnt-dark-theme');\n            }\n        }\n\n        function autoDetectTheme() {\n            \/\/ Sensor 1: Cek nama class umum pada tema WordPress ('dark', 'night', dsb)\n            const htmlClass = document.documentElement.className || '';\n            const bodyClass = document.body.className || '';\n            const dataTheme = document.documentElement.getAttribute('data-theme') || '';\n            \n            const isDarkClass = \/(dark|night)\/i.test(htmlClass + ' ' + bodyClass + ' ' + dataTheme);\n            const isLightClass = \/(light|day)\/i.test(htmlClass + ' ' + bodyClass + ' ' + dataTheme);\n            \n            if (isDarkClass) {\n                setTheme(true);\n            } else if (isLightClass) {\n                setTheme(false);\n            } else {\n                \/\/ Sensor 2: Jika tidak terdeteksi dari tema WP, ikuti preferensi sistem operasi\n                const isOsDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;\n                setTheme(isOsDark);\n            }\n        }\n\n        \/\/ Jalankan deteksi saat pertama kali load\n        autoDetectTheme();\n\n        \/\/ Sensor 3: Dengarkan perubahan secara instan jika user mengubah tema di setting OS\n        if (window.matchMedia) {\n            window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', autoDetectTheme);\n        }\n\n        \/\/ Sensor 4: \"Mata-mata\" (MutationObserver) untuk menangkap perubahan class saat user klik tombol Dark Mode dari Tema WP\n        const observer = new MutationObserver(autoDetectTheme);\n        observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class', 'data-theme'] });\n        observer.observe(document.body, { attributes: true, attributeFilter: ['class'] });\n\n        \/\/ --- UPLOAD HANDLER ---\n        dropzone.addEventListener('click', () => fileInput.click());\n        dropzone.addEventListener('dragover', (e) => { e.preventDefault(); dropzone.classList.add('dragover'); });\n        dropzone.addEventListener('dragleave', () => dropzone.classList.remove('dragover'));\n        dropzone.addEventListener('drop', (e) => {\n            e.preventDefault(); dropzone.classList.remove('dragover');\n            if (e.dataTransfer.files.length) handleFile(e.dataTransfer.files[0]);\n        });\n        fileInput.addEventListener('change', (e) => {\n            if (e.target.files.length) handleFile(e.target.files[0]);\n        });\n\n        function handleFile(file) {\n            if (!file.type.startsWith('image\/')) return alert('Gunakan file gambar yang valid.');\n            tfState = { rot: 0, flipH: 1, flipV: 1 }; \/\/ Reset state transform\n            \n            const reader = new FileReader();\n            reader.onload = (e) => {\n                originalImage.onload = () => { applyTransform(); };\n                originalImage.src = e.target.result;\n            };\n            reader.readAsDataURL(file);\n        }\n\n        \/\/ --- SMART TRANSFORM ENGINE ---\n        function applyTransform() {\n            const canvas = document.createElement('canvas');\n            const ctx = canvas.getContext('2d');\n            const isRotated = Math.abs(tfState.rot) % 180 === 90;\n            canvas.width = isRotated ? originalImage.height : originalImage.width;\n            canvas.height = isRotated ? originalImage.width : originalImage.height;\n\n            ctx.translate(canvas.width \/ 2, canvas.height \/ 2);\n            ctx.rotate(tfState.rot * Math.PI \/ 180);\n            ctx.scale(tfState.flipH, tfState.flipV);\n            ctx.drawImage(originalImage, -originalImage.width \/ 2, -originalImage.height \/ 2);\n\n            flattenedImage.onload = () => {\n                imgState.natW = flattenedImage.width;\n                imgState.natH = flattenedImage.height;\n                sourceImage.src = flattenedImage.src;\n                \n                dropzone.style.display = 'none';\n                workspace.style.display = 'block';\n                setTimeout(() => initCropBox(), 50);\n            };\n            flattenedImage.src = canvas.toDataURL('image\/png');\n        }\n\n        \/\/ Toolbar Listeners\n        btnRotL.addEventListener('click', () => { tfState.rot = (tfState.rot - 90) % 360; applyTransform(); });\n        btnFlipH.addEventListener('click', () => { tfState.flipH *= -1; applyTransform(); });\n        btnFlipV.addEventListener('click', () => { tfState.flipV *= -1; applyTransform(); });\n        btnReset.addEventListener('click', () => { \n            tfState = { rot: 0, flipH: 1, flipV: 1 }; \n            ratioSelect.value = \"free\";\n            applyTransform(); \n        });\n\n        \/\/ --- KUALITAS GAMBAR ---\n        qualityInput.addEventListener('input', (e) => {\n            qualityVal.innerText = e.target.value + '%';\n        });\n        formatSelect.addEventListener('change', (e) => {\n            \/\/ Sembunyikan slider kualitas jika pilih PNG (karena PNG lossless)\n            if(e.target.value === 'image\/png') {\n                qualityGroup.style.opacity = '0.5';\n                qualityInput.disabled = true;\n            } else {\n                qualityGroup.style.opacity = '1';\n                qualityInput.disabled = false;\n            }\n        });\n\n        \/\/ --- RENDER & LOGIKA BOX ---\n        function initCropBox() {\n            const rect = sourceImage.getBoundingClientRect();\n            imgState.w = rect.width;\n            imgState.h = rect.height;\n            imgState.x = sourceImage.offsetLeft + (sourceImage.offsetWidth - rect.width) \/ 2;\n            imgState.y = sourceImage.offsetTop + (sourceImage.offsetHeight - rect.height) \/ 2;\n\n            let initSize = Math.min(imgState.w, imgState.h) * 0.8;\n            boxState.w = initSize;\n            boxState.h = initSize;\n            applyRatio();\n\n            boxState.x = imgState.x + (imgState.w - boxState.w) \/ 2;\n            boxState.y = imgState.y + (imgState.h - boxState.h) \/ 2;\n            updateBoxDOM();\n        }\n\n        function updateBoxDOM() {\n            if (boxState.x < imgState.x) boxState.x = imgState.x;\n            if (boxState.y < imgState.y) boxState.y = imgState.y;\n            if (boxState.w > imgState.w) boxState.w = imgState.w;\n            if (boxState.h > imgState.h) boxState.h = imgState.h;\n            if (boxState.x + boxState.w > imgState.x + imgState.w) boxState.x = imgState.x + imgState.w - boxState.w;\n            if (boxState.y + boxState.h > imgState.y + imgState.h) boxState.y = imgState.y + imgState.h - boxState.h;\n\n            cropBox.style.left = boxState.x + 'px';\n            cropBox.style.top = boxState.y + 'px';\n            cropBox.style.width = boxState.w + 'px';\n            cropBox.style.height = boxState.h + 'px';\n            updateDimensionDisplay();\n        }\n\n        function updateDimensionDisplay() {\n            const scaleX = imgState.natW \/ imgState.w;\n            const finalW = Math.round(boxState.w * scaleX);\n            const finalH = Math.round(boxState.h * scaleX);\n            dimDisplay.innerText = `${finalW} x ${finalH} px`;\n        }\n\n        ratioSelect.addEventListener('change', () => {\n            applyRatio();\n            boxState.x = imgState.x + (imgState.w - boxState.w) \/ 2;\n            boxState.y = imgState.y + (imgState.h - boxState.h) \/ 2;\n            updateBoxDOM();\n        });\n\n        function applyRatio() {\n            const ratioVal = ratioSelect.value;\n            if (ratioVal === 'free') return;\n            const targetRatio = parseFloat(ratioVal);\n            boxState.h = boxState.w \/ targetRatio;\n            if (boxState.h > imgState.h * 0.9) {\n                boxState.h = imgState.h * 0.9;\n                boxState.w = boxState.h * targetRatio;\n            }\n        }\n\n        \/\/ --- DRAG & DROP & RESIZE ---\n        function getPos(e) { return { x: e.touches ? e.touches[0].clientX : e.clientX, y: e.touches ? e.touches[0].clientY : e.clientY }; }\n        \n        function onStart(e) {\n            if (e.target.classList.contains('wpnt-resizer')) action = e.target.dataset.dir;\n            else if (e.target.closest('#wpnt-crop-box')) action = 'move';\n            else return;\n            \n            e.preventDefault();\n            const pos = getPos(e);\n            startX = pos.x; startY = pos.y;\n            startBox = { ...boxState };\n            cropBox.classList.add('active'); \n        }\n\n        function onMove(e) {\n            if (!action) return;\n            e.preventDefault();\n            const pos = getPos(e);\n            const dx = pos.x - startX;\n            const dy = pos.y - startY;\n\n            if (action === 'move') {\n                boxState.x = startBox.x + dx;\n                boxState.y = startBox.y + dy;\n            } else {\n                let newW = startBox.w, newH = startBox.h, newX = startBox.x, newY = startBox.y;\n                if (action.includes('e')) newW = startBox.w + dx;\n                if (action.includes('s')) newH = startBox.h + dy;\n                if (action.includes('w')) { newW = startBox.w - dx; newX = startBox.x + dx; }\n                if (action.includes('n')) { newH = startBox.h - dy; newY = startBox.y + dy; }\n\n                if (newW < 40) { newW = 40; if (action.includes('w')) newX = startBox.x + startBox.w - 40; }\n                if (newH < 40) { newH = 40; if (action.includes('n')) newY = startBox.y + startBox.h - 40; }\n\n                const ratioVal = ratioSelect.value;\n                if (ratioVal !== 'free') {\n                    const targetRatio = parseFloat(ratioVal);\n                    if (action === 'se' || action === 'ne') {\n                        newH = newW \/ targetRatio;\n                        if (action === 'ne') newY = startBox.y + (startBox.h - newH);\n                    } else if (action === 'sw' || action === 'nw') {\n                        newH = newW \/ targetRatio;\n                        if (action === 'nw') newY = startBox.y + (startBox.h - newH);\n                    }\n                }\n                boxState.w = newW; boxState.h = newH; boxState.x = newX; boxState.y = newY;\n            }\n            updateBoxDOM();\n        }\n\n        function onEnd() { action = null; cropBox.classList.remove('active'); }\n\n        editorContainer.addEventListener('mousedown', onStart);\n        document.addEventListener('mousemove', onMove);\n        document.addEventListener('mouseup', onEnd);\n        editorContainer.addEventListener('touchstart', onStart, {passive: false});\n        document.addEventListener('touchmove', onMove, {passive: false});\n        document.addEventListener('touchend', onEnd);\n\n        \/\/ --- PREVIEW ---\n        const startPreview = (e) => { e.preventDefault(); editorContainer.classList.add('preview-mode'); };\n        const endPreview = (e) => { e.preventDefault(); editorContainer.classList.remove('preview-mode'); };\n        btnPreview.addEventListener('mousedown', startPreview);\n        btnPreview.addEventListener('mouseup', endPreview);\n        btnPreview.addEventListener('mouseleave', endPreview);\n        btnPreview.addEventListener('touchstart', startPreview, {passive: false});\n        btnPreview.addEventListener('touchend', endPreview);\n\n        \/\/ --- NEW IMAGE & DOWNLOAD ---\n        btnNew.addEventListener('click', () => {\n            fileInput.value = '';\n            workspace.style.display = 'none';\n            dropzone.style.display = 'block';\n        });\n\n        btnDownload.addEventListener('click', () => {\n            const btnOriginalText = btnDownload.innerHTML;\n            btnDownload.innerHTML = 'Processing...';\n            btnDownload.disabled = true;\n\n            setTimeout(() => {\n                const scaleX = imgState.natW \/ imgState.w;\n                const scaleY = imgState.natH \/ imgState.h;\n                const cropX = (boxState.x - imgState.x) * scaleX;\n                const cropY = (boxState.y - imgState.y) * scaleY;\n                const cropW = boxState.w * scaleX;\n                const cropH = boxState.h * scaleY;\n\n                const canvas = document.createElement('canvas');\n                canvas.width = cropW;\n                canvas.height = cropH;\n                const ctx = canvas.getContext('2d');\n\n                ctx.drawImage(flattenedImage, cropX, cropY, cropW, cropH, 0, 0, cropW, cropH);\n\n                const format = formatSelect.value;\n                const ext = format === 'image\/jpeg' ? 'jpg' : format === 'image\/webp' ? 'webp' : 'png';\n                \n                \/\/ Ambil nilai dari slider quality (misal 90 menjadi 0.9)\n                const qualityScore = parseInt(qualityInput.value) \/ 100;\n                const dataUrl = canvas.toDataURL(format, qualityScore);\n\n                const link = document.createElement('a');\n                link.download = `sanepo-crop-${Date.now()}.${ext}`;\n                link.href = dataUrl;\n                document.body.appendChild(link);\n                link.click();\n                document.body.removeChild(link);\n\n                btnDownload.innerHTML = btnOriginalText;\n                btnDownload.disabled = false;\n            }, 100);\n        });\n\n        window.addEventListener('resize', () => { if (workspace.style.display === 'block') initCropBox(); });\n    })();\n<\/script>\n","protected":false},"excerpt":{"rendered":"<p>Free Online Photo Cropper Advanced precision image cutting by Sanepo Tools. 100% Secure: Local Browser Processing Drag &#038; Drop your image here or click to browse from device Aspect Ratio Free \/ Custom Form1:1 (Square &#8211; Profile)4:5 (Insta Portrait)3:4 (Standard Portrait)16:9 (Widescreen)4:3 (Landscape) Export Format JPG (Best for Photos)PNG (Best for Graphics)WebP (Modern Web) Quality [&hellip;]<\/p>\n","protected":false},"featured_media":0,"template":"","meta":[],"class_list":["post-71","tool","type-tool","status-publish","hentry"],"_links":{"self":[{"href":"https:\/\/tools.sanepo.com\/hi\/wp-json\/wp\/v2\/tool\/71","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/tools.sanepo.com\/hi\/wp-json\/wp\/v2\/tool"}],"about":[{"href":"https:\/\/tools.sanepo.com\/hi\/wp-json\/wp\/v2\/types\/tool"}],"wp:attachment":[{"href":"https:\/\/tools.sanepo.com\/hi\/wp-json\/wp\/v2\/media?parent=71"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}