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-08-12 10:19:53 +0700
Browse Files
Options
Browse Files
Tag
Download
Email Patches
Plain Diff
Commit
a8852132556facc94b8fce7a817dcdd79228e36b
a8852132
1 parent
55c901a1
demo
Hide whitespace changes
Inline
Side-by-side
Showing
4 changed files
with
1104 additions
and
0 deletions
app/Services/OCR/read_pdf.py
public/image/data_picking_detail_1754967679.jpg
public/image/data_picking_detail_1754967679.json
select.html
app/Services/OCR/read_pdf.py
0 → 100644
View file @
a885213
from
paddleocr
import
PaddleOCR
from
pdf2image
import
convert_from_path
import
os
import
time
import
numpy
as
np
import
json
# ==== Config ====
pdf_path
=
"D:/Learning_Tien/OCR/PaddleOCR/pdf/data_picking_detail.pdf"
output_folder
=
"D:/Learning_Tien/OCR/ocr-mapping/public/image"
os
.
makedirs
(
output_folder
,
exist_ok
=
True
)
pdf_name
=
"data_picking_detail"
timestamp
=
int
(
time
.
time
())
img_base_name
=
f
"{pdf_name}_{timestamp}"
# ==== OCR Init ====
ocr
=
PaddleOCR
(
use_doc_orientation_classify
=
False
,
use_doc_unwarping
=
False
,
use_textline_orientation
=
False
)
# ==== PDF to Image ====
pages
=
convert_from_path
(
pdf_path
,
first_page
=
1
,
last_page
=
1
)
image_path
=
os
.
path
.
join
(
output_folder
,
f
"{img_base_name}.jpg"
)
pages
[
0
]
.
save
(
image_path
,
"JPEG"
)
# ==== Run OCR ====
image_np
=
np
.
array
(
pages
[
0
])
results
=
ocr
.
predict
(
image_np
)
# ==== Convert polygon to bbox ====
def
poly_to_bbox
(
poly
):
xs
=
[
p
[
0
]
for
p
in
poly
]
ys
=
[
p
[
1
]
for
p
in
poly
]
return
[
int
(
min
(
xs
)),
int
(
min
(
ys
)),
int
(
max
(
xs
)),
int
(
max
(
ys
))]
# ==== Build ocrData ====
ocr_data_list
=
[]
for
res
in
results
:
for
text
,
poly
in
zip
(
res
[
'rec_texts'
],
res
[
'rec_polys'
]):
bbox
=
poly_to_bbox
(
poly
)
ocr_data_list
.
append
({
"text"
:
text
,
"bbox"
:
bbox
,
"field"
:
""
,
"hideBorder"
:
False
})
# ==== Save JSON ====
json_path
=
os
.
path
.
join
(
output_folder
,
f
"{pdf_name}_{timestamp}.json"
)
with
open
(
json_path
,
"w"
,
encoding
=
"utf-8"
)
as
f
:
json
.
dump
(
ocr_data_list
,
f
,
ensure_ascii
=
False
,
indent
=
2
)
print
(
f
"Saved OCR data JSON to: {json_path}"
)
public/image/data_picking_detail_1754967679.jpg
0 → 100644
View file @
a885213
175 KB
public/image/data_picking_detail_1754967679.json
0 → 100644
View file @
a885213
[
{
"text"
:
"出庫指示書"
,
"bbox"
:
[
65
,
73
,
449
,
128
],
"field"
:
""
,
"hideBorder"
:
false
},
{
"text"
:
"出庫指示No."
,
"bbox"
:
[
1303
,
76
,
1472
,
111
],
"field"
:
""
,
"hideBorder"
:
false
},
{
"text"
:
"391189"
,
"bbox"
:
[
1498
,
78
,
1604
,
110
],
"field"
:
""
,
"hideBorder"
:
false
},
{
"text"
:
"2025/06/24"
,
"bbox"
:
[
952
,
94
,
1106
,
121
],
"field"
:
""
,
"hideBorder"
:
false
},
{
"text"
:
"18:57迄"
,
"bbox"
:
[
1139
,
89
,
1250
,
124
],
"field"
:
""
,
"hideBorder"
:
false
},
{
"text"
:
"PAGE1/1"
,
"bbox"
:
[
1473
,
121
,
1594
,
153
],
"field"
:
""
,
"hideBorder"
:
false
},
{
"text"
:
"運送形態"
,
"bbox"
:
[
83
,
145
,
239
,
184
],
"field"
:
""
,
"hideBorder"
:
false
},
{
"text"
:
"30西濃運輸"
,
"bbox"
:
[
234
,
144
,
485
,
185
],
"field"
:
""
,
"hideBorder"
:
false
},
{
"text"
:
"得意先"
,
"bbox"
:
[
84
,
206
,
202
,
246
],
"field"
:
""
,
"hideBorder"
:
false
},
{
"text"
:
"42031(株)フジカケ"
,
"bbox"
:
[
243
,
206
,
556
,
244
],
"field"
:
""
,
"hideBorder"
:
false
},
{
"text"
:
"ミタケ"
,
"bbox"
:
[
536
,
205
,
707
,
247
],
"field"
:
""
,
"hideBorder"
:
false
},
{
"text"
:
"住所"
,
"bbox"
:
[
84
,
266
,
179
,
298
],
"field"
:
""
,
"hideBorder"
:
false
},
{
"text"
:
"〒5050100岐阜県可児郡御嵩町中2411-7"
,
"bbox"
:
[
207
,
267
,
834
,
297
],
"field"
:
""
,
"hideBorder"
:
false
},
{
"text"
:
"電話番号"
,
"bbox"
:
[
88
,
308
,
214
,
340
],
"field"
:
""
,
"hideBorder"
:
false
},
{
"text"
:
"0574673181"
,
"bbox"
:
[
215
,
310
,
382
,
338
],
"field"
:
""
,
"hideBorder"
:
false
},
{
"text"
:
"出庫者"
,
"bbox"
:
[
925
,
331
,
1008
,
367
],
"field"
:
""
,
"hideBorder"
:
false
},
{
"text"
:
"検品者"
,
"bbox"
:
[
1176
,
330
,
1261
,
366
],
"field"
:
""
,
"hideBorder"
:
false
},
{
"text"
:
"包者"
,
"bbox"
:
[
1431
,
331
,
1516
,
367
],
"field"
:
""
,
"hideBorder"
:
false
},
{
"text"
:
"担当者"
,
"bbox"
:
[
86
,
344
,
182
,
381
],
"field"
:
""
,
"hideBorder"
:
false
},
{
"text"
:
"NAS00240渡邊雅章"
,
"bbox"
:
[
239
,
343
,
518
,
380
],
"field"
:
""
,
"hideBorder"
:
false
},
{
"text"
:
"摘要"
,
"bbox"
:
[
83
,
386
,
183
,
429
],
"field"
:
""
,
"hideBorder"
:
false
},
{
"text"
:
"棚番"
,
"bbox"
:
[
34
,
515
,
97
,
554
],
"field"
:
""
,
"hideBorder"
:
false
},
{
"text"
:
"品"
,
"bbox"
:
[
345
,
519
,
378
,
552
],
"field"
:
""
,
"hideBorder"
:
false
},
{
"text"
:
"名"
,
"bbox"
:
[
423
,
519
,
456
,
551
],
"field"
:
""
,
"hideBorder"
:
false
},
{
"text"
:
"規"
,
"bbox"
:
[
870
,
517
,
909
,
552
],
"field"
:
""
,
"hideBorder"
:
false
},
{
"text"
:
"格"
,
"bbox"
:
[
948
,
517
,
986
,
552
],
"field"
:
""
,
"hideBorder"
:
false
},
{
"text"
:
"数量"
,
"bbox"
:
[
1110
,
516
,
1174
,
554
],
"field"
:
""
,
"hideBorder"
:
false
},
{
"text"
:
"受注番号"
,
"bbox"
:
[
1393
,
519
,
1505
,
551
],
"field"
:
""
,
"hideBorder"
:
false
},
{
"text"
:
"B0504"
,
"bbox"
:
[
39
,
567
,
124
,
600
],
"field"
:
""
,
"hideBorder"
:
false
},
{
"text"
:
"ダービー"
,
"bbox"
:
[
297
,
565
,
419
,
600
],
"field"
:
""
,
"hideBorder"
:
false
},
{
"text"
:
"斜ニッパー"
,
"bbox"
:
[
444
,
564
,
600
,
602
],
"field"
:
""
,
"hideBorder"
:
false
},
{
"text"
:
"#30 150MM"
,
"bbox"
:
[
861
,
568
,
1014
,
602
],
"field"
:
""
,
"hideBorder"
:
false
},
{
"text"
:
"2"
,
"bbox"
:
[
1146
,
568
,
1176
,
606
],
"field"
:
""
,
"hideBorder"
:
false
},
{
"text"
:
"("
,
"bbox"
:
[
1232
,
566
,
1256
,
601
],
"field"
:
""
,
"hideBorder"
:
false
},
{
"text"
:
")"
,
"bbox"
:
[
1363
,
567
,
1385
,
600
],
"field"
:
""
,
"hideBorder"
:
false
},
{
"text"
:
"250430015"
,
"bbox"
:
[
1419
,
567
,
1562
,
598
],
"field"
:
""
,
"hideBorder"
:
false
},
{
"text"
:
"4562144610607"
,
"bbox"
:
[
295
,
611
,
474
,
638
],
"field"
:
""
,
"hideBorder"
:
false
},
{
"text"
:
"3220060"
,
"bbox"
:
[
567
,
610
,
668
,
638
],
"field"
:
""
,
"hideBorder"
:
false
},
{
"text"
:
"C3101"
,
"bbox"
:
[
40
,
654
,
121
,
687
],
"field"
:
""
,
"hideBorder"
:
false
},
{
"text"
:
"タジマ"
,
"bbox"
:
[
296
,
653
,
389
,
685
],
"field"
:
""
,
"hideBorder"
:
false
},
{
"text"
:
"スーパー墨汁"
,
"bbox"
:
[
414
,
654
,
599
,
685
],
"field"
:
""
,
"hideBorder"
:
false
},
{
"text"
:
"180ML PSB2-180"
,
"bbox"
:
[
787
,
656
,
1013
,
687
],
"field"
:
""
,
"hideBorder"
:
false
},
{
"text"
:
"3"
,
"bbox"
:
[
1145
,
655
,
1176
,
693
],
"field"
:
""
,
"hideBorder"
:
false
},
{
"text"
:
"("
,
"bbox"
:
[
1232
,
653
,
1257
,
687
],
"field"
:
""
,
"hideBorder"
:
false
},
{
"text"
:
")"
,
"bbox"
:
[
1362
,
653
,
1386
,
686
],
"field"
:
""
,
"hideBorder"
:
false
},
{
"text"
:
"250430015"
,
"bbox"
:
[
1420
,
655
,
1563
,
686
],
"field"
:
""
,
"hideBorder"
:
false
},
{
"text"
:
"4975364054074"
,
"bbox"
:
[
295
,
698
,
474
,
725
],
"field"
:
""
,
"hideBorder"
:
false
},
{
"text"
:
"550207"
,
"bbox"
:
[
567
,
697
,
655
,
726
],
"field"
:
""
,
"hideBorder"
:
false
},
{
"text"
:
"C3101"
,
"bbox"
:
[
40
,
741
,
122
,
774
],
"field"
:
""
,
"hideBorder"
:
false
},
{
"text"
:
"タジマ"
,
"bbox"
:
[
295
,
738
,
390
,
774
],
"field"
:
""
,
"hideBorder"
:
false
},
{
"text"
:
"雨の日墨汁"
,
"bbox"
:
[
414
,
740
,
570
,
774
],
"field"
:
""
,
"hideBorder"
:
false
},
{
"text"
:
"PSB3-180"
,
"bbox"
:
[
879
,
743
,
1013
,
774
],
"field"
:
""
,
"hideBorder"
:
false
},
{
"text"
:
"2"
,
"bbox"
:
[
1146
,
742
,
1176
,
780
],
"field"
:
""
,
"hideBorder"
:
false
},
{
"text"
:
"("
,
"bbox"
:
[
1232
,
740
,
1257
,
774
],
"field"
:
""
,
"hideBorder"
:
false
},
{
"text"
:
")"
,
"bbox"
:
[
1361
,
740
,
1386
,
774
],
"field"
:
""
,
"hideBorder"
:
false
},
{
"text"
:
"250430015"
,
"bbox"
:
[
1419
,
741
,
1562
,
772
],
"field"
:
""
,
"hideBorder"
:
false
},
{
"text"
:
"49270501"
,
"bbox"
:
[
294
,
783
,
406
,
814
],
"field"
:
""
,
"hideBorder"
:
false
},
{
"text"
:
"548140"
,
"bbox"
:
[
567
,
783
,
655
,
812
],
"field"
:
""
,
"hideBorder"
:
false
},
{
"text"
:
"明细行数= 3"
,
"bbox"
:
[
882
,
822
,
1136
,
871
],
"field"
:
""
,
"hideBorder"
:
false
}
]
\ No newline at end of file
select.html
0 → 100644
View file @
a885213
<html
lang=
"en"
><head>
<meta
charset=
"UTF-8"
>
<title>
OCR Mapping with Manual Select Tool
</title>
<script
src=
"https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"
></script>
<style>
body
{
font-family
:
sans-serif
;
background
:
#f5f5f5
;
}
#app
{
display
:
flex
;
gap
:
20px
;
padding
:
20px
;
}
.left-panel
{
width
:
500px
;
background
:
#fff
;
padding
:
15px
;
border-radius
:
8px
;
box-shadow
:
0
0
5px
rgba
(
0
,
0
,
0
,
0.1
);
}
.form-group
{
margin-bottom
:
15px
;
}
.form-group
label
{
font-weight
:
bold
;
display
:
block
;
margin-bottom
:
5px
;
}
.form-group
input
{
width
:
100%
;
padding
:
6px
;
border
:
1px
solid
#ccc
;
border-radius
:
4px
;
}
.right-panel
{
flex
:
1
;
position
:
relative
;
background
:
#eee
;
border-radius
:
8px
;
overflow
:
hidden
;
user-select
:
none
;
}
.pdf-container
{
position
:
relative
;
display
:
inline-block
;
}
.bbox
{
position
:
absolute
;
border
:
2px
solid
#ff5252
;
/*background-color: rgba(255, 82, 82, 0.2);*/
cursor
:
pointer
;
}
.bbox.active
{
/*border-color: #2196F3;*/
background-color
:
rgb
(
33
243
132
/
30%
);
}
select
{
position
:
absolute
;
z-index
:
10
;
background
:
#fff
;
border
:
1px
solid
#ccc
;
}
.select-box
{
position
:
absolute
;
/*border: 2px dashed #2196F3;*/
background-color
:
rgba
(
33
,
150
,
243
,
0.2
);
pointer-events
:
none
;
z-index
:
5
;
}
.delete-btn
{
position
:
absolute
;
bottom
:
-10px
;
right
:
-10px
;
background
:
#ff4d4d
;
color
:
#fff
;
border
:
none
;
border-radius
:
50%
;
cursor
:
pointer
;
font-size
:
14px
;
padding
:
3px
6px
;
z-index
:
20
;
}
</style>
</head>
<body>
<div
id=
"app"
>
<!-- Right: PDF viewer + select tool -->
<div
class=
"right-panel"
>
<div
class=
"pdf-container"
ref=
"pdfContainer"
@
mousedown=
"startSelect"
@
mousemove=
"onSelect"
@
mouseup=
"endSelect"
>
<img
ref=
"pdfImage"
:src=
"pdfImageUrl"
@
load=
"onImageLoad"
style=
"width: 100%; height: auto;pointer-events: none;"
/>
<!-- Vùng kéo chọn -->
<div
v-if=
"selectBox.show"
class=
"select-box"
:style=
"{ left: selectBox.x + 'px', top: selectBox.y + 'px', width: selectBox.width + 'px', height: selectBox.height + 'px' }"
></div>
<!-- Vẽ bbox OCR -->
<div
v-for=
"(item, index) in ocrData"
:key=
"index"
v-if=
"!item.isDeleted"
class=
"bbox"
:class=
"{ active: index === activeIndex }"
:data-field=
"item.field"
:style=
"getBoxStyle(item, index)"
@
click=
"selectingIndex = index"
>
<button
v-if=
"item.isManual && item.showDelete"
class=
"delete-btn"
@
click
.
stop=
"deleteBox(index)"
>
🗑
</button>
</div>
<!-- Dropdown OCR -->
<select
v-if=
"selectingIndex !== null"
:style=
"getSelectStyle(ocrData[selectingIndex])"
v-model=
"ocrData[selectingIndex].field"
@
change=
"applyMapping"
>
<option
disabled
value=
""
>
-- Chọn trường dữ liệu --
</option>
<option
v-for=
"field in fieldOptions"
:value=
"field.value"
>
{{ field.label }}
</option>
</select>
<!-- Dropdown thủ công -->
<select
v-if=
"selectBox.showDropdown"
:style=
"{ left: selectBox.x + 'px', top: (selectBox.y + selectBox.height) + 'px' }"
v-model=
"manualField"
@
change=
"applyManualMapping"
@
click
.
stop
>
<option
disabled
value=
""
>
-- Chọn trường dữ liệu --
</option>
<option
v-for=
"field in fieldOptions"
:value=
"field.value"
>
{{ field.label }}
</option>
</select>
</div>
</div>
<!-- Left: Form inputs -->
<div
class=
"left-panel"
>
<div
v-for=
"field in fieldOptions"
:key=
"field.value"
class=
"form-group"
>
<label>
{{ field.label }}
</label>
<input
v-model=
"formData[field.value]"
@
focus=
"highlightField(field.value)"
>
</div>
</div>
</div>
<script>
new
Vue
({
el
:
'#app'
,
data
()
{
return
{
pdfImageUrl
:
""
,
selectingIndex
:
null
,
isMappingManually
:
false
,
isSelecting
:
false
,
activeField
:
null
,
manualField
:
""
,
formData
:
{
export_date
:
""
,
order_code
:
""
,
customer
:
""
,
address
:
""
,
staff
:
""
},
fieldOptions
:
[
{
value
:
"export_date"
,
label
:
"Ngày xuất"
},
{
value
:
"order_code"
,
label
:
"Mã đơn hàng"
},
{
value
:
"customer"
,
label
:
"Khách hàng"
},
{
value
:
"address"
,
label
:
"Địa chỉ"
},
{
value
:
"staff"
,
label
:
"Nhân viên"
}
],
ocrData
:
[],
selectBox
:
{
show
:
false
,
showDropdown
:
false
,
x
:
0
,
y
:
0
,
width
:
0
,
height
:
0
,
startX
:
0
,
startY
:
0
},
manualIndex
:
null
}
},
mounted
()
{
this
.
pdfImageUrl
=
"/public/image/data_picking_detail_1754967679.jpg"
;
// ảnh xuất từ Python
this
.
initData
();
},
methods
:
{
deleteBox
(
index
)
{
const
item
=
this
.
ocrData
[
index
];
if
(
item
.
isManual
)
{
const
manualBbox
=
item
.
bbox
;
// Hiện lại border các box OCR gốc nằm trong vùng thủ công
this
.
ocrData
.
forEach
(
o
=>
{
if
(
!
o
.
isManual
&&
this
.
isBoxInside
(
o
.
bbox
,
manualBbox
))
{
o
.
hideBorder
=
false
;
}
});
// Đánh dấu xoá vùng thủ công
this
.
ocrData
[
index
].
isDeleted
=
true
;
this
.
ocrData
[
index
].
showDelete
=
false
;
// Reset trạng thái nếu đây là vùng đang chọn
if
(
this
.
manualIndex
===
index
)
{
this
.
isMappingManually
=
false
;
this
.
selectBox
.
show
=
false
;
this
.
selectBox
.
showDropdown
=
false
;
this
.
manualField
=
""
;
this
.
manualIndex
=
null
;
}
}
},
async
initData
()
{
await
this
.
loadOCRData
();
},
async
loadOCRData
()
{
const
res
=
await
fetch
(
"/public/image/data_picking_detail_1754967679.json"
);
this
.
ocrData
=
await
res
.
json
();
},
onImageLoad
()
{
const
img
=
this
.
$refs
.
pdfImage
;
this
.
imageWidth
=
img
.
naturalWidth
;
this
.
imageHeight
=
img
.
naturalHeight
;
},
getBoxStyle
(
item
)
{
if
(
!
this
.
imageWidth
||
!
this
.
imageHeight
||
!
this
.
$refs
.
pdfImage
)
return
{};
const
[
x1
,
y1
,
x2
,
y2
]
=
item
.
bbox
;
const
displayedWidth
=
this
.
$refs
.
pdfImage
.
clientWidth
;
const
displayedHeight
=
this
.
$refs
.
pdfImage
.
clientHeight
;
const
scaleX
=
displayedWidth
/
this
.
imageWidth
;
const
scaleY
=
displayedHeight
/
this
.
imageHeight
;
return
{
position
:
'absolute'
,
left
:
`
${
Math
.
round
(
x1
*
scaleX
)}
px`
,
top
:
`
${
Math
.
round
(
y1
*
scaleY
)}
px`
,
width
:
`
${
Math
.
round
((
x2
-
x1
)
*
scaleX
)}
px`
,
height
:
`
${
Math
.
round
((
y2
-
y1
)
*
scaleY
)}
px`
,
border
:
item
.
hideBorder
?
'none'
:
'2px solid '
+
(
this
.
activeField
===
item
.
field
?
'#199601'
:
'#ff5252'
),
//backgroundColor: item.hideBorder ? 'transparent' : (this.activeField === item.field ? 'rgba(33,150,243,0.3)' : 'rgba(255,82,82,0.2)'),
boxSizing
:
'border-box'
,
cursor
:
'pointer'
,
zIndex
:
item
.
isManual
?
30
:
10
};
},
highlightField
(
field
)
{
this
.
activeField
=
field
;
},
startSelect
(
e
)
{
if
(
this
.
isMappingManually
||
e
.
button
!==
0
)
return
;
this
.
isSelecting
=
true
;
const
rect
=
this
.
$refs
.
pdfContainer
.
getBoundingClientRect
();
this
.
selectBox
.
startX
=
e
.
clientX
-
rect
.
left
;
this
.
selectBox
.
startY
=
e
.
clientY
-
rect
.
top
;
this
.
selectBox
.
x
=
this
.
selectBox
.
startX
;
this
.
selectBox
.
y
=
this
.
selectBox
.
startY
;
this
.
selectBox
.
width
=
0
;
this
.
selectBox
.
height
=
0
;
this
.
selectBox
.
show
=
true
;
this
.
selectBox
.
showDropdown
=
false
;
this
.
manualField
=
""
;
},
onSelect
(
e
)
{
if
(
!
this
.
isSelecting
)
return
;
const
rect
=
this
.
$refs
.
pdfContainer
.
getBoundingClientRect
();
const
currentX
=
e
.
clientX
-
rect
.
left
;
const
currentY
=
e
.
clientY
-
rect
.
top
;
this
.
selectBox
.
x
=
Math
.
min
(
currentX
,
this
.
selectBox
.
startX
);
this
.
selectBox
.
y
=
Math
.
min
(
currentY
,
this
.
selectBox
.
startY
);
this
.
selectBox
.
width
=
Math
.
abs
(
currentX
-
this
.
selectBox
.
startX
);
this
.
selectBox
.
height
=
Math
.
abs
(
currentY
-
this
.
selectBox
.
startY
);
},
endSelect
(
e
)
{
if
(
!
this
.
isSelecting
)
return
;
this
.
isSelecting
=
false
;
if
(
this
.
selectBox
.
width
<
10
||
this
.
selectBox
.
height
<
10
)
{
this
.
selectBox
.
show
=
false
;
return
;
}
// displayed coords (như hiện tại, dùng để hiển thị select overlay)
const
dispX1
=
this
.
selectBox
.
x
;
const
dispY1
=
this
.
selectBox
.
y
;
const
dispX2
=
this
.
selectBox
.
x
+
this
.
selectBox
.
width
;
const
dispY2
=
this
.
selectBox
.
y
+
this
.
selectBox
.
height
;
// scale: displayed -> original
const
displayedWidth
=
this
.
$refs
.
pdfImage
.
clientWidth
;
const
displayedHeight
=
this
.
$refs
.
pdfImage
.
clientHeight
;
const
scaleX
=
this
.
imageWidth
/
displayedWidth
;
const
scaleY
=
this
.
imageHeight
/
displayedHeight
;
// bbox ở hệ gốc (original image pixels) — dùng để so sánh với ocrData và lưu vào ocrData
const
origBbox
=
[
Math
.
round
(
dispX1
*
scaleX
),
Math
.
round
(
dispY1
*
scaleY
),
Math
.
round
(
dispX2
*
scaleX
),
Math
.
round
(
dispY2
*
scaleY
)
];
// Ẩn border các box OCR gốc nằm giao nhau với vùng thủ công (dùng coords gốc)
this
.
ocrData
.
forEach
(
item
=>
{
if
(
!
item
.
isManual
&&
this
.
isBoxInside
(
item
.
bbox
,
origBbox
))
{
item
.
hideBorder
=
true
;
}
});
// Thêm box thủ công (lưu theo coords gốc)
this
.
ocrData
.
push
({
text
:
""
,
bbox
:
origBbox
,
field
:
""
,
isManual
:
true
,
showDelete
:
true
,
isDeleted
:
false
,
hideBorder
:
false
});
this
.
manualIndex
=
this
.
ocrData
.
length
-
1
;
this
.
isMappingManually
=
true
;
this
.
selectBox
.
showDropdown
=
true
;
e
.
stopPropagation
();
e
.
preventDefault
();
}
,
applyMapping
()
{
const
item
=
this
.
ocrData
[
this
.
selectingIndex
];
if
(
item
&&
item
.
isManual
)
{
this
.
manualIndex
=
this
.
selectingIndex
;
this
.
manualField
=
item
.
field
;
// đảm bảo sync field hiện tại
this
.
applyManualMapping
();
return
;
}
if
(
item
.
field
)
{
this
.
formData
[
item
.
field
]
=
item
.
text
;
this
.
activeField
=
item
.
field
;
}
this
.
selectingIndex
=
null
;
},
applyManualMapping
()
{
if
(
!
this
.
manualField
)
return
;
const
manualIndex
=
this
.
manualIndex
;
const
newBbox
=
this
.
ocrData
[
manualIndex
].
bbox
;
let
combinedText
=
[];
this
.
ocrData
.
forEach
(
item
=>
{
if
(
!
item
.
isManual
&&
this
.
isBoxInside
(
item
.
bbox
,
newBbox
)
&&
item
.
text
.
trim
())
{
const
partial
=
this
.
getPartialText
(
item
.
text
,
item
.
bbox
,
newBbox
);
if
(
partial
)
combinedText
.
push
(
partial
);
// combinedText.push(item.text.trim());
}
});
const
finalText
=
combinedText
.
join
(
" "
);
this
.
ocrData
[
manualIndex
].
field
=
this
.
manualField
;
this
.
formData
[
this
.
manualField
]
=
finalText
;
this
.
activeField
=
this
.
manualField
;
console
.
log
(
'manualField'
,
this
.
manualField
,
this
.
manualIndex
)
// Reset trạng thái chọn
this
.
isMappingManually
=
false
;
this
.
selectBox
.
show
=
false
;
this
.
selectBox
.
showDropdown
=
false
;
// this.manualField = "";
// this.manualIndex = null;
},
isBoxInside
(
inner
,
outer
)
{
return
!
(
inner
[
2
]
<
outer
[
0
]
||
// box bên trái vùng chọn
inner
[
0
]
>
outer
[
2
]
||
// box bên phải vùng chọn
inner
[
3
]
<
outer
[
1
]
||
// box phía trên vùng chọn
inner
[
1
]
>
outer
[
3
]
// box phía dưới vùng chọn
);
},
getPartialText
(
text
,
bbox
,
selectBbox
)
{
const
[
x1
,
y1
,
x2
,
y2
]
=
bbox
;
const
[
sx1
,
sy1
,
sx2
,
sy2
]
=
selectBbox
;
// Chiều rộng box OCR
const
boxWidth
=
x2
-
x1
;
// Vị trí start và end tương đối trong text
let
startRatio
=
Math
.
max
(
0
,
(
sx1
-
x1
)
/
boxWidth
);
let
endRatio
=
Math
.
min
(
1
,
(
sx2
-
x1
)
/
boxWidth
);
const
startIndex
=
Math
.
floor
(
startRatio
*
text
.
length
);
const
endIndex
=
Math
.
ceil
(
endRatio
*
text
.
length
);
return
text
.
substring
(
startIndex
,
endIndex
).
trim
();
},
getSelectStyle
(
item
)
{
if
(
!
this
.
imageWidth
)
return
{
position
:
'absolute'
};
const
[
x1
,
y1
,
x2
,
y2
]
=
item
.
bbox
;
const
displayedWidth
=
this
.
$refs
.
pdfImage
.
clientWidth
;
const
displayedHeight
=
this
.
$refs
.
pdfImage
.
clientHeight
;
const
scaleX
=
displayedWidth
/
this
.
imageWidth
;
const
scaleY
=
displayedHeight
/
this
.
imageHeight
;
return
{
position
:
'absolute'
,
left
:
`
${
Math
.
round
(
x1
*
scaleX
)}
px`
,
top
:
`
${
Math
.
round
(
y2
*
scaleY
)}
px`
,
zIndex
:
9999
};
}
}
});
</script>
</body></html>
Please
register
or
sign in
to post a comment