tien_nemo

demo

1 +<?php
2 +
3 +namespace App\Http\Controllers;
4 +
5 +use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
6 +use Illuminate\Foundation\Bus\DispatchesJobs;
7 +use Illuminate\Foundation\Validation\ValidatesRequests;
8 +use Illuminate\Routing\Controller as BaseController;
9 +
10 +class Controller extends BaseController
11 +{
12 + use AuthorizesRequests, DispatchesJobs, ValidatesRequests;
13 +}
1 +<?php
2 +
3 +namespace App\Http\Controllers;
4 +
5 +use App\Models\DtTemplate;
6 +use App\Models\MstTemplate;
7 +use Illuminate\Http\Request;
8 +
9 +class OcrController extends Controller
10 +{
11 + public function index()
12 + {
13 + return view('ocr.index');
14 + }
15 +
16 +
17 + public function store(Request $request)
18 + {
19 +
20 + $request->validate([
21 + 'customer_name_text' => 'required|string',
22 + 'customer_name_xy' => 'required|string',
23 + 'tpl_name' => 'unique:mst_template',
24 + ]);
25 +
26 + // Lưu vào bảng mst_template
27 + $mst = MstTemplate::create([
28 + 'tpl_name' => $request->template_name,
29 + 'tpl_text' => $request->customer_name_text,
30 + 'tpl_xy' => $request->customer_name_xy,
31 + ]);
32 +
33 + // Lưu các field khác vào dt_template
34 + foreach ($request->fields as $field) {
35 + DtTemplate::create([
36 + 'tpl_id' => $mst->id,
37 + 'field_name' => $field['name'],
38 + 'field_xy' => $field['xy'],
39 + ]);
40 + }
41 +
42 + return response()->json(['success' => true, 'message' => 'Lưu template thành công']);
43 + }
44 +
45 +
46 + public function getData()
47 + {
48 + // Giả sử file OCR JSON & ảnh nằm trong storage/app/public/image/
49 + $jsonPath = public_path("image/data_picking_detail_1754967679.json");
50 + $imgPath = ("image/data_picking_detail_1754967679.jpg");
51 +
52 +
53 + $templateName = 'nemo_4';
54 + /// Lấy từ request hoặc mặc định
55 +
56 +
57 + if (!file_exists($jsonPath)) {
58 + return response()->json(['error' => 'File not found'], 404);
59 + }
60 +
61 + $ocrData = json_decode(file_get_contents($jsonPath), true);
62 + $formData = [];
63 +
64 + if ($templateName) {
65 + $mst = MstTemplate::where('tpl_name', $templateName)->first();
66 +
67 + if ($mst) {
68 + // Lấy detail của template
69 + $details = DtTemplate::where('tpl_id', $mst->id)->get();
70 +
71 + foreach ($details as $detail) {
72 + $coords = array_map('intval', explode(',', $detail->field_xy));
73 + // coords = [x1, y1, x2, y2]
74 +
75 + // Tìm text OCR nằm trong bbox này
76 + $text = $this->findTextInBBox($ocrData, $coords);
77 +
78 + // field_name => text
79 + $formData[$detail->field_name] = $text;
80 + }
81 + } else{
82 + $formData = [
83 + 'export_date' => "",
84 + 'order_code' => "",
85 + 'customer' => "",
86 + 'address' => "",
87 + 'staff' => "",
88 + 'customer_name' => ""
89 + ];
90 + }
91 + }
92 + return response()->json([
93 + 'ocrData' => $ocrData,
94 + 'pdfImageUrl' => $imgPath,
95 + 'formData' => $formData,
96 + 'fieldOptions' => [
97 + [ 'value' => 'template_name', 'label' => 'Tên Mẫu PDF' ],
98 + [ 'value' => 'customer_name', 'label' => 'Tên khách hàng' ],
99 + [ 'value' => 'export_date', 'label' => 'Ngày xuất' ],
100 + [ 'value' => 'order_code', 'label' => 'Mã đơn hàng' ],
101 + [ 'value' => 'customer', 'label' => 'Khách hàng' ],
102 + [ 'value' => 'address', 'label' => 'Địa chỉ' ],
103 + [ 'value' => 'staff', 'label' => 'Nhân viên' ],
104 + ]
105 + ]);
106 + }
107 +
108 + private function findTextInBBox($ocrData, $coords)
109 + {
110 + [$x1, $y1, $x2, $y2] = $coords;
111 + foreach ($ocrData as $item) {
112 + [$ix1, $iy1, $ix2, $iy2] = $item['bbox'];
113 + // Kiểm tra nếu bbox OCR nằm trong vùng bbox template
114 + if ($ix1 >= $x1 && $iy1 >= $y1 && $ix2 <= $x2 && $iy2 <= $y2) {
115 + return $item['text'];
116 + }
117 + }
118 + return '';
119 + }
120 +
121 +
122 +}
1 +<?php
2 +
3 +namespace App\Models;
4 +
5 +use Illuminate\Database\Eloquent\Factories\HasFactory;
6 +use Illuminate\Database\Eloquent\Model;
7 +
8 +class DtTemplate extends Model
9 +{
10 + public $timestamps = false;
11 +
12 + protected $table = 'dt_template';
13 + protected $guarded = [];
14 +
15 +
16 +}
1 +<?php
2 +
3 +namespace App\Models;
4 +
5 +use Illuminate\Database\Eloquent\Factories\HasFactory;
6 +use Illuminate\Database\Eloquent\Model;
7 +
8 +class MstTemplate extends Model
9 +{
10 + public $timestamps = false;
11 +
12 + protected $table = 'mst_template';
13 +
14 + protected $guarded = [];
15 +}
1 +<?php
2 +
3 +use Illuminate\Contracts\Http\Kernel;
4 +use Illuminate\Http\Request;
5 +
6 +define('LARAVEL_START', microtime(true));
7 +
8 +/*
9 +|--------------------------------------------------------------------------
10 +| Check If The Application Is Under Maintenance
11 +|--------------------------------------------------------------------------
12 +|
13 +| If the application is in maintenance / demo mode via the "down" command
14 +| we will load this file so that any pre-rendered content can be shown
15 +| instead of starting the framework, which could cause an exception.
16 +|
17 +*/
18 +
19 +if (file_exists($maintenance = __DIR__.'/../storage/framework/maintenance.php')) {
20 + require $maintenance;
21 +}
22 +
23 +/*
24 +|--------------------------------------------------------------------------
25 +| Register The Auto Loader
26 +|--------------------------------------------------------------------------
27 +|
28 +| Composer provides a convenient, automatically generated class loader for
29 +| this application. We just need to utilize it! We'll simply require it
30 +| into the script here so we don't need to manually load our classes.
31 +|
32 +*/
33 +
34 +require __DIR__.'/../vendor/autoload.php';
35 +
36 +/*
37 +|--------------------------------------------------------------------------
38 +| Run The Application
39 +|--------------------------------------------------------------------------
40 +|
41 +| Once we have the application, we can handle the incoming request using
42 +| the application's HTTP kernel. Then, we will send the response back
43 +| to this client's browser, allowing them to enjoy our application.
44 +|
45 +*/
46 +
47 +$app = require_once __DIR__.'/../bootstrap/app.php';
48 +
49 +$kernel = $app->make(Kernel::class);
50 +
51 +$response = $kernel->handle(
52 + $request = Request::capture()
53 +)->send();
54 +
55 +$kernel->terminate($request, $response);
1 +<html lang="en"><head>
2 + <meta charset="UTF-8">
3 + <title>OCR Mapping with Manual Select Tool</title>
4 + <script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
5 + <style>
6 + body { font-family: sans-serif; background: #f5f5f5; }
7 + #app { display: flex; gap: 20px; padding: 20px; }
8 + .left-panel {
9 + width: 500px; background: #fff; padding: 15px;
10 + border-radius: 8px; box-shadow: 0 0 5px rgba(0,0,0,0.1);
11 + }
12 + .form-group { margin-bottom: 15px; }
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; }
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; }
17 + .bbox {
18 + position: absolute;
19 + border: 2px solid #ff5252;
20 + /*background-color: rgba(255, 82, 82, 0.2);*/
21 + cursor: pointer;
22 + }
23 + .bbox.active {
24 + /*border-color: #2196F3;*/
25 + background-color: rgb(33 243 132 / 30%);
26 + }
27 + select {
28 + position: absolute;
29 + z-index: 10;
30 + background: #fff;
31 + border: 1px solid #ccc;
32 + }
33 + .select-box {
34 + position: absolute;
35 + /*border: 2px dashed #2196F3;*/
36 + background-color: rgba(33, 150, 243, 0.2);
37 + pointer-events: none;
38 + z-index: 5;
39 + }
40 + .delete-btn {
41 + position: absolute;
42 + bottom: -10px;
43 + right: -10px;
44 + background: #ff4d4d;
45 + color: #fff;
46 + border: none;
47 + border-radius: 50%;
48 + cursor: pointer;
49 + font-size: 14px;
50 + padding: 3px 6px;
51 + z-index: 20;
52 + }
53 + </style>
54 +
55 +</head>
56 +<body>
57 +<meta name="csrf-token" content="{{ csrf_token() }}">
58 +<div id="app">
59 + <!-- Right: PDF viewer + select tool -->
60 + <div class="right-panel" >
61 + <div class="pdf-container" ref="pdfContainer"
62 + @mousedown="startSelect"
63 + @mousemove="onSelect"
64 + @mouseup="endSelect">
65 + <img
66 + ref="pdfImage"
67 + :src="pdfImageUrl"
68 + @load="onImageLoad"
69 + style="width: 100%; height: auto;pointer-events: none;"
70 + />
71 +
72 +
73 + <!-- Vùng kéo chọn -->
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>
76 +
77 + <!-- Vẽ bbox OCR -->
78 + <div
79 + v-for="(item, index) in ocrData"
80 + :key="index"
81 + v-if="!item.isDeleted"
82 + class="bbox"
83 + :class="{ active: index === activeIndex }"
84 + :data-field="item.field"
85 + :style="getBoxStyle(item, index)"
86 + @click="selectingIndex = index">
87 +
88 + <button v-if="item.isManual && item.showDelete"
89 + class="delete-btn"
90 + @click.stop="deleteBox(index)">🗑</button>
91 + </div>
92 +
93 +
94 + <!-- Dropdown OCR -->
95 + <select v-if="selectingIndex !== null"
96 + :style="getSelectStyle(ocrData[selectingIndex])"
97 + v-model="ocrData[selectingIndex].field"
98 + @change="applyMapping"
99 + >
100 + <option disabled value="">-- Chọn trường dữ liệu --</option>
101 + <option v-for="field in fieldData" :value="field.value">@{{ field.label }}</option>
102 + </select>
103 +
104 + <!-- Dropdown thủ công -->
105 + <select v-if="selectBox.showDropdown"
106 + :style="{ left: selectBox.x + 'px', top: (selectBox.y + selectBox.height) + 'px' }"
107 + v-model="manualField"
108 + @change="applyManualMapping"
109 + @click.stop
110 + >
111 + <option disabled value="">-- Chọn trường dữ liệu --</option>
112 + <option v-for="field in fieldData" :value="field.value">@{{ field.label }}</option>
113 + </select>
114 + </div>
115 + </div>
116 +
117 + <!-- Left: Form inputs -->
118 + <div class="left-panel">
119 + <div v-for="field in fieldOptions" :key="field.value" class="form-group">
120 + <label>@{{ field.label }}</label>
121 + <input v-model="formData[field.value]"
122 + @focus="highlightField(field.value)"
123 + :readonly="field.value === 'customer_name' && !hasCustomerNameXY"
124 + >
125 + </div>
126 + <button @click="saveTemplate">💾Save</button>
127 + </div>
128 +
129 +</div>
130 +
131 +
132 +<script>
133 + new Vue({
134 + el: '#app',
135 + data() {
136 + return {
137 + pdfImageUrl: "",
138 + selectingIndex: null,
139 + isMappingManually: false,
140 + isSelecting: false,
141 + activeIndex: null,
142 + manualField: "",
143 + formData: {},
144 + fieldOptions: [],
145 + customer_name_xy: '',
146 + hasCustomerNameXY: false,
147 + ocrData: [],
148 + selectBox: { show: false, showDropdown: false, x: 0, y: 0, width: 0, height: 0, startX: 0, startY: 0 },
149 + manualIndex: null
150 + }
151 + },
152 + created() {
153 + // Chỉ tạo formData cho các field cần mapping
154 + this.fieldOptions
155 + .filter(f => f.value !== "template_name")
156 + .forEach(f => {
157 + this.$set(this.formData, f.value, "");
158 + });
159 + },
160 + mounted() {
161 + this.loadOCRData();
162 + },
163 + computed: {
164 + fieldData() {
165 + // Lọc bỏ template_name nếu không cần cho phần form mapping
166 + return this.fieldOptions.filter(f => f.value !== "template_name");
167 + }
168 + },
169 + methods: {
170 + assignFieldToBox(index, fieldName, text = null) {
171 + if (index == null) return;
172 +
173 + // Xóa fieldName ở box khác
174 + this.ocrData.forEach((box, i) => {
175 + if (i !== index && box.field === fieldName) {
176 + box.field = null;
177 + box.field_xy = null;
178 + }
179 + });
180 +
181 + // Nếu box này từng gán field khác thì bỏ
182 + const prev = this.ocrData[index].field;
183 + if (prev && prev !== fieldName) {
184 + if (prev === 'customer_name') {
185 + this.hasCustomerNameXY = false;
186 + this.customer_name_xy = '';
187 + }
188 + this.ocrData[index].field = null;
189 + this.ocrData[index].field_xy = null;
190 + }
191 +
192 + // Gán field mới
193 + const bbox = this.ocrData[index].bbox; // tọa độ OCR gốc [x1, y1, x2, y2]
194 +
195 + console.log('1111111111111111',bbox);
196 + const x1 = bbox[0];
197 + const y1 = bbox[1];
198 + const w = bbox[2];
199 + const h = bbox[3];
200 +
201 + const xyStr = `${x1},${y1},${w},${h}`;
202 +
203 + this.ocrData[index].field = fieldName;
204 + this.ocrData[index].field_xy = xyStr;
205 +
206 + // Set text
207 + this.formData[fieldName] = (text !== null ? text : (this.ocrData[index].text || '')).trim();
208 +
209 + // Active index
210 + this.activeIndex = index;
211 +
212 + // Nếu là customer_name
213 + if (fieldName === 'customer_name') {
214 + this.hasCustomerNameXY = true;
215 + this.customer_name_xy = xyStr;
216 + }
217 + },
218 +
219 +
220 + async saveTemplate() {
221 +
222 + if (!this.hasCustomerNameXY) {
223 + alert("Bạn phải map customer_name (quét/select) trước khi lưu.");
224 + return;
225 + }
226 +
227 + // build fields array: lấy những box có field gán, map unique by field name -> sử dụng field_xy
228 + const fieldsByName = {};
229 + this.ocrData.forEach(box => {
230 + if (box.field && !box.isDeleted) {
231 + // chỉ giữ 1 bản ghi cuối cùng cho mỗi field (box gần nhất)
232 + fieldsByName[box.field] = {
233 + name: box.field,
234 + xy: box.field_xy || ''
235 + };
236 + }
237 + });
238 + // convert to array
239 + const fields = Object.values(fieldsByName);
240 +
241 + const payload = {
242 + customer_name_text: this.formData.customer_name || '',
243 + template_name: this.formData.template_name || this.formData.customer_name,
244 + customer_name_xy: this.customer_name_xy,
245 + fields: fields
246 + };
247 + console.log(fields);
248 + try {
249 + const res = await fetch('/ocr/save-template', {
250 + method: 'POST',
251 + headers: {
252 + 'Content-Type': 'application/json',
253 + 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute('content')
254 + },
255 + body: JSON.stringify(payload)
256 + });
257 + const json = await res.json();
258 + if (json.success) {
259 + alert(json.message);
260 + } else {
261 + alert('Save failed');
262 + }
263 + } catch (err) {
264 + console.error(err);
265 + alert('Save error');
266 + }
267 + },
268 +
269 +
270 + deleteBox(index) {
271 + const item = this.ocrData[index];
272 + if (item.isManual) {
273 + const manualBbox = item.bbox;
274 +
275 + // Hiện lại border các box OCR gốc nằm trong vùng thủ công
276 + this.ocrData.forEach(o => {
277 + if (!o.isManual && this.isBoxInside(o.bbox, manualBbox)) {
278 + o.hideBorder = false;
279 + }
280 + });
281 +
282 + // Đánh dấu xoá vùng thủ công
283 + this.ocrData[index].isDeleted = true;
284 + this.ocrData[index].showDelete = false;
285 +
286 + // Reset trạng thái nếu đây là vùng đang chọn
287 + if (this.manualIndex === index) {
288 + this.isMappingManually = false;
289 + this.selectBox.show = false;
290 + this.selectBox.showDropdown = false;
291 + this.manualField = "";
292 + this.manualIndex = null;
293 + }
294 + }
295 + },
296 +
297 +
298 + async loadOCRData() {
299 +
300 + const res = await fetch(`/ocr/data-list`);
301 + const data = await res.json();
302 +
303 + if (data.error) {
304 + console.error(data.error);
305 + return;
306 + }
307 +
308 + this.ocrData = data.ocrData;
309 + this.pdfImageUrl = data.pdfImageUrl;
310 + this.formData = data.formData;
311 + this.fieldOptions = data.fieldOptions;
312 + },
313 + onImageLoad() {
314 + const img = this.$refs.pdfImage;
315 + this.imageWidth = img.naturalWidth;
316 + this.imageHeight = img.naturalHeight;
317 + },
318 + getBoxStyle(item, index) {
319 + if (!this.imageWidth || !this.imageHeight || !this.$refs.pdfImage) return {};
320 +
321 + const [x1, y1, x2, y2] = item.bbox;
322 + const displayedWidth = this.$refs.pdfImage.clientWidth;
323 + const displayedHeight = this.$refs.pdfImage.clientHeight;
324 +
325 + const scaleX = displayedWidth / this.imageWidth;
326 + const scaleY = displayedHeight / this.imageHeight;
327 +
328 + return {
329 + position: 'absolute',
330 + left: `${Math.round(x1 * scaleX)}px`,
331 + top: `${Math.round(y1 * scaleY)}px`,
332 + width: `${Math.round((x2 - x1) * scaleX)}px`,
333 + height: `${Math.round((y2 - y1) * scaleY)}px`,
334 + border: item.hideBorder ? 'none' : '2px solid ' + (index === this.activeIndex ? '#199601' : '#ff5252'),
335 + //backgroundColor: item.hideBorder ? 'transparent' : (this.activeIndex === item.field ? 'rgba(33,150,243,0.3)' : 'rgba(255,82,82,0.2)'),
336 + boxSizing: 'border-box',
337 + cursor: 'pointer',
338 + zIndex: item.isManual ? 30 : 10
339 + };
340 + },
341 +
342 + highlightField(field) {
343 + let idx = -1;
344 + for (let i = this.ocrData.length - 1; i >= 0; i--) {
345 + const it = this.ocrData[i];
346 + if (!it.isDeleted && it.field === field) {
347 + idx = i;
348 + break;
349 + }
350 + }
351 + this.activeIndex = idx === -1 ? null : idx;
352 + },
353 + startSelect(e) {
354 + if (this.isMappingManually || e.button !== 0) return;
355 + this.isSelecting = true;
356 + const rect = this.$refs.pdfContainer.getBoundingClientRect();
357 + this.selectBox.startX = e.clientX - rect.left;
358 + this.selectBox.startY = e.clientY - rect.top;
359 + this.selectBox.x = this.selectBox.startX;
360 + this.selectBox.y = this.selectBox.startY;
361 + this.selectBox.width = 0;
362 + this.selectBox.height = 0;
363 + this.selectBox.show = true;
364 + this.selectBox.showDropdown = false;
365 + this.manualField = "";
366 + },
367 +
368 + onSelect(e) {
369 + if (!this.isSelecting) return;
370 + const rect = this.$refs.pdfContainer.getBoundingClientRect();
371 + const currentX = e.clientX - rect.left;
372 + const currentY = e.clientY - rect.top;
373 + this.selectBox.x = Math.min(currentX, this.selectBox.startX);
374 + this.selectBox.y = Math.min(currentY, this.selectBox.startY);
375 + this.selectBox.width = Math.abs(currentX - this.selectBox.startX);
376 + this.selectBox.height = Math.abs(currentY - this.selectBox.startY);
377 + },
378 +
379 + endSelect(e) {
380 + if (!this.isSelecting) return;
381 + this.isSelecting = false;
382 +
383 + if (this.selectBox.width < 10 || this.selectBox.height < 10) {
384 + this.selectBox.show = false;
385 + return;
386 + }
387 +
388 + // displayed coords (như hiện tại, dùng để hiển thị select overlay)
389 + const dispX1 = this.selectBox.x;
390 + const dispY1 = this.selectBox.y;
391 + const dispX2 = this.selectBox.x + this.selectBox.width;
392 + const dispY2 = this.selectBox.y + this.selectBox.height;
393 +
394 + // scale: displayed -> original
395 + const displayedWidth = this.$refs.pdfImage.clientWidth;
396 + const displayedHeight = this.$refs.pdfImage.clientHeight;
397 + const scaleX = this.imageWidth / displayedWidth;
398 + const scaleY = this.imageHeight / displayedHeight;
399 +
400 + // bbox ở hệ gốc (original image pixels) — dùng để so sánh với ocrData và lưu vào ocrData
401 + const origBbox = [
402 + Math.round(dispX1 * scaleX),
403 + Math.round(dispY1 * scaleY),
404 + Math.round(dispX2 * scaleX),
405 + Math.round(dispY2 * scaleY)
406 + ];
407 +
408 + // Ẩn border các box OCR gốc nằm giao nhau với vùng thủ công (dùng coords gốc)
409 + this.ocrData.forEach(item => {
410 + if (!item.isManual && this.isBoxInside(item.bbox, origBbox)) {
411 + item.hideBorder = true;
412 + }
413 + });
414 +
415 + // Thêm box thủ công (lưu theo coords gốc)
416 + this.ocrData.push({
417 + text: "",
418 + bbox: origBbox,
419 + field: "",
420 + isManual: true,
421 + showDelete: true,
422 + isDeleted: false,
423 + hideBorder: false
424 + });
425 +
426 + this.manualIndex = this.ocrData.length - 1;
427 + this.isMappingManually = true;
428 + this.selectBox.showDropdown = true;
429 +
430 + e.stopPropagation();
431 + e.preventDefault();
432 + }
433 + ,
434 + applyMapping() {
435 + const item = this.ocrData[this.selectingIndex];
436 +
437 + if (item && item.isManual) {
438 + this.manualIndex = this.selectingIndex;
439 + this.manualField = item.field || "";
440 + this.applyManualMapping();
441 + return;
442 + }
443 +
444 + if (item.field) {
445 + // this.formData[item.field] = item.text;
446 + // this.activeIndex = this.selectingIndex;
447 + this.assignFieldToBox(this.selectingIndex, item.field, item.text);
448 + }
449 + this.selectingIndex = null;
450 + },
451 + applyManualMapping() {
452 + if (!this.manualField) return;
453 + const manualIndex = this.manualIndex;
454 + const newBbox = this.ocrData[manualIndex].bbox;
455 +
456 + let combinedText = [];
457 + this.ocrData.forEach(item => {
458 + if (!item.isManual && this.isBoxInside(item.bbox, newBbox) && item.text.trim()) {
459 + const partial = this.getPartialText(item.text, item.bbox, newBbox);
460 + if (partial) combinedText.push(partial);
461 + // combinedText.push(item.text.trim());
462 + }
463 + });
464 +
465 + const finalText = combinedText.join(" ");
466 + // this.ocrData[manualIndex].field = this.manualField;
467 + // this.formData[this.manualField] = finalText;
468 + // this.activeIndex = manualIndex;
469 +
470 + this.assignFieldToBox(manualIndex, this.manualField, finalText);
471 +
472 + // Reset trạng thái chọn
473 + this.isMappingManually = false;
474 + this.selectBox.show = false;
475 + this.selectBox.showDropdown = false;
476 + // this.manualField = "";
477 + // this.manualIndex = null;
478 + },
479 +
480 + isBoxInside(inner, outer) {
481 + return !(
482 + inner[2] < outer[0] || // box bên trái vùng chọn
483 + inner[0] > outer[2] || // box bên phải vùng chọn
484 + inner[3] < outer[1] || // box phía trên vùng chọn
485 + inner[1] > outer[3] // box phía dưới vùng chọn
486 + );
487 + },
488 +
489 +
490 +
491 + getPartialText(text, bbox, selectBbox) {
492 + const [x1, y1, x2, y2] = bbox;
493 + const [sx1, sy1, sx2, sy2] = selectBbox;
494 +
495 + // Chiều rộng box OCR
496 + const boxWidth = x2 - x1;
497 +
498 + // Vị trí start và end tương đối trong text
499 + let startRatio = Math.max(0, (sx1 - x1) / boxWidth);
500 + let endRatio = Math.min(1, (sx2 - x1) / boxWidth);
501 +
502 + const startIndex = Math.floor(startRatio * text.length);
503 + const endIndex = Math.ceil(endRatio * text.length);
504 +
505 + return text.substring(startIndex, endIndex).trim();
506 + },
507 + getSelectStyle(item) {
508 + if (!this.imageWidth) return { position: 'absolute' };
509 +
510 + const [x1, y1, x2, y2] = item.bbox;
511 + const displayedWidth = this.$refs.pdfImage.clientWidth;
512 + const displayedHeight = this.$refs.pdfImage.clientHeight;
513 + const scaleX = displayedWidth / this.imageWidth;
514 + const scaleY = displayedHeight / this.imageHeight;
515 +
516 + return {
517 + position: 'absolute',
518 + left: `${Math.round(x1 * scaleX)}px`,
519 + top: `${Math.round(y2 * scaleY)}px`,
520 + zIndex: 9999
521 + };
522 + }
523 +
524 + }
525 + });
526 +
527 +</script>
528 +
529 +
530 +</body></html>
1 +<?php
2 +
3 +use Illuminate\Support\Facades\Route;
4 +use App\Http\Controllers\OcrController;
5 +/*
6 +|--------------------------------------------------------------------------
7 +| Web Routes
8 +|--------------------------------------------------------------------------
9 +|
10 +| Here is where you can register web routes for your application. These
11 +| routes are loaded by the RouteServiceProvider within a group which
12 +| contains the "web" middleware group. Now create something great!
13 +|
14 +*/
15 +
16 +Route::get('/', function () {
17 + return view('welcome');
18 +});
19 +Route::get('/mapping', [OcrController::class, 'index']);
20 +Route::get('/ocr/data-list', [OcrController::class, 'getData']);
21 +Route::post('/ocr/save-template', [OcrController::class, 'store'])->name('store');