Yaz Hobooti commited on
Commit
9a98b3f
·
1 Parent(s): 170c264

Add ChatGPT-recommended barcode debug function

Browse files

- Add debug_scan_pdf() function for comprehensive barcode detection diagnostics
- Renders PDF pages at 600/900/1200 DPI for resolution analysis
- Tests multiple variants: original, grayscale, binarized, and rotations
- Scans embedded images (XObjects) for barcodes
- Saves debug PNGs to inspect bar width and quality
- Helps identify if barcodes are too thin/low resolution
- Includes helper functions _binarize() and _decode_once()
- Comprehensive diagnostic tool for troubleshooting barcode detection issues

Files changed (1) hide show
  1. pdf_comparator.py +208 -212
pdf_comparator.py CHANGED
@@ -548,240 +548,150 @@ def find_misspell_boxes(
548
  return boxes
549
 
550
 
551
- # -------------------- Barcode / QR -----------------
552
- def ean_like_checksum_ok(digits: str) -> bool:
553
- if not digits.isdigit():
554
- return False
555
- n = len(digits)
556
- if n not in (8, 12, 13):
557
- return True
558
- nums = [int(c) for c in digits]
559
- if n == 8:
560
- body, check = nums[:7], nums[7]
561
- s = sum(body[i] * (3 if i % 2 == 0 else 1) for i in range(7))
562
- return (10 - (s % 10)) % 10 == check
563
- if n == 12:
564
- body, check = nums[:11], nums[11]
565
- s = sum(body[i] * (3 if i % 2 == 0 else 1) for i in range(11))
566
- return (10 - (s % 10)) % 10 == check
567
- if n == 13:
568
- body, check = nums[:12], nums[12]
569
- s = sum(body[i] * (1 if i % 2 == 0 else 3) for i in range(12))
570
- return (10 - (s % 10)) % 10 == check
571
- return True
572
-
573
- def validate_symbology(symbology: str, data: bytes) -> bool:
574
- """Validate barcode symbology with improved UPC-A/EAN-13 handling"""
575
- try:
576
- text = data.decode('utf-8', errors='ignore')
577
- except Exception:
578
- return False
579
-
580
- sym = (symbology or '').upper()
581
-
582
- if sym in ("EAN13", "EAN-13", "EAN8", "EAN-8", "UPCA", "UPC-A"):
583
- # Extract digits only
584
- digits = re.sub(r"\D", "", text)
585
-
586
- # Handle UPC-A reported as EAN-13 with leading 0
587
- if sym in ("EAN13", "EAN-13") and len(digits) == 13 and digits.startswith('0'):
588
- # Remove leading 0 to get UPC-A format
589
- digits = digits[1:]
590
-
591
- return ean_like_checksum_ok(digits)
592
-
593
- if sym in ("QRCODE", "QRCODEMODEL2", "QR-CODE"):
594
- return len(text) > 0
595
-
596
- # Default validation for other symbologies
597
- return len(text) > 0
598
 
599
- def boxes_from_rect(x: int, y: int, w: int, h: int) -> Box:
600
- return Box(y, x, y + h, x + w, w * h)
 
 
 
601
 
602
- def decode_with_variants(img: Image.Image):
603
- """Robust barcode/QR code detection with multiple variants"""
604
  if not HAS_BARCODE:
605
  return []
606
-
607
- results = []
608
-
609
- def do_decode(pil_img):
 
 
 
 
 
 
 
 
 
 
 
610
  try:
611
- dec = zbar_decode(pil_img)
612
- if dec:
613
- results.extend(dec)
614
  except Exception:
615
- pass
616
-
617
- # Try original image
618
- do_decode(img)
619
-
620
- # Try grayscale conversion
621
- if not results:
622
- do_decode(img.convert('L'))
623
-
624
- # Try scaling up (better for small barcodes)
625
- if not results:
626
- try:
627
- # Use LANCZOS for better quality in newer Pillow versions
628
- scaled_img = img.resize((img.width*2, img.height*2), Image.LANCZOS)
629
- do_decode(scaled_img)
630
- except AttributeError:
631
- # Fallback for older Pillow versions
632
- scaled_img = img.resize((img.width*2, img.height*2), Image.BICUBIC)
633
- do_decode(scaled_img)
634
-
635
- # Try different color modes
636
- if not results and img.mode != 'RGB':
637
- do_decode(img.convert('RGB'))
638
-
639
- # Try rotated versions (common for QR codes)
640
- if not results:
641
- for angle in [90, 180, 270]:
642
- try:
643
- rotated = img.rotate(angle, expand=True)
644
- do_decode(rotated)
645
- if results:
646
- break
647
- except Exception:
648
- continue
649
-
650
- return results
651
-
652
- def extract_pdf_images(pdf_path: str, max_pages: int = 5, dpi: int = 300) -> List[Image.Image]:
653
- """Extract images directly from PDF using PyMuPDF"""
654
- if not HAS_PYMUPDF:
655
  return []
656
-
657
  try:
658
- doc = fitz.open(pdf_path)
659
- images = []
660
-
661
- for page_num in range(min(len(doc), max_pages)):
662
- page = doc[page_num]
663
-
664
- # Get images from the page
665
- image_list = page.get_images()
666
-
667
- for img_index, img in enumerate(image_list):
668
- # Get the image data
669
- xref = img[0]
670
- pix = fitz.Pixmap(doc, xref)
671
-
672
- # Convert to PIL Image
673
- if pix.n - pix.alpha < 4: # GRAY or RGB
674
- img_data = pix.tobytes("ppm")
675
- pil_img = Image.open(io.BytesIO(img_data))
676
- images.append(pil_img)
677
- else: # CMYK: convert to RGB first
678
- pix1 = fitz.Pixmap(fitz.csRGB, pix)
679
- img_data = pix1.tobytes("ppm")
680
- pil_img = Image.open(io.BytesIO(img_data))
681
- images.append(pil_img)
682
- pix1 = None
683
- pix = None
684
-
685
- doc.close()
686
- return images
687
  except Exception:
688
  return []
689
 
690
- def find_barcode_boxes_and_info_from_pdf(
691
- pdf_path: str,
692
- *,
693
- max_pages: int = 5,
694
- image_size: Optional[Tuple[int, int]] = None,
695
- dpi: int = 300
696
- ) -> Tuple[List[Box], List[dict]]:
697
- """Find barcodes/QR codes by analyzing extracted PDF images directly"""
698
- if not HAS_BARCODE:
 
 
 
 
 
 
 
 
 
 
 
699
  return [], []
700
-
701
- boxes: List[Box] = []
702
- infos = []
703
-
704
  try:
705
  doc = fitz.open(pdf_path)
706
-
707
- for page_num in range(min(len(doc), max_pages)):
708
- page = doc[page_num]
709
-
710
- # Get page dimensions for coordinate conversion
711
- page_rect = page.rect
712
- pdf_width = page_rect.width
713
- pdf_height = page_rect.height
714
-
715
- # Render page to image for barcode detection
716
- mat = fitz.Matrix(dpi/72, dpi/72) # Scale factor for DPI
717
- pix = page.get_pixmap(matrix=mat)
718
- img_data = pix.tobytes("ppm")
719
- pil_img = Image.open(io.BytesIO(img_data))
720
-
721
- # Detect barcodes in the page image
722
- decodes = decode_with_variants(pil_img)
723
-
724
- for d in decodes:
 
 
 
725
  rect = d.rect
726
-
727
- # Convert coordinates if image_size is provided
728
- if image_size:
729
- img_width, img_height = image_size
730
- scale_x = img_width / pil_img.width
731
- scale_y = img_height / pil_img.height
732
-
733
- left = int(rect.left * scale_x)
734
- top = int(rect.top * scale_y) + (page_num * img_height)
735
- width = int(rect.width * scale_x)
736
- height = int(rect.height * scale_y)
737
- else:
738
- # Use PDF coordinates directly without arbitrary offset
739
- left = rect.left
740
- top = rect.top
741
- width = rect.width
742
- height = rect.height
743
-
744
- boxes.append(Box(
745
- y1=top,
746
- x1=left,
747
- y2=top + height,
748
- x2=left + width,
749
- area=width * height
750
- ))
751
-
752
- valid = validate_symbology(d.type, d.data)
753
  infos.append({
754
- 'type': d.type,
755
- 'data': (d.data.decode('utf-8', errors='ignore') if isinstance(d.data, (bytes, bytearray)) else str(d.data)),
756
- 'left': left, 'top': top, 'width': width, 'height': height,
757
- 'valid': bool(valid),
758
- 'page': page_num
759
  })
760
-
761
  doc.close()
762
-
763
  except Exception:
764
- # Fallback to original method if PDF processing fails
765
  return [], []
766
-
767
  return boxes, infos
768
 
769
- def find_barcode_boxes_and_info(img: Image.Image):
770
- """Legacy barcode detection (kept for fallback)"""
771
- decodes = decode_with_variants(img)
772
- boxes: List[Box] = []
773
- infos = []
774
- for d in decodes:
775
- rect = d.rect
776
- boxes.append(boxes_from_rect(rect.left, rect.top, rect.width, rect.height))
777
- valid = validate_symbology(d.type, d.data)
778
- infos.append({
779
- 'type': d.type,
780
- 'data': (d.data.decode('utf-8', errors='ignore') if isinstance(d.data, (bytes, bytearray)) else str(d.data)),
781
- 'left': rect.left, 'top': rect.top, 'width': rect.width, 'height': rect.height,
782
- 'valid': bool(valid)
783
- })
784
- return boxes, infos
785
 
786
  # -------------------- CMYK Panel -------------------
787
  def rgb_to_cmyk_array(img: Image.Image) -> np.ndarray:
@@ -977,6 +887,92 @@ def create_demo():
977
 
978
  return demo
979
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
980
  if __name__ == "__main__":
981
  demo = create_demo()
982
  demo.launch(
 
548
  return boxes
549
 
550
 
551
+ # --- Robust PDF barcode scan (page render + embedded images) ---
552
+ from typing import List, Tuple, Optional
553
+ from PIL import Image, ImageOps
554
+ import io, regex as re
555
+
556
+ try:
557
+ from pyzbar.pyzbar import decode as zbar_decode, ZBarSymbol
558
+ HAS_BARCODE = True
559
+ except Exception:
560
+ HAS_BARCODE = False
561
+ ZBarSymbol = None
562
+
563
+ try:
564
+ import fitz # PyMuPDF
565
+ HAS_PYMUPDF = True
566
+ except Exception:
567
+ HAS_PYMUPDF = False
568
+
569
+ try:
570
+ from pylibdmtx.pylibdmtx import decode as dmtx_decode # DataMatrix
571
+ HAS_DMTX = True
572
+ except Exception:
573
+ HAS_DMTX = False
574
+
575
+ # assumes you already have: class Box(y1, x1, y2, x2, area)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
576
 
577
+ def _binarize(pil_img: Image.Image) -> Image.Image:
578
+ g = ImageOps.grayscale(pil_img)
579
+ g = ImageOps.autocontrast(g)
580
+ # simple global threshold around midtone; adjust if needed
581
+ return g.point(lambda x: 255 if x > 140 else 0, mode='1').convert('L')
582
 
583
+ def _decode_pyzbar(img: Image.Image) -> list:
 
584
  if not HAS_BARCODE:
585
  return []
586
+ symbols = [ZBarSymbol.QRCODE, ZBarSymbol.EAN13, ZBarSymbol.EAN8, ZBarSymbol.UPCA, ZBarSymbol.CODE128] if ZBarSymbol else None
587
+ res = zbar_decode(img, symbols=symbols) if symbols else zbar_decode(img)
588
+ if res:
589
+ return res
590
+ # try grayscale, binarized, rotations, and 2x upscale
591
+ variants = [ImageOps.grayscale(img), _binarize(img)]
592
+ for v in variants:
593
+ res = zbar_decode(v, symbols=symbols) if symbols else zbar_decode(v)
594
+ if res: return res
595
+ for angle in (90, 180, 270):
596
+ r = v.rotate(angle, expand=True)
597
+ res = zbar_decode(r, symbols=symbols) if symbols else zbar_decode(r)
598
+ if res: return res
599
+ w, h = img.size
600
+ if max(w, h) < 1600:
601
  try:
602
+ from PIL import Image as _PIL
603
+ u = img.resize((w*2, h*2), resample=_PIL.Resampling.BICUBIC)
 
604
  except Exception:
605
+ u = img.resize((w*2, h*2), resample=Image.BICUBIC)
606
+ res = zbar_decode(u, symbols=symbols) if symbols else zbar_decode(u)
607
+ if res: return res
608
+ return []
609
+
610
+ def _decode_datamatrix(img: Image.Image) -> list:
611
+ if not HAS_DMTX:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
612
  return []
 
613
  try:
614
+ res = dmtx_decode(ImageOps.grayscale(img))
615
+ # shape into pyzbar-like objects
616
+ outs = []
617
+ for r in res:
618
+ rect = r.rect # (left, top, width, height)
619
+ outs.append(type("DM", (), {
620
+ "type": "DATAMATRIX",
621
+ "data": r.data,
622
+ "rect": type("R", (), {"left": rect.left, "top": rect.top, "width": rect.width, "height": rect.height})
623
+ }))
624
+ return outs
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
625
  except Exception:
626
  return []
627
 
628
+ def _decode_all(img: Image.Image) -> list:
629
+ out = _decode_pyzbar(img)
630
+ if not out:
631
+ out = _decode_datamatrix(img) or out
632
+ return out
633
+
634
+ def _pix_to_pil(pix) -> Image.Image:
635
+ # pix: fitz.Pixmap
636
+ if pix.alpha: # drop alpha; reduces zbar confusion
637
+ pix = fitz.Pixmap(pix, 0) # copy without alpha
638
+ # use grayscale to avoid color AA artifacts
639
+ try:
640
+ pix = fitz.Pixmap(fitz.csGRAY, pix)
641
+ except Exception:
642
+ pass
643
+ return Image.open(io.BytesIO(pix.tobytes("ppm")))
644
+
645
+ def find_barcode_boxes_and_info_from_pdf(pdf_path: str, *, max_pages: int = 5, dpi: int = 600) -> Tuple[List["Box"], List[dict]]:
646
+ """Render each page at high DPI + scan embedded images. Return (boxes, infos)."""
647
+ if not HAS_PYMUPDF:
648
  return [], []
649
+ boxes: List["Box"] = []
650
+ infos: List[dict] = []
 
 
651
  try:
652
  doc = fitz.open(pdf_path)
653
+ n_pages = min(len(doc), max_pages)
654
+ scale = dpi / 72.0
655
+ mat = fitz.Matrix(scale, scale)
656
+ for page_idx in range(n_pages):
657
+ page = doc[page_idx]
658
+
659
+ # A) Render the page raster (grayscale, high DPI)
660
+ pix = page.get_pixmap(matrix=mat, alpha=False)
661
+ img = _pix_to_pil(pix)
662
+ decs = _decode_all(img)
663
+
664
+ # B) Also try each embedded image/XObject as-is (often barcodes are placed as images)
665
+ for xref, *_rest in page.get_images(full=True):
666
+ try:
667
+ ipix = fitz.Pixmap(doc, xref)
668
+ pil = _pix_to_pil(ipix)
669
+ decs += _decode_all(pil)
670
+ except Exception:
671
+ pass
672
+
673
+ # Collect results
674
+ for d in decs:
675
  rect = d.rect
676
+ left, top, width, height = int(rect.left), int(rect.top), int(rect.width), int(rect.height)
677
+ boxes.append(Box(top, left, top + height, left + width, width * height))
678
+ # basic validation (you already have ean_like_checksum_ok / validate_symbology)
679
+ try:
680
+ payload = d.data.decode("utf-8", errors="ignore") if isinstance(d.data, (bytes, bytearray)) else str(d.data)
681
+ except Exception:
682
+ payload = ""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
683
  infos.append({
684
+ "type": getattr(d, "type", "UNKNOWN"),
685
+ "data": payload,
686
+ "left": left, "top": top, "width": width, "height": height,
687
+ "page": page_idx + 1,
 
688
  })
 
689
  doc.close()
 
690
  except Exception:
 
691
  return [], []
 
692
  return boxes, infos
693
 
694
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
695
 
696
  # -------------------- CMYK Panel -------------------
697
  def rgb_to_cmyk_array(img: Image.Image) -> np.ndarray:
 
887
 
888
  return demo
889
 
890
+ def _binarize(pil_img: Image.Image) -> Image.Image:
891
+ """Create a binarized (black/white) version of the image for better barcode detection"""
892
+ g = ImageOps.grayscale(pil_img)
893
+ g = ImageOps.autocontrast(g)
894
+ return g.point(lambda x: 255 if x > 140 else 0, mode='1').convert('L')
895
+
896
+ def _decode_once(img: Image.Image):
897
+ """Single decode attempt with common barcode symbols"""
898
+ if not HAS_BARCODE:
899
+ return []
900
+ syms = [ZBarSymbol.QRCODE, ZBarSymbol.EAN13, ZBarSymbol.EAN8, ZBarSymbol.UPCA, ZBarSymbol.CODE128]
901
+ return zbar_decode(img, symbols=syms)
902
+
903
+ def debug_scan_pdf(pdf_path: str, outdir: str = "barcode_debug", max_pages=2):
904
+ """
905
+ Debug function to scan PDF at multiple DPIs and variants to diagnose barcode detection issues.
906
+
907
+ This function:
908
+ - Renders pages at 600/900/1200 DPI
909
+ - Tries grayscale, binarized, and rotated versions
910
+ - Scans embedded images (XObjects)
911
+ - Prints what it finds and writes debug PNGs
912
+ - Helps identify if barcodes are too thin/low resolution
913
+
914
+ Usage:
915
+ debug_scan_pdf("your.pdf", outdir="barcode_debug", max_pages=2)
916
+ """
917
+ if not (HAS_BARCODE and HAS_PYMUPDF):
918
+ print("ERROR: Missing dependencies (pyzbar or PyMuPDF)")
919
+ return
920
+
921
+ os.makedirs(outdir, exist_ok=True)
922
+ doc = fitz.open(pdf_path)
923
+
924
+ for dpi in (600, 900, 1200):
925
+ scale = dpi / 72.0
926
+ mat = fitz.Matrix(scale, scale)
927
+ print(f"\n=== DPI {dpi} ===")
928
+
929
+ for p in range(min(len(doc), max_pages)):
930
+ page = doc[p]
931
+ pix = page.get_pixmap(matrix=mat, alpha=False)
932
+ img = Image.open(io.BytesIO(pix.tobytes("ppm")))
933
+ img.save(f"{outdir}/page{p+1}_{dpi}.png")
934
+
935
+ # Try different image variants
936
+ variants = [
937
+ ("orig", img),
938
+ ("gray", ImageOps.grayscale(img)),
939
+ ("bin", _binarize(img)),
940
+ ]
941
+ found = []
942
+
943
+ for tag, v in variants:
944
+ r = _decode_once(v)
945
+ if r:
946
+ found.extend((tag, rr.type, rr.data) for rr in r)
947
+ else:
948
+ # Try rotations
949
+ for angle in (90, 180, 270):
950
+ rr = _decode_once(v.rotate(angle, expand=True))
951
+ if rr:
952
+ found.extend((f"{tag}_rot{angle}", rri.type, rri.data) for rri in rr)
953
+ break
954
+
955
+ print(f"Page {p+1}: {len(found)} hits at DPI {dpi} -> {found}")
956
+
957
+ # Scan embedded images too
958
+ imgs = page.get_images(full=True)
959
+ for ix, (xref, *_) in enumerate(imgs):
960
+ try:
961
+ ipix = fitz.Pixmap(doc, xref)
962
+ if ipix.alpha:
963
+ ipix = fitz.Pixmap(ipix, 0)
964
+ pil = Image.open(io.BytesIO(ipix.tobytes("ppm")))
965
+ pil.save(f"{outdir}/page{p+1}_embed{ix+1}.png")
966
+ rr = _decode_once(pil) or _decode_once(_binarize(pil))
967
+ if rr:
968
+ print(f" Embedded image {ix+1}: {[(r.type, r.data) for r in rr]}")
969
+ except Exception as e:
970
+ print(" Embedded image error:", e)
971
+
972
+ doc.close()
973
+ print(f"\nDebug images saved to: {outdir}/")
974
+ print("Open the PNGs and zoom in to check bar width. If narrow bars are <2px at 600 DPI, you need 900-1200 DPI.")
975
+
976
  if __name__ == "__main__":
977
  demo = create_demo()
978
  demo.launch(