File size: 10,605 Bytes
78431ff
 
 
 
61b711a
78431ff
 
 
92348d4
 
 
 
 
 
 
 
 
 
 
 
 
78431ff
 
 
 
92348d4
 
 
 
 
 
 
 
 
78431ff
 
 
 
 
 
 
 
 
92348d4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
78431ff
92348d4
 
78431ff
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
/**
 * Image Viewer β€” upload, display, bbox overlay
 */

import { state, emit, on, api, fitZoom } from '../app.js';

const $ = id => document.getElementById(id);

const IMAGE_EXTENSIONS = new Set(['.jpg', '.jpeg', '.png', '.tif', '.tiff', '.bmp', '.gif', '.webp']);

function extensionOf(file) {
    const name = file?.name || '';
    const dot = name.lastIndexOf('.');
    return dot >= 0 ? name.slice(dot).toLowerCase() : '';
}

function isImageOrPdf(file) {
    const ext = extensionOf(file);
    return file.type.startsWith('image/') || ext === '.pdf' || IMAGE_EXTENSIONS.has(ext);
}

export function initImageViewer() {
    const uploadArea = $('upload-area');
    const fileInput = $('file-input');
    const xmlInput = $('xml-input');
    const viewerScroll = $('viewer-scroll');
    const viewerPlaceholder = $('viewer-placeholder');

    const handleDroppedFiles = files => {
        const img = files.find(isImageOrPdf);
        const xml = files.find(f => f.name.toLowerCase().endsWith('.xml'));
        if (img) uploadFile(img);
        if (xml) uploadXml(xml);  // queued after image upload sets imageId
    };

    // Click to browse image
    uploadArea.addEventListener('click', () => fileInput.click());

    // File selected
    fileInput.addEventListener('change', () => {
        if (fileInput.files.length > 0) uploadFile(fileInput.files[0]);
    });

    // Drag & drop β€” accept image, PDF, and XML dropped on the upload box OR the viewer area.
    // Users naturally drop on the large center viewer, not just the small upload box.
    const dropTargets = [uploadArea, viewerScroll, viewerPlaceholder].filter(Boolean);
    dropTargets.forEach(target => {
        target.addEventListener('dragover', e => {
            e.preventDefault();
            uploadArea.classList.add('dragover');
            if (viewerPlaceholder && !state.imageId) viewerPlaceholder.classList.add('dragover');
        });
        target.addEventListener('dragleave', e => {
            if (!e.currentTarget.contains(e.relatedTarget)) {
                uploadArea.classList.remove('dragover');
                viewerPlaceholder?.classList.remove('dragover');
            }
        });
        target.addEventListener('drop', e => {
            e.preventDefault();
            uploadArea.classList.remove('dragover');
            viewerPlaceholder?.classList.remove('dragover');
            handleDroppedFiles(Array.from(e.dataTransfer.files));
        });
    });
    // Keep upload-area explicitly compatible with batch-panel's capture-phase handler
    uploadArea.addEventListener('drop', e => { e.preventDefault(); });

    // XML file picker
    xmlInput.addEventListener('change', () => {
        if (xmlInput.files.length > 0) uploadXml(xmlInput.files[0]);
    });

    // Batch panel: load a completed item's image into the viewer
    on('batch-item-start', ({ imageId, filename }) => {
        state.imageId = imageId;
        // Clear bboxes immediately for the new item
        currentBboxes  = [];
        currentRegions = [];
        const img = $('page-image');
        img.src = `/api/image/${imageId}`;
        $('image-container').classList.remove('hidden');
        $('viewer-placeholder').classList.add('hidden');
        img.onload = () => {
            const canvas = $('overlay-canvas');
            canvas.width  = img.naturalWidth;
            canvas.height = img.naturalHeight;
            fitZoom();
            // Redraw any bboxes that arrived before the image finished loading
            if (currentBboxes.length > 0) {
                drawBboxes(currentBboxes, -1, currentRegions);
            } else {
                const ctx = canvas.getContext('2d');
                ctx.clearRect(0, 0, canvas.width, canvas.height);
            }
        };
        $('image-info').textContent = filename;
        $('xml-upload-row').classList.remove('hidden');
        $('xml-status').textContent = 'No PAGE XML';
        $('xml-status').classList.remove('xml-ok');
        emit('transcription-start', {});
    });

    // Draw bboxes after segmentation; keep state.regions in sync
    on('sse-segmentation', data => {
        state.regions = data.regions || [];
        if (data.source === 'page') {
            // Page-level engine: clear any old line bboxes, don't draw full-page box
            drawBboxes([], -1, []);
        } else {
            drawBboxes(data.bboxes, -1, state.regions);
        }
        if (data.source === 'pagexml') {
            $('xml-status').textContent = `PAGE XML: ${data.num_lines} lines`;
        }
    });

    // Highlight line on click from transcription panel
    on('highlight-line', ({ index }) => highlightBbox(index));

    // Click on canvas β†’ highlight the clicked bbox and emit highlight-line
    const canvas = $('overlay-canvas');
    canvas.addEventListener('click', e => {
        if (currentBboxes.length === 0) return;

        const img = $('page-image');
        // Scale factor: natural image coords / displayed canvas coords
        const scaleX = img.naturalWidth / img.clientWidth;
        const scaleY = img.naturalHeight / img.clientHeight;

        const rect = canvas.getBoundingClientRect();
        const clickX = (e.clientX - rect.left) * scaleX;
        const clickY = (e.clientY - rect.top) * scaleY;

        for (let i = 0; i < currentBboxes.length; i++) {
            const [x1, y1, x2, y2] = currentBboxes[i];
            if (clickX >= x1 && clickX <= x2 && clickY >= y1 && clickY <= y2) {
                emit('highlight-line', { index: i });
                break;
            }
        }
    });
}

