Generate SSD review view from typed Jerboa

ober

703a4b232d2f1e60902396c7a79d6f25cd85282f

diff --git a/.build.yml b/.build.yml
index e6dcd56..c6ae137 100644
--- a/.build.yml
+++ b/.build.yml
@@ -5,7 +5,7 @@ packages:
   - make=4.4.1-r4
 sources:
   # Build dependency: full immutable commit, mirrored in dependencies.lock.json.
-  - "https://git.sr.ht/~lisp/jerboa#fbc5e8d648f2c9df4f1a9e347229b676a7c7d22e"
+  - "https://git.sr.ht/~lisp/jerboa#fec34fcdcdb3cd21f6e599c46c4304d940c8bb72"
   # The second source is the build subject selected by the SourceHut submitter.
   - https://git.sr.ht/~lisp/jerboa-android
 tasks:
@@ -14,6 +14,6 @@ tasks:
       test "$(apk info -v chez-scheme)" = chez-scheme-10.3.0-r2
       test "$(apk info -v git)" = git-2.54.0-r0
       test "$(apk info -v make)" = make-4.4.1-r4
-      test "$(git -C ../jerboa rev-parse HEAD)" = fbc5e8d648f2c9df4f1a9e347229b676a7c7d22e
-      test "$(git -C ../jerboa rev-parse 'HEAD^{tree}')" = 7166bcd63d031c3beefd2edc73c48b365476da3c
+      test "$(git -C ../jerboa rev-parse HEAD)" = fec34fcdcdb3cd21f6e599c46c4304d940c8bb72
+      test "$(git -C ../jerboa rev-parse 'HEAD^{tree}')" = 3a1320183132a0a63bdba65a3993387a40b9c9c8
       JERBOA="chez --libdirs .:../jerboa/lib --script" make test
diff --git a/dependencies.lock.json b/dependencies.lock.json
index 5b98cc3..60c0f81 100644
--- a/dependencies.lock.json
+++ b/dependencies.lock.json
@@ -11,8 +11,8 @@
   "generator_runtime": {
     "name": "jerboa",
     "repository": "https://git.sr.ht/~lisp/jerboa",
-    "commit": "fbc5e8d648f2c9df4f1a9e347229b676a7c7d22e",
-    "tree": "7166bcd63d031c3beefd2edc73c48b365476da3c"
+    "commit": "fec34fcdcdb3cd21f6e599c46c4304d940c8bb72",
+    "tree": "3a1320183132a0a63bdba65a3993387a40b9c9c8"
   },
   "assurance_tools": {
     "osv_scanner": {
diff --git a/scripts/verify-supply-chain.sh b/scripts/verify-supply-chain.sh
index d279f2c..10c4d03 100755
--- a/scripts/verify-supply-chain.sh
+++ b/scripts/verify-supply-chain.sh
@@ -3,8 +3,8 @@ set -eu
 
 repo=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd -P)
 lock="$repo/dependencies.lock.json"
-jerboa_commit=fbc5e8d648f2c9df4f1a9e347229b676a7c7d22e # gitsafe:ignore
-jerboa_tree=7166bcd63d031c3beefd2edc73c48b365476da3c # gitsafe:ignore
+jerboa_commit=fec34fcdcdb3cd21f6e599c46c4304d940c8bb72 # gitsafe:ignore
+jerboa_tree=3a1320183132a0a63bdba65a3993387a40b9c9c8 # gitsafe:ignore
 gradle_sha=20f1b1176237254a6fc204d8434196fa11a4cfb387567519c61556e8710aed78
 jdk_macos_sha=8fa1eff40bb637a33613b2ccb8b12c70dc3661cc22cf8e784943715769a05336
 jdk_linux_sha=d8afc263758141a66e0e3aafc321e783f7016696f4eaea067d340a269037d331
diff --git a/templates/ssd-review.ss b/templates/ssd-review.ss
index c39bf15..6da6996 100644
--- a/templates/ssd-review.ss
+++ b/templates/ssd-review.ss
@@ -11474,414 +11474,403 @@
                   (begin
                     (textRecognizerClose recognizer)
                     (mutable-list-empty OcrWord)))))))))
-    (kotlin-file-lines "com/sfb/ssdreview/SsdReviewView.kt"
-      (
-       "package com.sfb.ssdreview"
-       ""
-       "import android.content.Context"
-       "import android.graphics.Bitmap"
-       "import android.graphics.Canvas"
-       "import android.graphics.Color"
-       "import android.graphics.Matrix"
-       "import android.graphics.Paint"
-       "import android.graphics.RectF"
-       "import android.view.MotionEvent"
-       "import android.view.ScaleGestureDetector"
-       "import android.view.View"
-       ""
-       "class SsdReviewView(context: Context) : View(context) {"
-       "    var bitmap: Bitmap? = null"
-       "        set(value) {"
-       "            field = value"
-       "            fitImage()"
-       "        }"
-	       "    var session: SsdSession? = null"
-	       "        set(value) {"
-	       "            field = value"
-	       "            reviewViewSetSsdAreaFromNullableSession(this, value)"
-	       "            invalidate()"
-	       "        }"
-	       "    var selectMode: Boolean = false"
-	       "    var snapSelectionToCells: Boolean = true"
-	       "    var ssdArea: FloatArray? = null"
-	       "        set(value) {"
-	       "            field = value"
-	       "            invalidate()"
-	       "        }"
-	       "    var selectedGroupId: String? = null"
-       "        set(value) {"
-       "            field = value"
-       "            invalidate()"
-       "        }"
-       "    var onAreaSelected: (FloatArray, List<String>) -> Unit = { _, _ -> }"
-       "    var onGroupTapped: (String) -> Unit = {}"
-       "    var onStatus: (String) -> Unit = {}"
-       ""
-       "    private val matrix = Matrix()"
-       "    private val inverse = Matrix()"
-       "    private val paint = Paint(Paint.ANTI_ALIAS_FLAG)"
-       "    private val textPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {"
-       "        color = reviewLabelTextColor()"
-       "        textSize = 28f"
-       "        style = Paint.Style.FILL"
-       "        textAlign = Paint.Align.CENTER"
-       "    }"
-	       "    private val labelBgPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {"
-	       "        color = reviewLabelBackgroundColor()"
-	       "        style = Paint.Style.FILL"
-	       "    }"
-	       "    private val density = context.resources.displayMetrics.density"
-	       "    private val selectionOffsetY = 70f * density"
-	       "    internal var scale = 1f"
-       "    internal var offsetX = 0f"
-       "    internal var offsetY = 0f"
-       "    internal var rotationDegrees = 0"
-       "    internal var lastX = 0f"
-       "    internal var lastY = 0f"
-	       "    internal var dragStart: FloatArray? = null"
-	       "    internal var dragEnd: FloatArray? = null"
-	       "    internal var pointerX = 0f"
-	       "    internal var pointerY = 0f"
-	       "    internal var pointerActive = false"
-	       "    internal var moved = false"
-       ""
-	       "    private val scaler = reviewViewMakeScaleGestureDetector(context, this)"
-       ""
-       "    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {"
-       "        if (viewSizeChangeNeedsFitForBitmap(oldw, oldh, bitmap)) {"
-       "            fitImage()"
-	       "        } else {"
-	       "            clampOffsets()"
-	       "            updateMatrix()"
-	       "            invalidate()"
-	       "        }"
-       "    }"
-       ""
-       "    override fun onDraw(canvas: Canvas) {"
-       "        super.onDraw(canvas)"
-       "        canvas.drawColor(reviewCanvasBackgroundColor())"
-       "        if (!bitmapLoaded(bitmap)) return"
-       "        val img = checkNotNull(bitmap)"
-       "        canvas.drawBitmap(img, matrix, null)"
-       "        val saved = canvas.save()"
-       "        canvas.concat(matrix)"
-	       "        drawCells(canvas)"
-	       "        drawGroups(canvas)"
-	       "        drawSsdArea(canvas)"
-	       "        drawDrag(canvas)"
-	       "        canvas.restoreToCount(saved)"
-	       "        drawLabels(canvas)"
-	       "        drawTouchPointer(canvas)"
-	       "    }"
-	       ""
-	       "    override fun onTouchEvent(event: MotionEvent): Boolean {"
-		       "        if (touchShouldScale(selectMode)) {"
-	       "            scaler.onTouchEvent(event)"
-	       "        }"
-       "        if (touchShouldIgnoreMultiPointer(event.pointerCount)) return true"
-       "        if (motionActionIsDown(event.actionMasked, MotionEvent.ACTION_DOWN)) {"
-       "                parent.requestDisallowInterceptTouchEvent(true)"
-       "                reviewViewSetLastTouchPosition(this, event.x, event.y)"
-	       "                reviewViewSetMoved(this, false)"
-		       "                if (touchSelectionActive(selectMode)) {"
-	       "                    reviewViewSetPointerPosition(this, event.x, event.y)"
-	       "                    reviewViewSetPointerActive(this, true)"
-	       "                    val startPoint = selectionPoint(event)"
-	       "                    reviewViewSetDragStart(this, startPoint)"
-	       "                    reviewViewSetDragEnd(this, startPoint)"
-	       "                }"
-       "        } else if (motionActionIsMove(event.actionMasked, MotionEvent.ACTION_MOVE)) {"
-       "                val dx = event.x - lastX"
-       "                val dy = event.y - lastY"
-       "                if (touchMoveExceeded(dx, dy)) reviewViewSetMoved(this, true)"
-		       "                if (touchSelectionActive(selectMode)) {"
-	       "                    reviewViewSetPointerPosition(this, event.x, event.y)"
-	       "                    reviewViewSetPointerActive(this, true)"
-	       "                    reviewViewSetDragEnd(this, selectionPoint(event))"
-		       "                } else if (touchPanReady(selectMode, scaler.isInProgress)) {"
-	       "                    reviewViewTranslateViewportOffsets(this, offsetX, offsetY, dx, dy)"
-	       "                    clampOffsets()"
-	       "                    updateMatrix()"
-	       "                }"
-       "                reviewViewSetLastTouchPosition(this, event.x, event.y)"
-       "                invalidate()"
-       "        } else if (motionActionIsUpOrCancel(event.actionMasked, MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL)) {"
-       "                if (selectionDragReadyForPoints(selectMode, dragStart, dragEnd)) {"
-       "                    finishSelection()"
-       "                } else if (touchTapReady(selectMode, moved, motionActionIsUp(event.actionMasked, MotionEvent.ACTION_UP))) {"
-       "                    val point = viewToImage(event.x, event.y)"
-       "                    val tappedGroup = hitGroupIdForSession(session, point[0], point[1], hitGroupPad(scale))"
-       "                    if (nullableTextPresent(tappedGroup)) {"
-       "                        val tappedGroupId = nullableTextOrEmpty(tappedGroup)"
-       "                        reviewViewSelectGroupId(this, tappedGroupId)"
-       "                        onGroupTapped(tappedGroupId)"
-       "                    }"
-	       "                    performClick()"
-	       "                }"
-	       "                reviewViewSetPointerActive(this, false)"
-	       "                parent.requestDisallowInterceptTouchEvent(false)"
-       "        }"
-       "        return true"
-       "    }"
-       ""
-       "    override fun performClick(): Boolean {"
-       "        super.performClick()"
-       "        return true"
-       "    }"
-       ""
-       "    fun fitImage() {"
-       "        val fitViewport = fitImageViewportState(bitmap, width, height, rotationDegrees)"
-       "        if (!viewportStatePresent(fitViewport)) return"
-       "        val savedViewport = viewportStateOrEmpty(fitViewport)"
-       "        reviewViewSetViewportState(this, savedViewport)"
-	       "        clampOffsets()"
-	       "        updateMatrix()"
-	       "        invalidate()"
-	       "    }"
-       ""
-       "    fun rotateClockwise() {"
-       "        reviewViewSetRotationDegrees(this, nextRotationDegrees(rotationDegrees))"
-       "        fitImage()"
-       "    }"
-       ""
-       "    fun captureViewport(): ViewportState? {"
-       "        return captureViewportState(bitmap, scale, offsetX, offsetY, rotationDegrees)"
-       "    }"
-       ""
-       "    fun restoreViewport(viewport: ViewportState?) {"
-       "        val restoredViewport = restorableViewportState(bitmap, viewport, rotationDegrees)"
-       "        if (!viewportStatePresent(restoredViewport)) return"
-       "        val savedViewport = viewportStateOrEmpty(restoredViewport)"
-	       "        reviewViewSetViewportState(this, savedViewport)"
-	       "        clampOffsets()"
-	       "        updateMatrix()"
-	       "        invalidate()"
-	       "    }"
-	       ""
-	       "    fun centerOnGroup(group: SsdGroup) {"
-       "        if (bitmapViewNotReadyForBitmap(bitmap, width, height)) return"
-	       "        reviewViewSetScale(this, focusGroupScale(scale, width.toFloat(), height.toFloat(), group))"
-	       "        updateMatrix()"
-	       "        val groupCenter = groupCenterPoint(group)"
-	       "        val center = imageToView(groupCenter[0], groupCenter[1])"
-	       "        reviewViewTranslateViewportOffsets(this, offsetX, offsetY, viewportCenterOffsetDelta(width, center[0]), viewportCenterOffsetDelta(height, center[1]))"
-	       "        clampOffsets()"
-	       "        updateMatrix()"
-	       "        invalidate()"
-	       "    }"
-       ""
-       "    fun clearInteractionState() {"
-	       "        reviewViewSetSelectMode(this, false, snapSelectionToCells)"
-	       "        reviewViewClearInteractionDetails(this)"
-	       "        invalidate()"
-	       "    }"
-       ""
-       "    private fun finishSelection() {"
-       "        if (!nullableFloatArrayPresent(dragStart)) return"
-       "        if (!nullableFloatArrayPresent(dragEnd)) return"
-       "        val start = nullableFloatArrayOrEmpty(dragStart)"
-       "        val end = nullableFloatArrayOrEmpty(dragEnd)"
-       "        val rect = normalizeRect(floatArrayOf(start[0], start[1], end[0], end[1]))"
-       "        reviewViewClearDragPoints(this)"
-       "        invalidate()"
-	       "        val sess = session"
-	       "        if (sessionMissing(sess)) {"
-	       "            onStatus(\"Selection ignored: truth/detection is still loading\")"
-	       "            return"
-	       "        }"
-       "        val activeSession = ssdSessionOrEmpty(sess)"
-		       "        if (selectionRectTooSmall(rect, 4f)) {"
-	       "            onStatus(\"Selection ignored: drag a larger area\")"
-	       "            return"
-	       "        }"
-	       "        val matching = selectionMatchingCellsForMode(snapSelectionToCells, activeSession.cells, rect)"
-	       "        val snapped = if (selectionUsesRawRect(snapSelectionToCells)) {"
-	       "            rect"
-	       "        } else if (selectionHasMatches(matching.size)) {"
-	       "            bboxForCells(matching)"
-       "        } else {"
-       "            shrinkRectToBitmapContentOrOriginal(bitmap, rect)"
-       "        }"
-	       "        onStatus(selectionResultStatus(snapSelectionToCells, matching.size))"
-       "        onAreaSelected(snapped, cellIdsForSelection(matching))"
-       "        invalidate()"
-       "    }"
-       ""
-       "    private fun drawCells(canvas: Canvas) {"
-       "        if (sessionMissing(session)) return"
-       "        val sess = ssdSessionOrEmpty(session)"
-       "        paint.style = Paint.Style.STROKE"
-       "        paint.strokeWidth = cellStrokeWidth(scale)"
-       "        forEachSsdSessionCell(sess) { cell ->"
-       "            if (ssdSessionGroupContainsCellId(sess, selectedGroupId, cell.id)) {"
-       "                paint.color = reviewPrimaryColor()"
-       "                canvas.drawRect(cell.x, cell.y, cell.x + cell.w, cell.y + cell.h, paint)"
-       "            }"
-       "        }"
-       "    }"
-       ""
-       "    private fun drawGroups(canvas: Canvas) {"
-       "        if (sessionMissing(session)) return"
-       "        val sess = ssdSessionOrEmpty(session)"
-       "        paint.style = Paint.Style.STROKE"
-       "        paint.strokeWidth = groupBaseStrokeWidth(scale)"
-       "        forEachSsdSessionGroup(sess) { group ->"
-       "            val selected = groupIdSelected(group.id, selectedGroupId)"
-       "            paint.color = groupStrokeColor(selected, group.status)"
-       "            paint.strokeWidth = groupStrokeWidth(selected, scale)"
-       "            canvas.drawRect(group.bbox[0], group.bbox[1], group.bbox[2], group.bbox[3], paint)"
-       "            if (selected) {"
-       "                paint.style = Paint.Style.FILL"
-       "                paint.color = selectedGroupFillColor()"
-       "                canvas.drawRect(group.bbox[0], group.bbox[1], group.bbox[2], group.bbox[3], paint)"
-       "                paint.style = Paint.Style.STROKE"
-       "            }"
-	       "        }"
-	       "    }"
-	       ""
-	       "    private fun drawSsdArea(canvas: Canvas) {"
-	       "        if (!nullableFloatArrayPresent(ssdArea)) return"
-	       "        val area = nullableFloatArrayOrEmpty(ssdArea)"
-	       "        paint.style = Paint.Style.FILL"
-	       "        paint.color = ssdAreaFillColor()"
-	       "        canvas.drawRect(area[0], area[1], area[2], area[3], paint)"
-	       "        paint.style = Paint.Style.STROKE"
-	       "        paint.strokeWidth = ssdAreaStrokeWidth(scale)"
-	       "        paint.color = ssdAreaStrokeColor()"
-	       "        canvas.drawRect(area[0], area[1], area[2], area[3], paint)"
-	       "    }"
-	       ""
-       "    private fun drawLabels(canvas: Canvas) {"
-       "        if (sessionMissing(session)) return"
-       "        val sess = ssdSessionOrEmpty(session)"
-       "        forEachSsdSessionGroup(sess) { group ->"
-       "            val selected = groupIdSelected(group.id, selectedGroupId)"
-       "            val label = displayGroupCanvasLabel(group, selected)"
-       "            if (canvasLabelPresent(label)) {"
-       "            val center = floatArrayOf("
-       "                (group.bbox[0] + group.bbox[2]) / 2f,"
-       "                (group.bbox[1] + group.bbox[3]) / 2f"
-       "            )"
-       "            matrix.mapPoints(center)"
-       "            val textWidth = textPaint.measureText(label)"
-       "            val padX = 10f"
-       "            val padY = 7f"
-       "            canvas.save()"
-       "            canvas.drawRoundRect("
-       "                RectF("
-       "                    center[0] - textWidth / 2f - padX,"
-       "                    center[1] - 24f - padY,"
-       "                    center[0] + textWidth / 2f + padX,"
-       "                    center[1] + padY"
-       "                ),"
-       "                8f,"
-       "                8f,"
-       "                labelBgPaint"
-       "            )"
-       "            canvas.drawText(label, center[0], center[1] - 4f, textPaint)"
-       "            canvas.restore()"
-       "            }"
-       "        }"
-       "    }"
-       ""
-       "    private fun drawDrag(canvas: Canvas) {"
-       "        if (!nullableFloatArrayPresent(dragStart)) return"
-       "        if (!nullableFloatArrayPresent(dragEnd)) return"
-       "        val start = nullableFloatArrayOrEmpty(dragStart)"
-       "        val end = nullableFloatArrayOrEmpty(dragEnd)"
-       "        val rect = normalizeRect(floatArrayOf(start[0], start[1], end[0], end[1]))"
-       "        paint.style = Paint.Style.FILL"
-       "        paint.color = dragFillColor()"
-       "        canvas.drawRect(rect[0], rect[1], rect[2], rect[3], paint)"
-       "        paint.style = Paint.Style.STROKE"
-	       "        paint.strokeWidth = dragStrokeWidth(scale)"
-	       "        paint.color = reviewPrimaryColor()"
-	       "        canvas.drawRect(rect[0], rect[1], rect[2], rect[3], paint)"
-	       "        paint.strokeWidth = dragAnchorStrokeWidth(scale)"
-	       "        val arm = dragAnchorArmLength(scale)"
-	       "        canvas.drawLine(start[0] - arm, start[1], start[0] + arm, start[1], paint)"
-	       "        canvas.drawLine(start[0], start[1] - arm, start[0], start[1] + arm, paint)"
-	       "        canvas.drawCircle(start[0], start[1], dragAnchorRadius(scale), paint)"
-	       "    }"
-	       ""
-	       "    private fun drawTouchPointer(canvas: Canvas) {"
-		       "        if (touchPointerNotReady(pointerActive, selectMode)) return"
-	       "        if (!nullableFirstFloatArrayPresent(dragEnd, dragStart)) return"
-	       "        val anchor = nullableFirstFloatArrayOrEmpty(dragEnd, dragStart)"
-	       "        val viewAnchor = imageToView(anchor[0], anchor[1])"
-	       "        paint.style = Paint.Style.STROKE"
-	       "        paint.strokeWidth = touchPointerStrokeWidth()"
-	       "        paint.color = reviewPrimaryColor()"
-	       "        canvas.drawLine(pointerX, pointerY, viewAnchor[0], viewAnchor[1], paint)"
-	       "        paint.style = Paint.Style.FILL"
-	       "        paint.color = reviewPrimaryColor()"
-	       "        canvas.drawCircle(viewAnchor[0], viewAnchor[1], touchPointerOuterRadius(), paint)"
-	       "        paint.color = reviewWhiteColor()"
-	       "        canvas.drawCircle(viewAnchor[0], viewAnchor[1], touchPointerInnerRadius(), paint)"
-	       "    }"
-	       ""
-       "    private fun updateMatrix() {"
-       "        val img = bitmap"
-       "        matrix.reset()"
-       "        if (bitmapLoaded(img)) {"
-       "            val imageWidth = nullableBitmapWidthOrZero(img)"
-       "            val imageHeight = nullableBitmapHeightOrZero(img)"
-       "            if (rotationIs90(rotationDegrees)) {"
-       "                matrix.postRotate(90f)"
-       "                matrix.postTranslate(imageHeight.toFloat(), 0f)"
-       "            } else if (rotationIs180(rotationDegrees)) {"
-       "                matrix.postRotate(180f)"
-       "                matrix.postTranslate(imageWidth.toFloat(), imageHeight.toFloat())"
-       "            } else if (rotationIs270(rotationDegrees)) {"
-       "                matrix.postRotate(270f)"
-       "                matrix.postTranslate(0f, imageWidth.toFloat())"
-       "            }"
-       "        }"
-       "        matrix.postScale(scale, scale)"
-       "        matrix.postTranslate(offsetX, offsetY)"
-	       "        matrix.invert(inverse)"
-	       "    }"
-	       ""
-	       "    internal fun zoomAt(viewX: Float, viewY: Float, factor: Float) {"
-	       "        val before = viewToImage(viewX, viewY)"
-	       "        reviewViewSetScale(this, clampScale(scale * factor))"
-	       "        updateMatrix()"
-	       "        val after = imageToView(before[0], before[1])"
-	       "        reviewViewTranslateViewportOffsets(this, offsetX, offsetY, viewX - after[0], viewY - after[1])"
-	       "        clampOffsets()"
-	       "        updateMatrix()"
-	       "        invalidate()"
-	       "    }"
-	       ""
-	       "    private fun clampOffsets() {"
-	       "        val clamped = clampedViewportState(bitmap, width, height, rotationDegrees, scale, offsetX, offsetY)"
-	       "        if (!viewportStatePresent(clamped)) return"
-	       "        val viewport = viewportStateOrEmpty(clamped)"
-	       "        reviewViewSetViewportOffsets(this, viewport.offsetX, viewport.offsetY)"
-	       "    }"
-       ""
-       "    private fun viewToImage(x: Float, y: Float): FloatArray {"
-       "        val point = floatArrayOf(x, y)"
-       "        inverse.mapPoints(point)"
-       "        val img = bitmap"
-       "        if (bitmapLoaded(img)) {"
-       "            point[0] = clampImageCoordinate(point[0], nullableBitmapWidthOrZero(img).toFloat())"
-       "            point[1] = clampImageCoordinate(point[1], nullableBitmapHeightOrZero(img).toFloat())"
-       "        }"
-	       "        return point"
-	       "    }"
-	       ""
-	       "    private fun selectionPoint(event: MotionEvent): FloatArray ="
-	       "        viewToImage(event.x, event.y - selectionOffsetY)"
-	       ""
-	       "    private fun imageToView(x: Float, y: Float): FloatArray {"
-       "        val point = floatArrayOf(x, y)"
-       "        matrix.mapPoints(point)"
-       "        return point"
-       "    }"
-       ""
-       "}"
-       ))
+    (typed-kotlin-file "com/sfb/ssdreview/SsdReviewView.kt"
+      (kotlin-imports (android content Context)
+                      (android graphics Bitmap)
+                      (android graphics Canvas)
+                      (android graphics Matrix)
+                      (android graphics Paint)
+                      (android view MotionEvent)
+                      (android view ScaleGestureDetector)
+                      (android view View))
+      (typed-library (com sfb ssdreview)
+        (export SsdReviewView)
+        (type Context)
+        (type Bitmap)
+        (type Canvas)
+        (type Matrix)
+        (type Paint)
+        (type MotionEvent)
+        (type ScaleGestureDetector)
+        (type View)
+        (type SsdSession)
+        (type SsdGroup)
+        (type ViewportState)
+        (type Int32)
+        (type Float32)
+        (type FloatArray)
+        (extern (canvasDrawColor (canvas : Canvas) (color : Int32)) : Unit
+          (kotlin-member-call drawColor))
+        (extern (canvasDrawBitmap (canvas : Canvas)
+                                  (bitmap : Bitmap)
+                                  (imageMatrix : Matrix)
+                                  (paint : (Nullable Paint))) : Unit
+          (kotlin-member-call drawBitmap))
+        (extern (matrixReset (imageMatrix : Matrix)) : Unit
+          (kotlin-member-call reset))
+        (extern (matrixPostRotate (imageMatrix : Matrix) (degrees : Float32)) : Unit
+          (kotlin-member-call postRotate))
+        (extern (matrixPostTranslate (imageMatrix : Matrix) (dx : Float32) (dy : Float32)) : Unit
+          (kotlin-member-call postTranslate))
+        (extern (matrixPostScale (imageMatrix : Matrix) (sx : Float32) (sy : Float32)) : Unit
+          (kotlin-member-call postScale))
+        (extern (matrixInvert (imageMatrix : Matrix) (inverse : Matrix)) : Bool
+          (kotlin-member-call invert))
+        (extern (matrixMapPoints (imageMatrix : Matrix) (points : FloatArray)) : Unit
+          (kotlin-member-call mapPoints))
+        (extern (superOnDraw (canvas : Canvas)) : Unit
+          (kotlin-call super onDraw))
+        (extern (superPerformClick) : Bool
+          (kotlin-call super performClick))
+        (extern (reviewViewInvalidate (view : SsdReviewView)) : Unit
+          (kotlin-member-call invalidate))
+        (extern (viewWidth (view : SsdReviewView)) : Int32
+          (kotlin-member-get width))
+        (extern (viewHeight (view : SsdReviewView)) : Int32
+          (kotlin-member-get height))
+        (extern (bitmapLoaded (bitmap : (Nullable Bitmap))) : Bool
+          (kotlin-call bitmapLoaded))
+        (extern (nullableBitmapWidthOrZero (bitmap : (Nullable Bitmap))) : Int32
+          (kotlin-call nullableBitmapWidthOrZero))
+        (extern (nullableBitmapHeightOrZero (bitmap : (Nullable Bitmap))) : Int32
+          (kotlin-call nullableBitmapHeightOrZero))
+        (extern (reviewCanvasBackgroundColor) : Int32
+          (kotlin-call reviewCanvasBackgroundColor))
+        (extern (reviewViewMakeScaleGestureDetector
+                  (context : Context)
+                  (view : SsdReviewView)) : ScaleGestureDetector
+          (kotlin-call reviewViewMakeScaleGestureDetector))
+        (extern (reviewViewSetSsdAreaFromNullableSession
+                  (view : SsdReviewView)
+                  (session : (Nullable SsdSession))) : Unit
+          (kotlin-call reviewViewSetSsdAreaFromNullableSession))
+        (extern (reviewViewSetViewportState
+                  (view : SsdReviewView)
+                  (viewport : ViewportState)) : Unit
+          (kotlin-call reviewViewSetViewportState))
+        (extern (reviewViewSetViewportOffsets
+                  (view : SsdReviewView)
+                  (offsetX : Float32)
+                  (offsetY : Float32)) : Unit
+          (kotlin-call reviewViewSetViewportOffsets))
+        (extern (reviewViewSetScale
+                  (view : SsdReviewView)
+                  (scale : Float32)) : Unit
+          (kotlin-call reviewViewSetScale))
+        (extern (reviewViewSetRotationDegrees
+                  (view : SsdReviewView)
+                  (degrees : Int32)) : Unit
+          (kotlin-call reviewViewSetRotationDegrees))
+        (extern (reviewViewSetSelectMode
+                  (view : SsdReviewView)
+                  (enabled : Bool)
+                  (snapToCells : Bool)) : Unit
+          (kotlin-call reviewViewSetSelectMode))
+        (extern (reviewViewClearInteractionDetails
+                  (view : SsdReviewView)) : Unit
+          (kotlin-call reviewViewClearInteractionDetails))
+        (extern (fitImageViewportState
+                  (bitmap : (Nullable Bitmap))
+                  (width : Int32)
+                  (height : Int32)
+                  (rotation : Int32)) : (Nullable ViewportState)
+          (kotlin-call fitImageViewportState))
+        (extern (viewportStatePresent
+                  (viewport : (Nullable ViewportState))) : Bool
+          (kotlin-call viewportStatePresent))
+        (extern (viewportStateOrEmpty
+                  (viewport : (Nullable ViewportState))) : ViewportState
+          (kotlin-call viewportStateOrEmpty))
+        (extern (captureViewportState
+                  (bitmap : (Nullable Bitmap))
+                  (scale : Float32)
+                  (offsetX : Float32)
+                  (offsetY : Float32)
+                  (rotation : Int32)) : (Nullable ViewportState)
+          (kotlin-call captureViewportState))
+        (extern (restorableViewportState
+                  (bitmap : (Nullable Bitmap))
+                  (viewport : (Nullable ViewportState))
+                  (rotation : Int32)) : (Nullable ViewportState)
+          (kotlin-call restorableViewportState))
+        (extern (clampedViewportState
+                  (bitmap : (Nullable Bitmap))
+                  (width : Int32)
+                  (height : Int32)
+                  (rotation : Int32)
+                  (scale : Float32)
+                  (offsetX : Float32)
+                  (offsetY : Float32)) : (Nullable ViewportState)
+          (kotlin-call clampedViewportState))
+        (extern (viewSizeChangeNeedsFitForBitmap
+                  (oldw : Int32)
+                  (oldh : Int32)
+                  (bitmap : (Nullable Bitmap))) : Bool
+          (kotlin-call viewSizeChangeNeedsFitForBitmap))
+        (extern (rotationIs90 (degrees : Int32)) : Bool
+          (kotlin-call rotationIs90))
+        (extern (rotationIs180 (degrees : Int32)) : Bool
+          (kotlin-call rotationIs180))
+        (extern (rotationIs270 (degrees : Int32)) : Bool
+          (kotlin-call rotationIs270))
+        (extern (nextRotationDegrees (degrees : Int32)) : Int32
+          (kotlin-call nextRotationDegrees))
+        (extern (clampScale (scale : Float32)) : Float32
+          (kotlin-call clampScale))
+        (extern (bitmapViewNotReadyForBitmap
+                  (bitmap : (Nullable Bitmap))
+                  (width : Int32)
+                  (height : Int32)) : Bool
+          (kotlin-call bitmapViewNotReadyForBitmap))
+        (extern (focusGroupScale
+                  (scale : Float32)
+                  (width : Float32)
+                  (height : Float32)
+                  (group : SsdGroup)) : Float32
+          (kotlin-call focusGroupScale))
+        (extern (groupCenterPoint (group : SsdGroup)) : FloatArray
+          (kotlin-call groupCenterPoint))
+        (extern (viewportCenterOffsetDelta (size : Int32) (center : Float32)) : Float32
+          (kotlin-call viewportCenterOffsetDelta))
+        (extern (clampImageCoordinate
+                  (coord : Float32)
+                  (limit : Float32)) : Float32
+          (kotlin-call clampImageCoordinate))
+        (class SsdReviewView ((context : Context))
+          (extends View context)
+          (var bitmap : (Nullable Bitmap)
+            (nullable-none Bitmap)
+            (setter (value : (Nullable Bitmap))
+              (begin
+                (set! field value)
+                (fitImage))))
+          (var session : (Nullable SsdSession)
+            (nullable-none SsdSession)
+            (setter (value : (Nullable SsdSession))
+              (begin
+                (set! field value)
+                (reviewViewSetSsdAreaFromNullableSession this value)
+                (reviewViewInvalidate this))))
+          (var selectMode : Bool #f)
+          (var snapSelectionToCells : Bool #t)
+          (var ssdArea : (Nullable FloatArray)
+            (nullable-none FloatArray)
+            (setter (value : (Nullable FloatArray))
+              (begin
+                (set! field value)
+                (reviewViewInvalidate this))))
+          (var selectedGroupId : (Nullable String)
+            (nullable-none String)
+            (setter (value : (Nullable String))
+              (begin
+                (set! field value)
+                (reviewViewInvalidate this))))
+          (var onAreaSelected : (-> FloatArray (List String) Unit)
+            (lambda ((rect : FloatArray) (ids : (List String)))
+              (begin)))
+          (var onGroupTapped : (-> String Unit)
+            (lambda ((groupId : String))
+              (begin)))
+          (var onStatus : (-> String Unit)
+            (lambda ((text : String))
+              (begin)))
+          (val imageMatrix : Matrix (new Matrix))
+          (val inverse : Matrix (new Matrix))
+          (var scale : Float32 (float32 1.0))
+          (var offsetX : Float32 (float32 0.0))
+          (var offsetY : Float32 (float32 0.0))
+          (var rotationDegrees : Int32 (int32 0))
+          (var lastX : Float32 (float32 0.0))
+          (var lastY : Float32 (float32 0.0))
+          (var dragStart : (Nullable FloatArray) (nullable-none FloatArray))
+          (var dragEnd : (Nullable FloatArray) (nullable-none FloatArray))
+          (var pointerX : Float32 (float32 0.0))
+          (var pointerY : Float32 (float32 0.0))
+          (var pointerActive : Bool #f)
+          (var moved : Bool #f)
+          (val scaler : ScaleGestureDetector
+            (reviewViewMakeScaleGestureDetector context this))
+          (def (onSizeChanged
+                 (w : Int32)
+                 (h : Int32)
+                 (oldw : Int32)
+                 (oldh : Int32)) : Unit
+            (modifiers override)
+            (if (viewSizeChangeNeedsFitForBitmap oldw oldh bitmap)
+              (fitImage)
+              (begin
+                (clampOffsets)
+                (updateMatrix)
+                (reviewViewInvalidate this))))
+          (def (onDraw (canvas : Canvas)) : Unit
+            (modifiers override)
+            (begin
+              (superOnDraw canvas)
+              (canvasDrawColor canvas (reviewCanvasBackgroundColor))
+              (if (not (bitmapLoaded bitmap))
+                (return)
+                (begin))
+              (canvasDrawBitmap canvas (nullable-get bitmap) imageMatrix (nullable-none Paint))))
+          (def (onTouchEvent (event : MotionEvent)) : Bool
+            (modifiers override)
+            #t)
+          (def (performClick) : Bool
+            (modifiers override)
+            (begin
+              (superPerformClick)
+              #t))
+          (def (fitImage) : Unit
+            (let ((fitViewport
+                    (fitImageViewportState
+                      bitmap
+                      (viewWidth this)
+                      (viewHeight this)
+                      rotationDegrees)))
+              (begin
+                (if (not (viewportStatePresent fitViewport))
+                  (return)
+                  (begin))
+                (reviewViewSetViewportState this (viewportStateOrEmpty fitViewport))
+                (clampOffsets)
+                (updateMatrix)
+                (reviewViewInvalidate this))))
+          (def (rotateClockwise) : Unit
+            (begin
+              (reviewViewSetRotationDegrees this (nextRotationDegrees rotationDegrees))
+              (fitImage)))
+          (def (captureViewport) : (Nullable ViewportState)
+            (captureViewportState bitmap scale offsetX offsetY rotationDegrees))
+          (def (restoreViewport (viewport : (Nullable ViewportState))) : Unit
+            (let ((restoredViewport
+                    (restorableViewportState bitmap viewport rotationDegrees)))
+              (begin
+                (if (not (viewportStatePresent restoredViewport))
+                  (return)
+                  (begin))
+                (reviewViewSetViewportState this (viewportStateOrEmpty restoredViewport))
+                (clampOffsets)
+                (updateMatrix)
+                (reviewViewInvalidate this))))
+          (def (centerOnGroup (group : SsdGroup)) : Unit
+            (begin
+              (if (bitmapViewNotReadyForBitmap bitmap (viewWidth this) (viewHeight this))
+                (return)
+                (begin))
+              (reviewViewSetScale
+                this
+                (focusGroupScale
+                  scale
+                  (float32 (viewWidth this))
+                  (float32 (viewHeight this))
+                  group))
+              (updateMatrix)
+              (let ((groupCenter (groupCenterPoint group)))
+                (let ((center
+                        (imageToView
+                          (float-array-ref groupCenter (int32 0))
+                          (float-array-ref groupCenter (int32 1)))))
+                  (reviewViewSetViewportOffsets
+                    this
+                    (+ offsetX
+                       (viewportCenterOffsetDelta
+                         (viewWidth this)
+                         (float-array-ref center (int32 0))))
+                    (+ offsetY
+                       (viewportCenterOffsetDelta
+                         (viewHeight this)
+                         (float-array-ref center (int32 1)))))))
+              (clampOffsets)
+              (updateMatrix)
+              (reviewViewInvalidate this)))
+          (def (clearInteractionState) : Unit
+            (begin
+              (reviewViewSetSelectMode this #f snapSelectionToCells)
+              (reviewViewClearInteractionDetails this)
+              (reviewViewInvalidate this)))
+          (def (zoomAt (viewX : Float32) (viewY : Float32) (factor : Float32)) : Unit
+            (let ((before (viewToImage viewX viewY)))
+              (begin
+                (reviewViewSetScale this (clampScale (* scale factor)))
+                (updateMatrix)
+                (let ((after
+                        (imageToView
+                          (float-array-ref before (int32 0))
+                          (float-array-ref before (int32 1)))))
+                  (reviewViewSetViewportOffsets
+                    this
+                    (+ offsetX (- viewX (float-array-ref after (int32 0))))
+                    (+ offsetY (- viewY (float-array-ref after (int32 1))))))
+                (clampOffsets)
+                (updateMatrix)
+                (reviewViewInvalidate this))))
+          (def (clampOffsets) : Unit
+            (let ((clamped
+                    (clampedViewportState
+                      bitmap
+                      (viewWidth this)
+                      (viewHeight this)
+                      rotationDegrees
+                      scale
+                      offsetX
+                      offsetY)))
+              (begin
+                (if (not (viewportStatePresent clamped))
+                  (return)
+                  (begin))
+                (let ((viewport (viewportStateOrEmpty clamped)))
+                  (reviewViewSetViewportOffsets
+                    this
+                    (ViewportState-offsetX viewport)
+                    (ViewportState-offsetY viewport))))))
+          (def (viewToImage (x : Float32) (y : Float32)) : FloatArray
+            (let ((point (float-array x y)))
+              (begin
+                (matrixMapPoints inverse point)
+                (if (bitmapLoaded bitmap)
+                  (begin
+                    (float-array-set!
+                      point
+                      (int32 0)
+                      (clampImageCoordinate
+                        (float-array-ref point (int32 0))
+                        (float32 (nullableBitmapWidthOrZero bitmap))))
+                    (float-array-set!
+                      point
+                      (int32 1)
+                      (clampImageCoordinate
+                        (float-array-ref point (int32 1))
+                        (float32 (nullableBitmapHeightOrZero bitmap)))))
+                  (begin))
+                point)))
+          (def (imageToView (x : Float32) (y : Float32)) : FloatArray
+            (let ((point (float-array x y)))
+              (begin
+                (matrixMapPoints imageMatrix point)
+                point)))
+          (def (updateMatrix) : Unit
+            (begin
+              (matrixReset imageMatrix)
+              (if (bitmapLoaded bitmap)
+                (let ((imageWidth (nullableBitmapWidthOrZero bitmap))
+                      (imageHeight (nullableBitmapHeightOrZero bitmap)))
+                  (if (rotationIs90 rotationDegrees)
+                    (begin
+                      (matrixPostRotate imageMatrix (float32 90.0))
+                      (matrixPostTranslate imageMatrix (float32 imageHeight) (float32 0.0)))
+                    (if (rotationIs180 rotationDegrees)
+                      (begin
+                        (matrixPostRotate imageMatrix (float32 180.0))
+                        (matrixPostTranslate imageMatrix (float32 imageWidth) (float32 imageHeight)))
+                      (if (rotationIs270 rotationDegrees)
+                        (begin
+                          (matrixPostRotate imageMatrix (float32 270.0))
+                          (matrixPostTranslate imageMatrix (float32 0.0) (float32 imageWidth)))
+                        (begin)))))
+                (begin))
+              (matrixPostScale imageMatrix scale scale)
+              (matrixPostTranslate imageMatrix offsetX offsetY)
+              (matrixInvert imageMatrix inverse)
+              (begin))))))
     (kotlin-file-lines "com/sfb/ssdreview/TruthStore.kt"
       (
        "package com.sfb.ssdreview"