Toggle navigation
Toggle navigation
This project
Loading...
Sign in
Satini_pvduc
/
ocrpdf
Go to a project
Toggle navigation
Toggle navigation pinning
Projects
Groups
Snippets
Help
Project
Activity
Repository
Pipelines
Graphs
Issues
0
Merge Requests
0
Wiki
Snippets
Network
Create a new issue
Commits
Issue Boards
Files
Commits
Network
Compare
Branches
Tags
Authored by
tien_nemo
2025-09-09 13:15:59 +0700
Browse Files
Options
Browse Files
Tag
Download
Email Patches
Plain Diff
Commit
fec2e1e888ea619ea8b4c3d84f89bd1d332d4fdd
fec2e1e8
1 parent
0a53c2c3
demo2
Show whitespace changes
Inline
Side-by-side
Showing
4 changed files
with
359 additions
and
41 deletions
app/Http/Controllers/OcrController.php
app/Services/OCR/extrac_table.py
app/Services/OCR/table_detector.py
resources/views/ocr/index.blade.php
app/Http/Controllers/OcrController.php
View file @
fec2e1e
...
...
@@ -22,6 +22,7 @@ class OcrController extends Controller
'customer_name_xy'
=>
'required|string'
,
]);
$dataDetail
=
$request
->
fields
??
[];
$tableColumns
=
$request
->
table_columns
??
[];
try
{
$masterTemplate
=
MstTemplate
::
updateOrCreate
(
[
'tpl_name'
=>
$request
->
template_name
],
...
...
@@ -49,6 +50,24 @@ class OcrController extends Controller
);
}
// Lưu mapping cột bảng (lưu col index vào field_xy với field_name đặc biệt)
if
(
!
empty
(
$tableColumns
)
&&
is_array
(
$tableColumns
))
{
foreach
(
$tableColumns
as
$name
=>
$colIdx
)
{
if
(
$colIdx
===
null
||
$colIdx
===
''
||
$colIdx
===
false
)
{
continue
;
}
DtTemplate
::
updateOrInsert
(
[
'tpl_id'
=>
$masterTemplate
->
id
,
'field_name'
=>
'__table_col__'
.
$name
,
],
[
'field_xy'
=>
(
string
)
$colIdx
,
]
);
}
}
return
response
()
->
json
([
'success'
=>
true
,
'message'
=>
'Lưu template thành công'
,
...
...
@@ -70,8 +89,8 @@ class OcrController extends Controller
$templateName
=
$request
->
get
(
'template_name'
,
''
);
// Giả sử file OCR JSON & ảnh nằm trong storage/app/public/image/
$jsonPath
=
public_path
(
"image/
3_1757295841
_with_table.json"
);
$imgPath
=
(
"image/
3_1757295841
.jpg"
);
$jsonPath
=
public_path
(
"image/
nemo_new_1757393338
_with_table.json"
);
$imgPath
=
(
"image/
nemo_new_1757393338
.jpg"
);
if
(
!
file_exists
(
$jsonPath
))
{
return
response
()
->
json
([
'error'
=>
'File OCR JSON không tìm thấy'
],
404
);
...
...
@@ -100,7 +119,13 @@ class OcrController extends Controller
// Lấy detail của template
$details
=
DtTemplate
::
where
(
'tpl_id'
,
$mst
->
id
)
->
get
();
$tableColumnMapping
=
[];
foreach
(
$details
as
$detail
)
{
if
(
strpos
(
$detail
->
field_name
,
'__table_col__'
)
===
0
)
{
$name
=
substr
(
$detail
->
field_name
,
strlen
(
'__table_col__'
));
$tableColumnMapping
[
$name
]
=
is_numeric
(
$detail
->
field_xy
)
?
intval
(
$detail
->
field_xy
)
:
null
;
continue
;
}
$coords
=
array_map
(
'intval'
,
explode
(
','
,
$detail
->
field_xy
));
// coords = [x1, y1, x2, y2]
...
...
@@ -130,6 +155,7 @@ class OcrController extends Controller
'pdfImageUrl'
=>
$imgPath
,
'dataMapping'
=>
$dataMapping
,
'is_template'
=>
$is_template
,
'tableColumnMapping'
=>
$tableColumnMapping
??
[],
//?? new \stdClass(),
'fieldOptions'
=>
[
[
'value'
=>
'template_name'
,
'label'
=>
'Tên Mẫu PDF'
],
[
'value'
=>
'customer_name'
,
'label'
=>
'Tên khách hàng'
],
...
...
app/Services/OCR/extrac_table.py
View file @
fec2e1e
...
...
@@ -11,14 +11,14 @@ from PIL import Image, ImageEnhance
# ==== Config ====
BASE_DIR
=
os
.
path
.
abspath
(
os
.
path
.
join
(
os
.
path
.
dirname
(
__file__
),
".."
,
".."
,
".."
))
# PDF_NAME = 'aaaa
'
PDF_NAME
=
'nemo_new
'
# PDF path
pdf_path
=
Path
(
BASE_DIR
)
/
"storage"
/
"pdf"
/
"
3
.pdf"
pdf_path
=
Path
(
BASE_DIR
)
/
"storage"
/
"pdf"
/
"
2
.pdf"
# Output folder
output_folder
=
Path
(
BASE_DIR
)
/
"public"
/
"image"
PDF_NAME
=
pdf_path
.
stem
# Get the stem of the PDF file
#
PDF_NAME = pdf_path.stem # Get the stem of the PDF file
#print(PDF_NAME)
os
.
makedirs
(
output_folder
,
exist_ok
=
True
)
...
...
@@ -151,9 +151,31 @@ for table in table_info:
x1
,
y1
,
x2
,
y2
=
cell
[
"cell"
]
cell_texts
=
[]
# Helper: compute overlap ratio of bbox against cell
def
overlap_ratio
(
bbox
,
cell_box
):
ix1
=
max
(
bbox
[
0
],
cell_box
[
0
])
iy1
=
max
(
bbox
[
1
],
cell_box
[
1
])
ix2
=
min
(
bbox
[
2
],
cell_box
[
2
])
iy2
=
min
(
bbox
[
3
],
cell_box
[
3
])
iw
=
max
(
0
,
ix2
-
ix1
)
ih
=
max
(
0
,
iy2
-
iy1
)
inter
=
iw
*
ih
bbox_area
=
max
(
1
,
(
bbox
[
2
]
-
bbox
[
0
])
*
(
bbox
[
3
]
-
bbox
[
1
]))
return
inter
/
float
(
bbox_area
)
# Helper: check center inside cell
def
center_inside
(
bbox
,
cell_box
):
cx
=
(
bbox
[
0
]
+
bbox
[
2
])
/
2.0
cy
=
(
bbox
[
1
]
+
bbox
[
3
])
/
2.0
return
(
cx
>=
cell_box
[
0
]
and
cx
<=
cell_box
[
2
]
and
cy
>=
cell_box
[
1
]
and
cy
<=
cell_box
[
3
])
cell_box
=
[
x1
,
y1
,
x2
,
y2
]
for
item
in
ocr_data_list
:
bx1
,
by1
,
bx2
,
by2
=
item
[
"bbox"
]
if
bx1
>=
x1
and
by1
>=
y1
and
bx2
<=
x2
and
by2
<=
y2
:
bbox
=
[
bx1
,
by1
,
bx2
,
by2
]
# Accept if bbox is largely inside the cell, or its center lies inside the cell
if
overlap_ratio
(
bbox
,
cell_box
)
>=
0.3
or
center_inside
(
bbox
,
cell_box
):
cell_texts
.
append
(
item
[
"text"
])
# thêm vào cell gốc
...
...
app/Services/OCR/table_detector.py
View file @
fec2e1e
...
...
@@ -2,46 +2,73 @@ import cv2
import
numpy
as
np
import
os
def
detect_tables
(
image_path
):
def
filter_horizontal_lines
(
lines_h
,
img_width
,
min_h_len_ratio
=
0.8
,
tol_y
=
10
):
if
lines_h
is
None
:
return
[],
[]
ys_candidates
=
[]
for
l
in
lines_h
:
x1
,
y1
,
x2
,
y2
=
l
[
0
]
if
abs
(
y1
-
y2
)
<=
3
:
# ngang
line_len
=
abs
(
x2
-
x1
)
y_mid
=
int
(
round
((
y1
+
y2
)
/
2
))
ys_candidates
.
append
((
y_mid
,
line_len
,
x1
,
x2
))
ys_candidates
.
sort
(
key
=
lambda
x
:
x
[
0
])
filtered_lines
,
line_segments
,
current_group
=
[],
[],
[]
for
y
,
length
,
x1
,
x2
in
ys_candidates
:
if
not
current_group
:
current_group
.
append
((
y
,
length
,
x1
,
x2
))
else
:
if
abs
(
y
-
current_group
[
-
1
][
0
])
<=
tol_y
:
current_group
.
append
((
y
,
length
,
x1
,
x2
))
else
:
longest
=
max
(
current_group
,
key
=
lambda
x
:
x
[
1
])
if
longest
[
1
]
>=
min_h_len_ratio
*
img_width
:
filtered_lines
.
append
(
longest
[
0
])
line_segments
.
append
((
longest
[
2
],
longest
[
3
],
longest
[
0
]))
else
:
break
current_group
=
[(
y
,
length
,
x1
,
x2
)]
if
current_group
:
longest
=
max
(
current_group
,
key
=
lambda
x
:
x
[
1
])
if
longest
[
1
]
>=
min_h_len_ratio
*
img_width
:
filtered_lines
.
append
(
longest
[
0
])
line_segments
.
append
((
longest
[
2
],
longest
[
3
],
longest
[
0
]))
total_rows
=
max
(
0
,
len
(
filtered_lines
)
-
1
)
print
(
f
"Detected {total_rows} rows"
)
return
filtered_lines
,
line_segments
def
detect_tables
(
image_path
,
gap_threshold
=
50
):
img
=
cv2
.
imread
(
image_path
)
if
img
is
None
:
raise
FileNotFoundError
(
f
"Không đọc được ảnh: {image_path}"
)
gray
=
cv2
.
cvtColor
(
img
,
cv2
.
COLOR_BGR2GRAY
)
blur
=
cv2
.
GaussianBlur
(
gray
,
(
3
,
3
),
0
)
# Edge detection
edges
=
cv2
.
Canny
(
blur
,
50
,
150
,
apertureSize
=
3
)
# --- Horizontal lines ---
lines_h
=
cv2
.
HoughLinesP
(
edges
,
1
,
np
.
pi
/
180
,
threshold
=
120
,
minLineLength
=
int
(
img
.
shape
[
1
]
*
0.6
),
maxLineGap
=
20
)
ys_candidates
,
line_segments
=
[],
[]
if
lines_h
is
not
None
:
for
l
in
lines_h
:
x1
,
y1
,
x2
,
y2
=
l
[
0
]
if
abs
(
y1
-
y2
)
<=
3
:
# ngang
y_mid
=
int
(
round
((
y1
+
y2
)
/
2
))
ys_candidates
.
append
(
y_mid
)
line_segments
.
append
((
x1
,
x2
,
y_mid
))
# gom nhóm y
ys
,
tol_y
=
[],
10
for
y
in
sorted
(
ys_candidates
):
if
not
ys
or
abs
(
y
-
ys
[
-
1
])
>
tol_y
:
ys
.
append
(
y
)
img_height
,
img_width
=
img
.
shape
[:
2
]
ys
,
line_segments
=
filter_horizontal_lines
(
lines_h
,
img_width
,
min_h_len_ratio
=
0.8
,
tol_y
=
10
)
total_rows
=
max
(
0
,
len
(
ys
)
-
1
)
# --- Vertical lines ---
lines_v
=
cv2
.
HoughLinesP
(
edges
,
1
,
np
.
pi
/
180
,
threshold
=
100
,
minLineLength
=
int
(
img
.
shape
[
0
]
*
0.
5
),
maxLineGap
=
20
)
xs
=
[]
minLineLength
=
int
(
img
.
shape
[
0
]
*
0.
4
),
maxLineGap
=
20
)
v_lines
,
xs
=
[],
[]
if
lines_v
is
not
None
:
for
l
in
lines_v
:
x1
,
y1
,
x2
,
y2
=
l
[
0
]
if
abs
(
x1
-
x2
)
<=
3
:
xs
.
append
(
int
(
round
((
x1
+
x2
)
/
2
)))
v_lines
.
append
((
int
(
round
((
x1
+
x2
)
/
2
)),
min
(
y1
,
y2
),
max
(
y1
,
y2
)))
# gom nhóm x
x_pos
,
tol_v
=
[],
10
...
...
@@ -50,26 +77,66 @@ def detect_tables(image_path):
x_pos
.
append
(
v
)
total_cols
=
max
(
0
,
len
(
x_pos
)
-
1
)
tables
=
[]
if
total_rows
>
0
and
total_cols
>
0
:
y_min
,
y_max
=
ys
[
0
],
ys
[
-
1
]
x_min
,
x_max
=
x_pos
[
0
],
x_pos
[
-
1
]
table_box
=
(
x_min
,
y_min
,
x_max
,
y_max
)
# build cells
rows_data
=
[]
for
i
in
range
(
total_rows
):
row_cells
=
[]
for
j
in
range
(
total_cols
):
j
=
0
while
j
<
total_cols
:
cell_box
=
(
x_pos
[
j
],
ys
[
i
],
x_pos
[
j
+
1
],
ys
[
i
+
1
])
row_height
=
cell_box
[
3
]
-
cell_box
[
1
]
# Check vertical line coverage (>=70% chiều cao hàng)
has_left
=
any
(
abs
(
x
-
cell_box
[
0
])
<=
tol_v
and
(
min
(
y_end
,
cell_box
[
3
])
-
max
(
y_start
,
cell_box
[
1
]))
>=
0.7
*
row_height
for
x
,
y_start
,
y_end
in
v_lines
)
has_right
=
any
(
abs
(
x
-
cell_box
[
2
])
<=
tol_v
and
(
min
(
y_end
,
cell_box
[
3
])
-
max
(
y_start
,
cell_box
[
1
]))
>=
0.7
*
row_height
for
x
,
y_start
,
y_end
in
v_lines
)
if
has_left
and
has_right
:
col_start
=
j
col_end
=
j
# nếu cột tiếp theo không có line → merge
while
col_end
+
1
<
total_cols
:
next_box
=
(
x_pos
[
col_end
+
1
],
ys
[
i
],
x_pos
[
col_end
+
2
],
ys
[
i
+
1
])
has_next_left
=
any
(
abs
(
x
-
next_box
[
0
])
<=
tol_v
and
(
min
(
y_end
,
next_box
[
3
])
-
max
(
y_start
,
next_box
[
1
]))
>=
0.7
*
row_height
for
x
,
y_start
,
y_end
in
v_lines
)
if
not
has_next_left
:
# merge tiếp
col_end
+=
1
else
:
break
merged_box
=
(
x_pos
[
col_start
],
ys
[
i
],
x_pos
[
col_end
+
1
],
ys
[
i
+
1
])
if
col_start
==
col_end
:
col_id
=
col_start
else
:
col_id
=
f
"{col_start}-{col_end}"
row_cells
.
append
({
"cell"
:
cell
_box
,
"cell"
:
merged
_box
,
"row_idx"
:
i
,
"col_idx"
:
j
"col_idx"
:
col_id
})
# Vẽ ô
cv2
.
rectangle
(
img
,
(
cell_box
[
0
],
cell_box
[
1
]),
(
cell_box
[
2
],
cell_box
[
3
]),
(
0
,
255
,
255
),
1
)
cv2
.
rectangle
(
img
,
(
merged_box
[
0
],
merged_box
[
1
]),
(
merged_box
[
2
],
merged_box
[
3
]),
(
0
,
255
,
255
),
1
)
j
=
col_end
+
1
else
:
j
+=
1
# skip ô lỗi (không có line đầy đủ)
rows_data
.
append
(
row_cells
)
tables
.
append
({
...
...
@@ -78,11 +145,9 @@ def detect_tables(image_path):
"table_box"
:
table_box
,
"cells"
:
rows_data
})
# vẽ viền bảng
cv2
.
rectangle
(
img
,
(
x_min
,
y_min
),
(
x_max
,
y_max
),
(
255
,
0
,
0
),
2
)
debug_path
=
os
.
path
.
splitext
(
image_path
)[
0
]
+
"_debug.jpg"
debug_path
=
os
.
path
.
splitext
(
image_path
)[
0
]
+
"_
fix_
debug.jpg"
cv2
.
imwrite
(
debug_path
,
img
)
return
tables
...
...
resources/views/ocr/index.blade.php
View file @
fec2e1e
...
...
@@ -79,7 +79,7 @@
@
change=
"applyMapping"
>
<option
disabled
value=
""
>
-- Chọn trường dữ liệu --
</option>
<option
v-for=
"field in
fieldData
"
:value=
"field.value"
>
@{{ field.label }}
</option>
<option
v-for=
"field in
getAllowedOptionsForBox(ocrData[selectingIndex])
"
:value=
"field.value"
>
@{{ field.label }}
</option>
</select>
<!-- Dropdown thủ công -->
...
...
@@ -93,7 +93,7 @@
@
click
.
stop
>
<option
disabled
value=
""
>
-- Chọn trường dữ liệu --
</option>
<option
v-for=
"field in
fieldData
"
:value=
"field.value"
>
@{{ field.label }}
</option>
<option
v-for=
"field in
getAllowedOptionsForManual()
"
:value=
"field.value"
>
@{{ field.label }}
</option>
</select>
</div>
</div>
...
...
@@ -118,24 +118,30 @@
</thead>
<tbody>
<tr
class=
"table-detail"
v-for=
"(row, rowIndex) in
total_rows
"
v-for=
"(row, rowIndex) in
dataRowsCount
"
:key=
"rowIndex"
:data-row=
"rowIndex"
>
<td
class=
"form-group"
>
<input
@
click=
"onTableInputClick('product_name', rowIndex)"
v-model=
"tableForm[rowIndex].product_name"
:data-row=
"rowIndex"
:data-field=
"'product_name'"
placeholder=
"Enter name"
>
</td>
<td
class=
"form-group"
>
<input
@
click=
"onTableInputClick('product_code', rowIndex)"
v-model=
"tableForm[rowIndex].product_code"
:data-row=
"rowIndex"
:data-field=
"'product_code'"
placeholder=
"Enter code"
>
</td>
<td
class=
"form-group"
>
<input
@
click=
"onTableInputClick('quantity', rowIndex)"
v-model
.
number=
"tableForm[rowIndex].quantity"
:data-row=
"rowIndex"
:data-field=
"'quantity'"
type=
"number"
...
...
@@ -166,8 +172,11 @@
tableInfo
:
{},
total_rows
:
0
,
table_box
:
[],
tableColumnMapping
:
{
product_name
:
null
,
product_code
:
null
,
quantity
:
null
},
tableForm
:
[],
manualBoxData
:
{},
fieldOptions
:
[],
tempFocusIndex
:
null
,
customer_name_xy
:
''
,
hasCustomerNameXY
:
false
,
ocrData
:
[],
...
...
@@ -220,9 +229,156 @@
fieldData
()
{
// Lọc bỏ template_name nếu không cần cho phần form mapping
return
this
.
fieldOptions
.
filter
(
f
=>
f
.
value
!==
"template_name"
);
},
dataRowsCount
()
{
return
this
.
total_rows
||
0
;
}
},
methods
:
{
onTableInputClick
(
field
,
uiRowIndex
)
{
// Determine column from saved mapping
const
colIdx
=
this
.
tableColumnMapping
?
this
.
tableColumnMapping
[
field
]
:
null
;
if
(
colIdx
===
null
||
colIdx
===
undefined
)
return
;
// uiRowIndex starts at 0 for first data row; tableInfo row_idx=uiRowIndex+1
const
tableRowIdx
=
(
uiRowIndex
+
1
);
if
(
!
Array
.
isArray
(
this
.
tableInfo
)
||
!
this
.
tableInfo
[
tableRowIdx
])
return
;
const
row
=
this
.
tableInfo
[
tableRowIdx
];
const
cell
=
row
.
find
(
c
=>
c
&&
c
.
col_idx
===
colIdx
);
if
(
!
cell
||
!
Array
.
isArray
(
cell
.
cell
))
return
;
const
coords
=
cell
.
cell
;
// [x1,y1,x2,y2]
// Remove previous temp focus
if
(
this
.
tempFocusIndex
!==
null
&&
this
.
ocrData
[
this
.
tempFocusIndex
])
{
this
.
$set
(
this
.
ocrData
[
this
.
tempFocusIndex
],
'isDeleted'
,
true
);
this
.
tempFocusIndex
=
null
;
}
// Create a temporary manual focus box for highlighting
const
tmpBox
=
{
text
:
''
,
bbox
:
coords
,
field
:
field
,
isManual
:
true
,
showDelete
:
false
,
isDeleted
:
false
,
hideBorder
:
false
};
this
.
ocrData
.
push
(
tmpBox
);
this
.
tempFocusIndex
=
this
.
ocrData
.
length
-
1
;
this
.
activeIndex
=
this
.
tempFocusIndex
;
this
.
selectingIndex
=
null
;
// Ensure it renders
this
.
$nextTick
(()
=>
this
.
$forceUpdate
());
},
ensureTableFormSize
()
{
const
rows
=
this
.
dataRowsCount
;
if
(
!
Array
.
isArray
(
this
.
tableForm
))
this
.
tableForm
=
[];
for
(
let
i
=
0
;
i
<
rows
;
i
++
)
{
if
(
!
this
.
tableForm
[
i
])
{
this
.
$set
(
this
.
tableForm
,
i
,
{
product_name
:
''
,
product_code
:
''
,
quantity
:
null
});
}
}
if
(
this
.
tableForm
.
length
>
rows
)
{
this
.
tableForm
.
splice
(
rows
);
}
},
getAllowedOptionsForBox
(
item
)
{
const
isInTable
=
this
.
isBboxInTable
(
item
.
bbox
);
if
(
isInTable
)
{
return
[
{
value
:
'product_name'
,
label
:
'Product Name'
},
{
value
:
'product_code'
,
label
:
'Product Code'
},
{
value
:
'quantity'
,
label
:
'Quantity'
},
];
}
return
this
.
fieldData
;
},
getAllowedOptionsForManual
()
{
// Use current selection box to determine
const
origBbox
=
this
.
getCurrentOrigSelectBbox
();
const
isInTable
=
origBbox
?
this
.
isBboxInTable
(
origBbox
)
:
false
;
if
(
isInTable
)
{
return
[
{
value
:
'product_name'
,
label
:
'Product Name'
},
{
value
:
'product_code'
,
label
:
'Product Code'
},
{
value
:
'quantity'
,
label
:
'Quantity'
},
];
}
return
this
.
fieldData
;
},
getCurrentOrigSelectBbox
()
{
if
(
!
this
.
$refs
.
pdfImage
||
!
this
.
imageWidth
||
!
this
.
imageHeight
)
return
null
;
const
displayedWidth
=
this
.
$refs
.
pdfImage
.
clientWidth
;
const
displayedHeight
=
this
.
$refs
.
pdfImage
.
clientHeight
;
const
scaleX
=
this
.
imageWidth
/
displayedWidth
;
const
scaleY
=
this
.
imageHeight
/
displayedHeight
;
return
[
Math
.
round
(
this
.
selectBox
.
x
*
scaleX
),
Math
.
round
(
this
.
selectBox
.
y
*
scaleY
),
Math
.
round
((
this
.
selectBox
.
x
+
this
.
selectBox
.
width
)
*
scaleX
),
Math
.
round
((
this
.
selectBox
.
y
+
this
.
selectBox
.
height
)
*
scaleY
)
];
},
isBboxInTable
(
bbox
)
{
if
(
!
this
.
table_box
||
this
.
table_box
.
length
!==
4
)
return
false
;
return
this
.
isBoxInside
(
bbox
,
this
.
table_box
);
},
getColumnIndexForBbox
(
bbox
)
{
// Determine which column this bbox belongs to using table header row (row_idx = 0)
if
(
!
Array
.
isArray
(
this
.
tableInfo
)
||
this
.
tableInfo
.
length
===
0
)
return
null
;
const
headerRow
=
this
.
tableInfo
[
0
];
if
(
!
Array
.
isArray
(
headerRow
))
return
null
;
// Use x-overlap with header cells to pick the best matching column
let
bestCol
=
null
;
let
bestOverlap
=
0
;
const
[
bx1
,
by1
,
bx2
,
by2
]
=
bbox
;
headerRow
.
forEach
(
cell
=>
{
if
(
!
cell
||
!
Array
.
isArray
(
cell
.
cell
))
return
;
const
[
cx1
,
cy1
,
cx2
,
cy2
]
=
cell
.
cell
;
const
overlapX
=
Math
.
max
(
0
,
Math
.
min
(
bx2
,
cx2
)
-
Math
.
max
(
bx1
,
cx1
));
const
headerWidth
=
cx2
-
cx1
;
const
ratio
=
headerWidth
>
0
?
overlapX
/
headerWidth
:
0
;
if
(
ratio
>
bestOverlap
)
{
bestOverlap
=
ratio
;
bestCol
=
cell
.
col_idx
;
}
});
if
(
bestOverlap
>
0
)
return
bestCol
;
return
null
;
},
populateTableColumn
(
field
,
colIdx
)
{
// Fill tableForm values from table cells for the given column index, skipping header (row_idx 0)
this
.
ensureTableFormSize
();
if
(
!
Array
.
isArray
(
this
.
tableInfo
))
return
;
for
(
let
r
=
1
;
r
<
this
.
tableInfo
.
length
;
r
++
)
{
const
row
=
this
.
tableInfo
[
r
];
const
dataRowIndex
=
r
-
1
;
// because header is row 0
if
(
!
row
||
!
Array
.
isArray
(
row
))
continue
;
const
cell
=
row
.
find
(
c
=>
c
&&
c
.
col_idx
===
colIdx
);
const
value
=
cell
?
(
cell
.
text
||
(
Array
.
isArray
(
cell
.
texts
)
?
cell
.
texts
.
join
(
' '
)
:
''
))
:
''
;
if
(
!
this
.
tableForm
[
dataRowIndex
])
{
this
.
$set
(
this
.
tableForm
,
dataRowIndex
,
{
product_name
:
''
,
product_code
:
''
,
quantity
:
null
});
}
if
(
field
===
'quantity'
)
{
const
num
=
value
?
Number
((
value
+
''
).
replace
(
/
[^
0-9.-
]
/g
,
''
))
:
null
;
this
.
$set
(
this
.
tableForm
[
dataRowIndex
],
field
,
isNaN
(
num
)
?
null
:
num
);
}
else
{
this
.
$set
(
this
.
tableForm
[
dataRowIndex
],
field
,
(
value
||
''
).
toString
());
}
}
},
applyTableMappingIfInTable
(
itemField
,
bbox
)
{
if
(
!
itemField
)
return
false
;
if
(
!
this
.
isBboxInTable
(
bbox
))
return
false
;
// Restrict to table fields only
if
(
!
[
'product_name'
,
'product_code'
,
'quantity'
].
includes
(
itemField
))
return
false
;
const
colIdx
=
this
.
getColumnIndexForBbox
(
bbox
);
console
.
log
(
`Mapping field "
${
itemField
}
" to table column index:`
,
colIdx
);
if
(
colIdx
===
null
||
colIdx
===
undefined
)
return
false
;
this
.
$set
(
this
.
tableColumnMapping
,
itemField
,
colIdx
);
this
.
populateTableColumn
(
itemField
,
colIdx
);
return
true
;
},
startResize
(
e
,
index
,
handle
)
{
e
.
stopPropagation
();
this
.
resizing
=
{
...
...
@@ -505,6 +661,13 @@
this
.
activeIndex
=
null
;
this
.
selectingIndex
=
null
;
// Cleanup temporary focus box if any
if
(
this
.
tempFocusIndex
!==
null
)
{
const
box
=
this
.
ocrData
[
this
.
tempFocusIndex
];
if
(
box
)
this
.
$set
(
box
,
'isDeleted'
,
true
);
this
.
tempFocusIndex
=
null
;
}
// Không ẩn nút xóa các box để tránh nhầm lẫn
// Chỉ reset focus
},
...
...
@@ -671,16 +834,34 @@
}
});
if
(
item
.
isManual
)
{
console
.
log
(
'aaaaaaaaaa'
)
// Nếu là manual box, chuyển sang chế độ manual mapping
this
.
manualIndex
=
this
.
selectingIndex
;
this
.
manualField
=
item
.
field
||
""
;
// Nếu trong vùng bảng, xử lý theo bảng trước
const
handled
=
this
.
applyTableMappingIfInTable
(
this
.
manualField
,
this
.
ocrData
[
this
.
manualIndex
].
bbox
);
if
(
!
handled
)
{
this
.
applyManualMapping
();
}
else
{
// reset selectingIndex if handled by table mapping
this
.
selectingIndex
=
null
;
this
.
selectBox
.
show
=
false
;
this
.
selectBox
.
showDropdown
=
false
;
this
.
isMappingManually
=
false
;
this
.
manualField
=
""
;
this
.
manualIndex
=
null
;
}
return
;
}
// Xử lý box OCR (có thể chưa có field hoặc đã có field)
if
(
item
.
field
)
{
// Nếu trong vùng bảng, xử lý theo bảng
const
handled
=
this
.
applyTableMappingIfInTable
(
item
.
field
,
item
.
bbox
);
console
.
log
(
`applyMapping: field=
${
item
.
field
}
, handled by table=
${
handled
}
`
);
if
(
handled
)
{
this
.
selectingIndex
=
null
;
return
;
}
// Cập nhật formData để hiển thị trong input
this
.
formData
[
item
.
field
]
=
item
.
text
||
''
;
...
...
@@ -706,6 +887,17 @@
const
newBbox
=
this
.
ocrData
[
manualIndex
].
bbox
;
// Nếu trong vùng bảng, ưu tiên xử lý bảng và thoát
const
handled
=
this
.
applyTableMappingIfInTable
(
this
.
manualField
,
newBbox
);
if
(
handled
)
{
this
.
isMappingManually
=
false
;
this
.
selectBox
.
show
=
false
;
this
.
selectBox
.
showDropdown
=
false
;
this
.
manualField
=
""
;
this
.
manualIndex
=
null
;
return
;
}
//console.log(`manual for field "${this.manualField}" at index ${manualIndex} with bbox:`, newBbox);
this
.
ocrData
.
forEach
((
box
,
i
)
=>
{
if
(
i
!==
manualIndex
&&
box
.
field
===
this
.
ocrData
[
manualIndex
].
field
)
{
...
...
@@ -885,6 +1077,17 @@
this
.
fieldOptions
=
data
.
fieldOptions
;
this
.
dataMapping
=
data
.
dataMapping
;
this
.
is_template
=
data
.
is_template
;
this
.
tableColumnMapping
=
data
.
tableColumnMapping
||
{
product_name
:
null
,
product_code
:
null
,
quantity
:
null
};
// Prepare table form structure
this
.
ensureTableFormSize
();
// If template provides column mapping, populate values now
Object
.
keys
(
this
.
tableColumnMapping
||
{}).
forEach
(
k
=>
{
const
col
=
this
.
tableColumnMapping
[
k
];
if
(
col
!==
null
&&
col
!==
undefined
)
{
this
.
populateTableColumn
(
k
,
col
);
}
});
//console.log('Loaded OCR data:', this.ocrData);
}
catch
(
error
)
{
...
...
@@ -896,6 +1099,7 @@
let
customer_name
=
null
;
let
customer_coords
=
null
;
let
fields
=
[];
const
table_columns
=
this
.
tableColumnMapping
||
{};
if
(
this
.
manualBoxData
.
customer_name
)
{
// Lấy từ manualBoxData nếu có
customer_name
=
this
.
manualBoxData
.
customer_name
.
text
;
...
...
@@ -931,7 +1135,8 @@
customer_name_text
:
customer_name
||
''
,
template_name
:
this
.
formData
.
template_name
||
this
.
formData
.
customer_name
,
customer_name_xy
:
customer_coords
||
[],
fields
:
fields
fields
:
fields
,
table_columns
:
table_columns
};
try
{
...
...
Please
register
or
sign in
to post a comment