async function uploadFile(file) {
    const formData = new FormData();
    formData.append('file', file);

    $('image-info').textContent = 'Uploading...';

    try {
        const resp = await fetch('/api/image/upload', {
            method: 'POST',
            body: formData,
        });
        if (!resp.ok) {
            const err = await resp.json();
            throw new Error(err.detail);
        }
        const data = await resp.json();

        // PDF: redirect all pages to batch panel
        if (data.is_pdf) {
            $('image-info').textContent = `PDF: ${data.num_pages} page(s) β€” added to batch queue`;
            emit('pdf-pages-ready', data);
            return;
        }

        state.imageId = data.image_id;
        state.imageInfo = data;

        // Display image β€” show container, hide placeholder
        const img = $('page-image');
        img.src = `/api/image/${data.image_id}`;
        $('image-container').classList.remove('hidden');
        $('viewer-placeholder').classList.add('hidden');

        // Wait for image to load to size canvas and fit zoom
        img.onload = () => {
            const canvas = $('overlay-canvas');
            canvas.width = img.naturalWidth;
            canvas.height = img.naturalHeight;
            fitZoom();  // sets img.style.width/height and canvas display size
        };

        $('image-info').textContent = `${data.filename} (${data.width}Γ—${data.height})`;
        // Show XML upload row
        $('xml-upload-row').classList.remove('hidden');
        $('xml-status').textContent = 'No PAGE XML';
        $('xml-status').classList.remove('xml-ok');
        emit('image-uploaded', data);
    } catch (err) {
        $('image-info').textContent = `Error: ${err.message}`;
    }
}

async function uploadXml(file) {
    if (!state.imageId) {
        // Will retry after image upload finishes
        on('image-uploaded', () => uploadXml(file), { once: true });
        return;
    }
    const xmlStatus = $('xml-status');
    xmlStatus.textContent = 'Uploading XML...';
    xmlStatus.classList.remove('xml-ok');
    try {
        const formData = new FormData();
        formData.append('file', file);
        const resp = await fetch(`/api/image/${state.imageId}/xml`, {
            method: 'POST',
            body: formData,
        });
        if (!resp.ok) {
            const err = await resp.json();
            throw new Error(err.detail);
        }
        xmlStatus.textContent = `βœ“ ${file.name}`;
        xmlStatus.classList.add('xml-ok');
        emit('xml-uploaded', { filename: file.name });
    } catch (err) {
        xmlStatus.textContent = `XML error: ${err.message}`;
    }
}

let currentBboxes = [];
let currentRegions = [];

// Distinct colours for up to 8 regions (cycling)
const REGION_COLORS = [
    'rgba(255, 160,  30, 0.55)',  // orange
    'rgba( 46, 213, 115, 0.55)',  // green
    'rgba(232,  65, 24,  0.55)',  // red
    'rgba( 52, 172, 224, 0.55)',  // blue
    'rgba(162,  16, 213, 0.55)',  // purple
    'rgba(255, 211,  42, 0.55)',  // yellow
    'rgba( 18, 203, 196, 0.55)',  // teal
    'rgba(253,  89, 166, 0.55)',  // pink
];

function drawBboxes(bboxes, highlightIndex = -1, regions = []) {
    currentBboxes = bboxes;
    currentRegions = regions;
    const canvas = $('overlay-canvas');
    const img = $('page-image');
    const ctx = canvas.getContext('2d');

    // Keep canvas display size in sync with zoom-controlled img size
    canvas.style.width  = img.style.width  || img.clientWidth  + 'px';
    canvas.style.height = img.style.height || img.clientHeight + 'px';

    ctx.clearRect(0, 0, canvas.width, canvas.height);

    // Draw region outlines first (underneath line boxes)
    regions.forEach((r, ri) => {
        const [x1, y1, x2, y2] = r.bbox;
        const color = REGION_COLORS[ri % REGION_COLORS.length];
        ctx.strokeStyle = color;
        ctx.lineWidth = 2.5;
        ctx.setLineDash([8, 4]);
        ctx.strokeRect(x1, y1, x2 - x1, y2 - y1);
        ctx.setLineDash([]);
        // Subtle fill
        ctx.fillStyle = color.replace('0.55', '0.07');
        ctx.fillRect(x1, y1, x2 - x1, y2 - y1);
        // Region label
        ctx.fillStyle = color.replace('0.55', '0.9');
        ctx.font = 'bold 13px sans-serif';
        ctx.fillText(`R${ri + 1} (${r.num_lines} lines)`, x1 + 4, y1 + 16);
    });

    // Draw line boxes on top
    for (let i = 0; i < bboxes.length; i++) {
        const [x1, y1, x2, y2] = bboxes[i];
        const isHighlighted = i === highlightIndex;

        ctx.strokeStyle = isHighlighted ? '#e94560' : 'rgba(58, 134, 255, 0.6)';
        ctx.lineWidth = isHighlighted ? 3 : 1.5;
        ctx.strokeRect(x1, y1, x2 - x1, y2 - y1);

        if (isHighlighted) {
            ctx.fillStyle = 'rgba(233, 69, 96, 0.1)';
            ctx.fillRect(x1, y1, x2 - x1, y2 - y1);
        }
    }
}

function highlightBbox(index) {
    if (currentBboxes.length > 0) {
        drawBboxes(currentBboxes, index, currentRegions);
    }
}