docling-studio/index.html
2025-05-15 16:36:44 +02:00

1306 lines
No EOL
47 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PDF DocTags Analyzer</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.4.120/pdf.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/toastify-js/1.12.0/toastify.min.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/toastify-js/1.12.0/toastify.min.css">
<style>
:root {
--primary-color: #3B82F6; /* Blue 500 */
--primary-hover: #2563EB; /* Blue 600 */
--secondary-color: #10B981; /* Green 500 */
--secondary-hover: #059669; /* Green 600 */
--danger-color: #EF4444; /* Red 500 */
--gray-100: #F3F4F6;
--gray-200: #E5E7EB;
--gray-300: #D1D5DB;
--gray-700: #374151;
--gray-800: #1F2937;
--gray-900: #111827;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
line-height: 1.5;
color: var(--gray-800);
height: 100vh;
display: flex;
flex-direction: column;
}
header {
background-color: var(--gray-900);
color: white;
padding: 1rem;
display: flex;
justify-content: space-between;
align-items: center;
}
h1 {
font-size: 1.25rem;
font-weight: bold;
}
button {
cursor: pointer;
border: none;
border-radius: 0.25rem;
padding: 0.5rem 1rem;
display: inline-flex;
align-items: center;
gap: 0.5rem;
font-weight: 500;
transition: background-color 0.2s;
}
.btn-primary {
background-color: var(--primary-color);
color: white;
}
.btn-primary:hover {
background-color: var(--primary-hover);
}
.btn-secondary {
background-color: var(--secondary-color);
color: white;
}
.btn-secondary:hover {
background-color: var(--secondary-hover);
}
.btn-outline {
background-color: transparent;
border: 1px solid var(--gray-300);
}
.btn-outline:hover {
background-color: var(--gray-100);
}
.icon {
width: 1rem;
height: 1rem;
}
.flex-row {
display: flex;
flex-direction: row;
}
.flex-col {
display: flex;
flex-direction: column;
}
.gap-2 {
gap: 0.5rem;
}
.gap-4 {
gap: 1rem;
}
main {
flex: 1;
display: flex;
overflow: hidden;
}
.settings-panel {
background-color: var(--gray-100);
padding: 1rem;
border-bottom: 1px solid var(--gray-300);
display: none;
}
.settings-panel.open {
display: block;
}
.settings-panel h2 {
font-size: 1rem;
margin-bottom: 0.5rem;
}
.settings-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1rem;
}
.form-group {
margin-bottom: 0.5rem;
}
.form-group label {
display: block;
font-size: 0.875rem;
margin-bottom: 0.25rem;
}
.form-group input[type="number"] {
width: 100%;
padding: 0.5rem;
border: 1px solid var(--gray-300);
border-radius: 0.25rem;
}
.checkbox-group {
display: flex;
align-items: center;
gap: 0.5rem;
}
.workspace {
flex: 2;
display: flex;
flex-direction: column;
padding: 1rem;
overflow: hidden;
}
.toolbar {
display: flex;
justify-content: space-between;
margin-bottom: 0.5rem;
}
.canvas-container {
flex: 1;
background-color: var(--gray-200);
border: 1px solid var(--gray-300);
border-radius: 0.25rem;
overflow: auto;
display: flex;
align-items: center;
justify-content: center;
position: relative;
}
#pdfCanvas {
background-color: white;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
}
.footer-bar {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 0.5rem;
}
.file-info {
font-size: 0.875rem;
color: var(--gray-700);
}
.sidebar {
flex: 1;
max-width: 400px;
border-left: 1px solid var(--gray-300);
background-color: var(--gray-100);
padding: 1rem;
overflow-y: auto;
}
.sidebar h2 {
font-size: 1.125rem;
margin-bottom: 0.5rem;
}
.status-card {
padding: 0.75rem;
border-radius: 0.25rem;
margin-bottom: 1rem;
}
.status-card.loading {
background-color: rgba(59, 130, 246, 0.1);
}
.status-card.success {
background-color: rgba(16, 185, 129, 0.1);
}
.zone-list {
margin-top: 1rem;
}
.zone-item {
margin-bottom: 0.75rem;
padding-bottom: 0.75rem;
border-bottom: 1px solid var(--gray-300);
}
.zone-label {
display: inline-block;
padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
color: white;
font-size: 0.875rem;
font-weight: 500;
margin-bottom: 0.25rem;
}
.zone-coords {
font-size: 0.75rem;
color: var(--gray-700);
margin-bottom: 0.25rem;
}
.zone-content {
font-size: 0.875rem;
padding-left: 0.5rem;
}
.legend {
display: flex;
flex-wrap: wrap;
gap: 0.75rem;
margin: 1rem 0;
}
.legend-item {
display: flex;
align-items: center;
gap: 0.25rem;
font-size: 0.75rem;
}
.legend-color {
width: 0.75rem;
height: 0.75rem;
border-radius: 0.125rem;
border: 1px solid rgba(0, 0, 0, 0.1);
}
.spinner {
border: 2px solid rgba(255, 255, 255, 0.3);
border-radius: 50%;
border-top: 2px solid white;
width: 1rem;
height: 1rem;
animation: spin 1s linear infinite;
}
.command-preview {
margin-top: 1rem;
padding: 0.75rem;
background-color: rgba(59, 130, 246, 0.1);
border-radius: 0.25rem;
}
.command-preview h3 {
font-size: 0.875rem;
margin-bottom: 0.5rem;
}
.code-block {
background-color: var(--gray-900);
color: white;
padding: 0.75rem;
border-radius: 0.25rem;
font-family: monospace;
font-size: 0.75rem;
overflow-x: auto;
white-space: pre-wrap;
}
.welcome {
max-width: 800px;
padding: 2rem;
text-align: center;
background-color: white;
border-radius: 0.5rem;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
}
.welcome h2 {
font-size: 1.5rem;
margin-bottom: 1rem;
}
.welcome p {
color: var(--gray-700);
margin-bottom: 1.5rem;
}
.feature-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 1rem;
margin-top: 2rem;
text-align: left;
}
.feature-item {
background-color: var(--gray-100);
padding: 1rem;
border-radius: 0.25rem;
}
.feature-item h3 {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.5rem;
font-size: 1rem;
}
.feature-item p {
font-size: 0.875rem;
}
.zone-overlay {
position: absolute;
border: 2px solid;
pointer-events: none;
background-color: rgba(255, 255, 255, 0.1);
transition: background-color 0.3s;
}
.zone-overlay-label {
position: absolute;
top: 0;
left: 0;
font-size: 10px;
padding: 2px 4px;
background-color: rgba(255, 255, 255, 0.8);
border-bottom-right-radius: 3px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* For smaller screens */
@media (max-width: 1024px) {
main {
flex-direction: column;
}
.sidebar {
max-width: 100%;
border-left: none;
border-top: 1px solid var(--gray-300);
}
.settings-grid {
grid-template-columns: 1fr;
}
}
</style>
</head>
<body>
<header>
<h1>PDF DocTags Analyzer</h1>
<div class="flex-row gap-2">
<button id="uploadBtn" class="btn-primary">
<svg class="icon" 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">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
<polyline points="17 8 12 3 7 8"></polyline>
<line x1="12" y1="3" x2="12" y2="15"></line>
</svg>
Upload PDF
</button>
<input type="file" id="fileInput" accept="application/pdf" style="display: none;">
<button id="settingsBtn" class="btn-outline">
<svg class="icon" 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">
<circle cx="12" cy="12" r="3"></circle>
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path>
</svg>
Settings
</button>
</div>
</header>
<div class="settings-panel" id="settingsPanel">
<h2>Processing Settings</h2>
<div class="settings-grid">
<div class="form-group">
<label for="dpi">DPI</label>
<input type="number" id="dpi" value="200" min="72" max="600">
</div>
<div class="form-group">
<label for="xFactor">X-Factor</label>
<input type="number" id="xFactor" value="0.7" min="0.1" max="2.0" step="0.1">
</div>
<div class="form-group">
<label for="yFactor">Y-Factor</label>
<input type="number" id="yFactor" value="0.7" min="0.1" max="2.0" step="0.1">
</div>
</div>
<div class="flex-row gap-4" style="margin-top: 0.5rem;">
<div class="checkbox-group">
<input type="checkbox" id="showLabels" checked>
<label for="showLabels">Show Labels</label>
</div>
<div class="checkbox-group">
<input type="checkbox" id="showZones" checked>
<label for="showZones">Show Zones</label>
</div>
<div class="checkbox-group">
<input type="checkbox" id="transparent" checked>
<label for="transparent">Transparent Zones</label>
</div>
<div class="checkbox-group">
<input type="checkbox" id="autoAdjust" checked>
<label for="autoAdjust">Auto-adjust Scaling</label>
</div>
</div>
</div>
<main id="appMain">
<!-- Welcome screen - shown when no PDF is loaded -->
<div class="welcome" id="welcomeScreen">
<svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="#9CA3AF" stroke-width="1" stroke-linecap="round" stroke-linejoin="round">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
<polyline points="14 2 14 8 20 8"></polyline>
<line x1="16" y1="13" x2="8" y2="13"></line>
<line x1="16" y1="17" x2="8" y2="17"></line>
<polyline points="10 9 9 9 8 9"></polyline>
</svg>
<h2>Upload a PDF to Get Started</h2>
<p>
The DocTags Analyzer will process your PDF document to identify and visualize
structured elements like headers, text blocks, tables, and images.
</p>
<button id="welcomeUploadBtn" class="btn-primary" style="padding: 0.75rem 1.5rem;">
<svg class="icon" 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">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
<polyline points="17 8 12 3 7 8"></polyline>
<line x1="12" y1="3" x2="12" y2="15"></line>
</svg>
Select PDF File
</button>
<div class="feature-grid">
<div class="feature-item">
<h3>
<svg class="icon" style="color: var(--primary-color);" 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">
<circle cx="12" cy="12" r="10"></circle>
<circle cx="12" cy="12" r="4"></circle>
<line x1="4.93" y1="4.93" x2="9.17" y2="9.17"></line>
<line x1="14.83" y1="14.83" x2="19.07" y2="19.07"></line>
<line x1="14.83" y1="9.17" x2="19.07" y2="4.93"></line>
<line x1="14.83" y1="9.17" x2="18.36" y2="5.64"></line>
<line x1="4.93" y1="19.07" x2="9.17" y2="14.83"></line>
</svg>
Visualize Content
</h3>
<p>
See how your document is structured with visual overlays showing text blocks,
headers, images and tables.
</p>
</div>
<div class="feature-item">
<h3>
<svg class="icon" style="color: var(--primary-color);" 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">
<circle cx="11" cy="11" r="8"></circle>
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
</svg>
Extract DocTags
</h3>
<p>
Generate structured DocTags markup that identifies document elements
with semantic information.
</p>
</div>
<div class="feature-item">
<h3>
<svg class="icon" style="color: var(--primary-color);" 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">
<path d="M12 2v6m0 0l3-3m-3 3L9 5"></path>
<circle cx="12" cy="14" r="3"></circle>
<path d="M3 9.75v4.5c0 4.5 3.6 8.25 9 8.25s9-3.75 9-8.25v-4.5"></path>
</svg>
Optimize Scale
</h3>
<p>
Find the optimal scaling factors to perfectly align the detected elements
with the original document.
</p>
</div>
<div class="feature-item">
<h3>
<svg class="icon" style="color: var(--primary-color);" 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">
<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>
Export Results
</h3>
<p>
Save visualizations as HTML and extract the processed content in multiple
formats including Markdown and HTML.
</p>
</div>
</div>
</div>
<!-- Main workspace - shown when a PDF is loaded -->
<div id="workspace" class="workspace" style="display: none;">
<div class="toolbar">
<div class="flex-row gap-2">
<button id="zoomOutBtn" class="btn-outline">
<svg class="icon" 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">
<circle cx="11" cy="11" r="8"></circle>
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
<line x1="8" y1="11" x2="14" y2="11"></line>
</svg>
</button>
<span id="zoomLevel">100%</span>
<button id="zoomInBtn" class="btn-outline">
<svg class="icon" 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">
<circle cx="11" cy="11" r="8"></circle>
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
<line x1="11" y1="8" x2="11" y2="14"></line>
<line x1="8" y1="11" x2="14" y2="11"></line>
</svg>
</button>
</div>
<div class="flex-row gap-2">
<button id="prevPageBtn" class="btn-outline">Previous</button>
<span id="pageInfo">Page 1 of 1</span>
<button id="nextPageBtn" class="btn-outline">Next</button>
</div>
</div>
<div class="canvas-container" id="canvasContainer">
<canvas id="pdfCanvas"></canvas>
<div id="zoneOverlays"></div>
</div>
<div class="footer-bar">
<div id="fileInfo" class="file-info"></div>
<div class="flex-row gap-2">
<button id="analyzeBtn" class="btn-secondary">
<svg class="icon" 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">
<circle cx="11" cy="11" r="8"></circle>
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
</svg>
Analyze Page
</button>
<button id="findOptimalBtn" class="btn-outline">
<svg class="icon" 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">
<path d="M12 2v6m0 0l3-3m-3 3L9 5"></path>
<circle cx="12" cy="14" r="3"></circle>
<path d="M3 9.75v4.5c0 4.5 3.6 8.25 9 8.25s9-3.75 9-8.25v-4.5"></path>
</svg>
Find Optimal Scale
</button>
<button id="exportBtn" class="btn-outline" disabled>
<svg class="icon" 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">
<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>
Export
</button>
</div>
</div>
</div>
<div id="sidebar" class="sidebar" style="display: none;">
<div>
<h2>Status</h2>
<div id="statusCard" class="status-card success">
Ready to process PDF
</div>
</div>
<div id="resultsSection" style="display: none;">
<h2>Detected Elements</h2>
<div class="legend" id="zoneLegend">
<!-- Legend will be populated by JavaScript -->
</div>
<div class="zone-list" id="zoneList">
<!-- Zone items will be populated by JavaScript -->
</div>
</div>
<div class="command-preview">
<h3>Command Preview</h3>
<pre class="code-block" id="commandPreview">python analyzer.py --image "your_file.pdf" --page 1 --dpi 200 &&
python fix_scaling.py --doctags output.doctags.txt --output fixed_output.doctags.txt --x-factor 0.7 --y-factor 0.7 &&
python visualizer.py --doctags fixed_output.doctags.txt --pdf "your_file.pdf" --page 1 --adjust --show</pre>
</div>
<div style="margin-top: 1.5rem;">
<h2>Processing Help</h2>
<div style="font-size: 0.875rem; margin-top: 0.5rem;">
<p>The DocTags analyzer extracts structured elements from your PDF and visualizes them.</p>
<ul style="margin-left: 1.25rem; margin-top: 0.5rem;">
<li>Set DPI higher (300-600) for better quality extraction</li>
<li>Adjust X/Y Factors to fix alignment issues</li>
<li>Use the "Find Optimal Scale" option for the best results</li>
<li>The Auto-adjust option will try to align zones automatically</li>
</ul>
</div>
</div>
</div>
</main>
<script>
// PDF.js initialization
const pdfjsLib = window['pdfjs-dist/build/pdf'];
pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.4.120/pdf.worker.min.js';
// DOM elements
const fileInput = document.getElementById('fileInput');
const uploadBtn = document.getElementById('uploadBtn');
const welcomeUploadBtn = document.getElementById('welcomeUploadBtn');
const settingsBtn = document.getElementById('settingsBtn');
const settingsPanel = document.getElementById('settingsPanel');
const welcomeScreen = document.getElementById('welcomeScreen');
const workspace = document.getElementById('workspace');
const sidebar = document.getElementById('sidebar');
const canvasContainer = document.getElementById('canvasContainer');
const pdfCanvas = document.getElementById('pdfCanvas');
const zoneOverlays = document.getElementById('zoneOverlays');
const ctx = pdfCanvas.getContext('2d');
// Settings
const dpiInput = document.getElementById('dpi');
const xFactorInput = document.getElementById('xFactor');
const yFactorInput = document.getElementById('yFactor');
const showLabelsCheck = document.getElementById('showLabels');
const showZonesCheck = document.getElementById('showZones');
const transparentCheck = document.getElementById('transparent');
const autoAdjustCheck = document.getElementById('autoAdjust');
// Controls
const zoomOutBtn = document.getElementById('zoomOutBtn');
const zoomInBtn = document.getElementById('zoomInBtn');
const zoomLevel = document.getElementById('zoomLevel');
const prevPageBtn = document.getElementById('prevPageBtn');
const nextPageBtn = document.getElementById('nextPageBtn');
const pageInfo = document.getElementById('pageInfo');
const fileInfo = document.getElementById('fileInfo');
const analyzeBtn = document.getElementById('analyzeBtn');
const findOptimalBtn = document.getElementById('findOptimalBtn');
const exportBtn = document.getElementById('exportBtn');
const statusCard = document.getElementById('statusCard');
const zoneLegend = document.getElementById('zoneLegend');
const zoneList = document.getElementById('zoneList');
const resultsSection = document.getElementById('resultsSection');
const commandPreview = document.getElementById('commandPreview');
// Zone colors for different types
const zoneColors = {
'section_header_level_1': '#FF5722', // Orange
'text': '#2196F3', // Blue
'picture': '#4CAF50', // Green
'table': '#9C27B0', // Purple
'page_header': '#FFC107', // Amber
'page_footer': '#795548', // Brown
'default': '#607D8B' // Blue Grey
};
// Application state
let appState = {
pdf: null,
file: null,
fileName: "",
fileSize: 0,
currentPage: 1,
totalPages: 0,
scale: 1.5,
zoom: 1.0,
isLoading: false,
isAnalyzed: false,
processingStep: "",
zones: [],
pdfDataBase64: null,
processingResults: null
};
// Initialize application
function init() {
// Event listeners for buttons
uploadBtn.addEventListener('click', () => fileInput.click());
welcomeUploadBtn.addEventListener('click', () => fileInput.click());
fileInput.addEventListener('change', handleFileSelect);
settingsBtn.addEventListener('click', toggleSettings);
// Zoom and page navigation
zoomOutBtn.addEventListener('click', () => setZoom(appState.zoom - 0.1));
zoomInBtn.addEventListener('click', () => setZoom(appState.zoom + 0.1));
prevPageBtn.addEventListener('click', () => changePage(appState.currentPage - 1));
nextPageBtn.addEventListener('click', () => changePage(appState.currentPage + 1));
// Processing buttons
analyzeBtn.addEventListener('click', analyzeCurrentPage);
findOptimalBtn.addEventListener('click', findOptimalScale);
exportBtn.addEventListener('click', exportResults);
// Settings change handlers
dpiInput.addEventListener('change', updateCommandPreview);
xFactorInput.addEventListener('change', updateCommandPreview);
yFactorInput.addEventListener('change', updateCommandPreview);
autoAdjustCheck.addEventListener('change', updateCommandPreview);
// Zone visibility controls
showLabelsCheck.addEventListener('change', updateZoneVisibility);
showZonesCheck.addEventListener('change', updateZoneVisibility);
transparentCheck.addEventListener('change', updateZoneVisibility);
// Initial view
updateCommandPreview();
}
// Handle file selection
function handleFileSelect(e) {
const file = e.target.files[0];
if (file && file.type === 'application/pdf') {
loadPDF(file);
} else if (file) {
showToast('Please select a valid PDF file', 'error');
}
}
// Toggle settings panel
function toggleSettings() {
settingsPanel.classList.toggle('open');
}
// Load PDF file
async function loadPDF(file) {
setLoading(true, 'Loading PDF...');
try {
// Save file information
appState.file = file;
appState.fileName = file.name;
appState.fileSize = file.size;
// Convert file to base64 for passing to backend
appState.pdfDataBase64 = await fileToBase64(file);
// Load PDF with PDF.js
const fileReader = new FileReader();
fileReader.onload = async function(event) {
const typedArray = new Uint8Array(event.target.result);
try {
const pdf = await pdfjsLib.getDocument({data: typedArray}).promise;
appState.pdf = pdf;
appState.totalPages = pdf.numPages;
appState.currentPage = 1;
// Update UI
switchToWorkspace();
updatePageInfo();
updateFileInfo();
renderPage();
setLoading(false, 'PDF loaded successfully. Ready to analyze.');
} catch (error) {
console.error('Error loading PDF:', error);
setLoading(false, 'Error loading PDF');
showToast('Failed to load PDF: ' + error.message, 'error');
}
};
fileReader.readAsArrayBuffer(file);
} catch (error) {
console.error('Error processing file:', error);
setLoading(false, 'Error processing file');
showToast('Failed to process file: ' + error.message, 'error');
}
}
// Convert file to base64
function fileToBase64(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => resolve(reader.result.split(',')[1]);
reader.onerror = error => reject(error);
});
}
// Switch from welcome screen to workspace
function switchToWorkspace() {
welcomeScreen.style.display = 'none';
workspace.style.display = 'flex';
sidebar.style.display = 'block';
}
// Update page information display
function updatePageInfo() {
pageInfo.textContent = `Page ${appState.currentPage} of ${appState.totalPages}`;
prevPageBtn.disabled = appState.currentPage <= 1;
nextPageBtn.disabled = appState.currentPage >= appState.totalPages;
}
// Update file information display
function updateFileInfo() {
const fileSizeFormatted = formatFileSize(appState.fileSize);
fileInfo.textContent = `${appState.fileName} (${fileSizeFormatted})`;
}
// Format file size in KB, MB, etc.
function formatFileSize(bytes) {
if (bytes < 1024) return bytes + ' B';
else if (bytes < 1048576) return (bytes / 1024).toFixed(1) + ' KB';
else return (bytes / 1048576).toFixed(1) + ' MB';
}
// Render current PDF page to canvas
async function renderPage() {
if (!appState.pdf) return;
try {
const page = await appState.pdf.getPage(appState.currentPage);
// Scale viewport to fit the container
const containerWidth = canvasContainer.clientWidth;
const containerHeight = canvasContainer.clientHeight;
const originalViewport = page.getViewport({scale: 1});
// Determine the scale to fit the container (with some margin)
const scaleX = (containerWidth - 40) / originalViewport.width;
const scaleY = (containerHeight - 40) / originalViewport.height;
appState.scale = Math.min(scaleX, scaleY);
// Apply zoom
const scaledViewport = page.getViewport({scale: appState.scale * appState.zoom});
// Set canvas dimensions
pdfCanvas.width = scaledViewport.width;
pdfCanvas.height = scaledViewport.height;
// Render PDF page
const renderContext = {
canvasContext: ctx,
viewport: scaledViewport
};
await page.render(renderContext).promise;
// Clear any existing zone overlays
zoneOverlays.innerHTML = '';
// If we have analyzed zones, render them
if (appState.isAnalyzed && appState.zones.length > 0) {
renderZones();
}
} catch (error) {
console.error('Error rendering page:', error);
showToast('Failed to render page: ' + error.message, 'error');
}
}
// Change to a different page
function changePage(pageNumber) {
if (pageNumber < 1 || pageNumber > appState.totalPages) return;
appState.currentPage = pageNumber;
updatePageInfo();
renderPage();
// Reset analysis results when changing pages
appState.isAnalyzed = false;
appState.zones = [];
resultsSection.style.display = 'none';
exportBtn.disabled = true;
setLoading(false, 'Ready to analyze page ' + pageNumber);
}
// Set zoom level
function setZoom(level) {
appState.zoom = Math.max(0.5, Math.min(2.0, level));
zoomLevel.textContent = Math.round(appState.zoom * 100) + '%';
renderPage();
}
// Set loading state
function setLoading(isLoading, message) {
appState.isLoading = isLoading;
appState.processingStep = message;
// Update status card
statusCard.className = isLoading ? 'status-card loading' : 'status-card success';
if (isLoading) {
statusCard.innerHTML = `
<div style="display: flex; align-items: center; gap: 0.5rem;">
<div class="spinner"></div>
<span>${message}</span>
</div>
`;
analyzeBtn.disabled = true;
findOptimalBtn.disabled = true;
} else {
statusCard.textContent = message;
analyzeBtn.disabled = false;
findOptimalBtn.disabled = false;
}
}
// Show toast notification
function showToast(message, type = 'info') {
Toastify({
text: message,
duration: 3000,
gravity: "top",
position: "right",
backgroundColor: type === 'error' ? "#EF4444" :
type === 'success' ? "#10B981" : "#3B82F6",
}).showToast();
}
// Update command preview in sidebar
function updateCommandPreview() {
const fileName = appState.fileName || "your_file.pdf";
const dpi = dpiInput.value;
const xFactor = xFactorInput.value;
const yFactor = yFactorInput.value;
const autoAdjust = autoAdjustCheck.checked ? " --adjust" : "";
commandPreview.textContent = `python analyzer.py --image "${fileName}" --page ${appState.currentPage} --dpi ${dpi} &&
python fix_scaling.py --doctags output.doctags.txt --output fixed_output.doctags.txt --x-factor ${xFactor} --y-factor ${yFactor} &&
python visualizer.py --doctags fixed_output.doctags.txt --pdf "${fileName}" --page ${appState.currentPage}${autoAdjust} --show`;
}
// Analyze current page
async function analyzeCurrentPage() {
if (!appState.pdf) return;
setLoading(true, 'Starting analysis process...');
// Simulate the multi-step analysis process
// In a production environment, this would make actual API calls to the backend Python scripts
await simulateProcessingStep('Converting PDF to image...', 1000);
await simulateProcessingStep('Generating DocTags...', 2000);
await simulateProcessingStep('Fixing scaling factors...', 1000);
await simulateProcessingStep('Creating visualization...', 1000);
// Generate mock results
generateMockResults();
// Update UI with results
displayResults();
setLoading(false, 'Analysis complete!');
showToast('Page analyzed successfully!', 'success');
}
// Simulate processing step with delay
function simulateProcessingStep(message, delay) {
return new Promise(resolve => {
setLoading(true, message);
setTimeout(() => resolve(), delay);
});
}
// Generate mock analysis results
function generateMockResults() {
appState.isAnalyzed = true;
// Sample zones that might be detected in a PDF
appState.zones = [
{
type: 'section_header_level_1',
x1: 50,
y1: 50,
x2: pdfCanvas.width - 100,
y2: 100,
content: 'Document Title'
},
{
type: 'text',
x1: 50,
y1: 120,
x2: pdfCanvas.width - 100,
y2: 200,
content: 'This is a sample paragraph of text that demonstrates the structure detection capabilities. The analyzer identifies different elements in the document and applies appropriate tags.'
},
{
type: 'picture',
x1: 150,
y1: 220,
x2: pdfCanvas.width - 150,
y2: 400,
content: 'Image content'
},
{
type: 'text',
x1: 50,
y1: 420,
x2: pdfCanvas.width - 100,
y2: 500,
content: 'Additional text paragraph that follows the image. This would typically describe or elaborate on the image content.'
},
{
type: 'table',
x1: 100,
y1: 520,
x2: pdfCanvas.width - 100,
y2: 650,
content: 'Table data'
},
{
type: 'page_footer',
x1: 250,
y1: pdfCanvas.height - 50,
x2: pdfCanvas.width - 250,
y2: pdfCanvas.height - 20,
content: `Page ${appState.currentPage}`
}
];
// In a real implementation, you would process the returned DocTags data
// and convert it into zone objects with coordinates
}
// Display analysis results
function displayResults() {
// Show results section
resultsSection.style.display = 'block';
exportBtn.disabled = false;
// Render zones on the PDF
renderZones();
// Build legend
buildLegend();
// Build zone list
buildZoneList();
// Update command preview with current page
updateCommandPreview();
}
// Render zones on top of the PDF
function renderZones() {
zoneOverlays.innerHTML = ''; // Clear existing overlays
if (!showZonesCheck.checked) return;
appState.zones.forEach((zone, index) => {
const zoneElement = document.createElement('div');
zoneElement.className = 'zone-overlay';
const zoneType = zone.type;
const color = zoneColors[zoneType] || zoneColors.default;
zoneElement.style.left = `${zone.x1}px`;
zoneElement.style.top = `${zone.y1}px`;
zoneElement.style.width = `${zone.x2 - zone.x1}px`;
zoneElement.style.height = `${zone.y2 - zone.y1}px`;
zoneElement.style.borderColor = color;
zoneElement.style.backgroundColor = transparentCheck.checked ?
'rgba(255, 255, 255, 0.1)' : 'rgba(255, 255, 255, 0.4)';
if (showLabelsCheck.checked) {
const labelElement = document.createElement('div');
labelElement.className = 'zone-overlay-label';
labelElement.style.borderRight = `2px solid ${color}`;
labelElement.style.borderBottom = `2px solid ${color}`;
labelElement.textContent = zoneType;
zoneElement.appendChild(labelElement);
}
zoneOverlays.appendChild(zoneElement);
});
}
// Build legend for zone types
function buildLegend() {
zoneLegend.innerHTML = '';
// Get unique zone types
const uniqueTypes = [...new Set(appState.zones.map(zone => zone.type))];
uniqueTypes.forEach(type => {
const color = zoneColors[type] || zoneColors.default;
const legendItem = document.createElement('div');
legendItem.className = 'legend-item';
const colorBox = document.createElement('div');
colorBox.className = 'legend-color';
colorBox.style.backgroundColor = color;
legendItem.appendChild(colorBox);
legendItem.appendChild(document.createTextNode(type));
zoneLegend.appendChild(legendItem);
});
}
// Build list of detected zones
function buildZoneList() {
zoneList.innerHTML = '';
appState.zones.forEach(zone => {
const zoneType = zone.type;
const color = zoneColors[zoneType] || zoneColors.default;
const zoneItem = document.createElement('div');
zoneItem.className = 'zone-item';
const zoneLabel = document.createElement('div');
zoneLabel.className = 'zone-label';
zoneLabel.style.backgroundColor = color;
zoneLabel.textContent = zoneType;
const zoneCoords = document.createElement('div');
zoneCoords.className = 'zone-coords';
zoneCoords.textContent = `Position: (${zone.x1}, ${zone.y1}) - (${zone.x2}, ${zone.y2})`;
zoneItem.appendChild(zoneLabel);
zoneItem.appendChild(zoneCoords);
if (zone.content) {
const zoneContent = document.createElement('div');
zoneContent.className = 'zone-content';
zoneContent.textContent = zone.content;
zoneItem.appendChild(zoneContent);
}
zoneList.appendChild(zoneItem);
});
}
// Update zone visibility based on checkboxes
function updateZoneVisibility() {
renderZones();
}
// Find optimal scale factors
async function findOptimalScale() {
if (!appState.pdf) return;
setLoading(true, 'Finding optimal scale factors...');
await simulateProcessingStep('Analyzing page boundaries...', 1000);
await simulateProcessingStep('Testing different scaling combinations...', 2000);
await simulateProcessingStep('Evaluating results...', 1000);
// Simulate finding optimal values
const optimalX = (Math.random() * 0.5 + 0.5).toFixed(2); // Random between 0.5 and 1.0
const optimalY = (Math.random() * 0.5 + 0.5).toFixed(2);
xFactorInput.value = optimalX;
yFactorInput.value = optimalY;
updateCommandPreview();
setLoading(false, `Optimal scaling: X=${optimalX}, Y=${optimalY}`);
showToast('Optimal scale factors determined', 'success');
}
// Export results
function exportResults() {
if (!appState.isAnalyzed) return;
const exportOptions = [
{ name: 'HTML Visualization', ext: 'html' },
{ name: 'DocTags Raw Format', ext: 'txt' },
{ name: 'Markdown Content', ext: 'md' }
];
const optionsHTML = exportOptions.map((option, index) =>
`<div class="checkbox-group" style="margin-bottom: 0.5rem;">
<input type="checkbox" id="export${index}" checked>
<label for="export${index}">${option.name} (.${option.ext})</label>
</div>`
).join('');
// Create a modal-like dialog (in a real app.py, you'd use a proper modal)
const dialog = document.createElement('div');
dialog.style.position = 'fixed';
dialog.style.top = '0';
dialog.style.left = '0';
dialog.style.right = '0';
dialog.style.bottom = '0';
dialog.style.backgroundColor = 'rgba(0, 0, 0, 0.5)';
dialog.style.display = 'flex';
dialog.style.alignItems = 'center';
dialog.style.justifyContent = 'center';
dialog.style.zIndex = '1000';
dialog.innerHTML = `
<div style="background: white; padding: 1.5rem; border-radius: 0.5rem; max-width: 400px; width: 100%;">
<h3 style="margin-bottom: 1rem;">Export Options</h3>
${optionsHTML}
<div style="margin-top: 1rem; display: flex; justify-content: flex-end; gap: 0.5rem;">
<button id="cancelExport" class="btn-outline">Cancel</button>
<button id="confirmExport" class="btn-primary">Export</button>
</div>
</div>
`;
document.body.appendChild(dialog);
document.getElementById('cancelExport').addEventListener('click', () => {
document.body.removeChild(dialog);
});
document.getElementById('confirmExport').addEventListener('click', () => {
document.body.removeChild(dialog);
// Simulate export process
setLoading(true, 'Exporting files...');
setTimeout(() => {
setLoading(false, 'Files exported successfully');
showToast('Export complete! Files saved to your output directory.', 'success');
}, 1500);
});
}
// In a real implementation, connect to the Python backend
// This would involve sending HTTP requests to an API that runs the Python scripts
// Example of how you might connect to a backend API:
async function callBackendAPI(endpoint, data) {
try {
const response = await fetch(`/api/${endpoint}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
});
if (!response.ok) {
throw new Error(`Error: ${response.status} ${response.statusText}`);
}
return await response.json();
} catch (error) {
console.error('API call failed:', error);
throw error;
}
}
// Initialize the application when the DOM is loaded
document.addEventListener('DOMContentLoaded', init);
</script>
</body>
</html>