streamlit-mini-toolkit 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mini_toolkit/__init__.py +3 -0
- mini_toolkit/__main__.py +3 -0
- mini_toolkit/app.py +111 -0
- mini_toolkit/cli.py +40 -0
- mini_toolkit/core/__init__.py +1 -0
- mini_toolkit/core/net.py +56 -0
- mini_toolkit/core/pdf.py +12 -0
- mini_toolkit/core/registry.py +75 -0
- mini_toolkit/core/ui.py +90 -0
- mini_toolkit/streamlit_config.toml +16 -0
- mini_toolkit/style/style.css +171 -0
- mini_toolkit/tools/__init__.py +1 -0
- mini_toolkit/tools/background_remover.py +65 -0
- mini_toolkit/tools/change_tracker.py +90 -0
- mini_toolkit/tools/chat_alert.py +42 -0
- mini_toolkit/tools/color_picker.py +40 -0
- mini_toolkit/tools/document_converter.py +121 -0
- mini_toolkit/tools/file_compressor.py +78 -0
- mini_toolkit/tools/github_trending.py +53 -0
- mini_toolkit/tools/hash_generator.py +43 -0
- mini_toolkit/tools/image_converter.py +63 -0
- mini_toolkit/tools/image_downloader.py +67 -0
- mini_toolkit/tools/image_enhancer.py +54 -0
- mini_toolkit/tools/image_watermarker.py +63 -0
- mini_toolkit/tools/ocr.py +93 -0
- mini_toolkit/tools/page_speed.py +45 -0
- mini_toolkit/tools/password_checker.py +53 -0
- mini_toolkit/tools/pdf_tools.py +83 -0
- mini_toolkit/tools/qr_code.py +83 -0
- mini_toolkit/tools/resume_builder.py +385 -0
- mini_toolkit/tools/seo_health.py +64 -0
- mini_toolkit/tools/signature.py +72 -0
- mini_toolkit/tools/ssl_checker.py +68 -0
- mini_toolkit/tools/text_to_speech.py +34 -0
- mini_toolkit/tools/text_utilities.py +36 -0
- mini_toolkit/tools/url_shortener.py +29 -0
- mini_toolkit/tools/website_status.py +39 -0
- streamlit_mini_toolkit-0.1.0.dist-info/METADATA +151 -0
- streamlit_mini_toolkit-0.1.0.dist-info/RECORD +42 -0
- streamlit_mini_toolkit-0.1.0.dist-info/WHEEL +4 -0
- streamlit_mini_toolkit-0.1.0.dist-info/entry_points.txt +2 -0
- streamlit_mini_toolkit-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import io
|
|
2
|
+
|
|
3
|
+
import streamlit as st
|
|
4
|
+
from PIL import Image, ImageDraw, ImageFont
|
|
5
|
+
|
|
6
|
+
from core.ui import sanitize_filename
|
|
7
|
+
|
|
8
|
+
POSITIONS = ["Bottom right", "Bottom left", "Top right", "Top left", "Center"]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _font(size: int) -> ImageFont.ImageFont:
|
|
12
|
+
for name in ("DejaVuSans-Bold.ttf", "DejaVuSans.ttf", "Arial.ttf", "arial.ttf"):
|
|
13
|
+
try:
|
|
14
|
+
return ImageFont.truetype(name, size)
|
|
15
|
+
except OSError:
|
|
16
|
+
continue
|
|
17
|
+
try:
|
|
18
|
+
return ImageFont.load_default(size=size) # Pillow >= 10.1 ships a scalable default font
|
|
19
|
+
except TypeError:
|
|
20
|
+
return ImageFont.load_default()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def watermark(image: Image.Image, text: str, size: int, opacity: int, color: str, position: str) -> Image.Image:
|
|
24
|
+
base = image.convert("RGBA")
|
|
25
|
+
layer = Image.new("RGBA", base.size, (0, 0, 0, 0))
|
|
26
|
+
draw = ImageDraw.Draw(layer)
|
|
27
|
+
font = _font(size)
|
|
28
|
+
left, top, right, bottom = draw.textbbox((0, 0), text, font=font)
|
|
29
|
+
w, h = right - left, bottom - top
|
|
30
|
+
margin = max(10, base.width // 50)
|
|
31
|
+
x = {"left": margin, "right": base.width - w - margin}.get(position.split()[-1], (base.width - w) / 2)
|
|
32
|
+
y = {"Top": margin, "Bottom": base.height - h - margin}.get(position.split()[0], (base.height - h) / 2)
|
|
33
|
+
rgb = tuple(int(color[i:i + 2], 16) for i in (1, 3, 5))
|
|
34
|
+
draw.text((x - left, y - top), text, font=font, fill=(*rgb, opacity))
|
|
35
|
+
return Image.alpha_composite(base, layer)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def render() -> None:
|
|
39
|
+
uploaded = st.file_uploader("Upload an image", type=["png", "jpg", "jpeg", "webp"], key="watermark_upload")
|
|
40
|
+
if not uploaded:
|
|
41
|
+
st.info("Upload an image to add a watermark.")
|
|
42
|
+
return
|
|
43
|
+
|
|
44
|
+
image = Image.open(uploaded)
|
|
45
|
+
settings, preview = st.columns([1, 2])
|
|
46
|
+
with settings:
|
|
47
|
+
text = st.text_input("Watermark text", "© Your Brand")
|
|
48
|
+
size = st.slider("Font size", 10, 300, max(24, image.width // 20))
|
|
49
|
+
opacity = st.slider("Opacity", 0, 255, 140)
|
|
50
|
+
color = st.color_picker("Colour", "#FFFFFF")
|
|
51
|
+
position = st.selectbox("Position", POSITIONS)
|
|
52
|
+
|
|
53
|
+
if not text.strip():
|
|
54
|
+
preview.image(image, caption="Original", width="stretch")
|
|
55
|
+
return
|
|
56
|
+
|
|
57
|
+
result = watermark(image, text, size, opacity, color, position)
|
|
58
|
+
preview.image(result, caption="Preview", width="stretch")
|
|
59
|
+
buf = io.BytesIO()
|
|
60
|
+
result.save(buf, format="PNG")
|
|
61
|
+
settings.download_button(
|
|
62
|
+
"Download PNG", buf.getvalue(), f"{sanitize_filename(uploaded.name)}_watermarked.png", "image/png", type="primary"
|
|
63
|
+
)
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""OCR with Tesseract (default) and EasyOCR (optional, heavier) engines."""
|
|
2
|
+
import os
|
|
3
|
+
import platform
|
|
4
|
+
import shutil
|
|
5
|
+
|
|
6
|
+
import streamlit as st
|
|
7
|
+
|
|
8
|
+
from core.ui import missing_dependency, sanitize_filename
|
|
9
|
+
|
|
10
|
+
TESSERACT_LANGS = {"English": "eng", "Bengali": "ben", "Hindi": "hin"}
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _find_tesseract():
|
|
14
|
+
if platform.system() != "Windows":
|
|
15
|
+
return shutil.which("tesseract")
|
|
16
|
+
for path in (
|
|
17
|
+
r"C:\Program Files\Tesseract-OCR\tesseract.exe",
|
|
18
|
+
r"C:\Program Files (x86)\Tesseract-OCR\tesseract.exe",
|
|
19
|
+
os.path.expanduser(r"~\AppData\Local\Programs\Tesseract-OCR\tesseract.exe"),
|
|
20
|
+
):
|
|
21
|
+
if os.path.exists(path):
|
|
22
|
+
return path
|
|
23
|
+
return shutil.which("tesseract")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@st.cache_data(show_spinner=False)
|
|
27
|
+
def run_tesseract(image_bytes: bytes, langs: str) -> str:
|
|
28
|
+
import cv2
|
|
29
|
+
import numpy as np
|
|
30
|
+
import pytesseract
|
|
31
|
+
|
|
32
|
+
pytesseract.pytesseract.tesseract_cmd = _find_tesseract()
|
|
33
|
+
image = cv2.imdecode(np.frombuffer(image_bytes, np.uint8), cv2.IMREAD_GRAYSCALE)
|
|
34
|
+
# Otsu binarisation noticeably improves Tesseract accuracy on photos and screenshots.
|
|
35
|
+
_, binary = cv2.threshold(image, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU)
|
|
36
|
+
return pytesseract.image_to_string(binary, lang=langs)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@st.cache_resource(show_spinner="Loading EasyOCR model (first run only)…")
|
|
40
|
+
def _easyocr_reader():
|
|
41
|
+
import easyocr
|
|
42
|
+
|
|
43
|
+
return easyocr.Reader(["en", "bn"], gpu=False)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@st.cache_data(show_spinner=False)
|
|
47
|
+
def run_easyocr(image_bytes: bytes) -> str:
|
|
48
|
+
return "\n".join(text for _, text, _ in _easyocr_reader().readtext(image_bytes))
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _easyocr_available() -> bool:
|
|
52
|
+
try:
|
|
53
|
+
import easyocr # noqa: F401
|
|
54
|
+
except ImportError:
|
|
55
|
+
return False
|
|
56
|
+
return True
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def render() -> None:
|
|
60
|
+
engines = ["Tesseract"] + (["EasyOCR"] if _easyocr_available() else [])
|
|
61
|
+
cols = st.columns(2)
|
|
62
|
+
engine = cols[0].radio("Engine", engines, horizontal=True)
|
|
63
|
+
langs = []
|
|
64
|
+
if engine == "Tesseract":
|
|
65
|
+
langs = cols[1].multiselect("Languages", list(TESSERACT_LANGS), default=["English"])
|
|
66
|
+
|
|
67
|
+
uploaded = st.file_uploader("Upload an image", type=["png", "jpg", "jpeg", "bmp", "tiff", "webp"], key="ocr_upload")
|
|
68
|
+
if not uploaded:
|
|
69
|
+
st.info("Upload an image containing text.")
|
|
70
|
+
return
|
|
71
|
+
|
|
72
|
+
if engine == "Tesseract" and not _find_tesseract():
|
|
73
|
+
missing_dependency("tesseract-ocr", "Install it from https://github.com/UB-Mannheim/tesseract/wiki (Windows) "
|
|
74
|
+
"or your package manager, then restart the app.")
|
|
75
|
+
return
|
|
76
|
+
|
|
77
|
+
image_bytes = uploaded.getvalue()
|
|
78
|
+
left, right = st.columns(2)
|
|
79
|
+
left.image(image_bytes, width="stretch")
|
|
80
|
+
with right, st.spinner("Extracting text…"):
|
|
81
|
+
try:
|
|
82
|
+
if engine == "Tesseract":
|
|
83
|
+
text = run_tesseract(image_bytes, "+".join(TESSERACT_LANGS[l] for l in langs) or "eng")
|
|
84
|
+
else:
|
|
85
|
+
text = run_easyocr(image_bytes)
|
|
86
|
+
except Exception as exc:
|
|
87
|
+
st.error(f"OCR failed: {exc}")
|
|
88
|
+
return
|
|
89
|
+
if not text.strip():
|
|
90
|
+
st.warning("No text was detected in this image.")
|
|
91
|
+
return
|
|
92
|
+
st.text_area("Extracted text", text, height=320)
|
|
93
|
+
st.download_button("Download .txt", text.encode("utf-8"), f"{sanitize_filename(uploaded.name)}.txt", "text/plain")
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import time
|
|
2
|
+
|
|
3
|
+
import streamlit as st
|
|
4
|
+
|
|
5
|
+
from core.net import fetch
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def measure(url: str) -> dict:
|
|
9
|
+
start = time.perf_counter()
|
|
10
|
+
response = fetch(url, timeout=20, stream=True)
|
|
11
|
+
first_byte = time.perf_counter() - start
|
|
12
|
+
size = sum(len(chunk) for chunk in response.iter_content(64 * 1024))
|
|
13
|
+
total = time.perf_counter() - start
|
|
14
|
+
return {"status": response.status_code, "ttfb": first_byte, "total": total, "size": size}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def render() -> None:
|
|
18
|
+
url = st.text_input("Website URL", placeholder="https://example.com")
|
|
19
|
+
runs = st.slider("Number of runs (results are averaged)", 1, 5, 3)
|
|
20
|
+
if not st.button("Measure", type="primary", disabled=not url.strip()):
|
|
21
|
+
st.caption("Measures the HTML document only — not images, scripts or rendering time.")
|
|
22
|
+
return
|
|
23
|
+
|
|
24
|
+
results = []
|
|
25
|
+
with st.spinner("Measuring…"):
|
|
26
|
+
for _ in range(runs):
|
|
27
|
+
try:
|
|
28
|
+
results.append(measure(url))
|
|
29
|
+
except Exception as exc:
|
|
30
|
+
st.error(f"Request failed: {exc}")
|
|
31
|
+
return
|
|
32
|
+
|
|
33
|
+
avg = lambda key: sum(r[key] for r in results) / len(results) # noqa: E731
|
|
34
|
+
total = avg("total")
|
|
35
|
+
cols = st.columns(4)
|
|
36
|
+
cols[0].metric("HTTP status", results[-1]["status"])
|
|
37
|
+
cols[1].metric("Time to first byte", f"{avg('ttfb') * 1000:.0f} ms")
|
|
38
|
+
cols[2].metric("Total download", f"{total * 1000:.0f} ms")
|
|
39
|
+
cols[3].metric("HTML size", f"{results[-1]['size'] / 1024:.1f} KB")
|
|
40
|
+
if total < 0.8:
|
|
41
|
+
st.success("Fast — the server responds quickly.")
|
|
42
|
+
elif total < 2:
|
|
43
|
+
st.warning("Moderate — there may be room to improve server response time.")
|
|
44
|
+
else:
|
|
45
|
+
st.error("Slow — consider caching, a CDN, or reducing server work.")
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import math
|
|
2
|
+
import re
|
|
3
|
+
import secrets
|
|
4
|
+
import string
|
|
5
|
+
|
|
6
|
+
import streamlit as st
|
|
7
|
+
|
|
8
|
+
COMMON = {"password", "123456", "12345678", "qwerty", "abc123", "111111", "letmein", "welcome", "admin", "iloveyou", "monkey", "dragon"}
|
|
9
|
+
|
|
10
|
+
CHECKS = [
|
|
11
|
+
(lambda p: len(p) >= 12, "Use at least 12 characters."),
|
|
12
|
+
(lambda p: re.search(r"[A-Z]", p), "Add an uppercase letter."),
|
|
13
|
+
(lambda p: re.search(r"[a-z]", p), "Add a lowercase letter."),
|
|
14
|
+
(lambda p: re.search(r"\d", p), "Add a number."),
|
|
15
|
+
(lambda p: re.search(r"[^A-Za-z0-9]", p), "Add a symbol."),
|
|
16
|
+
(lambda p: not re.search(r"(.)\1{2,}", p), "Avoid repeating the same character."),
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
LEVELS = [(28, "Very weak", "error"), (36, "Weak", "error"), (60, "Reasonable", "warning"), (80, "Strong", "success"), (math.inf, "Very strong", "success")]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def entropy_bits(password: str) -> float:
|
|
23
|
+
pool = sum(size for pattern, size in ((r"[a-z]", 26), (r"[A-Z]", 26), (r"\d", 10), (r"[^A-Za-z0-9]", 32)) if re.search(pattern, password))
|
|
24
|
+
return len(password) * math.log2(pool) if pool else 0.0
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def generate(length: int) -> str:
|
|
28
|
+
alphabet = string.ascii_letters + string.digits + "!@#$%^&*-_=+"
|
|
29
|
+
while True:
|
|
30
|
+
pw = "".join(secrets.choice(alphabet) for _ in range(length))
|
|
31
|
+
if all(check(pw) for check, _ in CHECKS):
|
|
32
|
+
return pw
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def render() -> None:
|
|
36
|
+
st.caption("Your password is analysed in this session only and never stored.")
|
|
37
|
+
password = st.text_input("Password", type="password")
|
|
38
|
+
if password:
|
|
39
|
+
bits = 0.0 if password.lower() in COMMON else entropy_bits(password)
|
|
40
|
+
label, kind = next((lbl, k) for limit, lbl, k in LEVELS if bits < limit)
|
|
41
|
+
st.progress(min(bits / 100, 1.0))
|
|
42
|
+
getattr(st, kind)(f"**{label}** — about {bits:.0f} bits of entropy.")
|
|
43
|
+
if password.lower() in COMMON:
|
|
44
|
+
st.error("This is one of the most common passwords. Don't use it.")
|
|
45
|
+
tips = [tip for check, tip in CHECKS if not check(password)]
|
|
46
|
+
for tip in tips:
|
|
47
|
+
st.markdown(f"- {tip}")
|
|
48
|
+
|
|
49
|
+
st.divider()
|
|
50
|
+
st.markdown("**Need a strong one?**")
|
|
51
|
+
length = st.slider("Length", 12, 64, 20)
|
|
52
|
+
if st.button("Generate password"):
|
|
53
|
+
st.code(generate(length), language=None)
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import io
|
|
2
|
+
|
|
3
|
+
import streamlit as st
|
|
4
|
+
|
|
5
|
+
from core.ui import build_zip, missing_dependency, sanitize_filename
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def merge(files) -> bytes:
|
|
9
|
+
from pypdf import PdfWriter
|
|
10
|
+
|
|
11
|
+
writer = PdfWriter()
|
|
12
|
+
for f in files:
|
|
13
|
+
writer.append(io.BytesIO(f.getvalue()))
|
|
14
|
+
buf = io.BytesIO()
|
|
15
|
+
writer.write(buf)
|
|
16
|
+
return buf.getvalue()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def extract(reader, pages) -> bytes:
|
|
20
|
+
from pypdf import PdfWriter
|
|
21
|
+
|
|
22
|
+
writer = PdfWriter()
|
|
23
|
+
for index in pages:
|
|
24
|
+
writer.add_page(reader.pages[index])
|
|
25
|
+
buf = io.BytesIO()
|
|
26
|
+
writer.write(buf)
|
|
27
|
+
return buf.getvalue()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _render_merge() -> None:
|
|
31
|
+
files = st.file_uploader("Upload PDFs (merged in upload order)", type="pdf", accept_multiple_files=True, key="pdf_merge")
|
|
32
|
+
if not files or len(files) < 2:
|
|
33
|
+
st.info("Upload two or more PDF files to merge.")
|
|
34
|
+
return
|
|
35
|
+
st.caption(" → ".join(f.name for f in files))
|
|
36
|
+
if st.button("Merge PDFs", type="primary"):
|
|
37
|
+
try:
|
|
38
|
+
st.download_button("Download merged PDF", merge(files), "merged.pdf", "application/pdf")
|
|
39
|
+
except Exception as exc:
|
|
40
|
+
st.error(f"Could not merge: {exc}")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _render_split() -> None:
|
|
44
|
+
from pypdf import PdfReader
|
|
45
|
+
|
|
46
|
+
uploaded = st.file_uploader("Upload a PDF", type="pdf", key="pdf_split")
|
|
47
|
+
if not uploaded:
|
|
48
|
+
st.info("Upload a PDF to split.")
|
|
49
|
+
return
|
|
50
|
+
try:
|
|
51
|
+
reader = PdfReader(io.BytesIO(uploaded.getvalue()))
|
|
52
|
+
except Exception as exc:
|
|
53
|
+
st.error(f"Could not read PDF: {exc}")
|
|
54
|
+
return
|
|
55
|
+
total = len(reader.pages)
|
|
56
|
+
stem = sanitize_filename(uploaded.name)
|
|
57
|
+
st.caption(f"{total} page{'s' if total != 1 else ''}")
|
|
58
|
+
|
|
59
|
+
mode = st.radio("Split mode", ["Extract a page range", "Every page as a separate file"], horizontal=True)
|
|
60
|
+
if mode == "Extract a page range":
|
|
61
|
+
left, right = st.columns(2)
|
|
62
|
+
start = left.number_input("From page", 1, total, 1)
|
|
63
|
+
end = right.number_input("To page", int(start), total, total)
|
|
64
|
+
if st.button("Extract pages", type="primary"):
|
|
65
|
+
data = extract(reader, range(int(start) - 1, int(end)))
|
|
66
|
+
st.download_button(f"Download pages {start}–{end}", data, f"{stem}_p{start}-{end}.pdf", "application/pdf")
|
|
67
|
+
elif st.button("Split all pages", type="primary"):
|
|
68
|
+
with st.spinner("Splitting…"):
|
|
69
|
+
pages = [(f"{stem}_page_{i + 1}.pdf", extract(reader, [i])) for i in range(total)]
|
|
70
|
+
st.download_button("Download pages (ZIP)", build_zip(pages), f"{stem}_pages.zip", "application/zip")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def render() -> None:
|
|
74
|
+
try:
|
|
75
|
+
import pypdf # noqa: F401
|
|
76
|
+
except ImportError:
|
|
77
|
+
missing_dependency("pypdf")
|
|
78
|
+
return
|
|
79
|
+
merge_tab, split_tab = st.tabs(["Merge", "Split"])
|
|
80
|
+
with merge_tab:
|
|
81
|
+
_render_merge()
|
|
82
|
+
with split_tab:
|
|
83
|
+
_render_split()
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import io
|
|
2
|
+
|
|
3
|
+
import qrcode
|
|
4
|
+
import streamlit as st
|
|
5
|
+
from PIL import Image, ImageChops
|
|
6
|
+
from qrcode.constants import ERROR_CORRECT_H, ERROR_CORRECT_L, ERROR_CORRECT_M, ERROR_CORRECT_Q
|
|
7
|
+
|
|
8
|
+
ERROR_LEVELS = {
|
|
9
|
+
"Low (7%)": ERROR_CORRECT_L,
|
|
10
|
+
"Medium (15%)": ERROR_CORRECT_M,
|
|
11
|
+
"Quartile (25%)": ERROR_CORRECT_Q,
|
|
12
|
+
"High (30%)": ERROR_CORRECT_H,
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _escape_wifi(value: str) -> str:
|
|
17
|
+
for ch in ("\\", ";", ",", ":", '"'):
|
|
18
|
+
value = value.replace(ch, "\\" + ch)
|
|
19
|
+
return value
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def build_qr(data: str, fg: str, bg: str, transparent: bool, size: int, error_level: int) -> bytes:
|
|
23
|
+
qr = qrcode.QRCode(error_correction=error_level, box_size=10, border=4)
|
|
24
|
+
qr.add_data(data)
|
|
25
|
+
qr.make(fit=True)
|
|
26
|
+
img = qr.make_image(fill_color=fg, back_color=bg).convert("RGBA")
|
|
27
|
+
if transparent:
|
|
28
|
+
# Alpha = 0 wherever the pixel matches the background colour.
|
|
29
|
+
solid_bg = Image.new("RGB", img.size, bg)
|
|
30
|
+
alpha = ImageChops.difference(img.convert("RGB"), solid_bg).convert("L").point(lambda v: 255 if v else 0)
|
|
31
|
+
img.putalpha(alpha)
|
|
32
|
+
img = img.resize((size, size), Image.NEAREST)
|
|
33
|
+
buf = io.BytesIO()
|
|
34
|
+
img.save(buf, format="PNG")
|
|
35
|
+
return buf.getvalue()
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _payload(kind: str) -> str:
|
|
39
|
+
if kind == "Text / URL":
|
|
40
|
+
return st.text_area("Text or URL", placeholder="https://example.com", key="qr_text")
|
|
41
|
+
if kind == "Contact (vCard)":
|
|
42
|
+
left, right = st.columns(2)
|
|
43
|
+
name = left.text_input("Full name")
|
|
44
|
+
phone = left.text_input("Phone")
|
|
45
|
+
email = left.text_input("Email")
|
|
46
|
+
org = right.text_input("Organisation")
|
|
47
|
+
title = right.text_input("Job title")
|
|
48
|
+
url = right.text_input("Website")
|
|
49
|
+
if not any((name, phone, email, org, title, url)):
|
|
50
|
+
return ""
|
|
51
|
+
return "\n".join([
|
|
52
|
+
"BEGIN:VCARD", "VERSION:3.0", f"FN:{name}", f"N:{name}", f"ORG:{org}", f"TITLE:{title}",
|
|
53
|
+
f"TEL:{phone}", f"EMAIL:{email}", f"URL:{url}", "END:VCARD",
|
|
54
|
+
])
|
|
55
|
+
left, right = st.columns(2)
|
|
56
|
+
ssid = left.text_input("Network name (SSID)")
|
|
57
|
+
password = right.text_input("Password", type="password")
|
|
58
|
+
security = left.selectbox("Security", ["WPA", "WEP", "nopass"])
|
|
59
|
+
hidden = right.checkbox("Hidden network")
|
|
60
|
+
if not ssid:
|
|
61
|
+
return ""
|
|
62
|
+
return f"WIFI:T:{security};S:{_escape_wifi(ssid)};P:{_escape_wifi(password)};H:{'true' if hidden else 'false'};;"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def render() -> None:
|
|
66
|
+
kind = st.radio("Content type", ["Text / URL", "Contact (vCard)", "Wi-Fi"], horizontal=True)
|
|
67
|
+
data = _payload(kind)
|
|
68
|
+
|
|
69
|
+
with st.expander("Appearance"):
|
|
70
|
+
cols = st.columns(4)
|
|
71
|
+
fg = cols[0].color_picker("Foreground", "#000000")
|
|
72
|
+
bg = cols[1].color_picker("Background", "#FFFFFF")
|
|
73
|
+
size = cols[2].slider("Size (px)", 200, 1000, 400, 50)
|
|
74
|
+
level = cols[3].selectbox("Error correction", list(ERROR_LEVELS), index=1)
|
|
75
|
+
transparent = st.checkbox("Transparent background")
|
|
76
|
+
|
|
77
|
+
if not data.strip():
|
|
78
|
+
st.info("Enter some content to generate a QR code.")
|
|
79
|
+
return
|
|
80
|
+
|
|
81
|
+
png = build_qr(data, fg, bg, transparent, size, ERROR_LEVELS[level])
|
|
82
|
+
st.image(png, width=min(size, 400))
|
|
83
|
+
st.download_button("Download PNG", png, "qr_code.png", "image/png", type="primary")
|