6767 EnumCapturedResultItemType ,
6868 EnumErrorCode ,
6969 EnumImagePixelFormat ,
70- EnumLayoutElementSource ,
71- EnumLayoutPattern ,
7270 EnumPresetTemplate ,
7371 FileImageTag ,
74- LayoutAnalysisParameter ,
75- LayoutAnalyzer ,
7672 LicenseManager ,
7773 Quadrilateral ,
7874)
7975
76+ try :
77+ from dynamsoft_barcode_reader_bundle import (
78+ EnumLayoutElementSource ,
79+ EnumLayoutPattern ,
80+ LayoutAnalysisParameter ,
81+ LayoutAnalyzer ,
82+ )
83+ except ImportError :
84+ EnumLayoutElementSource = None
85+ EnumLayoutPattern = None
86+ LayoutAnalysisParameter = None
87+ LayoutAnalyzer = None
88+
8089LICENSE_KEY = (
8190 "DLS2eyJoYW5kc2hha2VDb2RlIjoiMjAwMDAxLTE2NDk4Mjk3OTI2MzUiLCJvcmdhbml6"
8291 "YXRpb25JRCI6IjIwMDAwMSIsInNlc3Npb25QYXNzd29yZCI6IndTcGR6Vm05WDJrcEQ5"
96105LAYOUT_DEEP_TEMPLATE_PATH = str (BASE_DIR / "GridDeepDecode.json" )
97106LAYOUT_DEEP_TEMPLATE_NAME = "GridDeepDecode"
98107LAYOUT_ROI_SCALE_FACTOR = 2.0
108+ LAYOUT_ANALYSIS_AVAILABLE = all (
109+ symbol is not None
110+ for symbol in (
111+ EnumLayoutElementSource ,
112+ EnumLayoutPattern ,
113+ LayoutAnalysisParameter ,
114+ LayoutAnalyzer ,
115+ )
116+ )
117+ LAYOUT_ANALYSIS_UNAVAILABLE_REASON = (
118+ ""
119+ if LAYOUT_ANALYSIS_AVAILABLE
120+ else "Layout analysis is unavailable in the installed Dynamsoft Barcode Reader Bundle version."
121+ )
99122
100123TEMPLATES : List [Tuple [str , str ]] = [
101124 ("Read Barcodes (default)" , EnumPresetTemplate .PT_READ_BARCODES .value ),
@@ -137,6 +160,7 @@ class FileScanMetrics:
137160 barcode_count : int = 0
138161 decode_elapsed_ms : Optional [int ] = None
139162 used_layout_analysis : bool = False
163+ template : Optional [str ] = None
140164
141165
142166@dataclass
@@ -273,6 +297,9 @@ def decode(self, image: np.ndarray, quad: Quadrilateral) -> Optional[BarcodeHit]
273297
274298
275299def decode_with_layout_analysis (image : np .ndarray ) -> Tuple [List [BarcodeHit ], Optional [str ]]:
300+ if not LAYOUT_ANALYSIS_AVAILABLE :
301+ return [], LAYOUT_ANALYSIS_UNAVAILABLE_REASON
302+
276303 common_result , error = _capture_layout_fast_scan (image )
277304 if error :
278305 return [], error
@@ -413,7 +440,7 @@ def render_detection_mats(file_path: str, page_records: List[PageData]) -> List[
413440class ScannerSignals (QObject ):
414441 fileStarted = Signal (str , int )
415442 pageReady = Signal (object )
416- fileMetricsReady = Signal (str , int , int , bool )
443+ fileMetricsReady = Signal (str , int , int , bool , str )
417444 fileFinished = Signal (str )
418445 allFinished = Signal ()
419446 error = Signal (str , str )
@@ -431,7 +458,7 @@ def __init__(
431458 super ().__init__ (parent )
432459 self .files = list (files )
433460 self .template = template
434- self .use_layout_analysis = use_layout_analysis
461+ self .use_layout_analysis = use_layout_analysis and LAYOUT_ANALYSIS_AVAILABLE
435462 # {(file_path, page_idx) -> PageData} - rendered pages we can reuse
436463 # so a template-switch re-decode does not re-render PDFs/TIFFs.
437464 self .cached_pages = cached_pages or {}
@@ -571,7 +598,7 @@ def run(self) -> None:
571598 file_elapsed_ms = int ((time .perf_counter () - file_start ) * 1000 )
572599 total_barcodes = sum (len (page_record .barcodes ) for page_record in page_records )
573600 self .signals .fileMetricsReady .emit (
574- file_path , total_barcodes , file_elapsed_ms , True
601+ file_path , total_barcodes , file_elapsed_ms , True , self . template
575602 )
576603 self .signals .fileFinished .emit (file_path )
577604 continue
@@ -592,7 +619,9 @@ def run(self) -> None:
592619 results = result_array .get_results () if result_array else None
593620 if not results :
594621 file_elapsed_ms = int ((time .perf_counter () - file_start ) * 1000 )
595- self .signals .fileMetricsReady .emit (file_path , 0 , file_elapsed_ms , False )
622+ self .signals .fileMetricsReady .emit (
623+ file_path , 0 , file_elapsed_ms , False , self .template
624+ )
596625 self .signals .fileFinished .emit (file_path )
597626 continue
598627
@@ -626,7 +655,7 @@ def run(self) -> None:
626655 page_records [0 ].decode_elapsed_ms = file_elapsed_ms
627656 self .signals .pageReady .emit (page_records [0 ])
628657 self .signals .fileMetricsReady .emit (
629- file_path , total_barcodes , file_elapsed_ms , False
658+ file_path , total_barcodes , file_elapsed_ms , False , self . template
630659 )
631660 self .signals .fileFinished .emit (file_path )
632661
@@ -899,6 +928,11 @@ def _build_ui(self) -> None:
899928 self ._layout_analysis_checkbox .toggled .connect (
900929 self ._on_layout_analysis_toggled
901930 )
931+ if not LAYOUT_ANALYSIS_AVAILABLE :
932+ self ._layout_analysis_checkbox .setEnabled (False )
933+ self ._layout_analysis_checkbox .setToolTip (
934+ LAYOUT_ANALYSIS_UNAVAILABLE_REASON
935+ )
902936 toolbar .addWidget (self ._layout_analysis_checkbox )
903937
904938 toolbar .addSeparator ()
@@ -1003,7 +1037,14 @@ def _init_license(self) -> None:
10031037 return
10041038 if code in (EnumErrorCode .EC_OK , EnumErrorCode .EC_LICENSE_CACHE_USED ):
10051039 self ._license_ok = True
1006- self ._status_label .setText ("License OK. Drop files/folders to scan." )
1040+ if LAYOUT_ANALYSIS_AVAILABLE :
1041+ self ._status_label .setText (
1042+ "License OK. Drop files/folders to scan. Check 'Layout Analysis' to enable grid analysis."
1043+ )
1044+ else :
1045+ self ._status_label .setText (
1046+ "License OK. Layout analysis is unavailable in this bundle version; standard decoding only."
1047+ )
10071048 else :
10081049 self ._license_ok = False
10091050 QMessageBox .warning (
@@ -1077,6 +1118,14 @@ def _on_fit_to_window(self) -> None:
10771118 self ._status_label .setText ("Fitted current page to window." )
10781119
10791120 def _on_layout_analysis_toggled (self , checked : bool ) -> None :
1121+ if checked and not LAYOUT_ANALYSIS_AVAILABLE :
1122+ self ._layout_analysis_checkbox .blockSignals (True )
1123+ self ._layout_analysis_checkbox .setChecked (False )
1124+ self ._layout_analysis_checkbox .blockSignals (False )
1125+ self ._layout_analysis_enabled = False
1126+ self ._status_label .setText (LAYOUT_ANALYSIS_UNAVAILABLE_REASON )
1127+ return
1128+
10801129 self ._layout_analysis_enabled = checked
10811130 file_path = self ._current_file_path ()
10821131 mode_label = "layout analysis" if checked else "standard decoding"
@@ -1227,11 +1276,13 @@ def _on_file_metrics_ready(
12271276 barcode_count : int ,
12281277 decode_elapsed_ms : int ,
12291278 used_layout_analysis : bool ,
1279+ template : str ,
12301280 ) -> None :
12311281 self ._file_metrics [file_path ] = FileScanMetrics (
12321282 barcode_count = barcode_count ,
12331283 decode_elapsed_ms = decode_elapsed_ms ,
12341284 used_layout_analysis = used_layout_analysis ,
1285+ template = template ,
12351286 )
12361287
12371288 item = self ._file_items .get (file_path )
@@ -1350,10 +1401,7 @@ def _on_tree_changed(self, current: Optional[QTreeWidgetItem], _prev) -> None:
13501401
13511402 file_path = current .data (0 , self .FILE_ROLE )
13521403 if file_path :
1353- metrics = self ._file_metrics .get (file_path )
1354- if metrics is not None and (
1355- metrics .used_layout_analysis != self ._layout_analysis_enabled
1356- ):
1404+ if self ._file_needs_redecode (file_path ):
13571405 mode_label = (
13581406 "layout analysis" if self ._layout_analysis_enabled else "standard decoding"
13591407 )
@@ -1405,23 +1453,38 @@ def _page_from_item(self, item: QTreeWidgetItem) -> Optional[PageData]:
14051453 return None
14061454 return self ._pages .get ((file_path , int (page_idx )))
14071455
1456+ def _decode_mode_label (self , file_path : str ) -> str :
1457+ metrics = self ._file_metrics .get (file_path )
1458+ if metrics is not None and metrics .used_layout_analysis :
1459+ return "Layout analysis"
1460+ return "Standard decoding"
1461+
1462+ def _file_needs_redecode (self , file_path : str ) -> bool :
1463+ metrics = self ._file_metrics .get (file_path )
1464+ if metrics is None :
1465+ return False
1466+ if self ._layout_analysis_enabled :
1467+ return not metrics .used_layout_analysis
1468+ return metrics .used_layout_analysis or metrics .template != self ._current_template
1469+
14081470 def _show_page (self , page : PageData ) -> None :
14091471 self .viewer .set_page (page )
14101472 self .results .clear ()
14111473
14121474 metrics = self ._file_metrics .get (page .file_path )
1475+ mode_label = self ._decode_mode_label (page .file_path )
14131476 decode_elapsed_ms = page .decode_elapsed_ms
14141477 decode_scope = "Decode"
14151478 if decode_elapsed_ms is None and metrics is not None :
14161479 decode_elapsed_ms = metrics .decode_elapsed_ms
14171480 decode_scope = "File decode"
14181481
1482+ info_text = f"[info] Mode: { mode_label } | Count: { len (page .barcodes )} barcode(s)"
14191483 if decode_elapsed_ms is not None :
1420- info_item = QListWidgetItem (
1421- f"[info] Count: { len (page .barcodes )} barcode(s) | { decode_scope } time: { decode_elapsed_ms } ms"
1422- )
1423- info_item .setForeground (QBrush (QColor ("#1c7ed6" )))
1424- self .results .addItem (info_item )
1484+ info_text += f" | { decode_scope } time: { decode_elapsed_ms } ms"
1485+ info_item = QListWidgetItem (info_text )
1486+ info_item .setForeground (QBrush (QColor ("#1c7ed6" )))
1487+ self .results .addItem (info_item )
14251488
14261489 if page .error :
14271490 err_item = QListWidgetItem (f"[error] { page .error } " )
@@ -1438,7 +1501,8 @@ def _show_page(self, page: PageData) -> None:
14381501 page_text = (
14391502 f"{ os .path .basename (page .file_path )} "
14401503 f"Page { page .page_index + 1 } / { page .total_pages } "
1441- f"{ len (page .barcodes )} barcode(s)"
1504+ f"{ len (page .barcodes )} barcode(s) "
1505+ f"Mode: { mode_label } "
14421506 )
14431507 if decode_elapsed_ms is not None :
14441508 page_text += f" { decode_scope } : { decode_elapsed_ms } ms"
0 commit comments