Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
P
projeto-recortador-pdf
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Pedro Bueno
projeto-recortador-pdf
Commits
fc0133eb
Commit
fc0133eb
authored
May 14, 2026
by
Pedro Bueno
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Ajuste nos arquivos
parent
996791ac
Show whitespace changes
Inline
Side-by-side
Showing
4 changed files
with
247 additions
and
63 deletions
+247
-63
app.py
app.py
+46
-28
style.css
static/style.css
+10
-12
index.html
templates/index.html
+191
-23
MP_-_Colecao_360_-_Matematica_1.pdf
uploads/MP_-_Colecao_360_-_Matematica_1.pdf
+0
-0
No files found.
app.py
View file @
fc0133eb
from
flask
import
Flask
,
render_template
,
request
,
send_file
,
redirect
import
fitz
,
os
,
zipfile
,
sqlite3
from
flask
import
Flask
,
render_template
,
request
,
send_file
import
io
import
fitz
# PyMuPDF
import
os
import
zipfile
from
PIL
import
Image
import
sqlite3
app
=
Flask
(
__name__
)
UPLOAD_FOLDER
=
'uploads'
os
.
makedirs
(
UPLOAD_FOLDER
,
exist_ok
=
True
)
# Configuração do Banco de Dados (CRUD)
def
init_db
():
conn
=
sqlite3
.
connect
(
'database.db'
)
cursor
=
conn
.
cursor
()
cursor
.
execute
(
'''CREATE TABLE IF NOT EXISTS configuracoes
conn
.
execute
(
'''CREATE TABLE IF NOT EXISTS configuracoes
(id INTEGER PRIMARY KEY AUTOINCREMENT, nome TEXT, coords TEXT)'''
)
conn
.
commit
()
conn
.
close
()
@app.route
(
'/'
)
...
...
@@ -22,42 +23,59 @@ def index():
conn
.
close
()
return
render_template
(
'index.html'
,
configs
=
configs
)
@app.route
(
'/salvar'
,
methods
=
[
'POST'
])
def
salvar_config
():
nome
=
request
.
form
[
'nome_config'
]
coords
=
request
.
form
[
'coords'
]
conn
=
sqlite3
.
connect
(
'database.db'
)
conn
.
execute
(
'INSERT INTO configuracoes (nome, coords) VALUES (?, ?)'
,
(
nome
,
coords
))
conn
.
commit
()
conn
.
close
()
return
redirect
(
'/'
)
@app.route
(
'/upload_preview'
,
methods
=
[
'POST'
])
def
upload_preview
():
if
'pdf'
not
in
request
.
files
:
return
{
"error"
:
"Sem arquivo"
},
400
pdf
=
request
.
files
[
'pdf'
]
pdf_path
=
os
.
path
.
join
(
UPLOAD_FOLDER
,
pdf
.
filename
)
pdf
.
save
(
pdf_path
)
return
{
"filename"
:
pdf
.
filename
}
@app.route
(
'/preview/<filename>/<int:pagnumber>'
)
def
preview
(
filename
,
pagnumber
):
pdf_path
=
os
.
path
.
join
(
UPLOAD_FOLDER
,
filename
)
if
not
os
.
path
.
exists
(
pdf_path
):
return
"Erro"
,
404
doc
=
fitz
.
open
(
pdf_path
)
# Garante que a página solicitada existe no PDF
total
=
len
(
doc
)
if
pagnumber
>=
total
:
pagnumber
=
total
-
1
if
pagnumber
<
0
:
pagnumber
=
0
page
=
doc
[
pagnumber
]
pix
=
page
.
get_pixmap
(
dpi
=
100
)
img_data
=
pix
.
tobytes
(
"png"
)
doc
.
close
()
return
send_file
(
io
.
BytesIO
(
img_data
),
mimetype
=
'image/png'
)
@app.route
(
'/processar'
,
methods
=
[
'POST'
])
def
processar
():
pdf
=
request
.
files
[
'pdf
'
]
filename
=
request
.
form
[
'filename
'
]
inicio
=
int
(
request
.
form
[
'inicio'
])
fim
=
int
(
request
.
form
[
'fim'
])
coords
=
list
(
map
(
float
,
request
.
form
[
'coords'
]
.
split
(
','
)))
pdf_path
=
os
.
path
.
join
(
UPLOAD_FOLDER
,
pdf
.
filename
)
pdf
.
save
(
pdf_path
)
c
=
list
(
map
(
float
,
request
.
form
[
'coords'
]
.
split
(
','
)))
pdf_path
=
os
.
path
.
join
(
UPLOAD_FOLDER
,
filename
)
doc
=
fitz
.
open
(
pdf_path
)
zip_path
=
os
.
path
.
join
(
UPLOAD_FOLDER
,
"re
sultado
.zip"
)
zip_path
=
os
.
path
.
join
(
UPLOAD_FOLDER
,
"re
cortes
.zip"
)
with
zipfile
.
ZipFile
(
zip_path
,
'w'
)
as
zip_f
:
for
p
in
range
(
inicio
,
fim
+
1
):
if
p
>=
len
(
doc
):
break
page
=
doc
[
p
]
# Lógica +1 integrada
page
=
doc
[
p
]
pix
=
page
.
get_pixmap
(
dpi
=
300
)
img
=
Image
.
frombytes
(
"RGB"
,
[
pix
.
width
,
pix
.
height
],
pix
.
samples
)
fator
=
300
/
72
area
=
(
coords
[
0
]
*
fator
,
coords
[
1
]
*
fator
,
coords
[
2
]
*
fator
,
coords
[
3
]
*
fator
)
s
=
300
/
100
# Escala Preview vs Corte Final
area
=
(
c
[
0
]
*
s
,
c
[
1
]
*
s
,
(
c
[
0
]
+
c
[
2
])
*
s
,
(
c
[
1
]
+
c
[
3
])
*
s
)
img_cortada
=
img
.
crop
(
area
)
img_tmp
=
f
"pag_{p}.png"
img_cortada
.
save
(
img_tmp
)
zip_f
.
write
(
img_tmp
)
os
.
remove
(
img_tmp
)
img_name
=
f
"pagina_{p}.png"
img_path
=
os
.
path
.
join
(
UPLOAD_FOLDER
,
img_name
)
img_cortada
.
save
(
img_path
)
zip_f
.
write
(
img_path
,
img_name
)
os
.
remove
(
img_path
)
doc
.
close
()
return
send_file
(
zip_path
,
as_attachment
=
True
)
...
...
static/style.css
View file @
fc0133eb
body
{
background
:
#121212
;
color
:
#eee
;
font-family
:
sans-serif
;
display
:
flex
;
justify-content
:
center
;
padding
:
50px
;
}
.container
{
background
:
#1e1e1e
;
padding
:
30px
;
border-radius
:
12px
;
width
:
450px
;
box-shadow
:
0
10px
30px
rgba
(
0
,
0
,
0
,
0.5
);
}
h1
{
text-align
:
center
;
color
:
#4CAF50
;
}
input
{
width
:
100%
;
padding
:
10px
;
margin
:
10px
0
;
background
:
#333
;
border
:
1px
solid
#444
;
color
:
white
;
border-radius
:
5px
;
box-sizing
:
border-box
;
}
.row
{
display
:
flex
;
gap
:
10px
;
}
button
{
width
:
100%
;
padding
:
12px
;
background
:
#4CAF50
;
border
:
none
;
color
:
white
;
font-weight
:
bold
;
border-radius
:
5px
;
cursor
:
pointer
;
margin-top
:
10px
;
}
button
:hover
{
background
:
#45a049
;
}
hr
{
border
:
0
;
border-top
:
1px
solid
#333
;
margin
:
20px
0
;
}
.config-list
{
list-style
:
none
;
padding
:
0
;
font-size
:
14px
;
}
.config-list
li
{
background
:
#252525
;
padding
:
10px
;
margin-bottom
:
5px
;
border-radius
:
4px
;
}
.copyright
{
text-align
:
center
;
font-size
:
10px
;
color
:
#666
;
margin-top
:
20px
;
}
\ No newline at end of file
body
{
background
:
#121212
;
color
:
#eee
;
font-family
:
sans-serif
;
display
:
flex
;
justify-content
:
center
;
padding
:
20px
;
}
.container
{
background
:
#1e1e1e
;
padding
:
30px
;
border-radius
:
15px
;
width
:
100%
;
max-width
:
700px
;
box-shadow
:
0
10px
30px
rgba
(
0
,
0
,
0
,
0.5
);
text-align
:
center
;
}
h1
{
color
:
#4CAF50
;
}
.custom-file-upload
{
border
:
2px
dashed
#4CAF50
;
padding
:
20px
;
cursor
:
pointer
;
display
:
block
;
border-radius
:
10px
;
background
:
#252525
;
margin-bottom
:
20px
;
}
.row
{
display
:
flex
;
gap
:
15px
;
margin-bottom
:
20px
;
}
.input-group
{
flex
:
1
;
text-align
:
left
;
}
input
[
type
=
"number"
]
{
width
:
100%
;
padding
:
10px
;
background
:
#333
;
border
:
1px
solid
#444
;
color
:
white
;
border-radius
:
5px
;
}
.btn-primary
{
width
:
100%
;
padding
:
15px
;
background
:
#4CAF50
;
border
:
none
;
color
:
white
;
font-weight
:
bold
;
border-radius
:
8px
;
cursor
:
pointer
;
}
.footer
{
margin-top
:
20px
;
font-size
:
11px
;
color
:
#666
;
}
\ No newline at end of file
templates/index.html
View file @
fc0133eb
...
...
@@ -2,40 +2,207 @@
<html
lang=
"pt-br"
>
<head>
<meta
charset=
"UTF-8"
>
<title>
PDF
Supremo CRUD
</title>
<title>
PDF
Visual - Pedro Bueno
</title>
<link
rel=
"stylesheet"
href=
"/static/style.css"
>
<style>
#preview-container
{
display
:
none
;
margin
:
20px
auto
;
text-align
:
center
;
}
#preview-wrapper
{
position
:
relative
;
display
:
inline-block
;
border
:
2px
solid
#555
;
line-height
:
0
;
overflow
:
hidden
;
/* Garante que nada saia visualmente */
user-select
:
none
;
}
#pdf-img
{
max-width
:
100%
;
display
:
block
;
}
#selector
{
position
:
absolute
;
border
:
2px
dashed
#4CAF50
;
background
:
rgba
(
76
,
175
,
80
,
0.2
);
cursor
:
move
;
box-sizing
:
border-box
;
width
:
150px
;
height
:
150px
;
top
:
0
;
left
:
0
;
}
#resizer
{
position
:
absolute
;
right
:
0
;
bottom
:
0
;
width
:
15px
;
height
:
15px
;
background
:
#4CAF50
;
cursor
:
nwse-resize
;
}
.nav-controls
{
margin
:
15px
0
;
display
:
flex
;
justify-content
:
center
;
align-items
:
center
;
gap
:
20px
;
}
.nav-btn
{
background
:
#444
;
color
:
white
;
border
:
none
;
padding
:
10px
20px
;
border-radius
:
5px
;
cursor
:
pointer
;
}
.nav-btn
:hover
{
background
:
#555
;
}
</style>
</head>
<body>
<div
class=
"container"
>
<h1>
Manipulador de PDF 🚀
</h1>
<h1>
Recortador Visual 2.0 🚀
</h1>
<label
for=
"pdf-file"
class=
"custom-file-upload"
>
📁 Selecionar PDF para Visualização
</label>
<input
type=
"file"
id=
"pdf-file"
accept=
"application/pdf"
onchange=
"uploadAndPreview()"
style=
"display:none;"
>
<div
id=
"loading"
style=
"display:none; color: #4CAF50;"
>
⏳ Carregando página...
</div>
<div
id=
"preview-container"
>
<div
class=
"nav-controls"
>
<button
type=
"button"
class=
"nav-btn"
onclick=
"changePage(-1)"
>
⬅️ Anterior
</button>
<span
id=
"page-info"
>
Página: 0
</span>
<button
type=
"button"
class=
"nav-btn"
onclick=
"changePage(1)"
>
Próxima ➡️
</button>
</div>
<div
id=
"preview-wrapper"
>
<img
id=
"pdf-img"
>
<div
id=
"selector"
>
<div
id=
"resizer"
></div>
</div>
</div>
</div>
<form
action=
"/processar"
method=
"post"
id=
"main-form"
>
<input
type=
"hidden"
name=
"filename"
id=
"hidden-filename"
>
<input
type=
"hidden"
name=
"coords"
id=
"coords-input"
>
<form
action=
"/processar"
method=
"post"
enctype=
"multipart/form-data"
>
<input
type=
"file"
name=
"pdf"
required
>
<div
class=
"row"
>
<input
type=
"number"
name=
"inicio"
placeholder=
"Início"
value=
"10"
>
<input
type=
"number"
name=
"fim"
placeholder=
"Fim"
value=
"11"
>
<div
class=
"input-group"
>
<label>
Página Início:
</label>
<input
type=
"number"
name=
"inicio"
id=
"input-inicio"
value=
"0"
>
</div>
<div
class=
"input-group"
>
<label>
Página Fim:
</label>
<input
type=
"number"
name=
"fim"
value=
"1"
>
</div>
<
input
type=
"text"
name=
"coords"
id=
"coords_input"
placeholder=
"x0, y0, x1, y1"
value=
"0, 51, 700, 745"
>
<button
type=
"submit"
class=
"btn-primary"
>
GERAR ZIP
</button>
<
/div
>
<button
type=
"submit"
class=
"btn-primary"
>
GERAR
E BAIXAR
ZIP
</button>
</form>
<p
class=
"footer"
>
© Pedro Bueno
</p>
</div>
<hr>
<script>
const
selector
=
document
.
getElementById
(
'selector'
);
const
resizer
=
document
.
getElementById
(
'resizer'
);
const
img
=
document
.
getElementById
(
'pdf-img'
);
const
coordsInput
=
document
.
getElementById
(
'coords-input'
);
const
inputInicio
=
document
.
getElementById
(
'input-inicio'
);
<h3>
Salvar Configuração de Corte
</h3>
<form
action=
"/salvar"
method=
"post"
>
<input
type=
"text"
name=
"nome_config"
placeholder=
"Nome (Ex: Livro de História)"
>
<input
type=
"text"
name=
"coords"
placeholder=
"Coordenadas"
>
<button
type=
"submit"
>
Salvar no Banco
</button>
</form>
let
currentPage
=
0
;
<ul
class=
"config-list"
>
{% for config in configs %}
<li>
<strong>
{{ config[1] }}
</strong>
: {{ config[2] }}
</li>
{% endfor %}
</ul>
async
function
uploadAndPreview
()
{
const
file
=
document
.
getElementById
(
'pdf-file'
).
files
[
0
];
if
(
!
file
)
return
;
</div>
document
.
getElementById
(
'loading'
).
style
.
display
=
'block'
;
const
formData
=
new
FormData
();
formData
.
append
(
'pdf'
,
file
);
const
res
=
await
fetch
(
'/upload_preview'
,
{
method
:
'POST'
,
body
:
formData
});
const
data
=
await
res
.
json
();
document
.
getElementById
(
'hidden-filename'
).
value
=
data
.
filename
;
currentPage
=
parseInt
(
inputInicio
.
value
)
||
0
;
updatePreview
();
}
function
updatePreview
()
{
const
filename
=
document
.
getElementById
(
'hidden-filename'
).
value
;
img
.
src
=
`/preview/
${
filename
}
/
${
currentPage
}
?t=
${
new
Date
().
getTime
()}
`
;
document
.
getElementById
(
'page-info'
).
innerText
=
`Visualizando Página:
${
currentPage
}
`
;
inputInicio
.
value
=
currentPage
;
// Sincroniza o form com a visão
img
.
onload
=
()
=>
{
document
.
getElementById
(
'loading'
).
style
.
display
=
'none'
;
document
.
getElementById
(
'preview-container'
).
style
.
display
=
'block'
;
updateCoords
();
};
}
function
changePage
(
delta
)
{
currentPage
+=
delta
;
if
(
currentPage
<
0
)
currentPage
=
0
;
updatePreview
();
}
// Lógica de Arrastar e Redimensionar com Contenção
let
isDragging
=
false
,
isResizing
=
false
;
let
startX
,
startY
,
startW
,
startH
;
selector
.
onmousedown
=
(
e
)
=>
{
if
(
e
.
target
===
resizer
)
return
;
isDragging
=
true
;
startX
=
e
.
clientX
-
selector
.
offsetLeft
;
startY
=
e
.
clientY
-
selector
.
offsetTop
;
};
resizer
.
onmousedown
=
(
e
)
=>
{
isResizing
=
true
;
startX
=
e
.
clientX
;
startY
=
e
.
clientY
;
startW
=
selector
.
offsetWidth
;
startH
=
selector
.
offsetHeight
;
e
.
stopPropagation
();
e
.
preventDefault
();
};
window
.
onmousemove
=
(
e
)
=>
{
if
(
isDragging
)
{
let
x
=
e
.
clientX
-
startX
;
let
y
=
e
.
clientY
-
startY
;
// Bloqueia saída das bordas
if
(
x
<
0
)
x
=
0
;
if
(
y
<
0
)
y
=
0
;
if
(
x
+
selector
.
offsetWidth
>
img
.
offsetWidth
)
x
=
img
.
offsetWidth
-
selector
.
offsetWidth
;
if
(
y
+
selector
.
offsetHeight
>
img
.
offsetHeight
)
y
=
img
.
offsetHeight
-
selector
.
offsetHeight
;
selector
.
style
.
left
=
x
+
'px'
;
selector
.
style
.
top
=
y
+
'px'
;
}
if
(
isResizing
)
{
let
newWidth
=
startW
+
(
e
.
clientX
-
startX
);
let
newHeight
=
startH
+
(
e
.
clientY
-
startY
);
// Bloqueia redimensionamento para fora da imagem
if
(
selector
.
offsetLeft
+
newWidth
>
img
.
offsetWidth
)
newWidth
=
img
.
offsetWidth
-
selector
.
offsetLeft
;
if
(
selector
.
offsetTop
+
newHeight
>
img
.
offsetHeight
)
newHeight
=
img
.
offsetHeight
-
selector
.
offsetTop
;
selector
.
style
.
width
=
Math
.
max
(
20
,
newWidth
)
+
'px'
;
selector
.
style
.
height
=
Math
.
max
(
20
,
newHeight
)
+
'px'
;
}
updateCoords
();
};
window
.
onmouseup
=
()
=>
{
isDragging
=
false
;
isResizing
=
false
;
};
function
updateCoords
()
{
coordsInput
.
value
=
`
${
selector
.
offsetLeft
}
,
${
selector
.
offsetTop
}
,
${
selector
.
offsetWidth
}
,
${
selector
.
offsetHeight
}
`
;
}
</script>
</body>
</html>
\ No newline at end of file
uploads/MP_-_Colecao_360_-_Matematica_1.pdf
0 → 100644
View file @
fc0133eb
File added
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment