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.
Files changed (42) hide show
  1. mini_toolkit/__init__.py +3 -0
  2. mini_toolkit/__main__.py +3 -0
  3. mini_toolkit/app.py +111 -0
  4. mini_toolkit/cli.py +40 -0
  5. mini_toolkit/core/__init__.py +1 -0
  6. mini_toolkit/core/net.py +56 -0
  7. mini_toolkit/core/pdf.py +12 -0
  8. mini_toolkit/core/registry.py +75 -0
  9. mini_toolkit/core/ui.py +90 -0
  10. mini_toolkit/streamlit_config.toml +16 -0
  11. mini_toolkit/style/style.css +171 -0
  12. mini_toolkit/tools/__init__.py +1 -0
  13. mini_toolkit/tools/background_remover.py +65 -0
  14. mini_toolkit/tools/change_tracker.py +90 -0
  15. mini_toolkit/tools/chat_alert.py +42 -0
  16. mini_toolkit/tools/color_picker.py +40 -0
  17. mini_toolkit/tools/document_converter.py +121 -0
  18. mini_toolkit/tools/file_compressor.py +78 -0
  19. mini_toolkit/tools/github_trending.py +53 -0
  20. mini_toolkit/tools/hash_generator.py +43 -0
  21. mini_toolkit/tools/image_converter.py +63 -0
  22. mini_toolkit/tools/image_downloader.py +67 -0
  23. mini_toolkit/tools/image_enhancer.py +54 -0
  24. mini_toolkit/tools/image_watermarker.py +63 -0
  25. mini_toolkit/tools/ocr.py +93 -0
  26. mini_toolkit/tools/page_speed.py +45 -0
  27. mini_toolkit/tools/password_checker.py +53 -0
  28. mini_toolkit/tools/pdf_tools.py +83 -0
  29. mini_toolkit/tools/qr_code.py +83 -0
  30. mini_toolkit/tools/resume_builder.py +385 -0
  31. mini_toolkit/tools/seo_health.py +64 -0
  32. mini_toolkit/tools/signature.py +72 -0
  33. mini_toolkit/tools/ssl_checker.py +68 -0
  34. mini_toolkit/tools/text_to_speech.py +34 -0
  35. mini_toolkit/tools/text_utilities.py +36 -0
  36. mini_toolkit/tools/url_shortener.py +29 -0
  37. mini_toolkit/tools/website_status.py +39 -0
  38. streamlit_mini_toolkit-0.1.0.dist-info/METADATA +151 -0
  39. streamlit_mini_toolkit-0.1.0.dist-info/RECORD +42 -0
  40. streamlit_mini_toolkit-0.1.0.dist-info/WHEEL +4 -0
  41. streamlit_mini_toolkit-0.1.0.dist-info/entry_points.txt +2 -0
  42. 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")