Generate SSD OCR traversal from typed Jerboa

ober

af938acca3f0d0b44e6ce4de295896d7c502b634

diff --git a/templates/ssd-review.ss b/templates/ssd-review.ss
index a900983..30c3468 100644
--- a/templates/ssd-review.ss
+++ b/templates/ssd-review.ss
@@ -10851,9 +10851,109 @@
 	                (let ((ink (shrinkRectToInk (nullable-get bitmap) rect)))
 	                  (if (nullable-null? ink)
 	                    rect
-	                    (nullable-get ink)))
+                (nullable-get ink)))
 	                (nullable-get box)))))))
 
+    (typed-kotlin-file "com/sfb/ssdreview/SsdOcrCollect.kt"
+      (kotlin-imports (android graphics Rect)
+                      (com google mlkit vision text Text Element)
+                      (com google mlkit vision text Text Line)
+                      (com google mlkit vision text Text TextBlock))
+      (typed-library (com sfb ssdreview)
+        (export ssdOcrCollectWordsFromBlocks)
+        (type Element)
+        (type Int32)
+        (type Line)
+        (type OcrWord)
+        (type Rect)
+        (type TextBlock)
+        (extern (textBlockLines (block : TextBlock)) : (List Line)
+          (kotlin-member-get lines))
+        (extern (textLineElements (line : Line)) : (List Element)
+          (kotlin-member-get elements))
+        (extern (textBlockBoundingBox (block : TextBlock)) : (Nullable Rect)
+          (kotlin-member-get boundingBox))
+        (extern (textLineBoundingBox (line : Line)) : (Nullable Rect)
+          (kotlin-member-get boundingBox))
+        (extern (textElementBoundingBox (element : Element)) : (Nullable Rect)
+          (kotlin-member-get boundingBox))
+        (extern (textElementText (element : Element)) : String
+          (kotlin-member-get text))
+        (extern (rectLeft (rect : Rect)) : Int32
+          (kotlin-member-get left))
+        (extern (rectTop (rect : Rect)) : Int32
+          (kotlin-member-get top))
+        (extern (rectRight (rect : Rect)) : Int32
+          (kotlin-member-get right))
+        (extern (rectBottom (rect : Rect)) : Int32
+          (kotlin-member-get bottom))
+        (def (ssdOcrCollectElementWord!
+               (words : (MutableList OcrWord))
+               (block : TextBlock)
+               (line : Line)
+               (element : Element)) : Unit
+          (let ((maybeBox
+                  (ocrBoundingBoxOrEmpty
+                    (textElementBoundingBox element)
+                    (textLineBoundingBox line)
+                    (textBlockBoundingBox block))))
+            (if (ocrBoundingBoxPresent maybeBox)
+                (let ((box (nullable-get maybeBox))
+                      (text (string-trim (textElementText element))))
+                  (if (ocrTextPresent text)
+                      (mutable-list-add!
+                        words
+                        (ocrWordFromBounds
+                          text
+                          (rectLeft box)
+                          (rectTop box)
+                          (rectRight box)
+                          (rectBottom box)))
+                      (begin)))
+                (begin))))
+        (def (ssdOcrCollectLineWords!
+               (words : (MutableList OcrWord))
+               (block : TextBlock)
+               (line : Line)) : Unit
+          (let ((elements (textLineElements line)))
+            (begin
+              (for/fold ((ignored (int32 0)))
+                        ((i (in-range (int32 0) (list-size elements))))
+                (begin
+                  (ssdOcrCollectElementWord!
+                    words
+                    block
+                    line
+                    (list-ref elements i))
+                  ignored))
+              (begin))))
+        (def (ssdOcrCollectBlockWords!
+               (words : (MutableList OcrWord))
+               (block : TextBlock)) : Unit
+          (let ((lines (textBlockLines block)))
+            (begin
+              (for/fold ((ignored (int32 0)))
+                        ((i (in-range (int32 0) (list-size lines))))
+                (begin
+                  (ssdOcrCollectLineWords!
+                    words
+                    block
+                    (list-ref lines i))
+                  ignored))
+              (begin))))
+        (def (ssdOcrCollectWordsFromBlocks
+               (blocks : (List TextBlock))) : (MutableList OcrWord)
+          (let ((words (mutable-list-empty OcrWord)))
+            (begin
+              (for/fold ((ignored (int32 0)))
+                        ((i (in-range (int32 0) (list-size blocks))))
+                (begin
+                  (ssdOcrCollectBlockWords!
+                    words
+                    (list-ref blocks i))
+                  ignored))
+              words)))))
+
     (typed-kotlin-file "com/sfb/ssdreview/SsdVisionBridgeCalls.kt"
       (kotlin-imports (android graphics Bitmap))
       (typed-library (com sfb ssdreview)
@@ -10938,20 +11038,7 @@
        "        val recognizer = TextRecognition.getClient(TextRecognizerOptions.DEFAULT_OPTIONS)"
        "        return try {"
        "            val result = Tasks.await(recognizer.process(InputImage.fromBitmap(bitmap, 0)))"
-       "            val words = mutableListOf<OcrWord>()"
-       "            result.textBlocks.forEach { block ->"
-       "                block.lines.forEach { line ->"
-       "                    line.elements.forEach { element ->"
-       "                        val maybeBox = ocrBoundingBoxOrEmpty(element.boundingBox, line.boundingBox, block.boundingBox)"
-       "                        if (!ocrBoundingBoxPresent(maybeBox)) return@forEach"
-       "                        val box = checkNotNull(maybeBox)"
-       "                        val text = element.text.trim()"
-       "                        if (ocrTextPresent(text)) {"
-       "                            words.add(ocrWordFromBounds(text, box.left, box.top, box.right, box.bottom))"
-       "                        }"
-       "                    }"
-       "                }"
-       "            }"
+       "            val words = ssdOcrCollectWordsFromBlocks(result.textBlocks)"
        "            words"
        "        } catch (_: Exception) {"
        "            emptyList()"