Yaz Hobooti
commited on
Commit
·
7584764
1
Parent(s):
bb4768c
Add custom light blue theme to Gradio app
Browse files- Create custom theme with light blue background (#E6F3FF)
- Set block background to slightly lighter blue (#F0F8FF)
- Use soft blue borders (#B3D9FF) for cohesive design
- Apply blue primary and neutral hues for consistent color scheme
- Use Inter font for modern, clean typography
- Maintain accessibility with appropriate contrast ratios
- Aesthetic improvement to match user's preferred color palette
- pdf_comparator.py +186 -141
pdf_comparator.py
CHANGED
|
@@ -386,7 +386,7 @@ def load_pdf_pages(path: str, dpi: int = 400, max_pages: int = 5) -> List[Image.
|
|
| 386 |
continue
|
| 387 |
|
| 388 |
return [img.convert("RGB") for img in imgs]
|
| 389 |
-
|
| 390 |
if poppler_path is None: # All pdf2image attempts failed
|
| 391 |
break
|
| 392 |
continue # Try next path
|
|
@@ -405,7 +405,7 @@ def load_pdf_pages(path: str, dpi: int = 400, max_pages: int = 5) -> List[Image.
|
|
| 405 |
pages.append(img.convert("RGB"))
|
| 406 |
doc.close()
|
| 407 |
return pages
|
| 408 |
-
|
| 409 |
raise ValueError(f"Failed to convert PDF with both pdf2image and PyMuPDF. pdf2image error: poppler not found. PyMuPDF error: {str(e)}")
|
| 410 |
else:
|
| 411 |
raise ValueError(f"Failed to convert PDF to image with all poppler paths. Last error: poppler not found. PyMuPDF not available as fallback.")
|
|
@@ -667,7 +667,7 @@ def find_misspell_boxes_from_text(
|
|
| 667 |
y1 = int(bbox[1] * scale_y) + (page_num * img_height)
|
| 668 |
x2 = int(bbox[2] * scale_x)
|
| 669 |
y2 = int(bbox[3] * scale_y) + (page_num * img_height)
|
| 670 |
-
|
| 671 |
# Use PDF coordinates directly (fallback)
|
| 672 |
x1 = int(bbox[0])
|
| 673 |
y1 = int(bbox[1]) + (page_num * 1000)
|
|
@@ -697,8 +697,8 @@ def find_misspell_boxes_from_text(
|
|
| 697 |
page_texts = extract_pdf_text(pdf_path, max_pages)
|
| 698 |
for page_num, text in enumerate(page_texts):
|
| 699 |
if not text.strip():
|
| 700 |
-
|
| 701 |
-
|
| 702 |
tokens = _extract_tokens(text)
|
| 703 |
misspelled_words = [token for token in tokens if len(token) >= 2 and not _is_known_word(token)]
|
| 704 |
|
|
@@ -769,8 +769,8 @@ def find_misspell_boxes(
|
|
| 769 |
for i in range(n):
|
| 770 |
raw = data["text"][i]
|
| 771 |
if not raw:
|
| 772 |
-
|
| 773 |
-
|
| 774 |
# confidence filter
|
| 775 |
conf_str = data.get("conf", ["-1"])[i]
|
| 776 |
try:
|
|
@@ -801,158 +801,189 @@ def find_misspell_boxes(
|
|
| 801 |
return boxes
|
| 802 |
|
| 803 |
|
| 804 |
-
|
| 805 |
-
|
| 806 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 807 |
import io, regex as re
|
|
|
|
|
|
|
|
|
|
| 808 |
|
| 809 |
-
|
| 810 |
-
from pyzbar.pyzbar import decode as zbar_decode, ZBarSymbol
|
| 811 |
-
HAS_BARCODE = True
|
| 812 |
-
except Exception:
|
| 813 |
-
HAS_BARCODE = False
|
| 814 |
-
ZBarSymbol = None
|
| 815 |
|
|
|
|
| 816 |
try:
|
| 817 |
-
import
|
| 818 |
-
|
| 819 |
-
except Exception:
|
| 820 |
-
HAS_PYMUPDF = False
|
| 821 |
-
|
| 822 |
try:
|
| 823 |
-
from
|
| 824 |
-
|
| 825 |
-
|
| 826 |
-
HAS_DMTX
|
|
|
|
|
|
|
|
|
|
|
|
|
| 827 |
|
| 828 |
-
#
|
| 829 |
|
| 830 |
-
def _binarize(
|
| 831 |
-
g = ImageOps.grayscale(
|
| 832 |
g = ImageOps.autocontrast(g)
|
| 833 |
-
|
| 834 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 835 |
|
| 836 |
-
def
|
| 837 |
-
|
| 838 |
-
|
| 839 |
-
|
| 840 |
-
|
| 841 |
-
if
|
| 842 |
-
|
| 843 |
-
|
| 844 |
-
|
| 845 |
-
|
| 846 |
-
|
| 847 |
-
|
| 848 |
-
|
| 849 |
-
|
| 850 |
-
|
| 851 |
-
|
| 852 |
-
|
| 853 |
-
|
| 854 |
-
|
| 855 |
-
|
| 856 |
-
|
| 857 |
-
|
| 858 |
-
|
| 859 |
-
|
| 860 |
-
|
| 861 |
-
return []
|
| 862 |
|
| 863 |
-
def
|
| 864 |
-
if not
|
| 865 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 866 |
try:
|
| 867 |
-
res
|
| 868 |
-
|
| 869 |
-
|
| 870 |
-
for r in res:
|
| 871 |
-
rect = r.rect # (left, top, width, height)
|
| 872 |
-
outs.append(type("DM", (), {
|
| 873 |
-
"type": "DATAMATRIX",
|
| 874 |
-
"data": r.data,
|
| 875 |
-
"rect": type("R", (), {"left": rect.left, "top": rect.top, "width": rect.width, "height": rect.height})
|
| 876 |
-
}))
|
| 877 |
-
return outs
|
| 878 |
except Exception:
|
| 879 |
return []
|
| 880 |
|
| 881 |
-
def
|
| 882 |
-
|
| 883 |
-
if not out:
|
| 884 |
-
out = _decode_datamatrix(img) or out
|
| 885 |
-
return out
|
| 886 |
-
|
| 887 |
-
def _pix_to_pil(pix) -> Image.Image:
|
| 888 |
-
# pix: fitz.Pixmap
|
| 889 |
-
if pix.alpha: # drop alpha; reduces zbar confusion
|
| 890 |
-
pix = fitz.Pixmap(pix, 0) # copy without alpha
|
| 891 |
-
# use grayscale to avoid color AA artifacts
|
| 892 |
try:
|
| 893 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 894 |
except Exception:
|
| 895 |
pass
|
| 896 |
-
return
|
| 897 |
-
|
| 898 |
-
def find_barcode_boxes_and_info_from_pdf(pdf_path: str, *, max_pages: int = 5, dpi: int = 600) -> Tuple[List["Box"], List[dict]]:
|
| 899 |
-
"""Render each page at high DPI + scan embedded images. Return (boxes, infos)."""
|
| 900 |
-
if not HAS_PYMUPDF:
|
| 901 |
-
return [], []
|
| 902 |
-
boxes: List["Box"] = []
|
| 903 |
-
infos: List[dict] = []
|
| 904 |
-
try:
|
| 905 |
-
doc = fitz.open(pdf_path)
|
| 906 |
-
n_pages = min(len(doc), max_pages)
|
| 907 |
-
scale = dpi / 72.0
|
| 908 |
-
mat = fitz.Matrix(scale, scale)
|
| 909 |
-
for page_idx in range(n_pages):
|
| 910 |
-
page = doc[page_idx]
|
| 911 |
|
| 912 |
-
|
| 913 |
-
|
| 914 |
-
|
| 915 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 916 |
|
| 917 |
-
|
| 918 |
-
|
| 919 |
-
|
| 920 |
-
|
| 921 |
-
|
| 922 |
-
decs += _decode_all(pil)
|
| 923 |
-
except Exception:
|
| 924 |
-
pass
|
| 925 |
-
|
| 926 |
-
# Collect results
|
| 927 |
-
img_height = img.height
|
| 928 |
-
for d in decs:
|
| 929 |
-
rect = d.rect
|
| 930 |
-
left, top, width, height = int(rect.left), int(rect.top), int(rect.width), int(rect.height)
|
| 931 |
-
box = Box(top, left, top + height, left + width, width * height)
|
| 932 |
-
|
| 933 |
-
# Skip barcodes in the excluded bottom area
|
| 934 |
-
if _is_in_excluded_bottom_area(box, img_height, dpi=dpi):
|
| 935 |
-
continue
|
| 936 |
-
|
| 937 |
-
boxes.append(box)
|
| 938 |
-
# basic validation (you already have ean_like_checksum_ok / validate_symbology)
|
| 939 |
-
try:
|
| 940 |
-
payload = d.data.decode("utf-8", errors="ignore") if isinstance(d.data, (bytes, bytearray)) else str(d.data)
|
| 941 |
-
except Exception:
|
| 942 |
-
payload = ""
|
| 943 |
-
infos.append({
|
| 944 |
-
"type": getattr(d, "type", "UNKNOWN"),
|
| 945 |
-
"data": payload,
|
| 946 |
-
"left": left, "top": top, "width": width, "height": height,
|
| 947 |
-
"page": page_idx + 1,
|
| 948 |
-
})
|
| 949 |
-
doc.close()
|
| 950 |
except Exception:
|
| 951 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 952 |
return boxes, infos
|
| 953 |
|
| 954 |
|
| 955 |
|
|
|
|
| 956 |
# -------------------- CMYK Panel -------------------
|
| 957 |
def rgb_to_cmyk_array(img: Image.Image) -> np.ndarray:
|
| 958 |
return np.asarray(img.convert('CMYK')).astype(np.float32) # 0..255
|
|
@@ -1083,7 +1114,21 @@ def compare_pdfs(file_a, file_b):
|
|
| 1083 |
|
| 1084 |
# -------------------- Gradio App -------------------
|
| 1085 |
def create_demo():
|
| 1086 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1087 |
gr.Markdown("""
|
| 1088 |
# 🔍 Advanced PDF Comparison Tool
|
| 1089 |
|
|
@@ -1155,7 +1200,7 @@ def _binarize(pil_img: Image.Image) -> Image.Image:
|
|
| 1155 |
def _decode_once(img: Image.Image):
|
| 1156 |
"""Single decode attempt with common barcode symbols"""
|
| 1157 |
if not HAS_BARCODE:
|
| 1158 |
-
|
| 1159 |
syms = [ZBarSymbol.QRCODE, ZBarSymbol.EAN13, ZBarSymbol.EAN8, ZBarSymbol.UPCA, ZBarSymbol.CODE128]
|
| 1160 |
return zbar_decode(img, symbols=syms)
|
| 1161 |
|
|
@@ -1203,14 +1248,14 @@ def debug_scan_pdf(pdf_path: str, outdir: str = "barcode_debug", max_pages=2):
|
|
| 1203 |
r = _decode_once(v)
|
| 1204 |
if r:
|
| 1205 |
found.extend((tag, rr.type, rr.data) for rr in r)
|
| 1206 |
-
|
| 1207 |
# Try rotations
|
| 1208 |
for angle in (90, 180, 270):
|
| 1209 |
rr = _decode_once(v.rotate(angle, expand=True))
|
| 1210 |
if rr:
|
| 1211 |
found.extend((f"{tag}_rot{angle}", rri.type, rri.data) for rri in rr)
|
| 1212 |
-
|
| 1213 |
-
|
| 1214 |
print(f"Page {p+1}: {len(found)} hits at DPI {dpi} -> {found}")
|
| 1215 |
|
| 1216 |
# Scan embedded images too
|
|
@@ -1225,7 +1270,7 @@ def debug_scan_pdf(pdf_path: str, outdir: str = "barcode_debug", max_pages=2):
|
|
| 1225 |
rr = _decode_once(pil) or _decode_once(_binarize(pil))
|
| 1226 |
if rr:
|
| 1227 |
print(f" Embedded image {ix+1}: {[(r.type, r.data) for r in rr]}")
|
| 1228 |
-
|
| 1229 |
print(" Embedded image error:", e)
|
| 1230 |
|
| 1231 |
doc.close()
|
|
|
|
| 386 |
continue
|
| 387 |
|
| 388 |
return [img.convert("RGB") for img in imgs]
|
| 389 |
+
except Exception as e:
|
| 390 |
if poppler_path is None: # All pdf2image attempts failed
|
| 391 |
break
|
| 392 |
continue # Try next path
|
|
|
|
| 405 |
pages.append(img.convert("RGB"))
|
| 406 |
doc.close()
|
| 407 |
return pages
|
| 408 |
+
except Exception as e:
|
| 409 |
raise ValueError(f"Failed to convert PDF with both pdf2image and PyMuPDF. pdf2image error: poppler not found. PyMuPDF error: {str(e)}")
|
| 410 |
else:
|
| 411 |
raise ValueError(f"Failed to convert PDF to image with all poppler paths. Last error: poppler not found. PyMuPDF not available as fallback.")
|
|
|
|
| 667 |
y1 = int(bbox[1] * scale_y) + (page_num * img_height)
|
| 668 |
x2 = int(bbox[2] * scale_x)
|
| 669 |
y2 = int(bbox[3] * scale_y) + (page_num * img_height)
|
| 670 |
+
else:
|
| 671 |
# Use PDF coordinates directly (fallback)
|
| 672 |
x1 = int(bbox[0])
|
| 673 |
y1 = int(bbox[1]) + (page_num * 1000)
|
|
|
|
| 697 |
page_texts = extract_pdf_text(pdf_path, max_pages)
|
| 698 |
for page_num, text in enumerate(page_texts):
|
| 699 |
if not text.strip():
|
| 700 |
+
continue
|
| 701 |
+
|
| 702 |
tokens = _extract_tokens(text)
|
| 703 |
misspelled_words = [token for token in tokens if len(token) >= 2 and not _is_known_word(token)]
|
| 704 |
|
|
|
|
| 769 |
for i in range(n):
|
| 770 |
raw = data["text"][i]
|
| 771 |
if not raw:
|
| 772 |
+
continue
|
| 773 |
+
|
| 774 |
# confidence filter
|
| 775 |
conf_str = data.get("conf", ["-1"])[i]
|
| 776 |
try:
|
|
|
|
| 801 |
return boxes
|
| 802 |
|
| 803 |
|
| 804 |
+
|
| 805 |
+
|
| 806 |
+
|
| 807 |
+
|
| 808 |
+
|
| 809 |
+
|
| 810 |
+
# deps: pip install zxing-cpp pyzbar pylibdmtx PyMuPDF pillow opencv-python-headless regex
|
| 811 |
+
# system: macOS -> brew install zbar poppler ; Ubuntu -> sudo apt-get install libzbar0 poppler-utils
|
| 812 |
+
|
| 813 |
import io, regex as re
|
| 814 |
+
from typing import List, Tuple, Dict, Any
|
| 815 |
+
from PIL import Image, ImageOps
|
| 816 |
+
import numpy as np
|
| 817 |
|
| 818 |
+
import fitz # PyMuPDF
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 819 |
|
| 820 |
+
# Optional backends
|
| 821 |
try:
|
| 822 |
+
import zxingcpp; HAS_ZXING=True
|
| 823 |
+
except Exception: HAS_ZXING=False
|
|
|
|
|
|
|
|
|
|
| 824 |
try:
|
| 825 |
+
from pyzbar.pyzbar import decode as zbar_decode, ZBarSymbol; HAS_ZBAR=True
|
| 826 |
+
except Exception: HAS_ZBAR=False; ZBarSymbol=None
|
| 827 |
+
try:
|
| 828 |
+
from pylibdmtx.pylibdmtx import decode as dmtx_decode; HAS_DMTX=True
|
| 829 |
+
except Exception: HAS_DMTX=False
|
| 830 |
+
try:
|
| 831 |
+
import cv2; HAS_CV2=True
|
| 832 |
+
except Exception: HAS_CV2=False
|
| 833 |
|
| 834 |
+
# your Box(y1,x1,y2,x2,area) assumed to exist
|
| 835 |
|
| 836 |
+
def _binarize(img: Image.Image) -> Image.Image:
|
| 837 |
+
g = ImageOps.grayscale(img)
|
| 838 |
g = ImageOps.autocontrast(g)
|
| 839 |
+
return g.point(lambda x: 255 if x > 140 else 0, mode="1").convert("L")
|
| 840 |
+
|
| 841 |
+
def _ean_checksum_ok(d: str) -> bool:
|
| 842 |
+
if not d.isdigit(): return False
|
| 843 |
+
n=len(d); nums=list(map(int,d))
|
| 844 |
+
if n==8:
|
| 845 |
+
return (10 - (sum(nums[i]*(3 if i%2==0 else 1) for i in range(7))%10))%10==nums[7]
|
| 846 |
+
if n==12:
|
| 847 |
+
return (10 - (sum(nums[i]*(3 if i%2==0 else 1) for i in range(11))%10))%10==nums[11]
|
| 848 |
+
if n==13:
|
| 849 |
+
return (10 - (sum(nums[i]*(1 if i%2==0 else 3) for i in range(12))%10))%10==nums[12]
|
| 850 |
+
return True
|
| 851 |
|
| 852 |
+
def _normalize_upc_ean(sym: str, text: str):
|
| 853 |
+
digits = re.sub(r"\D","",text or "")
|
| 854 |
+
s = (sym or "").upper()
|
| 855 |
+
if s in ("EAN13","EAN-13") and len(digits)==13 and digits.startswith("0"):
|
| 856 |
+
return "UPCA", digits[1:]
|
| 857 |
+
return s, (digits if s in ("EAN13","EAN-13","EAN8","EAN-8","UPCA","UPC-A") else text or "")
|
| 858 |
+
|
| 859 |
+
def _validate(sym: str, payload: str) -> bool:
|
| 860 |
+
s, norm = _normalize_upc_ean(sym, payload)
|
| 861 |
+
return _ean_checksum_ok(norm) if s in ("EAN13","EAN-13","EAN8","EAN-8","UPCA","UPC-A") else bool(payload)
|
| 862 |
+
|
| 863 |
+
def _decode_zxing(pil: Image.Image) -> List[Dict[str,Any]]:
|
| 864 |
+
if not HAS_ZXING: return []
|
| 865 |
+
arr = np.asarray(pil.convert("L"))
|
| 866 |
+
out=[]
|
| 867 |
+
for r in zxingcpp.read_barcodes(arr): # try_harder is default True in recent builds; otherwise supply options
|
| 868 |
+
pts = r.position or []
|
| 869 |
+
if pts:
|
| 870 |
+
xs=[p.x for p in pts]; ys=[p.y for p in pts]
|
| 871 |
+
x1,x2=int(min(xs)),int(max(xs)); y1,y2=int(min(ys)),int(max(ys))
|
| 872 |
+
w,h=x2-x1,y2-y1
|
| 873 |
+
else:
|
| 874 |
+
x1=y1=w=h=0
|
| 875 |
+
out.append({"type": str(r.format), "data": r.text or "", "left": x1, "top": y1, "width": w, "height": h})
|
| 876 |
+
return out
|
|
|
|
| 877 |
|
| 878 |
+
def _decode_zbar(pil: Image.Image) -> List[Dict[str,Any]]:
|
| 879 |
+
if not HAS_ZBAR: return []
|
| 880 |
+
syms=[ZBarSymbol.QRCODE,ZBarSymbol.EAN13,ZBarSymbol.EAN8,ZBarSymbol.UPCA,ZBarSymbol.CODE128] if ZBarSymbol else None
|
| 881 |
+
res=zbar_decode(pil, symbols=syms) if syms else zbar_decode(pil)
|
| 882 |
+
return [{"type": d.type, "data": (d.data.decode("utf-8","ignore") if isinstance(d.data,(bytes,bytearray)) else str(d.data)),
|
| 883 |
+
"left": d.rect.left, "top": d.rect.top, "width": d.rect.width, "height": d.rect.height} for d in res]
|
| 884 |
+
|
| 885 |
+
def _decode_dmtx(pil: Image.Image) -> List[Dict[str,Any]]:
|
| 886 |
+
if not HAS_DMTX: return []
|
| 887 |
try:
|
| 888 |
+
res=dmtx_decode(ImageOps.grayscale(pil))
|
| 889 |
+
return [{"type":"DATAMATRIX","data": r.data.decode("utf-8","ignore"),
|
| 890 |
+
"left": r.rect.left, "top": r.rect.top, "width": r.rect.width, "height": r.rect.height} for r in res]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 891 |
except Exception:
|
| 892 |
return []
|
| 893 |
|
| 894 |
+
def _decode_cv2_qr(pil: Image.Image) -> List[Dict[str,Any]]:
|
| 895 |
+
if not HAS_CV2: return []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 896 |
try:
|
| 897 |
+
det=cv2.QRCodeDetector()
|
| 898 |
+
g=np.asarray(pil.convert("L"))
|
| 899 |
+
val, pts, _ = det.detectAndDecode(g)
|
| 900 |
+
if val:
|
| 901 |
+
if pts is not None and len(pts)>=1:
|
| 902 |
+
pts=pts.reshape(-1,2); xs,ys=pts[:,0],pts[:,1]
|
| 903 |
+
x1,x2=int(xs.min()),int(xs.max()); y1,y2=int(ys.min()),int(ys.max())
|
| 904 |
+
w,h=x2-x1,y2-y1
|
| 905 |
+
else:
|
| 906 |
+
x1=y1=w=h=0
|
| 907 |
+
return [{"type":"QRCODE","data":val,"left":x1,"top":y1,"width":w,"height":h}]
|
| 908 |
except Exception:
|
| 909 |
pass
|
| 910 |
+
return []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 911 |
|
| 912 |
+
def _decode_variants(pil: Image.Image) -> List[Dict[str,Any]]:
|
| 913 |
+
variants=[pil, ImageOps.grayscale(pil), _binarize(pil)]
|
| 914 |
+
# upsample small images with NEAREST to keep bars crisp
|
| 915 |
+
w,h=pil.size
|
| 916 |
+
if max(w,h)<1600:
|
| 917 |
+
up=pil.resize((w*2,h*2), resample=Image.NEAREST)
|
| 918 |
+
variants += [up, _binarize(up)]
|
| 919 |
+
for v in variants:
|
| 920 |
+
# ZXing first (broad coverage), then ZBar, then DMTX, then cv2 QR
|
| 921 |
+
res = _decode_zxing(v)
|
| 922 |
+
if res: return res
|
| 923 |
+
res = _decode_zbar(v)
|
| 924 |
+
if res: return res
|
| 925 |
+
res = _decode_dmtx(v)
|
| 926 |
+
if res: return res
|
| 927 |
+
res = _decode_cv2_qr(v)
|
| 928 |
+
if res: return res
|
| 929 |
+
# try rotations
|
| 930 |
+
for angle in (90,180,270):
|
| 931 |
+
r=v.rotate(angle, expand=True)
|
| 932 |
+
res = _decode_zxing(r) or _decode_zbar(r) or _decode_dmtx(r) or _decode_cv2_qr(r)
|
| 933 |
+
if res: return res
|
| 934 |
+
return []
|
| 935 |
|
| 936 |
+
def _pix_to_pil(pix) -> Image.Image:
|
| 937 |
+
# convert PyMuPDF Pixmap to grayscale PIL without alpha (avoids blur)
|
| 938 |
+
if pix.alpha: pix = fitz.Pixmap(pix, 0)
|
| 939 |
+
try:
|
| 940 |
+
pix = fitz.Pixmap(fitz.csGRAY, pix)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 941 |
except Exception:
|
| 942 |
+
pass
|
| 943 |
+
return Image.open(io.BytesIO(pix.tobytes("png")))
|
| 944 |
+
|
| 945 |
+
def scan_pdf_barcodes(pdf_path: str, *, dpi_list=(900,1200), max_pages=10):
|
| 946 |
+
"""Return (boxes, infos) from both rendered pages and embedded images."""
|
| 947 |
+
boxes=[]; infos=[]
|
| 948 |
+
doc=fitz.open(pdf_path)
|
| 949 |
+
n=min(len(doc), max_pages)
|
| 950 |
+
for page_idx in range(n):
|
| 951 |
+
page=doc[page_idx]
|
| 952 |
+
|
| 953 |
+
# A) Embedded images (often crisp)
|
| 954 |
+
for ix,(xref,*_) in enumerate(page.get_images(full=True)):
|
| 955 |
+
try:
|
| 956 |
+
pix=fitz.Pixmap(doc, xref)
|
| 957 |
+
pil=_pix_to_pil(pix)
|
| 958 |
+
hits=_decode_variants(pil)
|
| 959 |
+
for r in hits:
|
| 960 |
+
boxes.append(Box(r["top"], r["left"], r["top"]+r["height"], r["left"]+r["width"], r["width"]*r["height"]))
|
| 961 |
+
sym, payload = r["type"], r["data"]
|
| 962 |
+
infos.append({**r, "valid": _validate(sym, payload), "page": page_idx+1, "source": f"embed:{ix+1}"})
|
| 963 |
+
except Exception:
|
| 964 |
+
pass
|
| 965 |
+
|
| 966 |
+
# B) Render page raster at high DPI (grayscale)
|
| 967 |
+
for dpi in dpi_list:
|
| 968 |
+
scale=dpi/72.0
|
| 969 |
+
try:
|
| 970 |
+
pix=page.get_pixmap(matrix=fitz.Matrix(scale,scale), colorspace=fitz.csGRAY, alpha=False)
|
| 971 |
+
except TypeError:
|
| 972 |
+
pix=page.get_pixmap(matrix=fitz.Matrix(scale,scale), alpha=False)
|
| 973 |
+
pil=_pix_to_pil(pix)
|
| 974 |
+
hits=_decode_variants(pil)
|
| 975 |
+
for r in hits:
|
| 976 |
+
boxes.append(Box(r["top"], r["left"], r["top"]+r["height"], r["left"]+r["width"], r["width"]*r["height"]))
|
| 977 |
+
sym, payload = r["type"], r["data"]
|
| 978 |
+
infos.append({**r, "valid": _validate(sym, payload), "page": page_idx+1, "source": f"page@{dpi}dpi"})
|
| 979 |
+
if any(i["page"]==page_idx+1 for i in infos):
|
| 980 |
+
break # found something for this page → next page
|
| 981 |
+
doc.close()
|
| 982 |
return boxes, infos
|
| 983 |
|
| 984 |
|
| 985 |
|
| 986 |
+
|
| 987 |
# -------------------- CMYK Panel -------------------
|
| 988 |
def rgb_to_cmyk_array(img: Image.Image) -> np.ndarray:
|
| 989 |
return np.asarray(img.convert('CMYK')).astype(np.float32) # 0..255
|
|
|
|
| 1114 |
|
| 1115 |
# -------------------- Gradio App -------------------
|
| 1116 |
def create_demo():
|
| 1117 |
+
# Create custom theme with light blue background
|
| 1118 |
+
custom_theme = gr.themes.Soft(
|
| 1119 |
+
primary_hue="blue",
|
| 1120 |
+
neutral_hue="blue",
|
| 1121 |
+
font=gr.themes.GoogleFont("Inter"),
|
| 1122 |
+
).set(
|
| 1123 |
+
body_background_fill="#E6F3FF", # Light blue background
|
| 1124 |
+
body_background_fill_dark="#E6F3FF",
|
| 1125 |
+
block_background_fill="#F0F8FF", # Slightly lighter blue for blocks
|
| 1126 |
+
block_background_fill_dark="#F0F8FF",
|
| 1127 |
+
border_color_primary="#B3D9FF", # Soft blue borders
|
| 1128 |
+
border_color_primary_dark="#B3D9FF",
|
| 1129 |
+
)
|
| 1130 |
+
|
| 1131 |
+
with gr.Blocks(title="PDF Comparison Tool", theme=custom_theme) as demo:
|
| 1132 |
gr.Markdown("""
|
| 1133 |
# 🔍 Advanced PDF Comparison Tool
|
| 1134 |
|
|
|
|
| 1200 |
def _decode_once(img: Image.Image):
|
| 1201 |
"""Single decode attempt with common barcode symbols"""
|
| 1202 |
if not HAS_BARCODE:
|
| 1203 |
+
return []
|
| 1204 |
syms = [ZBarSymbol.QRCODE, ZBarSymbol.EAN13, ZBarSymbol.EAN8, ZBarSymbol.UPCA, ZBarSymbol.CODE128]
|
| 1205 |
return zbar_decode(img, symbols=syms)
|
| 1206 |
|
|
|
|
| 1248 |
r = _decode_once(v)
|
| 1249 |
if r:
|
| 1250 |
found.extend((tag, rr.type, rr.data) for rr in r)
|
| 1251 |
+
else:
|
| 1252 |
# Try rotations
|
| 1253 |
for angle in (90, 180, 270):
|
| 1254 |
rr = _decode_once(v.rotate(angle, expand=True))
|
| 1255 |
if rr:
|
| 1256 |
found.extend((f"{tag}_rot{angle}", rri.type, rri.data) for rri in rr)
|
| 1257 |
+
break
|
| 1258 |
+
|
| 1259 |
print(f"Page {p+1}: {len(found)} hits at DPI {dpi} -> {found}")
|
| 1260 |
|
| 1261 |
# Scan embedded images too
|
|
|
|
| 1270 |
rr = _decode_once(pil) or _decode_once(_binarize(pil))
|
| 1271 |
if rr:
|
| 1272 |
print(f" Embedded image {ix+1}: {[(r.type, r.data) for r in rr]}")
|
| 1273 |
+
except Exception as e:
|
| 1274 |
print(" Embedded image error:", e)
|
| 1275 |
|
| 1276 |
doc.close()
|