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

Files changed (1) hide show
  1. 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
- except Exception as e:
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
- 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,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
- else:
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
- 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,8 +769,8 @@ def find_misspell_boxes(
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,158 +801,189 @@ def find_misspell_boxes(
801
  return boxes
802
 
803
 
804
- # --- Robust PDF barcode scan (page render + embedded images) ---
805
- from typing import List, Tuple, Optional
806
- from PIL import Image, ImageOps
 
 
 
 
 
 
807
  import io, regex as re
 
 
 
808
 
809
- try:
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 fitz # PyMuPDF
818
- HAS_PYMUPDF = True
819
- except Exception:
820
- HAS_PYMUPDF = False
821
-
822
  try:
823
- from pylibdmtx.pylibdmtx import decode as dmtx_decode # DataMatrix
824
- HAS_DMTX = True
825
- except Exception:
826
- HAS_DMTX = False
 
 
 
 
827
 
828
- # assumes you already have: class Box(y1, x1, y2, x2, area)
829
 
830
- def _binarize(pil_img: Image.Image) -> Image.Image:
831
- g = ImageOps.grayscale(pil_img)
832
  g = ImageOps.autocontrast(g)
833
- # simple global threshold around midtone; adjust if needed
834
- return g.point(lambda x: 255 if x > 140 else 0, mode='1').convert('L')
 
 
 
 
 
 
 
 
 
 
835
 
836
- def _decode_pyzbar(img: Image.Image) -> list:
837
- if not HAS_BARCODE:
838
- return []
839
- symbols = [ZBarSymbol.QRCODE, ZBarSymbol.EAN13, ZBarSymbol.EAN8, ZBarSymbol.UPCA, ZBarSymbol.CODE128] if ZBarSymbol else None
840
- res = zbar_decode(img, symbols=symbols) if symbols else zbar_decode(img)
841
- if res:
842
- return res
843
- # try grayscale, binarized, rotations, and 2x upscale
844
- variants = [ImageOps.grayscale(img), _binarize(img)]
845
- for v in variants:
846
- res = zbar_decode(v, symbols=symbols) if symbols else zbar_decode(v)
847
- if res: return res
848
- for angle in (90, 180, 270):
849
- r = v.rotate(angle, expand=True)
850
- res = zbar_decode(r, symbols=symbols) if symbols else zbar_decode(r)
851
- if res: return res
852
- w, h = img.size
853
- if max(w, h) < 1600:
854
- try:
855
- from PIL import Image as _PIL
856
- u = img.resize((w*2, h*2), resample=_PIL.Resampling.BICUBIC)
857
- except Exception:
858
- u = img.resize((w*2, h*2), resample=Image.BICUBIC)
859
- res = zbar_decode(u, symbols=symbols) if symbols else zbar_decode(u)
860
- if res: return res
861
- return []
862
 
863
- def _decode_datamatrix(img: Image.Image) -> list:
864
- if not HAS_DMTX:
865
- return []
 
 
 
 
 
 
866
  try:
867
- res = dmtx_decode(ImageOps.grayscale(img))
868
- # shape into pyzbar-like objects
869
- outs = []
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 _decode_all(img: Image.Image) -> list:
882
- out = _decode_pyzbar(img)
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
- pix = fitz.Pixmap(fitz.csGRAY, pix)
 
 
 
 
 
 
 
 
 
 
894
  except Exception:
895
  pass
896
- return Image.open(io.BytesIO(pix.tobytes("ppm")))
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
- # A) Render the page raster (grayscale, high DPI)
913
- pix = page.get_pixmap(matrix=mat, alpha=False)
914
- img = _pix_to_pil(pix)
915
- decs = _decode_all(img)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
916
 
917
- # B) Also try each embedded image/XObject as-is (often barcodes are placed as images)
918
- for xref, *_rest in page.get_images(full=True):
919
- try:
920
- ipix = fitz.Pixmap(doc, xref)
921
- pil = _pix_to_pil(ipix)
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
- return [], []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- with gr.Blocks(title="PDF Comparison Tool", theme=gr.themes.Soft()) as demo:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- return []
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
- else:
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
- break
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
- except Exception as e:
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()