tien_nemo

demo

Showing 1 changed file with 367 additions and 357 deletions
1 <html lang="en"><head> 1 <html lang="en"><head>
2 - <meta charset="UTF-8"> 2 + <meta charset="UTF-8">
3 - <title>OCR Mapping with Manual Select Tool</title> 3 + <title>OCR Mapping with Manual Select Tool</title>
4 - <script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script> 4 + <script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
5 - <style> 5 + <style>
6 - body { font-family: sans-serif; background: #f5f5f5; } 6 + body { font-family: sans-serif; background: #f5f5f5; }
7 - #app { display: flex; gap: 20px; padding: 20px; } 7 + #app { display: flex; gap: 20px; padding: 20px; }
8 - .left-panel { 8 + .left-panel {
9 - width: 500px; background: #fff; padding: 15px; 9 + width: 500px; background: #fff; padding: 15px;
10 - border-radius: 8px; box-shadow: 0 0 5px rgba(0,0,0,0.1); 10 + border-radius: 8px; box-shadow: 0 0 5px rgba(0,0,0,0.1);
11 - } 11 + }
12 - .form-group { margin-bottom: 15px; } 12 + .form-group { margin-bottom: 15px; }
13 - .form-group label { font-weight: bold; display: block; margin-bottom: 5px; } 13 + .form-group label { font-weight: bold; display: block; margin-bottom: 5px; }
14 - .form-group input { width: 100%; padding: 6px; border: 1px solid #ccc; border-radius: 4px; } 14 + .form-group input { width: 100%; padding: 6px; border: 1px solid #ccc; border-radius: 4px; }
15 - .right-panel { flex: 1; position: relative; background: #eee; border-radius: 8px; overflow: hidden; user-select: none; } 15 + .right-panel { flex: 1; position: relative; background: #eee; border-radius: 8px; overflow: hidden; user-select: none; }
16 - .pdf-container { position: relative; display: inline-block; } 16 + .pdf-container { position: relative; display: inline-block; }
17 - .bbox { 17 + .bbox {
18 - position: absolute; 18 + position: absolute;
19 - border: 2px solid #ff5252; 19 + border: 2px solid #ff5252;
20 - /*background-color: rgba(255, 82, 82, 0.2);*/ 20 + /*background-color: rgba(255, 82, 82, 0.2);*/
21 - cursor: pointer; 21 + cursor: pointer;
22 - } 22 + }
23 - .bbox.active { 23 + .bbox.active {
24 - /*border-color: #2196F3;*/ 24 + /*border-color: #2196F3;*/
25 - background-color: rgb(33 243 132 / 30%); 25 + background-color: rgb(33 243 132 / 30%);
26 - } 26 + }
27 - select { 27 + select {
28 - position: absolute; 28 + position: absolute;
29 - z-index: 10; 29 + z-index: 10;
30 - background: #fff; 30 + background: #fff;
31 - border: 1px solid #ccc; 31 + border: 1px solid #ccc;
32 - } 32 + }
33 - .select-box { 33 + .select-box {
34 - position: absolute; 34 + position: absolute;
35 - /*border: 2px dashed #2196F3;*/ 35 + /*border: 2px dashed #2196F3;*/
36 - background-color: rgba(33, 150, 243, 0.2); 36 + background-color: rgba(33, 150, 243, 0.2);
37 - pointer-events: none; 37 + pointer-events: none;
38 - z-index: 5; 38 + z-index: 5;
39 - } 39 + }
40 - .delete-btn { 40 + .delete-btn {
41 - position: absolute; 41 + position: absolute;
42 - bottom: -10px; 42 + bottom: -10px;
43 - right: -10px; 43 + right: -10px;
44 - background: #ff4d4d; 44 + background: #ff4d4d;
45 - color: #fff; 45 + color: #fff;
46 - border: none; 46 + border: none;
47 - border-radius: 50%; 47 + border-radius: 50%;
48 - cursor: pointer; 48 + cursor: pointer;
49 - font-size: 14px; 49 + font-size: 14px;
50 - padding: 3px 6px; 50 + padding: 3px 6px;
51 - z-index: 20; 51 + z-index: 20;
52 - } 52 + }
53 - </style> 53 + </style>
54 54
55 </head> 55 </head>
56 <body> 56 <body>
57 <div id="app"> 57 <div id="app">
58 58
59 - <!-- Right: PDF viewer + select tool --> 59 + <!-- Right: PDF viewer + select tool -->
60 <div class="right-panel" > 60 <div class="right-panel" >
61 <div class="pdf-container" ref="pdfContainer" 61 <div class="pdf-container" ref="pdfContainer"
62 @mousedown="startSelect" 62 @mousedown="startSelect"
...@@ -70,49 +70,49 @@ ...@@ -70,49 +70,49 @@
70 /> 70 />
71 71
72 72
73 - <!-- Vùng kéo chọn --> 73 + <!-- Vùng kéo chọn -->
74 - <div v-if="selectBox.show" class="select-box" 74 + <div v-if="selectBox.show" class="select-box"
75 - :style="{ left: selectBox.x + 'px', top: selectBox.y + 'px', width: selectBox.width + 'px', height: selectBox.height + 'px' }"></div> 75 + :style="{ left: selectBox.x + 'px', top: selectBox.y + 'px', width: selectBox.width + 'px', height: selectBox.height + 'px' }"></div>
76 - 76 +
77 - <!-- Vẽ bbox OCR --> 77 + <!-- Vẽ bbox OCR -->
78 - <div 78 + <div
79 - v-for="(item, index) in ocrData" 79 + v-for="(item, index) in ocrData"
80 - :key="index" 80 + :key="index"
81 - v-if="!item.isDeleted" 81 + v-if="!item.isDeleted"
82 - class="bbox" 82 + class="bbox"
83 - :class="{ active: index === activeIndex }" 83 + :class="{ active: index === activeIndex }"
84 - :data-field="item.field" 84 + :data-field="item.field"
85 - :style="getBoxStyle(item, index)" 85 + :style="getBoxStyle(item, index)"
86 - @click="selectingIndex = index"> 86 + @click="selectingIndex = index">
87 - 87 +
88 - <button v-if="item.isManual && item.showDelete" 88 + <button v-if="item.isManual && item.showDelete"
89 - class="delete-btn" 89 + class="delete-btn"
90 - @click.stop="deleteBox(index)">🗑</button> 90 + @click.stop="deleteBox(index)">🗑</button>
91 - </div> 91 + </div>
92 - 92 +
93 - 93 +
94 - <!-- Dropdown OCR --> 94 + <!-- Dropdown OCR -->
95 - <select v-if="selectingIndex !== null" 95 + <select v-if="selectingIndex !== null"
96 - :style="getSelectStyle(ocrData[selectingIndex])" 96 + :style="getSelectStyle(ocrData[selectingIndex])"
97 - v-model="ocrData[selectingIndex].field" 97 + v-model="ocrData[selectingIndex].field"
98 - @change="applyMapping" 98 + @change="applyMapping"
99 - > 99 + >
100 - <option disabled value="">-- Chọn trường dữ liệu --</option> 100 + <option disabled value="">-- Chọn trường dữ liệu --</option>
101 - <option v-for="field in fieldOptions" :value="field.value">{{ field.label }}</option> 101 + <option v-for="field in fieldOptions" :value="field.value">{{ field.label }}</option>
102 - </select> 102 + </select>
103 - 103 +
104 - <!-- Dropdown thủ công --> 104 + <!-- Dropdown thủ công -->
105 - <select v-if="selectBox.showDropdown" 105 + <select v-if="selectBox.showDropdown"
106 - :style="{ left: selectBox.x + 'px', top: (selectBox.y + selectBox.height) + 'px' }" 106 + :style="{ left: selectBox.x + 'px', top: (selectBox.y + selectBox.height) + 'px' }"
107 - v-model="manualField" 107 + v-model="manualField"
108 - @change="applyManualMapping" 108 + @change="applyManualMapping"
109 - @click.stop 109 + @click.stop
110 - > 110 + >
111 - <option disabled value="">-- Chọn trường dữ liệu --</option> 111 + <option disabled value="">-- Chọn trường dữ liệu --</option>
112 - <option v-for="field in fieldOptions" :value="field.value">{{ field.label }}</option> 112 + <option v-for="field in fieldOptions" :value="field.value">{{ field.label }}</option>
113 - </select> 113 + </select>
114 + </div>
114 </div> 115 </div>
115 - </div>
116 116
117 <!-- Left: Form inputs --> 117 <!-- Left: Form inputs -->
118 <div class="left-panel"> 118 <div class="left-panel">
...@@ -126,272 +126,282 @@ ...@@ -126,272 +126,282 @@
126 126
127 127
128 <script> 128 <script>
129 - new Vue({ 129 + new Vue({
130 - el: '#app', 130 + el: '#app',
131 - data() { 131 + data() {
132 - return { 132 + return {
133 - pdfImageUrl: "", 133 + pdfImageUrl: "",
134 - selectingIndex: null, 134 + selectingIndex: null,
135 - isMappingManually: false, 135 + isMappingManually: false,
136 - isSelecting: false, 136 + isSelecting: false,
137 - activeField: null, 137 + activeIndex: null,
138 - manualField: "", 138 + manualField: "",
139 - formData: { export_date: "", order_code: "", customer: "", address: "", staff: "" }, 139 + formData: { export_date: "", order_code: "", customer: "", address: "", staff: "" },
140 - fieldOptions: [ 140 + fieldOptions: [
141 - { value: "export_date", label: "Ngày xuất" }, 141 + { value: "export_date", label: "Ngày xuất" },
142 - { value: "order_code", label: "Mã đơn hàng" }, 142 + { value: "order_code", label: "Mã đơn hàng" },
143 - { value: "customer", label: "Khách hàng" }, 143 + { value: "customer", label: "Khách hàng" },
144 - { value: "address", label: "Địa chỉ" }, 144 + { value: "address", label: "Địa chỉ" },
145 - { value: "staff", label: "Nhân viên" } 145 + { value: "staff", label: "Nhân viên" }
146 - ], 146 + ],
147 - ocrData: [], 147 + ocrData: [],
148 - selectBox: { show: false, showDropdown: false, x: 0, y: 0, width: 0, height: 0, startX: 0, startY: 0 }, 148 + selectBox: { show: false, showDropdown: false, x: 0, y: 0, width: 0, height: 0, startX: 0, startY: 0 },
149 - manualIndex: null 149 + manualIndex: null
150 - } 150 + }
151 - }, 151 + },
152 - mounted() { 152 + mounted() {
153 - this.pdfImageUrl = "/public/image/data_picking_detail_1754967679.jpg"; // ảnh xuất từ Python 153 + this.pdfImageUrl = "/public/image/data_picking_detail_1754967679.jpg"; // ảnh xuất từ Python
154 - this.initData(); 154 + this.initData();
155 - }, 155 + },
156 - methods: { 156 + methods: {
157 - deleteBox(index) { 157 + deleteBox(index) {
158 - const item = this.ocrData[index]; 158 + const item = this.ocrData[index];
159 - if (item.isManual) { 159 + if (item.isManual) {
160 - const manualBbox = item.bbox; 160 + const manualBbox = item.bbox;
161 - 161 +
162 - // Hiện lại border các box OCR gốc nằm trong vùng thủ công 162 + // Hiện lại border các box OCR gốc nằm trong vùng thủ công
163 - this.ocrData.forEach(o => { 163 + this.ocrData.forEach(o => {
164 - if (!o.isManual && this.isBoxInside(o.bbox, manualBbox)) { 164 + if (!o.isManual && this.isBoxInside(o.bbox, manualBbox)) {
165 - o.hideBorder = false; 165 + o.hideBorder = false;
166 + }
167 + });
168 +
169 + // Đánh dấu xoá vùng thủ công
170 + this.ocrData[index].isDeleted = true;
171 + this.ocrData[index].showDelete = false;
172 +
173 + // Reset trạng thái nếu đây là vùng đang chọn
174 + if (this.manualIndex === index) {
175 + this.isMappingManually = false;
176 + this.selectBox.show = false;
177 + this.selectBox.showDropdown = false;
178 + this.manualField = "";
179 + this.manualIndex = null;
180 + }
181 + }
182 + },
183 +
184 + async initData() {
185 + await this.loadOCRData();
186 + },
187 + async loadOCRData() {
188 + const res = await fetch("/public/image/data_picking_detail_1754967679.json");
189 + this.ocrData = await res.json();
190 + },
191 + onImageLoad() {
192 + const img = this.$refs.pdfImage;
193 + this.imageWidth = img.naturalWidth;
194 + this.imageHeight = img.naturalHeight;
195 + },
196 + getBoxStyle(item, index) {
197 + if (!this.imageWidth || !this.imageHeight || !this.$refs.pdfImage) return {};
198 +
199 + const [x1, y1, x2, y2] = item.bbox;
200 + const displayedWidth = this.$refs.pdfImage.clientWidth;
201 + const displayedHeight = this.$refs.pdfImage.clientHeight;
202 +
203 + const scaleX = displayedWidth / this.imageWidth;
204 + const scaleY = displayedHeight / this.imageHeight;
205 +
206 + return {
207 + position: 'absolute',
208 + left: `${Math.round(x1 * scaleX)}px`,
209 + top: `${Math.round(y1 * scaleY)}px`,
210 + width: `${Math.round((x2 - x1) * scaleX)}px`,
211 + height: `${Math.round((y2 - y1) * scaleY)}px`,
212 + border: item.hideBorder ? 'none' : '2px solid ' + (index === this.activeIndex ? '#199601' : '#ff5252'),
213 + //backgroundColor: item.hideBorder ? 'transparent' : (this.activeIndex === item.field ? 'rgba(33,150,243,0.3)' : 'rgba(255,82,82,0.2)'),
214 + boxSizing: 'border-box',
215 + cursor: 'pointer',
216 + zIndex: item.isManual ? 30 : 10
217 + };
218 + },
219 +
220 + highlightField(field) {
221 + // tìm box gần nhất match field này
222 + let idx = -1;
223 + for (let i = this.ocrData.length - 1; i >= 0; i--) {
224 + const it = this.ocrData[i];
225 + if (!it.isDeleted && it.field === field) {
226 + idx = i;
227 + break;
228 + }
229 + }
230 + this.activeIndex = idx; // nếu không tìm thấy thì = -1
231 + },
232 +
233 + startSelect(e) {
234 + if (this.isMappingManually || e.button !== 0) return;
235 + this.isSelecting = true;
236 + const rect = this.$refs.pdfContainer.getBoundingClientRect();
237 + this.selectBox.startX = e.clientX - rect.left;
238 + this.selectBox.startY = e.clientY - rect.top;
239 + this.selectBox.x = this.selectBox.startX;
240 + this.selectBox.y = this.selectBox.startY;
241 + this.selectBox.width = 0;
242 + this.selectBox.height = 0;
243 + this.selectBox.show = true;
244 + this.selectBox.showDropdown = false;
245 + this.manualField = "";
246 + },
247 +
248 + onSelect(e) {
249 + if (!this.isSelecting) return;
250 + const rect = this.$refs.pdfContainer.getBoundingClientRect();
251 + const currentX = e.clientX - rect.left;
252 + const currentY = e.clientY - rect.top;
253 + this.selectBox.x = Math.min(currentX, this.selectBox.startX);
254 + this.selectBox.y = Math.min(currentY, this.selectBox.startY);
255 + this.selectBox.width = Math.abs(currentX - this.selectBox.startX);
256 + this.selectBox.height = Math.abs(currentY - this.selectBox.startY);
257 + },
258 +
259 + endSelect(e) {
260 + if (!this.isSelecting) return;
261 + this.isSelecting = false;
262 +
263 + if (this.selectBox.width < 10 || this.selectBox.height < 10) {
264 + this.selectBox.show = false;
265 + return;
266 + }
267 +
268 + // displayed coords (như hiện tại, dùng để hiển thị select overlay)
269 + const dispX1 = this.selectBox.x;
270 + const dispY1 = this.selectBox.y;
271 + const dispX2 = this.selectBox.x + this.selectBox.width;
272 + const dispY2 = this.selectBox.y + this.selectBox.height;
273 +
274 + // scale: displayed -> original
275 + const displayedWidth = this.$refs.pdfImage.clientWidth;
276 + const displayedHeight = this.$refs.pdfImage.clientHeight;
277 + const scaleX = this.imageWidth / displayedWidth;
278 + const scaleY = this.imageHeight / displayedHeight;
279 +
280 + // bbox ở hệ gốc (original image pixels) — dùng để so sánh với ocrData và lưu vào ocrData
281 + const origBbox = [
282 + Math.round(dispX1 * scaleX),
283 + Math.round(dispY1 * scaleY),
284 + Math.round(dispX2 * scaleX),
285 + Math.round(dispY2 * scaleY)
286 + ];
287 +
288 + // Ẩn border các box OCR gốc nằm giao nhau với vùng thủ công (dùng coords gốc)
289 + this.ocrData.forEach(item => {
290 + if (!item.isManual && this.isBoxInside(item.bbox, origBbox)) {
291 + item.hideBorder = true;
292 + }
293 + });
294 +
295 + // Thêm box thủ công (lưu theo coords gốc)
296 + this.ocrData.push({
297 + text: "",
298 + bbox: origBbox,
299 + field: "",
300 + isManual: true,
301 + showDelete: true,
302 + isDeleted: false,
303 + hideBorder: false
304 + });
305 +
306 + this.manualIndex = this.ocrData.length - 1;
307 + this.isMappingManually = true;
308 + this.selectBox.showDropdown = true;
309 +
310 + e.stopPropagation();
311 + e.preventDefault();
312 + }
313 + ,
314 + applyMapping() {
315 + const item = this.ocrData[this.selectingIndex];
316 +
317 + if (item && item.isManual) {
318 + this.manualIndex = this.selectingIndex;
319 + this.manualField = item.field || "";
320 + this.applyManualMapping();
321 + return;
322 + }
323 +
324 + if (item.field) {
325 + this.formData[item.field] = item.text;
326 + this.activeIndex = this.selectingIndex;
327 + }
328 + this.selectingIndex = null;
329 + },
330 + applyManualMapping() {
331 + if (!this.manualField) return;
332 + const manualIndex = this.manualIndex;
333 + const newBbox = this.ocrData[manualIndex].bbox;
334 +
335 + let combinedText = [];
336 + this.ocrData.forEach(item => {
337 + if (!item.isManual && this.isBoxInside(item.bbox, newBbox) && item.text.trim()) {
338 + const partial = this.getPartialText(item.text, item.bbox, newBbox);
339 + if (partial) combinedText.push(partial);
340 + // combinedText.push(item.text.trim());
341 + }
342 + });
343 +
344 + const finalText = combinedText.join(" ");
345 + this.ocrData[manualIndex].field = this.manualField;
346 + this.formData[this.manualField] = finalText;
347 + this.activeIndex = manualIndex;
348 +
349 + console.log('manualField', this.manualField, this.manualIndex)
350 + // Reset trạng thái chọn
351 + this.isMappingManually = false;
352 + this.selectBox.show = false;
353 + this.selectBox.showDropdown = false;
354 + // this.manualField = "";
355 + // this.manualIndex = null;
356 + },
357 +
358 + isBoxInside(inner, outer) {
359 + return !(
360 + inner[2] < outer[0] || // box bên trái vùng chọn
361 + inner[0] > outer[2] || // box bên phải vùng chọn
362 + inner[3] < outer[1] || // box phía trên vùng chọn
363 + inner[1] > outer[3] // box phía dưới vùng chọn
364 + );
365 + },
366 +
367 + getPartialText(text, bbox, selectBbox) {
368 + const [x1, y1, x2, y2] = bbox;
369 + const [sx1, sy1, sx2, sy2] = selectBbox;
370 +
371 + // Chiều rộng box OCR
372 + const boxWidth = x2 - x1;
373 +
374 + // Vị trí start và end tương đối trong text
375 + let startRatio = Math.max(0, (sx1 - x1) / boxWidth);
376 + let endRatio = Math.min(1, (sx2 - x1) / boxWidth);
377 +
378 + const startIndex = Math.floor(startRatio * text.length);
379 + const endIndex = Math.ceil(endRatio * text.length);
380 +
381 + return text.substring(startIndex, endIndex).trim();
382 + },
383 + getSelectStyle(item) {
384 + if (!this.imageWidth) return {position: 'absolute'};
385 +
386 + const [x1, y1, x2, y2] = item.bbox;
387 + const displayedWidth = this.$refs.pdfImage.clientWidth;
388 + const displayedHeight = this.$refs.pdfImage.clientHeight;
389 + const scaleX = displayedWidth / this.imageWidth;
390 + const scaleY = displayedHeight / this.imageHeight;
391 +
392 + return {
393 + position: 'absolute',
394 + left: `${Math.round(x1 * scaleX)}px`,
395 + top: `${Math.round(y2 * scaleY)}px`,
396 + zIndex: 9999
397 + };
166 } 398 }
167 - });
168 -
169 - // Đánh dấu xoá vùng thủ công
170 - this.ocrData[index].isDeleted = true;
171 - this.ocrData[index].showDelete = false;
172 -
173 - // Reset trạng thái nếu đây là vùng đang chọn
174 - if (this.manualIndex === index) {
175 - this.isMappingManually = false;
176 - this.selectBox.show = false;
177 - this.selectBox.showDropdown = false;
178 - this.manualField = "";
179 - this.manualIndex = null;
180 - }
181 - }
182 - },
183 -
184 - async initData() {
185 - await this.loadOCRData();
186 - },
187 - async loadOCRData() {
188 - const res = await fetch("/public/image/data_picking_detail_1754967679.json");
189 - this.ocrData = await res.json();
190 - },
191 - onImageLoad() {
192 - const img = this.$refs.pdfImage;
193 - this.imageWidth = img.naturalWidth;
194 - this.imageHeight = img.naturalHeight;
195 - },
196 - getBoxStyle(item) {
197 - if (!this.imageWidth || !this.imageHeight || !this.$refs.pdfImage) return {};
198 -
199 - const [x1, y1, x2, y2] = item.bbox;
200 - const displayedWidth = this.$refs.pdfImage.clientWidth;
201 - const displayedHeight = this.$refs.pdfImage.clientHeight;
202 -
203 - const scaleX = displayedWidth / this.imageWidth;
204 - const scaleY = displayedHeight / this.imageHeight;
205 -
206 - return {
207 - position: 'absolute',
208 - left: `${Math.round(x1 * scaleX)}px`,
209 - top: `${Math.round(y1 * scaleY)}px`,
210 - width: `${Math.round((x2 - x1) * scaleX)}px`,
211 - height: `${Math.round((y2 - y1) * scaleY)}px`,
212 - border: item.hideBorder ? 'none' : '2px solid ' + (this.activeField === item.field ? '#199601' : '#ff5252'),
213 - //backgroundColor: item.hideBorder ? 'transparent' : (this.activeField === item.field ? 'rgba(33,150,243,0.3)' : 'rgba(255,82,82,0.2)'),
214 - boxSizing: 'border-box',
215 - cursor: 'pointer',
216 - zIndex: item.isManual ? 30 : 10
217 - };
218 - },
219 -
220 - highlightField(field) {
221 - this.activeField = field;
222 - },
223 -
224 - startSelect(e) {
225 - if (this.isMappingManually || e.button !== 0) return;
226 - this.isSelecting = true;
227 - const rect = this.$refs.pdfContainer.getBoundingClientRect();
228 - this.selectBox.startX = e.clientX - rect.left;
229 - this.selectBox.startY = e.clientY - rect.top;
230 - this.selectBox.x = this.selectBox.startX;
231 - this.selectBox.y = this.selectBox.startY;
232 - this.selectBox.width = 0;
233 - this.selectBox.height = 0;
234 - this.selectBox.show = true;
235 - this.selectBox.showDropdown = false;
236 - this.manualField = "";
237 - },
238 -
239 - onSelect(e) {
240 - if (!this.isSelecting) return;
241 - const rect = this.$refs.pdfContainer.getBoundingClientRect();
242 - const currentX = e.clientX - rect.left;
243 - const currentY = e.clientY - rect.top;
244 - this.selectBox.x = Math.min(currentX, this.selectBox.startX);
245 - this.selectBox.y = Math.min(currentY, this.selectBox.startY);
246 - this.selectBox.width = Math.abs(currentX - this.selectBox.startX);
247 - this.selectBox.height = Math.abs(currentY - this.selectBox.startY);
248 - },
249 -
250 - endSelect(e) {
251 - if (!this.isSelecting) return;
252 - this.isSelecting = false;
253 -
254 - if (this.selectBox.width < 10 || this.selectBox.height < 10) {
255 - this.selectBox.show = false;
256 - return;
257 - }
258 -
259 - // displayed coords (như hiện tại, dùng để hiển thị select overlay)
260 - const dispX1 = this.selectBox.x;
261 - const dispY1 = this.selectBox.y;
262 - const dispX2 = this.selectBox.x + this.selectBox.width;
263 - const dispY2 = this.selectBox.y + this.selectBox.height;
264 -
265 - // scale: displayed -> original
266 - const displayedWidth = this.$refs.pdfImage.clientWidth;
267 - const displayedHeight = this.$refs.pdfImage.clientHeight;
268 - const scaleX = this.imageWidth / displayedWidth;
269 - const scaleY = this.imageHeight / displayedHeight;
270 -
271 - // bbox ở hệ gốc (original image pixels) — dùng để so sánh với ocrData và lưu vào ocrData
272 - const origBbox = [
273 - Math.round(dispX1 * scaleX),
274 - Math.round(dispY1 * scaleY),
275 - Math.round(dispX2 * scaleX),
276 - Math.round(dispY2 * scaleY)
277 - ];
278 -
279 - // Ẩn border các box OCR gốc nằm giao nhau với vùng thủ công (dùng coords gốc)
280 - this.ocrData.forEach(item => {
281 - if (!item.isManual && this.isBoxInside(item.bbox, origBbox)) {
282 - item.hideBorder = true;
283 - }
284 - });
285 -
286 - // Thêm box thủ công (lưu theo coords gốc)
287 - this.ocrData.push({
288 - text: "",
289 - bbox: origBbox,
290 - field: "",
291 - isManual: true,
292 - showDelete: true,
293 - isDeleted: false,
294 - hideBorder: false
295 - });
296 -
297 - this.manualIndex = this.ocrData.length - 1;
298 - this.isMappingManually = true;
299 - this.selectBox.showDropdown = true;
300 -
301 - e.stopPropagation();
302 - e.preventDefault();
303 - }
304 - ,
305 - applyMapping() {
306 - const item = this.ocrData[this.selectingIndex];
307 -
308 - if (item && item.isManual) {
309 - this.manualIndex = this.selectingIndex;
310 - this.manualField = item.field; // đảm bảo sync field hiện tại
311 - this.applyManualMapping();
312 - return;
313 - }
314 399
315 - if (item.field) {
316 - this.formData[item.field] = item.text;
317 - this.activeField = item.field;
318 } 400 }
319 - this.selectingIndex = null; 401 + });
320 - },
321 - applyManualMapping() {
322 - if (!this.manualField) return;
323 - const manualIndex = this.manualIndex;
324 - const newBbox = this.ocrData[manualIndex].bbox;
325 -
326 - let combinedText = [];
327 - this.ocrData.forEach(item => {
328 - if (!item.isManual && this.isBoxInside(item.bbox, newBbox) && item.text.trim()) {
329 - const partial = this.getPartialText(item.text, item.bbox, newBbox);
330 - if (partial) combinedText.push(partial);
331 - // combinedText.push(item.text.trim());
332 - }
333 - });
334 -
335 - const finalText = combinedText.join(" ");
336 - this.ocrData[manualIndex].field = this.manualField;
337 - this.formData[this.manualField] = finalText;
338 - this.activeField = this.manualField;
339 -
340 - console.log('manualField',this.manualField,this.manualIndex)
341 - // Reset trạng thái chọn
342 - this.isMappingManually = false;
343 - this.selectBox.show = false;
344 - this.selectBox.showDropdown = false;
345 - // this.manualField = "";
346 - // this.manualIndex = null;
347 - },
348 -
349 - isBoxInside(inner, outer) {
350 - return !(
351 - inner[2] < outer[0] || // box bên trái vùng chọn
352 - inner[0] > outer[2] || // box bên phải vùng chọn
353 - inner[3] < outer[1] || // box phía trên vùng chọn
354 - inner[1] > outer[3] // box phía dưới vùng chọn
355 - );
356 - },
357 -
358 - getPartialText(text, bbox, selectBbox) {
359 - const [x1, y1, x2, y2] = bbox;
360 - const [sx1, sy1, sx2, sy2] = selectBbox;
361 -
362 - // Chiều rộng box OCR
363 - const boxWidth = x2 - x1;
364 -
365 - // Vị trí start và end tương đối trong text
366 - let startRatio = Math.max(0, (sx1 - x1) / boxWidth);
367 - let endRatio = Math.min(1, (sx2 - x1) / boxWidth);
368 -
369 - const startIndex = Math.floor(startRatio * text.length);
370 - const endIndex = Math.ceil(endRatio * text.length);
371 -
372 - return text.substring(startIndex, endIndex).trim();
373 - },
374 - getSelectStyle(item) {
375 - if (!this.imageWidth) return { position: 'absolute' };
376 -
377 - const [x1, y1, x2, y2] = item.bbox;
378 - const displayedWidth = this.$refs.pdfImage.clientWidth;
379 - const displayedHeight = this.$refs.pdfImage.clientHeight;
380 - const scaleX = displayedWidth / this.imageWidth;
381 - const scaleY = displayedHeight / this.imageHeight;
382 -
383 - return {
384 - position: 'absolute',
385 - left: `${Math.round(x1 * scaleX)}px`,
386 - top: `${Math.round(y2 * scaleY)}px`,
387 - zIndex: 9999
388 - };
389 - }
390 -
391 - }
392 - });
393 402
394 </script> 403 </script>
395 404
396 405
397 -</body></html> 406 +</body>
407 +</html>
......