-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathapp.py
More file actions
230 lines (213 loc) · 9.02 KB
/
app.py
File metadata and controls
230 lines (213 loc) · 9.02 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
# app.py — SafeSeal 5.6 · Streamlit + LibreOffice + Watermark
import io, os, shutil, subprocess, tempfile, time, pathlib
import streamlit as st
import streamlit.components.v1 as components
from PIL import Image, ImageDraw, ImageFont
import fitz # PyMuPDF
# ---------------------------
# Page setup
# ---------------------------
st.set_page_config(page_title="SafeSeal 5.6", layout="centered")
st.markdown("""
<style>
.block-container { padding-top: 3rem; }
.title-row { display:flex; align-items:center; gap:10px; margin:0 0 0.5rem 0; }
.title-row img { width:36px; height:36px; border-radius:4px; object-fit:cover; }
.title-text { font-size:1.5rem; font-weight:700; line-height:1.2; }
.subtitle { color:#888888; font-size:0.9rem; margin-bottom:1.5rem; }
.footer { text-align:center; margin-top:2rem; font-size:0.8rem; }
</style>
""", unsafe_allow_html=True)
logo_url = "https://raw.githubusercontent.com/FYCodeLab/SafeSeal/main/assets/seal.jpg"
st.markdown(f"""
<div class="title-row">
<img src="{logo_url}" alt="SafeSeal logo">
<div class="title-text">SafeSeal 5.6</div>
</div>
<div class="subtitle">
Upload a PPTX presentation or PDF (faster). The service returns a copy watermarked
with the provided name and flattened to avoid copying.
</div>
""", unsafe_allow_html=True)
# ---------------------------
# LibreOffice detection
# ---------------------------
def _resolve_libreoffice_bin():
for cand in ["soffice", "libreoffice", "/usr/bin/soffice", "/usr/bin/libreoffice"]:
path = shutil.which(cand) if os.path.basename(cand) == cand else (cand if os.path.exists(cand) else None)
if path:
try:
subprocess.run([path, "--version"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True)
return path
except Exception:
continue
return None
LO_BIN = _resolve_libreoffice_bin()
# ---------------------------
# Status rendering
# ---------------------------
def _html_escape(s: str) -> str:
return s.replace("&", "&").replace("<", "<").replace(">", ">")
def _render_status_box(buf: str, placeholder):
html = f"""
<div id="status-box"
style="background:#3a3a3a; color:#00ff00;
border:1px solid #5a5a5a; border-radius:6px;
padding:6px 8px;
font-family:'Courier New', Courier, monospace;
font-size:11px; line-height:1.2;
white-space:pre-wrap;
height:calc(1.2em * 4 + 12px);
overflow-y:auto; overflow-x:hidden;">
{_html_escape(buf)}
</div>
<script>
const el = document.getElementById('status-box');
if (el) {{ el.scrollTop = el.scrollHeight; }}
</script>
"""
with placeholder.container():
components.html(html, height=100, scrolling=False)
# ---------------------------
# Watermark helpers
# ---------------------------
def _load_font(px: int):
try:
return ImageFont.truetype("DejaVuSans.ttf", px)
except Exception:
return ImageFont.load_default()
def _draw_tiled_watermark(img_rgba, text, dpi=120, angle=45, opacity=60):
w, h = img_rgba.size
font_px = max(6, int(round(8 * dpi / 72.0)))
font = _load_font(font_px)
spacing_px = int(dpi)
layer = Image.new("RGBA", (w, h), (0, 0, 0, 0))
draw = ImageDraw.Draw(layer)
fill = (180, 180, 180, max(0, min(255, opacity)))
for y in range(-h, h*2, spacing_px):
for x in range(-w, w*2, spacing_px):
draw.text((x, y), text, font=font, fill=fill)
layer = layer.rotate(angle, expand=True, resample=Image.BICUBIC)
lw, lh = layer.size
left, top = (lw - w) // 2, (lh - h) // 2
layer = layer.crop((left, top, left + w, top + h))
return Image.alpha_composite(img_rgba, layer)
def pdf_to_imageonly_pdf_with_watermark(pdf_bytes, wm_text, dpi, quality,
progress_cb=None, log_cb=None):
src = fitz.open(stream=pdf_bytes, filetype="pdf")
out = fitz.open()
total = len(src)
for idx, page in enumerate(src, start=1):
if log_cb: log_cb(f"Watermarking page {idx}/{total}…")
mat = fitz.Matrix(dpi/72.0, dpi/72.0)
pix = page.get_pixmap(matrix=mat, alpha=False)
img = Image.frombytes("RGB", (pix.width, pix.height), pix.samples).convert("RGBA")
img = _draw_tiled_watermark(img, wm_text, dpi=dpi, opacity=60)
buf = io.BytesIO()
img.convert("RGB").save(buf, format="JPEG", quality=quality, optimize=True)
rect = fitz.Rect(0, 0, pix.width, pix.height)
new_page = out.new_page(width=rect.width, height=rect.height)
new_page.insert_image(rect, stream=buf.getvalue())
if progress_cb: progress_cb(idx, total)
result = out.tobytes()
out.close(); src.close()
return result
# ---------------------------
# Conversion pipeline
# ---------------------------
def convert_office_to_pdf_bytes(file_bytes: bytes, in_name: str, log_cb, pbar) -> bytes:
if not LO_BIN:
raise RuntimeError("LibreOffice is not available on this host.")
with tempfile.TemporaryDirectory() as td:
in_path = pathlib.Path(td) / in_name
out_dir = pathlib.Path(td) / "out"
out_dir.mkdir(parents=True, exist_ok=True)
in_path.write_bytes(file_bytes)
cmd = [
LO_BIN, "--headless", "--nologo", "--nodefault", "--nolockcheck",
"--norestore", "--nofirststartwizard",
"--convert-to", "pdf", "--outdir", str(out_dir), str(in_path),
]
log_cb("Launching LibreOffice conversion…")
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
soft = 0
while proc.poll() is None:
if proc.stdout:
outl = proc.stdout.readline()
if outl: log_cb(outl.strip())
soft = min(soft + 2, 90)
pbar.progress(soft); time.sleep(0.05)
if proc.stdout:
rest = proc.stdout.read() or ""
for l in rest.splitlines(): log_cb(l)
if proc.returncode != 0:
pbar.progress(0); raise RuntimeError(f"LibreOffice exit code {proc.returncode}.")
out_candidates = list(out_dir.glob("*.pdf"))
if not out_candidates: raise FileNotFoundError("No PDF produced by LibreOffice.")
pbar.progress(100); log_cb("LibreOffice conversion complete.")
return out_candidates[0].read_bytes()
# ---------------------------
# UI
# ---------------------------
left, right = st.columns([2, 1])
with left:
uploaded = st.file_uploader(
"PDF, PPTX, DOCX…",
type=["pdf","pptx","docx","doc","ppt","xls","xlsx","odt","odp","ods"],
help="PDF is faster. Office files are converted to PDF first."
)
with right:
profile = st.radio(
"Compression profile",
["High quality (180 dpi, q90)",
"Balanced (120 dpi, q75)",
"Smallest (100 dpi, q60)"],
index=1
)
wm_text = st.text_input("Watermark / name (≤ 15 chars)", value="JOHN SMITH", max_chars=15)
dpi, quality = (120, 75)
if profile.startswith("High"): dpi, quality = (180, 90)
elif profile.startswith("Smallest"): dpi, quality = (100, 60)
st.subheader("Status")
status_placeholder = st.empty()
pbar_placeholder = st.empty()
def log_line(msg):
nonlocal_buf = st.session_state.get("_logbuf", "") + msg + "\n"
st.session_state["_logbuf"] = nonlocal_buf
_render_status_box(nonlocal_buf, status_placeholder)
pbar = pbar_placeholder.progress(0)
run = st.button("Start conversion")
if run:
try:
name = getattr(uploaded, "name", "upload") if uploaded else None
if not uploaded: st.error("Please upload a file."); st.stop()
if not wm_text: st.error("Please provide a name for the watermark."); st.stop()
ext = pathlib.Path(name).suffix.lower()
if ext == ".pdf":
log_line("Input is PDF. Skipping LibreOffice conversion.")
pdf_bytes = uploaded.getbuffer().tobytes()
else:
log_line(f"Converting '{name}' to PDF via LibreOffice…")
pdf_bytes = convert_office_to_pdf_bytes(uploaded.getbuffer(), name, log_line, pbar)
log_line(f"Applying watermark '{wm_text}' and rebuilding PDF (dpi={dpi}, q={quality})…")
def page_progress(i, total): pbar.progress(min(10 + int(90 * i / max(1, total)), 100))
watermarked = pdf_to_imageonly_pdf_with_watermark(
pdf_bytes, wm_text, dpi, quality, progress_cb=page_progress, log_cb=log_line
)
pbar.progress(100); log_line("Watermarking complete.")
out_name = pathlib.Path(name).with_suffix(".pdf").stem + "_sealed.pdf"
st.success("Done.")
st.download_button("Download sealed PDF", data=watermarked,
file_name=out_name, mime="application/pdf")
except Exception as e:
st.error(f"Conversion failed: {e}")
# ---------------------------
# Footer (centered GitHub badge)
# ---------------------------
st.markdown("""
<div class="footer">
<a href="https://github.com/FYCodeLab/SafeSeal/tree/main" target="_blank">
<img src="https://img.shields.io/badge/GitHub-SafeSeal-blue?logo=github" alt="GitHub link">
</a>
</div>
""", unsafe_allow_html=True)