3939]
4040
4141
42+ def _resolve_hocr_language (language : dict [str , Any ]) -> str :
43+ """Resolve the language code to use in the hOCR export, falling back to 'en'.
44+
45+ Args:
46+ language: the page language dictionary `{"value": str | None, "confidence": float | None}`
47+
48+ Returns:
49+ the detected language code when available, 'en' otherwise
50+ """
51+ lang_value = language .get ("value" ) if isinstance (language , dict ) else None
52+ return lang_value if isinstance (lang_value , str ) and len (lang_value ) > 0 else "en"
53+
54+
55+ def _hocr_bbox (geometry : BoundingBox , width : int , height : int ) -> str :
56+ """Format a relative straight bounding box as an absolute hOCR `bbox` property string.
57+
58+ Args:
59+ geometry: the relative bounding box ((xmin, ymin), (xmax, ymax))
60+ width: the page width in pixels
61+ height: the page height in pixels
62+
63+ Returns:
64+ the hOCR `bbox` property string
65+ """
66+ (xmin , ymin ), (xmax , ymax ) = geometry
67+ return (
68+ f"bbox { int (round (xmin * width ))} { int (round (ymin * height ))} "
69+ f"{ int (round (xmax * width ))} { int (round (ymax * height ))} "
70+ )
71+
72+
4273class Element (NestedObject ):
4374 """Implements an abstract document element with exporting and text rendering capabilities"""
4475
@@ -518,7 +549,7 @@ def export_as_xml(self, file_title: str = "docTR - XML export (hOCR)") -> tuple[
518549 line_count : int = 1
519550 word_count : int = 1
520551 height , width = self .dimensions
521- language = self .language if "language" in self . language . keys () else "en"
552+ language = _resolve_hocr_language ( self .language )
522553 # Create the XML root element
523554 page_hocr = ETElement ("html" , attrib = {"xmlns" : "http://www.w3.org/1999/xhtml" , "xml:lang" : str (language )})
524555 # Create the header / SubElements of the root element
@@ -550,15 +581,14 @@ def export_as_xml(self, file_title: str = "docTR - XML export (hOCR)") -> tuple[
550581 for block in self .blocks :
551582 if len (block .geometry ) != 2 :
552583 raise TypeError ("XML export is only available for straight bounding boxes for now." )
553- ( xmin , ymin ), ( xmax , ymax ) = block . geometry
584+ block_bbox = _hocr_bbox ( block . geometry , width , height ) # type: ignore[arg-type]
554585 block_div = SubElement (
555586 page_div ,
556587 "div" ,
557588 attrib = {
558589 "class" : "ocr_carea" ,
559590 "id" : f"block_{ block_count } " ,
560- "title" : f"bbox { int (round (xmin * width ))} { int (round (ymin * height ))} \
561- { int (round (xmax * width ))} { int (round (ymax * height ))} " ,
591+ "title" : block_bbox ,
562592 },
563593 )
564594 paragraph = SubElement (
@@ -567,38 +597,37 @@ def export_as_xml(self, file_title: str = "docTR - XML export (hOCR)") -> tuple[
567597 attrib = {
568598 "class" : "ocr_par" ,
569599 "id" : f"par_{ block_count } " ,
570- "title" : f"bbox { int (round (xmin * width ))} { int (round (ymin * height ))} \
571- { int (round (xmax * width ))} { int (round (ymax * height ))} " ,
600+ "title" : block_bbox ,
572601 },
573602 )
574603 block_count += 1
575604 for line in block .lines :
576- (xmin , ymin ), (xmax , ymax ) = line .geometry
577605 # NOTE: baseline, x_size, x_descenders, x_ascenders is currently initalized to 0
578606 line_span = SubElement (
579607 paragraph ,
580608 "span" ,
581609 attrib = {
582610 "class" : "ocr_line" ,
583611 "id" : f"line_{ line_count } " ,
584- "title" : f"bbox { int (round (xmin * width ))} { int (round (ymin * height ))} \
585- { int (round (xmax * width ))} { int (round (ymax * height ))} ; \
586- baseline 0 0; x_size 0; x_descenders 0; x_ascenders 0" ,
612+ "title" : (
613+ f"{ _hocr_bbox (line .geometry , width , height )} ; " # type: ignore[arg-type]
614+ "baseline 0 0; x_size 0; x_descenders 0; x_ascenders 0"
615+ ),
587616 },
588617 )
589618 line_count += 1
590619 for word in line .words :
591- (xmin , ymin ), (xmax , ymax ) = word .geometry
592620 conf = word .confidence
593621 word_div = SubElement (
594622 line_span ,
595623 "span" ,
596624 attrib = {
597625 "class" : "ocrx_word" ,
598626 "id" : f"word_{ word_count } " ,
599- "title" : f"bbox { int (round (xmin * width ))} { int (round (ymin * height ))} \
600- { int (round (xmax * width ))} { int (round (ymax * height ))} ; \
601- x_wconf { int (round (conf * 100 ))} " ,
627+ "title" : (
628+ f"{ _hocr_bbox (word .geometry , width , height )} ; " # type: ignore[arg-type]
629+ f"x_wconf { int (round (conf * 100 ))} "
630+ ),
602631 },
603632 )
604633 # set the text
@@ -708,7 +737,7 @@ def export_as_xml(self, file_title: str = "docTR - XML export (hOCR)") -> tuple[
708737 p_idx = self .page_idx
709738 prediction_count : int = 1
710739 height , width = self .dimensions
711- language = self .language if "language" in self . language . keys () else "en"
740+ language = _resolve_hocr_language ( self .language )
712741 # Create the XML root element
713742 page_hocr = ETElement ("html" , attrib = {"xmlns" : "http://www.w3.org/1999/xhtml" , "xml:lang" : str (language )})
714743 # Create the header / SubElements of the root element
@@ -741,15 +770,14 @@ def export_as_xml(self, file_title: str = "docTR - XML export (hOCR)") -> tuple[
741770 for prediction in predictions :
742771 if len (prediction .geometry ) != 2 :
743772 raise TypeError ("XML export is only available for straight bounding boxes for now." )
744- ( xmin , ymin ), ( xmax , ymax ) = prediction . geometry
773+ prediction_bbox = _hocr_bbox ( prediction . geometry , width , height ) # type: ignore[arg-type]
745774 prediction_div = SubElement (
746775 body ,
747776 "div" ,
748777 attrib = {
749778 "class" : "ocr_carea" ,
750779 "id" : f"{ class_name } _prediction_{ prediction_count } " ,
751- "title" : f"bbox { int (round (xmin * width ))} { int (round (ymin * height ))} \
752- { int (round (xmax * width ))} { int (round (ymax * height ))} " ,
780+ "title" : prediction_bbox ,
753781 },
754782 )
755783 # NOTE: ocr_par, ocr_line and ocrx_word are the same because the KIE predictions contain only words
@@ -760,8 +788,7 @@ def export_as_xml(self, file_title: str = "docTR - XML export (hOCR)") -> tuple[
760788 attrib = {
761789 "class" : "ocr_par" ,
762790 "id" : f"{ class_name } _par_{ prediction_count } " ,
763- "title" : f"bbox { int (round (xmin * width ))} { int (round (ymin * height ))} \
764- { int (round (xmax * width ))} { int (round (ymax * height ))} " ,
791+ "title" : prediction_bbox ,
765792 },
766793 )
767794 line_span = SubElement (
@@ -770,9 +797,7 @@ def export_as_xml(self, file_title: str = "docTR - XML export (hOCR)") -> tuple[
770797 attrib = {
771798 "class" : "ocr_line" ,
772799 "id" : f"{ class_name } _line_{ prediction_count } " ,
773- "title" : f"bbox { int (round (xmin * width ))} { int (round (ymin * height ))} \
774- { int (round (xmax * width ))} { int (round (ymax * height ))} ; \
775- baseline 0 0; x_size 0; x_descenders 0; x_ascenders 0" ,
800+ "title" : f"{ prediction_bbox } ; baseline 0 0; x_size 0; x_descenders 0; x_ascenders 0" ,
776801 },
777802 )
778803 word_div = SubElement (
@@ -781,9 +806,7 @@ def export_as_xml(self, file_title: str = "docTR - XML export (hOCR)") -> tuple[
781806 attrib = {
782807 "class" : "ocrx_word" ,
783808 "id" : f"{ class_name } _word_{ prediction_count } " ,
784- "title" : f"bbox { int (round (xmin * width ))} { int (round (ymin * height ))} \
785- { int (round (xmax * width ))} { int (round (ymax * height ))} ; \
786- x_wconf { int (round (prediction .confidence * 100 ))} " ,
809+ "title" : f"{ prediction_bbox } ; x_wconf { int (round (prediction .confidence * 100 ))} " ,
787810 },
788811 )
789812 word_div .text = prediction .value
0 commit comments