Spaces:
Running
Running
| {% extends "base.html" %} | |
| {% block title %}Step 2: Draw Boxes ({{ image_index + 1 }} / {{ total_pages }}){% endblock %} | |
| {% block head %} | |
| <style> | |
| :root { --header-height: 56px; } | |
| html, body { height: 100%; overflow: hidden; } | |
| .main-container { display: flex; flex-direction: column; height: 100vh; } | |
| .app-header { height: var(--header-height); flex-shrink: 0; display: flex; align-items: center; justify-content: space-between; padding: 0 0.75rem; background: #2c3034; border-bottom: 1px solid #495057; } | |
| .header-title { font-size: 1.1rem; font-weight: 500; margin: 0; } | |
| .header-actions { display: flex; gap: 0.5rem; } | |
| .btn-header { padding: 0.375rem 0.75rem; font-size: 0.875rem; } | |
| .content-wrapper { flex: 1; min-height: 0; display: flex; flex-direction: column; } | |
| @media (min-width: 992px) { .content-wrapper { flex-direction: row; } } | |
| .image-pane { flex-grow: 1; min-height: 0; position: relative; background: #181a1c; display: flex; align-items: center; justify-content: center; padding: 0.5rem; } | |
| .controls-pane { flex-shrink: 0; background: #212529; border-top: 1px solid #495057; height: 280px; overflow-y: auto; } | |
| @media (min-width: 992px) { .controls-pane { width: 320px; height: 100%; border-top: none; border-left: 1px solid #495057; } } | |
| .controls-content { padding: 1rem; } | |
| #crop-area { position: relative; touch-action: none; line-height: 0; cursor: crosshair; } | |
| #main-image { position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; transition: filter 0.1s linear; } | |
| .control-label { font-size: 0.875rem; margin-bottom: 0.5rem; display: flex; justify-content: space-between; align-items: center; } | |
| .control-value { font-weight: 600; color: #0d6efd; } | |
| .status-message { position: fixed; top: calc(var(--header-height) + 0.5rem); left: 50%; transform: translateX(-50%); z-index: 1050; } | |
| #box-toolbar { position: absolute; background: rgba(44, 48, 52, 0.9); border: 1px solid #495057; border-radius: 8px; padding: 4px; display: none; z-index: 20; backdrop-filter: blur(5px); } | |
| #box-toolbar button { background: transparent; border: none; color: #adb5bd; padding: 6px 8px; font-size: 1.1rem; line-height: 1; } | |
| #box-toolbar button:hover { color: #fff; background: rgba(255, 255, 255, 0.1); } | |
| #loader-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.7); z-index: 9999; display: none; align-items: center; justify-content: center; } | |
| .page-skipper-container { flex-shrink: 0; background: #2c3034; padding: 0.5rem; border-top: 1px solid #495057; overflow-x: auto; } | |
| .page-skipper { display: flex; gap: 0.5rem; justify-content: center; } | |
| .dropdown-menu { max-height: 200px; overflow-y: auto; } | |
| </style> | |
| {% endblock %} | |
| {% block content %} | |
| <div id="loader-overlay"> | |
| <div class="spinner-border text-light" role="status"> | |
| <span class="visually-hidden">Loading...</span> | |
| </div> | |
| </div> | |
| <div class="main-container"> | |
| <header class="app-header"> | |
| <div class="d-flex align-items-center"> | |
| <h1 class="header-title me-3"><i class="bi bi-bounding-box me-2"></i>Page {{ image_index + 1 }} of {{ total_pages }}</h1> | |
| <div class="dropdown"> | |
| <button class="btn btn-sm btn-outline-secondary dropdown-toggle" type="button" id="pageDropdown" data-bs-toggle="dropdown" aria-expanded="false"> | |
| Go to Page | |
| </button> | |
| <ul class="dropdown-menu" aria-labelledby="pageDropdown"> | |
| {% for i in range(total_pages) %} | |
| <li><a class="dropdown-item {{ 'active' if i == image_index else '' }}" href="/cropv2/{{ session_id }}/{{ i }}">Page {{ i + 1 }}</a></li> | |
| {% endfor %} | |
| </ul> | |
| </div> | |
| </div> | |
| <div class="header-actions"> | |
| <button id="backBtn" class="btn btn-secondary btn-header" title="Previous Page"><i class="bi bi-arrow-left"></i><span class="d-none d-sm-inline"> Back</span></button> | |
| <button id="clearBtn" class="btn btn-info btn-header" title="Clear All Boxes"><i class="bi bi-eraser"></i><span class="d-none d-sm-inline"> Clear</span></button> | |
| <button id="saveBtn" class="btn btn-warning btn-header" title="Save Boxes"> | |
| <span class="spinner-border spinner-border-sm d-none" role="status" aria-hidden="true"></span> | |
| <i class="bi bi-save"></i> | |
| <span>Save</span> | |
| </button> | |
| <button id="processBtn" class="btn btn-success btn-header" title="Process Boxes & Next Page"> | |
| <span class="spinner-border spinner-border-sm d-none" role="status" aria-hidden="true"></span> | |
| <i class="bi bi-check-lg"></i> | |
| <span>Process & Next</span> | |
| </button> | |
| </div> | |
| </header> | |
| <div class="progress-container mt-2 mb-3 px-3"> | |
| <div class="d-flex justify-content-between"> | |
| <span>Progress: <span id="progress-text">{{ image_index + 1 }} of {{ total_pages }} pages</span></span> | |
| <span id="pages-left">{{ total_pages - image_index - 1 }} pages left</span> | |
| </div> | |
| <div class="progress" style="height: 10px;"> | |
| <div id="progress-bar" class="progress-bar" role="progressbar" style="width: {{ ((image_index + 1) / total_pages * 100)|int }}%" aria-valuenow="{{ ((image_index + 1) / total_pages * 100)|int }}" aria-valuemin="0" aria-valuemax="100"></div> | |
| </div> | |
| </div> | |
| <div class="content-wrapper"> | |
| <div class="image-pane" id="imagePane"> | |
| <div id="crop-area"> | |
| <img id="main-image" src="/image/upload/{{ image_info.filename }}" alt="PDF Page" crossorigin="anonymous" style="position: absolute; top: 0; left: 0; z-index: 5;"> | |
| <canvas id="draw-canvas" style="position: absolute; top: 0; left: 0; z-index: 10; pointer-events: none;"></canvas> | |
| <div id="box-toolbar"> | |
| <button id="move-up-btn" title="Move Up"><i class="bi bi-arrow-up-circle"></i></button> | |
| <button id="move-down-btn" title="Move Down"><i class="bi bi-arrow-down-circle"></i></button> | |
| <button id="delete-btn" title="Delete Box"><i class="bi bi-trash"></i></button> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="controls-pane"> | |
| <div class="controls-content"> | |
| <div class="alert alert-info small p-2"><i class="bi bi-info-circle me-2"></i>Click and drag to draw boxes. Click a box to select it. Drag corners to resize.</div><hr> | |
| <h6 class="text-white mb-3">Page Adjustments</h6> | |
| <div class="mb-3"><label for="brightness" class="control-label">Brightness<span class="control-value" id="brightnessValue">0</span></label><input type="range" class="form-range" id="brightness" min="-100" max="100" value="0" step="5"></div> | |
| <div class="mb-3"><label for="contrast" class="control-label">Contrast<span class="control-value" id="contrastValue">1.0</span></label><input type="range" class="form-range" id="contrast" min="0.5" max="2.5" value="1.0" step="0.05"></div> | |
| <div class="mb-3"><label for="gamma" class="control-label">Gamma<span class="control-value" id="gammaValue">1.0</span></label><input type="range" class="form-range" id="gamma" min="0.2" max="2.2" value="1.0" step="0.1"></div> | |
| </div> | |
| </div> | |
| </div> | |
| <div id="statusContainer"></div> | |
| <div class="page-skipper-container"> | |
| <div class="page-skipper"> | |
| {% for i in range(total_pages) %} | |
| <a href="/cropv2/{{ session_id }}/{{ i }}" class="btn btn-sm {{ 'btn-primary' if i == image_index else 'btn-outline-secondary' }}">{{ i + 1 }}</a> | |
| {% endfor %} | |
| </div> | |
| </div> | |
| </div> | |
| {% endblock %} | |
| {% block scripts %} | |
| <script> | |
| const image = document.getElementById('main-image'), imagePane = document.getElementById('imagePane'), cropArea = document.getElementById('crop-area'); | |
| const drawCanvas = document.getElementById('draw-canvas'), drawCtx = drawCanvas.getContext('2d'); | |
| const brightnessSlider = document.getElementById('brightness'), contrastSlider = document.getElementById('contrast'), gammaSlider = document.getElementById('gamma'); | |
| const toolbar = document.getElementById('box-toolbar'); | |
| const loader = document.getElementById('loader-overlay'); | |
| const processBtn = document.getElementById('processBtn'); | |
| const sessionId = '{{ session_id }}', imageIndex = parseInt('{{ image_index }}'), totalPages = parseInt('{{ total_pages }} | |
| </script> | |
| {% endblock %} |