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-20 11:37:55 +0700
Browse Files
Options
Browse Files
Tag
Download
Email Patches
Plain Diff
Commit
ad395fb924613693a51da319f6aaa7e49d543fb1
ad395fb9
1 parent
6e8d2e23
resize box
Show whitespace changes
Inline
Side-by-side
Showing
2 changed files
with
142 additions
and
207 deletions
public/css/ocr.css
resources/views/ocr/index.blade.php
public/css/ocr.css
View file @
ad395fb
...
...
@@ -7,7 +7,7 @@ body {
display
:
flex
;
gap
:
20px
;
padding
:
20px
;
}
.
lef
t-panel
{
.
righ
t-panel
{
width
:
500px
;
background
:
#fff
;
padding
:
15px
;
border-radius
:
8px
;
box-shadow
:
0
0
5px
rgba
(
0
,
0
,
0
,
0.1
);
}
...
...
@@ -27,7 +27,7 @@ body {
border-radius
:
4px
;
}
.
righ
t-panel
{
.
lef
t-panel
{
flex
:
1
;
position
:
relative
;
background
:
#eee
;
...
...
resources/views/ocr/index.blade.php
View file @
ad395fb
...
...
@@ -9,7 +9,7 @@
<body>
<meta
name=
"csrf-token"
content=
"{{ csrf_token() }}"
>
<div
id=
"app"
>
<div
class=
"
righ
t-panel"
>
<div
class=
"
lef
t-panel"
>
<div
class=
"pdf-container"
ref=
"pdfContainer"
@
mousedown=
"startSelect"
@
mousemove=
"onSelect"
...
...
@@ -33,6 +33,7 @@
v-if=
"!item.isDeleted"
class=
"bbox"
:class=
"{ active: index === activeIndex }"
:data-index=
"index"
:data-field=
"item.field"
:style=
"getBoxStyle(item, index)"
@
click=
"onBoxClick(index)"
>
...
...
@@ -94,13 +95,12 @@
</div>
</div>
<div
class=
"
lef
t-panel"
>
<div
class=
"
righ
t-panel"
>
<div
v-for=
"field in fieldOptions"
:key=
"field.value"
class=
"form-group"
>
<label>
@{{ field.label }}
</label>
<input
v-model=
"formData[field.value]"
@
click=
"onInputClick(field.value)"
@
blur=
"removeAllFocus()"
>
</div>
<button
@
click=
"saveTemplate"
>
💾Save
</button>
...
...
@@ -141,7 +141,7 @@
//Thêm event listener để xóa focus khi click ra ngoài
document
.
addEventListener
(
'click'
,
(
e
)
=>
{
if
(
!
e
.
target
.
closest
(
'.
lef
t-panel'
)
&&
!
e
.
target
.
closest
(
'.
righ
t-panel'
)
&&
!
e
.
target
.
closest
(
'.bbox'
)
&&
!
e
.
target
.
closest
(
'select'
)
)
{
...
...
@@ -169,7 +169,6 @@
document
.
addEventListener
(
"mousemove"
,
this
.
onResizing
);
document
.
addEventListener
(
"mouseup"
,
this
.
stopResize
);
},
onResizing
(
e
)
{
if
(
!
this
.
resizing
)
return
;
...
...
@@ -197,129 +196,70 @@
x2
+=
dx
;
}
// giữ không cho x1 > x2, y1 > y2
if
(
x1
<
x2
&&
y1
<
y2
)
{
this
.
$set
(
this
.
ocrData
[
index
],
"bbox"
,
[
x1
,
y1
,
x2
,
y2
]);
const
scaleX
=
this
.
$refs
.
pdfImage
.
clientWidth
/
this
.
imageWidth
;
const
scaleY
=
this
.
$refs
.
pdfImage
.
clientHeight
/
this
.
imageHeight
;
const
newBbox
=
[
Math
.
round
(
x1
),
Math
.
round
(
y1
),
Math
.
round
(
x2
),
Math
.
round
(
y2
)
];
if
(
this
.
ocrData
[
index
].
isManual
)
{
this
.
$set
(
this
.
ocrData
[
index
],
"bbox"
,
newBbox
);
}
else
{
this
.
selectBox
=
{
...
this
.
selectBox
,
x
:
Math
.
round
(
x1
*
scaleX
),
y
:
Math
.
round
(
y1
*
scaleY
),
width
:
Math
.
round
((
x2
-
x1
)
*
scaleX
),
height
:
Math
.
round
((
y2
-
y1
)
*
scaleY
),
show
:
true
,
};
this
.
resizing
.
newBbox
=
newBbox
;
}
}
},
},
stopResize
()
{
document
.
removeEventListener
(
"mousemove"
,
this
.
onResizing
);
document
.
removeEventListener
(
"mouseup"
,
this
.
stopResize
);
this
.
resizing
=
null
;
},
// Map field cho box (không set active, chỉ dùng để load data từ DB)
mapFieldToBox
(
index
,
fieldName
,
text
=
null
)
{
if
(
index
==
null
)
return
;
// Xóa tất cả box có fieldName trùng lặp, chỉ giữ lại box hiện tại
this
.
ocrData
=
this
.
ocrData
.
filter
((
box
,
i
)
=>
{
if
(
i
===
index
)
return
true
;
if
(
box
.
field
===
fieldName
)
{
return
false
;
}
return
true
;
});
// Cập nhật lại index sau khi filter
const
newIndex
=
this
.
ocrData
.
findIndex
(
box
=>
box
.
bbox
===
this
.
ocrData
[
index
]?.
bbox
);
if
(
newIndex
===
-
1
)
return
;
if
(
!
this
.
resizing
)
return
;
const
{
index
,
newBbox
}
=
this
.
resizing
;
const
targetBox
=
this
.
ocrData
[
index
];
if
(
targetBox
)
{
if
(
!
targetBox
.
isManual
&&
newBbox
)
{
const
newBox
=
{
...
targetBox
,
bbox
:
newBbox
,
isManual
:
true
,
isDeleted
:
false
,
showDelete
:
true
,
text
:
""
,
};
this
.
$set
(
this
.
ocrData
[
index
],
"isDeleted"
,
true
);
this
.
ocrData
.
push
(
newBox
);
this
.
selectingIndex
=
this
.
ocrData
.
length
-
1
;
this
.
updateHiddenBorders
(
newBox
.
bbox
);
// Nếu box này từng gán field khác thì bỏ reset flag và tọa độ liên quan.
const
prev
=
this
.
ocrData
[
newIndex
].
field
;
if
(
prev
&&
prev
!==
fieldName
)
{
if
(
prev
===
'customer_name'
)
{
this
.
hasCustomerNameXY
=
false
;
this
.
customer_name_xy
=
''
;
}
else
if
(
targetBox
.
isManual
)
{
this
.
updateHiddenBorders
(
targetBox
.
bbox
);
}
this
.
ocrData
[
newIndex
].
field
=
null
;
this
.
ocrData
[
newIndex
].
field_xy
=
null
;
}
const
bbox
=
this
.
ocrData
[
newIndex
].
bbox
;
const
x1
=
bbox
[
0
];
const
y1
=
bbox
[
1
];
const
w
=
bbox
[
2
];
const
h
=
bbox
[
3
];
const
xyStr
=
`
${
x1
}
,
${
y1
}
,
${
w
}
,
${
h
}
`
;
this
.
ocrData
[
newIndex
].
field
=
fieldName
;
this
.
ocrData
[
newIndex
].
field_xy
=
xyStr
;
this
.
formData
[
fieldName
]
=
(
text
!==
null
?
text
:
(
this
.
ocrData
[
newIndex
].
text
||
''
)).
trim
();
if
(
fieldName
===
'customer_name'
)
{
this
.
hasCustomerNameXY
=
true
;
this
.
customer_name_xy
=
xyStr
;
}
this
.
resizing
=
null
;
this
.
selectBox
.
show
=
false
;
},
async
saveTemplate
()
{
let
customer_name
=
null
;
let
customer_coords
=
null
;
let
fields
=
[];
if
(
this
.
manualBoxData
.
customer_name
)
{
// Lấy từ manualBoxData nếu có
customer_name
=
this
.
manualBoxData
.
customer_name
.
text
;
customer_coords
=
this
.
manualBoxData
.
customer_name
.
coords
.
join
(
','
);
fields
=
this
.
manualBoxData
;
console
.
log
(
'Using manualBoxData for customer_name:'
,
customer_name
,
customer_coords
);
}
else
{
const
found
=
this
.
ocrData
.
find
(
item
=>
item
.
field
===
'customer_name'
);
if
(
found
)
{
customer_name
=
found
.
text
;
customer_coords
=
found
.
field_xy
;
}
const
fieldsByName
=
{};
this
.
ocrData
.
forEach
(
box
=>
{
if
(
box
.
field
&&
!
box
.
isDeleted
)
{
fieldsByName
[
box
.
field
]
=
{
text
:
box
.
field
,
coords
:
box
.
field_xy
||
''
};
}
});
fields
=
(
fieldsByName
);
console
.
log
(
'Using ocrData for customer_name:'
,
customer_name
,
customer_coords
);
}
if
(
!
customer_coords
||
!
customer_name
)
{
alert
(
"Bạn phải map customer_name (quét/select) trước khi lưu."
);
return
;
updateHiddenBorders
(
manualBox
)
{
this
.
ocrData
.
forEach
(
item
=>
{
if
(
!
item
.
isManual
)
{
item
.
hideBorder
=
this
.
isBoxInside
(
item
.
bbox
,
manualBox
);
}
const
payload
=
{
customer_name_text
:
customer_name
||
''
,
template_name
:
this
.
formData
.
template_name
||
this
.
formData
.
customer_name
,
customer_name_xy
:
customer_coords
||
[],
fields
:
fields
};
try
{
const
res
=
await
fetch
(
'/ocr/save-template'
,
{
method
:
'POST'
,
headers
:
{
'Content-Type'
:
'application/json'
,
'X-CSRF-TOKEN'
:
document
.
querySelector
(
'meta[name="csrf-token"]'
).
getAttribute
(
'content'
)
},
body
:
JSON
.
stringify
(
payload
)
});
const
json
=
await
res
.
json
();
if
(
json
.
success
)
{
alert
(
json
.
message
);
}
else
{
alert
(
'Save failed'
);
}
}
catch
(
err
)
{
console
.
error
(
err
);
alert
(
'Save error'
);
}
},
deleteBox
(
index
)
{
const
item
=
this
.
ocrData
[
index
];
if
(
item
.
isManual
)
{
...
...
@@ -345,30 +285,6 @@
this
.
selectingIndex
=
null
;
}
},
async
loadOCRData
()
{
try
{
const
res
=
await
fetch
(
`/ocr/data-list`
);
const
data
=
await
res
.
json
();
if
(
data
.
error
)
{
console
.
error
(
'Error loading data:'
,
data
.
error
);
return
;
}
this
.
ocrData
=
data
.
ocrData
;
this
.
pdfImageUrl
=
data
.
pdfImageUrl
;
this
.
fieldOptions
=
data
.
fieldOptions
;
this
.
dataMapping
=
data
.
dataMapping
;
this
.
is_template
=
data
.
is_template
;
}
catch
(
error
)
{
console
.
error
(
'Error in loadOCRData:'
,
error
);
}
},
// Xử lý data sau khi image đã load
processLoadedData
()
{
this
.
autoMapFieldsFromFormData
();
...
...
@@ -377,7 +293,6 @@
this
.
$forceUpdate
();
});
},
autoMapFieldsFromFormData
()
{
this
.
manualBoxData
=
{};
if
(
this
.
is_template
)
{
...
...
@@ -417,7 +332,6 @@
});
},
onImageLoad
()
{
const
img
=
this
.
$refs
.
pdfImage
;
this
.
imageWidth
=
img
.
naturalWidth
;
...
...
@@ -484,13 +398,7 @@
}
if
(
coords
)
{
if
(
isFromDB
)
{
// Tạo box manual từ DB (không có nút xóa)
this
.
createManualBoxFromDB
(
field
,
coords
,
text
);
}
else
{
// Hiển thị lại box manual đã quét chọn (có nút xóa)
this
.
showManualBox
(
field
,
coords
,
text
);
}
}
// Tìm lại index của box để set active
...
...
@@ -512,7 +420,6 @@
this
.
activeIndex
=
null
;
}
},
scrollToBox
(
index
)
{
if
(
!
this
.
$refs
.
pdfContainer
||
index
<
0
||
index
>=
this
.
ocrData
.
length
)
return
;
...
...
@@ -547,7 +454,6 @@
behavior
:
'smooth'
});
},
// Xử lý khi click vào input
onInputClick
(
fieldName
)
{
// Kiểm tra xem field này có data không
...
...
@@ -557,30 +463,26 @@
this
.
highlightField
(
fieldName
);
}
},
// Xóa tất cả focus
removeAllFocus
()
{
// Reset active index (chỉ mất màu xanh, không xóa box)
this
.
activeIndex
=
null
;
this
.
selectingIndex
=
null
;
// Ẩn hoàn toàn các box manual được tạo từ DB (không phải quét chọn thủ công)
// Đảm bảo tất cả box OCR đều hiển thị (chỉ ẩn border khi cần thiết)
this
.
ocrData
.
forEach
(
item
=>
{
if
(
item
.
isManual
&&
!
item
.
showDelete
)
{
// Box manual từ DB (không có nút xóa) - ẩn hoàn toàn
// if (!item.isManual && item.hideBorder) {
// item.hideBorder = true;
// }
if
(
item
.
isManual
)
{
if
(
!
item
.
showDelete
)
{
item
.
isDeleted
=
true
;
}
else
{
item
.
hideBorder
=
false
;
}
});
// Đảm bảo tất cả box OCR đều hiển thị (chỉ ẩn border khi cần thiết)
this
.
ocrData
.
forEach
(
item
=>
{
if
(
!
item
.
isManual
&&
item
.
hideBorder
)
{
// Chỉ ẩn border cho box OCR nằm trong vùng manual
item
.
hideBorder
=
true
;
// Hiển thị lại border cho manual box
}
});
},
// Xử lý khi click vào box
onBoxClick
(
index
)
{
const
item
=
this
.
ocrData
[
index
];
...
...
@@ -631,7 +533,6 @@
this
.
selectBox
.
showDropdown
=
false
;
this
.
manualField
=
""
;
},
onSelect
(
e
)
{
if
(
!
this
.
isSelecting
)
return
;
const
rect
=
this
.
$refs
.
pdfContainer
.
getBoundingClientRect
();
...
...
@@ -642,7 +543,6 @@
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
;
...
...
@@ -702,12 +602,14 @@
const
item
=
this
.
ocrData
[
this
.
selectingIndex
];
if
(
!
item
)
return
;
console
.
log
(
`Applying mapping for index:`
,
item
);
this
.
ocrData
.
forEach
((
box
,
i
)
=>
{
if
(
i
!==
this
.
selectingIndex
&&
box
.
field
===
item
.
field
)
{
box
.
field
=
''
;
}
});
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
||
""
;
...
...
@@ -742,7 +644,7 @@
const
newBbox
=
this
.
ocrData
[
manualIndex
].
bbox
;
console
.
log
(
`manual for field "
${
this
.
manualField
}
" at index
${
manualIndex
}
with bbox:`
,
newBbox
);
//
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
)
{
box
.
field
=
''
;
...
...
@@ -782,6 +684,8 @@
const
finalText
=
combinedText
.
join
(
" "
);
this
.
ocrData
[
manualIndex
].
field
=
this
.
manualField
;
console
.
log
(
'123'
,
this
.
ocrData
[
manualIndex
])
this
.
formData
[
this
.
manualField
]
=
finalText
.
trim
();
this
.
manualBoxData
[
this
.
manualField
]
=
{
coords
:
newBbox
,
...
...
@@ -800,8 +704,8 @@
this
.
selectBox
.
showDropdown
=
false
;
this
.
manualField
=
""
;
this
.
manualIndex
=
null
;
},
},
isBoxInside
(
inner
,
outer
)
{
// inner: bbox của OCR item [x1, y1, x2, y2]
// outer: bbox của vùng manual [x1, y1, x2, y2]
...
...
@@ -821,13 +725,10 @@
inner
[
3
]
<
outer
[
1
]
||
// inner.bottom < outer.top → hoàn toàn phía trên
inner
[
1
]
>
outer
[
3
]
// inner.top > outer.bottom → hoàn toàn phía dưới
);
// console.log(`isBoxInside: inner=${inner}, outer=${outer}, isFullyInside=${isFullyInside}, isOverlapping=${isOverlapping}`);
// Trả về true nếu box OCR nằm hoàn toàn trong hoặc giao nhau đáng kể
return
isFullyInside
||
isOverlapping
;
},
getPartialText
(
text
,
bbox
,
selectBbox
)
{
const
[
x1
,
y1
,
x2
,
y2
]
=
bbox
;
const
[
sx1
,
sy1
,
sx2
,
sy2
]
=
selectBbox
;
...
...
@@ -856,8 +757,6 @@
zIndex
:
9999
};
},
// Tạo box manual từ tọa độ trong DB
createManualBoxFromDB
(
fieldName
,
coordinates
,
text
)
{
if
(
!
this
.
imageWidth
||
!
this
.
imageHeight
)
{
console
.
log
(
'Cannot create manual box: Image not loaded'
);
...
...
@@ -888,7 +787,7 @@
bbox
:
coords
,
field
:
fieldName
,
isManual
:
true
,
showDelete
:
fals
e
,
showDelete
:
tru
e
,
isDeleted
:
false
,
hideBorder
:
true
};
...
...
@@ -901,55 +800,91 @@
console
.
warn
(
'Invalid coordinates for manual box:'
,
coords
);
}
},
async
loadOCRData
()
{
// Hiển thị lại box manual đã quét chọn (có nút xóa)
showManualBox
(
fieldName
,
coordinates
,
text
)
{
if
(
!
this
.
imageWidth
||
!
this
.
imageHeight
)
{
console
.
log
(
'Cannot show manual box: Image not loaded'
);
return
;
}
try
{
const
res
=
await
fetch
(
`/ocr/data-list`
);
const
data
=
await
res
.
json
();
// Parse coordinates
let
coords
;
if
(
typeof
coordinates
===
'string'
)
{
coords
=
coordinates
.
split
(
','
).
map
(
Number
);
}
else
if
(
Array
.
isArray
(
coordinates
))
{
coords
=
coordinates
;
}
else
{
console
.
error
(
'Invalid coordinates format:'
,
coordinates
);
if
(
data
.
error
)
{
console
.
error
(
'Error loading data:'
,
data
.
error
);
return
;
}
const
[
x1
,
y1
,
x2
,
y2
]
=
coords
;
this
.
ocrData
=
data
.
ocrData
;
this
.
pdfImageUrl
=
data
.
pdfImageUrl
;
this
.
fieldOptions
=
data
.
fieldOptions
;
this
.
dataMapping
=
data
.
dataMapping
;
this
.
is_template
=
data
.
is_template
;
console
.
log
(
'Loaded OCR data:'
,
this
.
ocrData
);
// Kiểm tra tọa độ có hợp lệ không
if
(
x1
>=
0
&&
y1
>=
0
&&
x2
>
x1
&&
y2
>
y1
&&
x2
<=
this
.
imageWidth
&&
y2
<=
this
.
imageHeight
)
{
}
catch
(
error
)
{
console
.
error
(
'Error in loadOCRData:'
,
error
);
}
},
async
saveTemplate
()
{
// Xóa box cũ có cùng fieldName trước khi hiển thị lại
this
.
ocrData
=
this
.
ocrData
.
filter
(
box
=>
!
(
box
.
field
===
fieldName
&&
box
.
isManual
));
let
customer_name
=
null
;
let
customer_coords
=
null
;
let
fields
=
[];
if
(
this
.
manualBoxData
.
customer_name
)
{
// Lấy từ manualBoxData nếu có
customer_name
=
this
.
manualBoxData
.
customer_name
.
text
;
customer_coords
=
this
.
manualBoxData
.
customer_name
.
coords
.
join
(
','
);
fields
=
this
.
manualBoxData
;
console
.
log
(
'Using manualBoxData for customer_name:'
,
customer_name
,
customer_coords
);
}
else
{
const
found
=
this
.
ocrData
.
find
(
item
=>
item
.
field
===
'customer_name'
);
if
(
found
)
{
customer_name
=
found
.
text
;
customer_coords
=
found
.
field_xy
;
}
// Kiểm tra xem đây có phải box quét chọn hay box OCR
const
isFromOCR
=
this
.
manualBoxData
[
fieldName
]
&&
this
.
manualBoxData
[
fieldName
].
isFromOCR
;
const
fieldsByName
=
{};
this
.
ocrData
.
forEach
(
box
=>
{
if
(
box
.
field
&&
!
box
.
isDeleted
)
{
fieldsByName
[
box
.
field
]
=
{
text
:
box
.
field
,
coords
:
box
.
field_xy
||
''
};
}
});
fields
=
(
fieldsByName
);
console
.
log
(
'Using ocrData for customer_name:'
,
customer_name
,
customer_coords
);
}
// Hiển thị lại box manual (có nút xóa nếu là quét chọn, không có nút xóa nếu là OCR)
const
manualBox
=
{
text
:
text
||
''
,
bbox
:
coords
,
field
:
fieldName
,
isManual
:
true
,
showDelete
:
!
isFromOCR
,
// Chỉ hiển thị nút xóa nếu KHÔNG phải từ OCR
isDeleted
:
false
,
hideBorder
:
false
if
(
!
customer_coords
||
!
customer_name
)
{
alert
(
"Bạn phải map customer_name (quét/select) trước khi lưu."
);
return
;
}
const
payload
=
{
customer_name_text
:
customer_name
||
''
,
template_name
:
this
.
formData
.
template_name
||
this
.
formData
.
customer_name
,
customer_name_xy
:
customer_coords
||
[],
fields
:
fields
};
this
.
ocrData
.
push
(
manualBox
);
// Force re-render
this
.
$forceUpdate
();
try
{
const
res
=
await
fetch
(
'/ocr/save-template'
,
{
method
:
'POST'
,
headers
:
{
'Content-Type'
:
'application/json'
,
'X-CSRF-TOKEN'
:
document
.
querySelector
(
'meta[name="csrf-token"]'
).
getAttribute
(
'content'
)
},
body
:
JSON
.
stringify
(
payload
)
});
const
json
=
await
res
.
json
();
if
(
json
.
success
)
{
alert
(
json
.
message
);
}
else
{
console
.
warn
(
'Invalid coordinates for manual box:'
,
coords
);
alert
(
'Save failed'
);
}
}
catch
(
err
)
{
console
.
error
(
err
);
alert
(
'Save error'
);
}
},
}
});
...
...
Please
register
or
sign in
to post a comment