From 064fff041b0ad01302fe471f1bcd5f7b33985dd6 Mon Sep 17 00:00:00 2001 From: andreia Date: Wed, 10 Jun 2026 18:00:08 +0200 Subject: [PATCH 1/8] add common layout components for PDF rendering --- .../feature/pdf/render/PdfConfig.kt | 51 ++++++++ .../feature/pdf/render/PdfGeometry.kt | 42 +++++++ .../pdf/render/components/PageFooterLayout.kt | 60 ++++++++++ .../pdf/render/components/PageHeaderLayout.kt | 64 ++++++++++ .../pdf/render/components/QrBlockLayout.kt | 52 ++++++++ .../pdf/render/components/TableLayout.kt | 113 ++++++++++++++++++ 6 files changed, 382 insertions(+) create mode 100644 feature/pdf/src/commonMain/kotlin/org/groundplatform/feature/pdf/render/PdfConfig.kt create mode 100644 feature/pdf/src/commonMain/kotlin/org/groundplatform/feature/pdf/render/PdfGeometry.kt create mode 100644 feature/pdf/src/commonMain/kotlin/org/groundplatform/feature/pdf/render/components/PageFooterLayout.kt create mode 100644 feature/pdf/src/commonMain/kotlin/org/groundplatform/feature/pdf/render/components/PageHeaderLayout.kt create mode 100644 feature/pdf/src/commonMain/kotlin/org/groundplatform/feature/pdf/render/components/QrBlockLayout.kt create mode 100644 feature/pdf/src/commonMain/kotlin/org/groundplatform/feature/pdf/render/components/TableLayout.kt diff --git a/feature/pdf/src/commonMain/kotlin/org/groundplatform/feature/pdf/render/PdfConfig.kt b/feature/pdf/src/commonMain/kotlin/org/groundplatform/feature/pdf/render/PdfConfig.kt new file mode 100644 index 0000000000..a502c8dccd --- /dev/null +++ b/feature/pdf/src/commonMain/kotlin/org/groundplatform/feature/pdf/render/PdfConfig.kt @@ -0,0 +1,51 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.groundplatform.feature.pdf.render + +/** + * Dimensional and type-scale constants shared by the Android and iOS PDF renderers. Keeping these + * in commonMain prevents the two platforms from drifting on page size, margins, or type scale. + * + * All measurements are in PDF points (1/72 inch). + */ +internal object PdfConfig { + const val PAGE_WIDTH = 595 // A4 page width + const val PAGE_HEIGHT = 842 // A4 page height + const val MARGIN = 40 + const val TITLE_SIZE = 11f + const val BODY_SIZE = 11f + const val CAPTION_SIZE = 9f + const val LINE_SPACING = 4f + const val QR_SIZE = 200 + const val HEADER_COLUMN_GAP = 16 + const val TABLE_TASK_LABEL_RATIO = 0.35f + const val CELL_PADDING = 6 + const val BORDER_WIDTH = 0.5f + const val PHOTO_MAX_HEIGHT_RATIO = 0.35f + const val HEADER_BOTTOM_GAP = 28f + const val FOOTER_TOP_GAP = 28f + const val MAX_HEADER_VALUE_LINES = 1 + const val MAX_FOOTER_LINES = 1 + const val IMAGE_RENDER_DPI = 300f + const val USABLE_WIDTH = PAGE_WIDTH - 2 * MARGIN + const val TABLE_TASK_COLUMN_WIDTH = (USABLE_WIDTH * TABLE_TASK_LABEL_RATIO).toInt() + const val TABLE_TASK_TEXT_WIDTH = TABLE_TASK_COLUMN_WIDTH - 2 * CELL_PADDING + const val TABLE_ANSWER_TEXT_WIDTH = USABLE_WIDTH - TABLE_TASK_COLUMN_WIDTH - 2 * CELL_PADDING + const val PHOTO_MAX_HEIGHT = ((PAGE_HEIGHT - 2 * MARGIN) * PHOTO_MAX_HEIGHT_RATIO).toInt() + const val PAGE_NUMBER_BAND_WIDTH = 60 + const val FOOTER_PAGE_NUMBER_GAP = 8 + const val FOOTER_TEXT_MAX_WIDTH = USABLE_WIDTH - PAGE_NUMBER_BAND_WIDTH - FOOTER_PAGE_NUMBER_GAP +} diff --git a/feature/pdf/src/commonMain/kotlin/org/groundplatform/feature/pdf/render/PdfGeometry.kt b/feature/pdf/src/commonMain/kotlin/org/groundplatform/feature/pdf/render/PdfGeometry.kt new file mode 100644 index 0000000000..3b55da2103 --- /dev/null +++ b/feature/pdf/src/commonMain/kotlin/org/groundplatform/feature/pdf/render/PdfGeometry.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.groundplatform.feature.pdf.render + +import kotlin.math.roundToInt + +internal fun fitInside(width: Int, height: Int, maxWidth: Int, maxHeight: Int): PdfItemSize { + val scale = minOf(maxWidth.toFloat() / width, maxHeight.toFloat() / height, 1f) + return PdfItemSize(width * scale, height * scale) +} + +internal fun pointsToRenderPixels(points: Float): Int = + // 1 point = 1/72 inch (standard PDF user-space unit) + (points / 72f * PdfConfig.IMAGE_RENDER_DPI).roundToInt() + +internal data class PdfItemSize(val width: Float, val height: Float) + +internal data class PdfOffset(val x: Float, val y: Float) + +internal data class PdfLine(val startX: Float, val startY: Float, val endX: Float, val endY: Float) + +/** Platform-agnostic rectangle defined by its top-left corner and dimensions. */ +internal data class PdfRect(val x: Float, val y: Float, val width: Float, val height: Float) { + val right: Float + get() = x + width + + val bottom: Float + get() = y + height +} diff --git a/feature/pdf/src/commonMain/kotlin/org/groundplatform/feature/pdf/render/components/PageFooterLayout.kt b/feature/pdf/src/commonMain/kotlin/org/groundplatform/feature/pdf/render/components/PageFooterLayout.kt new file mode 100644 index 0000000000..9abf5498de --- /dev/null +++ b/feature/pdf/src/commonMain/kotlin/org/groundplatform/feature/pdf/render/components/PageFooterLayout.kt @@ -0,0 +1,60 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.groundplatform.feature.pdf.render.components + +import org.groundplatform.feature.pdf.render.PdfConfig.FOOTER_TEXT_MAX_WIDTH +import org.groundplatform.feature.pdf.render.PdfConfig.FOOTER_TOP_GAP +import org.groundplatform.feature.pdf.render.PdfConfig.MARGIN +import org.groundplatform.feature.pdf.render.PdfConfig.PAGE_HEIGHT +import org.groundplatform.feature.pdf.render.PdfConfig.PAGE_NUMBER_BAND_WIDTH +import org.groundplatform.feature.pdf.render.PdfConfig.USABLE_WIDTH +import org.groundplatform.feature.pdf.render.PdfOffset + +/** + * Pre-computed layout for the page footer with separate left and right slots. + * + * @param footerTextOffset The top-left position where the footer text begins. + * @param footerMaxWidth The maximum width available for the footer text. + * @param pageNumberOffset The top-left position where the page number begins. + * @param pageNumberMaxWidth The maximum width available for the page number + */ +internal data class PageFooterLayout( + val footerTextOffset: PdfOffset, + val footerMaxWidth: Int, + val pageNumberOffset: PdfOffset, + val pageNumberMaxWidth: Int, +) { + companion object { + /** + * Vertical space the footer occupies, including the [FOOTER_TOP_GAP] separating it from page + * content. Feed this to [org.groundplatform.feature.pdf.render.PdfCursor] so pagination keeps + * the footer clear of content. + */ + fun reserve(footerHeight: Float): Float = footerHeight + FOOTER_TOP_GAP + + fun compute(footerHeight: Float): PageFooterLayout { + val top = PAGE_HEIGHT - MARGIN - footerHeight + val left = MARGIN.toFloat() + val pageNumberLeft = left + USABLE_WIDTH - PAGE_NUMBER_BAND_WIDTH + return PageFooterLayout( + footerTextOffset = PdfOffset(left, top), + footerMaxWidth = FOOTER_TEXT_MAX_WIDTH, + pageNumberOffset = PdfOffset(pageNumberLeft, top), + pageNumberMaxWidth = PAGE_NUMBER_BAND_WIDTH, + ) + } + } +} diff --git a/feature/pdf/src/commonMain/kotlin/org/groundplatform/feature/pdf/render/components/PageHeaderLayout.kt b/feature/pdf/src/commonMain/kotlin/org/groundplatform/feature/pdf/render/components/PageHeaderLayout.kt new file mode 100644 index 0000000000..c02bbb6025 --- /dev/null +++ b/feature/pdf/src/commonMain/kotlin/org/groundplatform/feature/pdf/render/components/PageHeaderLayout.kt @@ -0,0 +1,64 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.groundplatform.feature.pdf.render.components + +import org.groundplatform.feature.pdf.render.PdfConfig.HEADER_BOTTOM_GAP +import org.groundplatform.feature.pdf.render.PdfConfig.HEADER_COLUMN_GAP +import org.groundplatform.feature.pdf.render.PdfConfig.LINE_SPACING +import org.groundplatform.feature.pdf.render.PdfConfig.MARGIN +import org.groundplatform.feature.pdf.render.PdfConfig.USABLE_WIDTH +import org.groundplatform.feature.pdf.render.PdfOffset + +/** + * Pre-computed layout for the page header with three slots. Assumes uniform typography across + * columns. + * + * @param leftColumn Label and value positions for the left column. + * @param centerColumn Label and value positions for the center column. + * @param rightTextOffset The position where the right-aligned value begins . + * @param nextCursorY The Y position where the cursor should be positioned after the header. + */ +internal data class PageHeaderLayout( + val leftColumn: Column, + val centerColumn: Column, + val rightTextOffset: PdfOffset, + val nextCursorY: Float, +) { + companion object { + const val COLUMN_WIDTH: Int = (USABLE_WIDTH - 2 * HEADER_COLUMN_GAP) / 3 + const val LEFT_X: Float = MARGIN.toFloat() + const val CENTER_X: Float = LEFT_X + COLUMN_WIDTH + HEADER_COLUMN_GAP + const val RIGHT_X: Float = LEFT_X + 2 * (COLUMN_WIDTH + HEADER_COLUMN_GAP) + + fun compute(top: Float, labelHeight: Float, valueHeight: Float): PageHeaderLayout { + val columnBottom = top + labelHeight + LINE_SPACING + valueHeight + return PageHeaderLayout( + leftColumn = column(LEFT_X, top, labelHeight), + centerColumn = column(CENTER_X, top, labelHeight), + rightTextOffset = PdfOffset(RIGHT_X, top), + nextCursorY = columnBottom + HEADER_BOTTOM_GAP, + ) + } + + private fun column(x: Float, top: Float, labelHeight: Float) = + Column( + labelOffset = PdfOffset(x, top), + valueOffset = PdfOffset(x, top + labelHeight + LINE_SPACING), + ) + } + + data class Column(val labelOffset: PdfOffset, val valueOffset: PdfOffset) +} diff --git a/feature/pdf/src/commonMain/kotlin/org/groundplatform/feature/pdf/render/components/QrBlockLayout.kt b/feature/pdf/src/commonMain/kotlin/org/groundplatform/feature/pdf/render/components/QrBlockLayout.kt new file mode 100644 index 0000000000..aafb3b964a --- /dev/null +++ b/feature/pdf/src/commonMain/kotlin/org/groundplatform/feature/pdf/render/components/QrBlockLayout.kt @@ -0,0 +1,52 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.groundplatform.feature.pdf.render.components + +import org.groundplatform.feature.pdf.render.PdfConfig.LINE_SPACING +import org.groundplatform.feature.pdf.render.PdfConfig.MARGIN +import org.groundplatform.feature.pdf.render.PdfConfig.PAGE_WIDTH +import org.groundplatform.feature.pdf.render.PdfConfig.QR_SIZE +import org.groundplatform.feature.pdf.render.PdfOffset +import org.groundplatform.feature.pdf.render.PdfRect + +/** + * Pre-computed layout for the right-aligned QR code block with its caption. Compute should only be + * called when a QR image is available; the caption is meaningless without it. + * + * @param qrFrame Position and size of the QR image. + * @param captionOffset Top-left position of the caption text (centered under the QR). + * @param captionMaxWidth Maximum width for the caption. + * @param nextCursorY Cursor Y position after this block. + */ +internal data class QrBlockLayout( + val qrFrame: PdfRect, + val captionOffset: PdfOffset, + val captionMaxWidth: Int, + val nextCursorY: Float, +) { + companion object { + fun compute(top: Float, captionHeight: Float): QrBlockLayout { + val x = (PAGE_WIDTH - MARGIN - QR_SIZE).toFloat() + val captionTop = top + QR_SIZE + LINE_SPACING + return QrBlockLayout( + qrFrame = PdfRect(x, top, QR_SIZE.toFloat(), QR_SIZE.toFloat()), + captionOffset = PdfOffset(x, captionTop), + captionMaxWidth = QR_SIZE, + nextCursorY = captionTop + captionHeight + LINE_SPACING * 2, + ) + } + } +} diff --git a/feature/pdf/src/commonMain/kotlin/org/groundplatform/feature/pdf/render/components/TableLayout.kt b/feature/pdf/src/commonMain/kotlin/org/groundplatform/feature/pdf/render/components/TableLayout.kt new file mode 100644 index 0000000000..abae4d1589 --- /dev/null +++ b/feature/pdf/src/commonMain/kotlin/org/groundplatform/feature/pdf/render/components/TableLayout.kt @@ -0,0 +1,113 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.groundplatform.feature.pdf.render.components + +import org.groundplatform.feature.pdf.render.PdfConfig.CELL_PADDING +import org.groundplatform.feature.pdf.render.PdfConfig.LINE_SPACING +import org.groundplatform.feature.pdf.render.PdfConfig.MARGIN +import org.groundplatform.feature.pdf.render.PdfConfig.TABLE_TASK_COLUMN_WIDTH +import org.groundplatform.feature.pdf.render.PdfConfig.USABLE_WIDTH +import org.groundplatform.feature.pdf.render.PdfItemSize +import org.groundplatform.feature.pdf.render.PdfLine +import org.groundplatform.feature.pdf.render.PdfOffset +import org.groundplatform.feature.pdf.render.PdfRect + +internal object TableLayout { + /** + * Layout of a single two-column table row. Left cell holds a single text block; right cell may + * contain either text or an image. + * + * @param totalHeight Total height of the row including vertical padding. + * @param leftRowX X coordinate of the row's left edge. + * @param rightRowX X coordinate of the row's right edge. + * @param columnDividerX X coordinate of the vertical divider between the two columns. + * @param leftTextOffset Top-left position where the left cell text should be drawn. + * @param rightTextOffset Top-left position where the right cell text should be drawn, or null if + * the right cell has no text. + * @param rightImageFrame Frame (x, y, width, height) for the right cell image, or null if no + * image. + * @param borderLines The row's own frame: top border, bottom border, and column divider. + */ + data class Row( + val totalHeight: Float, + val leftRowX: Float, + val rightRowX: Float, + val columnDividerX: Float, + val leftTextOffset: PdfOffset, + val rightTextOffset: PdfOffset?, + val rightImageFrame: PdfRect?, + val borderLines: List, + ) + + data class Label(val labelOffset: PdfOffset, val nextCursorY: Float) + + fun getLabel(top: Float, labelHeight: Float): Label { + val labelTop = top + LINE_SPACING * 2 + return Label( + labelOffset = PdfOffset(MARGIN.toFloat(), labelTop), + nextCursorY = labelTop + labelHeight + LINE_SPACING, + ) + } + + /** Row height for the page-fit check, before the final row position is known. */ + fun getRowHeight( + leftTextHeight: Float, + rightTextHeight: Float, + rightImageSize: PdfItemSize?, + ): Float { + val imageHeight = rightImageSize?.height ?: 0f + val imageSpacing = if (imageHeight > 0f && rightTextHeight > 0f) LINE_SPACING else 0f + return maxOf(leftTextHeight, rightTextHeight + imageHeight + imageSpacing) + 2 * CELL_PADDING + } + + fun getRow( + rowTop: Float, + leftTextHeight: Float, + rightTextHeight: Float, + rightImageSize: PdfItemSize?, + ): Row { + val totalHeight = getRowHeight(leftTextHeight, rightTextHeight, rightImageSize) + + val left = MARGIN.toFloat() + val right = left + USABLE_WIDTH + val midX = left + TABLE_TASK_COLUMN_WIDTH + val rowBottom = rowTop + totalHeight + val contentTop = rowTop + CELL_PADDING + val rightCellLeft = midX + CELL_PADDING + + val rightTextOffset = if (rightTextHeight > 0f) PdfOffset(rightCellLeft, contentTop) else null + val rightImageFrame = rightImageSize?.let { + val y = contentTop + (rightTextOffset?.let { rightTextHeight + LINE_SPACING } ?: 0f) + PdfRect(rightCellLeft, y, rightImageSize.width, rightImageSize.height) + } + + return Row( + totalHeight = totalHeight, + leftRowX = left, + rightRowX = right, + columnDividerX = midX, + leftTextOffset = PdfOffset(left + CELL_PADDING, contentTop), + rightTextOffset = rightTextOffset, + rightImageFrame = rightImageFrame, + borderLines = + listOf( + PdfLine(startX = left, startY = rowTop, endX = right, endY = rowTop), + PdfLine(startX = left, startY = rowBottom, endX = right, endY = rowBottom), + PdfLine(startX = midX, startY = rowTop, endX = midX, endY = rowBottom), + ), + ) + } +} From 260bf8ec01b6c16a2347d7de8fd5e97c0ca7fcd7 Mon Sep 17 00:00:00 2001 From: andreia Date: Wed, 10 Jun 2026 18:00:14 +0200 Subject: [PATCH 2/8] add unit tests --- .../feature/pdf/render/PdfGeometryTest.kt | 85 +++++++ .../render/components/PageFooterLayoutTest.kt | 98 +++++++ .../render/components/PageHeaderLayoutTest.kt | 107 ++++++++ .../render/components/QrBlockLayoutTest.kt | 74 ++++++ .../pdf/render/components/TableLayoutTest.kt | 240 ++++++++++++++++++ 5 files changed, 604 insertions(+) create mode 100644 feature/pdf/src/commonTest/kotlin/org/groundplatform/feature/pdf/render/PdfGeometryTest.kt create mode 100644 feature/pdf/src/commonTest/kotlin/org/groundplatform/feature/pdf/render/components/PageFooterLayoutTest.kt create mode 100644 feature/pdf/src/commonTest/kotlin/org/groundplatform/feature/pdf/render/components/PageHeaderLayoutTest.kt create mode 100644 feature/pdf/src/commonTest/kotlin/org/groundplatform/feature/pdf/render/components/QrBlockLayoutTest.kt create mode 100644 feature/pdf/src/commonTest/kotlin/org/groundplatform/feature/pdf/render/components/TableLayoutTest.kt diff --git a/feature/pdf/src/commonTest/kotlin/org/groundplatform/feature/pdf/render/PdfGeometryTest.kt b/feature/pdf/src/commonTest/kotlin/org/groundplatform/feature/pdf/render/PdfGeometryTest.kt new file mode 100644 index 0000000000..aecfff52c0 --- /dev/null +++ b/feature/pdf/src/commonTest/kotlin/org/groundplatform/feature/pdf/render/PdfGeometryTest.kt @@ -0,0 +1,85 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.groundplatform.feature.pdf.render + +import kotlin.math.roundToInt +import kotlin.test.Test +import kotlin.test.assertEquals + +class PdfGeometryTest { + + @Test + fun `fitInside scales down to fit the width constraint`() { + val result = fitInside(width = 200, height = 100, maxWidth = 100, maxHeight = 100) + + assertEquals(100f, result.width) + assertEquals(50f, result.height) + } + + @Test + fun `fitInside scales down to fit the height constraint`() { + val result = fitInside(width = 100, height = 200, maxWidth = 100, maxHeight = 100) + + assertEquals(50f, result.width) + assertEquals(100f, result.height) + } + + @Test + fun `fitInside preserves aspect ratio`() { + val result = fitInside(width = 400, height = 300, maxWidth = 200, maxHeight = 200) + + assertEquals(200f, result.width) + assertEquals(150f, result.height) + assertEquals(result.width / result.height, 400f / 300f) + } + + @Test + fun `fitInside never upscales when the item already fits`() { + val result = fitInside(width = 50, height = 30, maxWidth = 100, maxHeight = 100) + + assertEquals(50f, result.width) + assertEquals(30f, result.height) + } + + @Test + fun `fitInside returns the same size when dimensions equal the bounds`() { + val result = fitInside(width = 100, height = 100, maxWidth = 100, maxHeight = 100) + + assertEquals(100f, result.width) + assertEquals(100f, result.height) + } + + @Test + fun `pointsToRenderPixels converts points to pixels at the configured DPI`() { + // 72 points = 1 inch, which at IMAGE_RENDER_DPI yields exactly that many pixels. + assertEquals(PdfConfig.IMAGE_RENDER_DPI.roundToInt(), pointsToRenderPixels(72f)) + } + + @Test + fun `pointsToRenderPixels scales linearly and rounds to the nearest pixel`() { + assertEquals(0, pointsToRenderPixels(0f)) + assertEquals((36f / 72f * PdfConfig.IMAGE_RENDER_DPI).roundToInt(), pointsToRenderPixels(36f)) + assertEquals((10f / 72f * PdfConfig.IMAGE_RENDER_DPI).roundToInt(), pointsToRenderPixels(10f)) + } + + @Test + fun `PdfRect exposes right and bottom derived from origin and size`() { + val rect = PdfRect(x = 10f, y = 20f, width = 30f, height = 40f) + + assertEquals(40f, rect.right) + assertEquals(60f, rect.bottom) + } +} diff --git a/feature/pdf/src/commonTest/kotlin/org/groundplatform/feature/pdf/render/components/PageFooterLayoutTest.kt b/feature/pdf/src/commonTest/kotlin/org/groundplatform/feature/pdf/render/components/PageFooterLayoutTest.kt new file mode 100644 index 0000000000..8b56b1b98b --- /dev/null +++ b/feature/pdf/src/commonTest/kotlin/org/groundplatform/feature/pdf/render/components/PageFooterLayoutTest.kt @@ -0,0 +1,98 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.groundplatform.feature.pdf.render.components + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue +import org.groundplatform.feature.pdf.render.PdfConfig + +class PageFooterLayoutTest { + + private val margin = PdfConfig.MARGIN.toFloat() + private val pageHeight = PdfConfig.PAGE_HEIGHT + private val usableWidth = PdfConfig.USABLE_WIDTH + private val pageNumberBand = PdfConfig.PAGE_NUMBER_BAND_WIDTH + private val footerTextMaxWidth = PdfConfig.FOOTER_TEXT_MAX_WIDTH + + @Test + fun `footer text anchors against the bottom margin`() { + val footerHeight = 12f + val layout = PageFooterLayout.compute(footerHeight = footerHeight) + + assertEquals(margin, layout.footerTextOffset.x) + assertEquals(pageHeight - margin - footerHeight, layout.footerTextOffset.y) + } + + @Test + fun `page number sits in the right-side slot on the same baseline as the footer text`() { + val layout = PageFooterLayout.compute(footerHeight = 12f) + + assertEquals(margin + usableWidth - pageNumberBand, layout.pageNumberOffset.x) + assertEquals(layout.footerTextOffset.y, layout.pageNumberOffset.y) + } + + @Test + fun `slot widths match their respective configuration constants`() { + val layout = PageFooterLayout.compute(footerHeight = 12f) + + assertEquals(footerTextMaxWidth, layout.footerMaxWidth) + assertEquals(pageNumberBand, layout.pageNumberMaxWidth) + } + + @Test + fun `footer and page number slots do not overlap`() { + val layout = PageFooterLayout.compute(footerHeight = 12f) + + val footerRight = layout.footerTextOffset.x + layout.footerMaxWidth + assertTrue(footerRight <= layout.pageNumberOffset.x) + } + + @Test + fun `page number band ends exactly at the right page margin`() { + val layout = PageFooterLayout.compute(footerHeight = 12f) + + assertEquals( + margin + usableWidth, + layout.pageNumberOffset.x + layout.pageNumberMaxWidth, + "Page number band's right edge must align with the right page margin", + ) + } + + @Test + fun `taller footer pushes the baseline higher up the page`() { + val short = PageFooterLayout.compute(footerHeight = 12f) + val tall = PageFooterLayout.compute(footerHeight = 30f) + + assertTrue(tall.footerTextOffset.y < short.footerTextOffset.y) + assertEquals(short.footerTextOffset.y - 18f, tall.footerTextOffset.y) + } + + @Test + fun `footer height plus baseline plus bottom margin equals page height`() { + val footerHeight = 18f + val layout = PageFooterLayout.compute(footerHeight = footerHeight) + + assertEquals(pageHeight.toFloat(), layout.footerTextOffset.y + footerHeight + margin) + } + + @Test + fun `reserve adds the top gap to the footer height`() { + val footerHeight = 12f + + assertEquals(footerHeight + PdfConfig.FOOTER_TOP_GAP, PageFooterLayout.reserve(footerHeight)) + } +} diff --git a/feature/pdf/src/commonTest/kotlin/org/groundplatform/feature/pdf/render/components/PageHeaderLayoutTest.kt b/feature/pdf/src/commonTest/kotlin/org/groundplatform/feature/pdf/render/components/PageHeaderLayoutTest.kt new file mode 100644 index 0000000000..b15573a95a --- /dev/null +++ b/feature/pdf/src/commonTest/kotlin/org/groundplatform/feature/pdf/render/components/PageHeaderLayoutTest.kt @@ -0,0 +1,107 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.groundplatform.feature.pdf.render.components + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue +import kotlin.text.compareTo +import org.groundplatform.feature.pdf.render.PdfConfig +import org.groundplatform.feature.pdf.render.PdfOffset + +class PageHeaderLayoutTest { + + private val lineSpacing = PdfConfig.LINE_SPACING + private val headerBottomGap = PdfConfig.HEADER_BOTTOM_GAP + private val headerColumnGap = PdfConfig.HEADER_COLUMN_GAP + private val usableWidth = PdfConfig.USABLE_WIDTH + + @Test + fun `column X positions span the usable width with gaps between them`() { + val left = PageHeaderLayout.LEFT_X + val center = PageHeaderLayout.CENTER_X + val right = PageHeaderLayout.RIGHT_X + val width = PageHeaderLayout.COLUMN_WIDTH + + assertTrue(left < center) + assertTrue(center < right) + assertEquals(headerColumnGap.toFloat(), center - (left + width)) + assertEquals(headerColumnGap.toFloat(), right - (center + width)) + assertTrue(3 * width + 2 * headerColumnGap <= usableWidth) + } + + @Test + fun `compute places survey column labels and values at LEFT_X`() { + val layout = PageHeaderLayout.compute(top = 0f, labelHeight = 10f, valueHeight = 14f) + + assertEquals(PdfOffset(PageHeaderLayout.LEFT_X, 0f), layout.leftColumn.labelOffset) + assertEquals( + PdfOffset(PageHeaderLayout.LEFT_X, 10f + lineSpacing), + layout.leftColumn.valueOffset, + ) + } + + @Test + fun `compute places job column labels and values at CENTER_X`() { + val layout = PageHeaderLayout.compute(top = 0f, labelHeight = 10f, valueHeight = 14f) + + assertEquals(PdfOffset(PageHeaderLayout.CENTER_X, 0f), layout.centerColumn.labelOffset) + assertEquals( + PdfOffset(PageHeaderLayout.CENTER_X, 10f + lineSpacing), + layout.centerColumn.valueOffset, + ) + } + + @Test + fun `compute places timestamp at RIGHT_X with the same top as labels`() { + val layout = PageHeaderLayout.compute(top = 50f, labelHeight = 10f, valueHeight = 14f) + + assertEquals(PdfOffset(PageHeaderLayout.RIGHT_X, 50f), layout.rightTextOffset) + assertEquals(layout.leftColumn.labelOffset.y, layout.rightTextOffset.y) + assertEquals(layout.centerColumn.labelOffset.y, layout.rightTextOffset.y) + } + + @Test + fun `value sits below its label by exactly line spacing`() { + val labelHeight = 12f + val layout = PageHeaderLayout.compute(top = 30f, labelHeight = labelHeight, valueHeight = 14f) + + val survey = layout.leftColumn + assertEquals(labelHeight + lineSpacing, survey.valueOffset.y - survey.labelOffset.y) + } + + @Test + fun `nextCursorY accounts for label, line spacing, value, and header bottom gap`() { + val top = 40f + val labelHeight = 10f + val valueHeight = 14f + + val layout = PageHeaderLayout.compute(top = top, labelHeight, valueHeight) + + assertEquals( + top + labelHeight + lineSpacing + valueHeight + headerBottomGap, + layout.nextCursorY, + ) + } + + @Test + fun `all three columns share the same label baseline and value baseline`() { + val layout = PageHeaderLayout.compute(top = 100f, labelHeight = 12f, valueHeight = 16f) + + assertEquals(layout.leftColumn.labelOffset.y, layout.centerColumn.labelOffset.y) + assertEquals(layout.leftColumn.valueOffset.y, layout.centerColumn.valueOffset.y) + } +} diff --git a/feature/pdf/src/commonTest/kotlin/org/groundplatform/feature/pdf/render/components/QrBlockLayoutTest.kt b/feature/pdf/src/commonTest/kotlin/org/groundplatform/feature/pdf/render/components/QrBlockLayoutTest.kt new file mode 100644 index 0000000000..befaa6a137 --- /dev/null +++ b/feature/pdf/src/commonTest/kotlin/org/groundplatform/feature/pdf/render/components/QrBlockLayoutTest.kt @@ -0,0 +1,74 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.groundplatform.feature.pdf.render.components + +import kotlin.test.Test +import kotlin.test.assertEquals +import org.groundplatform.feature.pdf.render.PdfConfig + +class QrBlockLayoutTest { + + private val margin = PdfConfig.MARGIN + private val pageWidth = PdfConfig.PAGE_WIDTH + private val qrSize = PdfConfig.QR_SIZE + private val lineSpacing = PdfConfig.LINE_SPACING + + private val expectedX = (pageWidth - margin - qrSize).toFloat() + + @Test + fun `QR frame is a square anchored at the right margin`() { + val layout = QrBlockLayout.compute(top = 0f, captionHeight = 10f) + + assertEquals(expectedX, layout.qrFrame.x) + assertEquals(0f, layout.qrFrame.y) + assertEquals(qrSize.toFloat(), layout.qrFrame.width) + assertEquals(qrSize.toFloat(), layout.qrFrame.height) + assertEquals((pageWidth - margin).toFloat(), layout.qrFrame.right) + } + + @Test + fun `caption sits directly below the QR with line spacing between them`() { + val top = 100f + val layout = QrBlockLayout.compute(top = top, captionHeight = 10f) + + assertEquals(expectedX, layout.captionOffset.x) + assertEquals(top + qrSize + lineSpacing, layout.captionOffset.y) + } + + @Test + fun `caption maxWidth equals QR size so it stays under the QR image`() { + val layout = QrBlockLayout.compute(top = 0f, captionHeight = 10f) + + assertEquals(qrSize, layout.captionMaxWidth) + } + + @Test + fun `caption shares its X with the QR frame`() { + val layout = QrBlockLayout.compute(top = 0f, captionHeight = 10f) + + assertEquals(layout.qrFrame.x, layout.captionOffset.x) + } + + @Test + fun `nextCursorY accounts for QR, caption, and trailing spacing`() { + val top = 50f + val captionHeight = 14f + val layout = QrBlockLayout.compute(top = top, captionHeight = captionHeight) + + val expectedCaptionTop = top + qrSize + lineSpacing + assertEquals(expectedCaptionTop + captionHeight + lineSpacing * 2, layout.nextCursorY) + } +} diff --git a/feature/pdf/src/commonTest/kotlin/org/groundplatform/feature/pdf/render/components/TableLayoutTest.kt b/feature/pdf/src/commonTest/kotlin/org/groundplatform/feature/pdf/render/components/TableLayoutTest.kt new file mode 100644 index 0000000000..287a7341da --- /dev/null +++ b/feature/pdf/src/commonTest/kotlin/org/groundplatform/feature/pdf/render/components/TableLayoutTest.kt @@ -0,0 +1,240 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.groundplatform.feature.pdf.render.components + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertNull +import kotlin.test.assertTrue +import org.groundplatform.feature.pdf.render.PdfConfig +import org.groundplatform.feature.pdf.render.PdfItemSize +import org.groundplatform.feature.pdf.render.PdfLine +import org.groundplatform.feature.pdf.render.PdfOffset + +class TableLayoutTest { + + private val cellPadding = PdfConfig.CELL_PADDING.toFloat() + private val lineSpacing = PdfConfig.LINE_SPACING + private val margin = PdfConfig.MARGIN.toFloat() + private val usableWidth = PdfConfig.USABLE_WIDTH + private val taskColumnWidth = PdfConfig.TABLE_TASK_COLUMN_WIDTH + + @Test + fun `label sits below a top gap at the left margin`() { + val layout = TableLayout.getLabel(top = 100f, labelHeight = 14f) + + assertEquals(PdfOffset(margin, 100f + 2 * lineSpacing), layout.labelOffset) + } + + @Test + fun `label leaves a bottom gap before the first row`() { + val top = 100f + val labelHeight = 14f + + val layout = TableLayout.getLabel(top = top, labelHeight = labelHeight) + + assertEquals(top + 2 * lineSpacing + labelHeight + lineSpacing, layout.nextCursorY) + } + + @Test + fun `taller label pushes the first row further down`() { + val short = TableLayout.getLabel(top = 0f, labelHeight = 10f) + val tall = TableLayout.getLabel(top = 0f, labelHeight = 30f) + + assertTrue(short.nextCursorY < tall.nextCursorY) + assertEquals(20f, tall.nextCursorY - short.nextCursorY) + } + + @Test + fun `rowHeight with only left text returns left height plus padding`() { + val height = + TableLayout.getRowHeight(leftTextHeight = 30f, rightTextHeight = 0f, rightImageSize = null) + + assertEquals(30f + 2 * cellPadding, height) + } + + @Test + fun `rowHeight picks the taller content height`() { + val tallerLeft = + TableLayout.getRowHeight(leftTextHeight = 50f, rightTextHeight = 20f, rightImageSize = null) + val tallerRight = + TableLayout.getRowHeight(leftTextHeight = 10f, rightTextHeight = 60f, rightImageSize = null) + val tallerImageRight = + TableLayout.getRowHeight( + leftTextHeight = 20f, + rightTextHeight = 0f, + rightImageSize = PdfItemSize(width = 100f, height = 80f), + ) + + assertEquals(50f + 2 * cellPadding, tallerLeft) + assertEquals(60f + 2 * cellPadding, tallerRight) + assertEquals(80f + 2 * cellPadding, tallerImageRight) + } + + @Test + fun `rowHeight with both right text and image stacks them with line spacing`() { + val height = + TableLayout.getRowHeight( + leftTextHeight = 10f, + rightTextHeight = 20f, + rightImageSize = PdfItemSize(width = 100f, height = 80f), + ) + + assertEquals(20f + lineSpacing + 80f + 2 * cellPadding, height) + } + + @Test + fun `row always places left text at the row's top-left content area`() { + val layout = + TableLayout.getRow( + rowTop = 100f, + leftTextHeight = 20f, + rightTextHeight = 0f, + rightImageSize = null, + ) + + assertEquals(PdfOffset(margin + cellPadding, 100f + cellPadding), layout.leftTextOffset) + } + + @Test + fun `row returns null right offsets when right cell has no content`() { + val layout = + TableLayout.getRow( + rowTop = 0f, + leftTextHeight = 20f, + rightTextHeight = 0f, + rightImageSize = null, + ) + + assertNull(layout.rightTextOffset) + assertNull(layout.rightImageFrame) + } + + @Test + fun `row places right text at the right cell's top`() { + val layout = + TableLayout.getRow( + rowTop = 50f, + leftTextHeight = 20f, + rightTextHeight = 20f, + rightImageSize = null, + ) + + val rightCellX = margin + taskColumnWidth + cellPadding + assertEquals(PdfOffset(rightCellX, 50f + cellPadding), layout.rightTextOffset) + assertNull(layout.rightImageFrame) + } + + @Test + fun `row places image at the right cell's top`() { + val imageSize = PdfItemSize(width = 80f, height = 60f) + val layout = + TableLayout.getRow( + rowTop = 50f, + leftTextHeight = 20f, + rightTextHeight = 0f, + rightImageSize = imageSize, + ) + + val rightCellX = margin + taskColumnWidth + cellPadding + val frame = assertNotNull(layout.rightImageFrame) + assertNull(layout.rightTextOffset) + with(frame) { + assertEquals(rightCellX, x) + assertEquals(50f + cellPadding, y) + assertEquals(imageSize.width, width) + assertEquals(imageSize.height, height) + } + } + + @Test + fun `row sets bounds and divider from page geometry`() { + val layout = + TableLayout.getRow( + rowTop = 0f, + leftTextHeight = 20f, + rightTextHeight = 0f, + rightImageSize = null, + ) + + assertEquals(margin, layout.leftRowX) + assertEquals(margin + usableWidth, layout.rightRowX) + assertEquals(margin + taskColumnWidth, layout.columnDividerX) + assertTrue(layout.leftRowX < layout.columnDividerX) + assertTrue(layout.columnDividerX < layout.rightRowX) + } + + @Test + fun `row frames itself with top, bottom, and column-divider border lines`() { + val rowTop = 100f + val layout = + TableLayout.getRow( + rowTop = rowTop, + leftTextHeight = 20f, + rightTextHeight = 20f, + rightImageSize = null, + ) + + val rowBottom = rowTop + layout.totalHeight + val right = margin + usableWidth + val midX = margin + taskColumnWidth + assertEquals( + listOf( + PdfLine(margin, rowTop, right, rowTop), + PdfLine(margin, rowBottom, right, rowBottom), + PdfLine(midX, rowTop, midX, rowBottom), + ), + layout.borderLines, + ) + } + + @Test + fun `consecutive rows produce abutting borders so the divider reads as one continuous line`() { + val first = + TableLayout.getRow( + rowTop = 50f, + leftTextHeight = 20f, + rightTextHeight = 0f, + rightImageSize = null, + ) + val second = + TableLayout.getRow( + rowTop = 50f + first.totalHeight, + leftTextHeight = 30f, + rightTextHeight = 0f, + rightImageSize = null, + ) + + // The first row's bottom border sits exactly where the second row's top border begins. + assertEquals(first.borderLines[1].startY, second.borderLines[0].startY) + // The per-row divider segments share an X and meet end-to-start, forming one unbroken line. + val firstDivider = first.borderLines[2] + val secondDivider = second.borderLines[2] + assertEquals(firstDivider.endX, secondDivider.startX) + assertEquals(firstDivider.endY, secondDivider.startY) + } + + @Test + fun `row totalHeight matches the rowHeight helper`() { + val left = 30f + val right = 20f + val image = PdfItemSize(width = 80f, height = 60f) + val layout = TableLayout.getRow(rowTop = 0f, left, right, image) + + assertEquals(TableLayout.getRowHeight(left, right, image), layout.totalHeight) + } +} From c55a49c29452282ab2166e1dff1b6c922a049752 Mon Sep 17 00:00:00 2001 From: andreia Date: Thu, 11 Jun 2026 14:52:32 +0200 Subject: [PATCH 3/8] update TableLayout to avoid drawing duplicated borders --- .../pdf/render/components/TableLayout.kt | 74 ++++++++++-------- .../pdf/render/components/TableLayoutTest.kt | 78 ++++++++++++------- 2 files changed, 93 insertions(+), 59 deletions(-) diff --git a/feature/pdf/src/commonMain/kotlin/org/groundplatform/feature/pdf/render/components/TableLayout.kt b/feature/pdf/src/commonMain/kotlin/org/groundplatform/feature/pdf/render/components/TableLayout.kt index abae4d1589..84d3a7e3af 100644 --- a/feature/pdf/src/commonMain/kotlin/org/groundplatform/feature/pdf/render/components/TableLayout.kt +++ b/feature/pdf/src/commonMain/kotlin/org/groundplatform/feature/pdf/render/components/TableLayout.kt @@ -31,26 +31,34 @@ internal object TableLayout { * contain either text or an image. * * @param totalHeight Total height of the row including vertical padding. - * @param leftRowX X coordinate of the row's left edge. - * @param rightRowX X coordinate of the row's right edge. - * @param columnDividerX X coordinate of the vertical divider between the two columns. - * @param leftTextOffset Top-left position where the left cell text should be drawn. - * @param rightTextOffset Top-left position where the right cell text should be drawn, or null if - * the right cell has no text. - * @param rightImageFrame Frame (x, y, width, height) for the right cell image, or null if no - * image. - * @param borderLines The row's own frame: top border, bottom border, and column divider. + * @property content Content-specific layout data. + * @property borders All the borders to draw around the row. */ - data class Row( - val totalHeight: Float, - val leftRowX: Float, - val rightRowX: Float, - val columnDividerX: Float, - val leftTextOffset: PdfOffset, - val rightTextOffset: PdfOffset?, - val rightImageFrame: PdfRect?, - val borderLines: List, - ) + data class Row(val totalHeight: Float, val content: Content, val borders: Borders) { + /** + * @param top Horizontal line along the row's top edge, or null if there is already a row + * directly above it on the same page. + * @param bottom Horizontal line along the row's bottom edge. + * @param divider Vertical line between the two columns, spanning the row's full height. + */ + data class Borders(val top: PdfLine?, val divider: PdfLine, val bottom: PdfLine) { + val drawableLines: List + get() = listOfNotNull(top, divider, bottom) + } + + /** + * @param leftTextOffset Top-left position where the left cell text should be drawn. + * @param rightTextOffset Top-left position where the right cell text should be drawn, or null + * if the right cell has no text. + * @param rightImageFrame Frame (x, y, width, height) for the right cell image, or null if no + * image. + */ + data class Content( + val leftTextOffset: PdfOffset, + val rightTextOffset: PdfOffset?, + val rightImageFrame: PdfRect?, + ) + } data class Label(val labelOffset: PdfOffset, val nextCursorY: Float) @@ -78,6 +86,7 @@ internal object TableLayout { leftTextHeight: Float, rightTextHeight: Float, rightImageSize: PdfItemSize?, + includeTopBorder: Boolean = true, ): Row { val totalHeight = getRowHeight(leftTextHeight, rightTextHeight, rightImageSize) @@ -91,22 +100,25 @@ internal object TableLayout { val rightTextOffset = if (rightTextHeight > 0f) PdfOffset(rightCellLeft, contentTop) else null val rightImageFrame = rightImageSize?.let { val y = contentTop + (rightTextOffset?.let { rightTextHeight + LINE_SPACING } ?: 0f) - PdfRect(rightCellLeft, y, rightImageSize.width, rightImageSize.height) + PdfRect(rightCellLeft, y, it.width, it.height) } return Row( totalHeight = totalHeight, - leftRowX = left, - rightRowX = right, - columnDividerX = midX, - leftTextOffset = PdfOffset(left + CELL_PADDING, contentTop), - rightTextOffset = rightTextOffset, - rightImageFrame = rightImageFrame, - borderLines = - listOf( - PdfLine(startX = left, startY = rowTop, endX = right, endY = rowTop), - PdfLine(startX = left, startY = rowBottom, endX = right, endY = rowBottom), - PdfLine(startX = midX, startY = rowTop, endX = midX, endY = rowBottom), + content = + Row.Content( + leftTextOffset = PdfOffset(left + CELL_PADDING, contentTop), + rightTextOffset = rightTextOffset, + rightImageFrame = rightImageFrame, + ), + borders = + Row.Borders( + top = + if (includeTopBorder) + PdfLine(startX = left, startY = rowTop, endX = right, endY = rowTop) + else null, + divider = PdfLine(startX = midX, startY = rowTop, endX = midX, endY = rowBottom), + bottom = PdfLine(startX = left, startY = rowBottom, endX = right, endY = rowBottom), ), ) } diff --git a/feature/pdf/src/commonTest/kotlin/org/groundplatform/feature/pdf/render/components/TableLayoutTest.kt b/feature/pdf/src/commonTest/kotlin/org/groundplatform/feature/pdf/render/components/TableLayoutTest.kt index 287a7341da..c6a48aa924 100644 --- a/feature/pdf/src/commonTest/kotlin/org/groundplatform/feature/pdf/render/components/TableLayoutTest.kt +++ b/feature/pdf/src/commonTest/kotlin/org/groundplatform/feature/pdf/render/components/TableLayoutTest.kt @@ -107,7 +107,7 @@ class TableLayoutTest { rightImageSize = null, ) - assertEquals(PdfOffset(margin + cellPadding, 100f + cellPadding), layout.leftTextOffset) + assertEquals(PdfOffset(margin + cellPadding, 100f + cellPadding), layout.content.leftTextOffset) } @Test @@ -120,8 +120,8 @@ class TableLayoutTest { rightImageSize = null, ) - assertNull(layout.rightTextOffset) - assertNull(layout.rightImageFrame) + assertNull(layout.content.rightTextOffset) + assertNull(layout.content.rightImageFrame) } @Test @@ -135,8 +135,8 @@ class TableLayoutTest { ) val rightCellX = margin + taskColumnWidth + cellPadding - assertEquals(PdfOffset(rightCellX, 50f + cellPadding), layout.rightTextOffset) - assertNull(layout.rightImageFrame) + assertEquals(PdfOffset(rightCellX, 50f + cellPadding), layout.content.rightTextOffset) + assertNull(layout.content.rightImageFrame) } @Test @@ -151,8 +151,8 @@ class TableLayoutTest { ) val rightCellX = margin + taskColumnWidth + cellPadding - val frame = assertNotNull(layout.rightImageFrame) - assertNull(layout.rightTextOffset) + val frame = assertNotNull(layout.content.rightImageFrame) + assertNull(layout.content.rightTextOffset) with(frame) { assertEquals(rightCellX, x) assertEquals(50f + cellPadding, y) @@ -171,11 +171,14 @@ class TableLayoutTest { rightImageSize = null, ) - assertEquals(margin, layout.leftRowX) - assertEquals(margin + usableWidth, layout.rightRowX) - assertEquals(margin + taskColumnWidth, layout.columnDividerX) - assertTrue(layout.leftRowX < layout.columnDividerX) - assertTrue(layout.columnDividerX < layout.rightRowX) + val leftX = layout.borders.bottom.startX + val rightX = layout.borders.bottom.endX + val dividerX = layout.borders.divider.startX + assertEquals(margin, leftX) + assertEquals(margin + usableWidth, rightX) + assertEquals(margin + taskColumnWidth, dividerX) + assertTrue(leftX < dividerX) + assertTrue(dividerX < rightX) } @Test @@ -192,18 +195,15 @@ class TableLayoutTest { val rowBottom = rowTop + layout.totalHeight val right = margin + usableWidth val midX = margin + taskColumnWidth - assertEquals( - listOf( - PdfLine(margin, rowTop, right, rowTop), - PdfLine(margin, rowBottom, right, rowBottom), - PdfLine(midX, rowTop, midX, rowBottom), - ), - layout.borderLines, - ) + val borders = layout.borders + assertEquals(PdfLine(margin, rowTop, right, rowTop), borders.top) + assertEquals(PdfLine(margin, rowBottom, right, rowBottom), borders.bottom) + assertEquals(PdfLine(midX, rowTop, midX, rowBottom), borders.divider) + assertEquals(listOf(borders.top, borders.divider, borders.bottom), borders.drawableLines) } @Test - fun `consecutive rows produce abutting borders so the divider reads as one continuous line`() { + fun `row omits its top border when drawTopBorder is false`() { val first = TableLayout.getRow( rowTop = 50f, @@ -217,15 +217,37 @@ class TableLayoutTest { leftTextHeight = 30f, rightTextHeight = 0f, rightImageSize = null, + includeTopBorder = false, ) - // The first row's bottom border sits exactly where the second row's top border begins. - assertEquals(first.borderLines[1].startY, second.borderLines[0].startY) - // The per-row divider segments share an X and meet end-to-start, forming one unbroken line. - val firstDivider = first.borderLines[2] - val secondDivider = second.borderLines[2] - assertEquals(firstDivider.endX, secondDivider.startX) - assertEquals(firstDivider.endY, secondDivider.startY) + assertNull(second.borders.top) + assertEquals(first.borders.bottom.startY, second.borders.divider.startY) + assertEquals(first.borders.divider.endX, second.borders.divider.startX) + assertEquals(first.borders.divider.endY, second.borders.divider.startY) + } + + @Test + fun `consecutive rows share borders so the divider reads as one continuous line`() { + val first = + TableLayout.getRow( + rowTop = 50f, + leftTextHeight = 20f, + rightTextHeight = 0f, + rightImageSize = null, + ) + val second = + TableLayout.getRow( + rowTop = 50f + first.totalHeight, + leftTextHeight = 30f, + rightTextHeight = 0f, + rightImageSize = null, + includeTopBorder = false, + ) + + assertNull(second.borders.top) + assertEquals(first.borders.bottom.startY, second.borders.divider.startY) + assertEquals(first.borders.divider.endX, second.borders.divider.startX) + assertEquals(first.borders.divider.endY, second.borders.divider.startY) } @Test From 4ac6082bba326e436d9190d9230f9319e4be0db6 Mon Sep 17 00:00:00 2001 From: andreia Date: Thu, 11 Jun 2026 14:53:27 +0200 Subject: [PATCH 4/8] add kdoc to PdfConfig variables --- .../feature/pdf/render/PdfConfig.kt | 63 +++++++++++++++++-- 1 file changed, 57 insertions(+), 6 deletions(-) diff --git a/feature/pdf/src/commonMain/kotlin/org/groundplatform/feature/pdf/render/PdfConfig.kt b/feature/pdf/src/commonMain/kotlin/org/groundplatform/feature/pdf/render/PdfConfig.kt index a502c8dccd..8a8640f64f 100644 --- a/feature/pdf/src/commonMain/kotlin/org/groundplatform/feature/pdf/render/PdfConfig.kt +++ b/feature/pdf/src/commonMain/kotlin/org/groundplatform/feature/pdf/render/PdfConfig.kt @@ -19,33 +19,84 @@ package org.groundplatform.feature.pdf.render * Dimensional and type-scale constants shared by the Android and iOS PDF renderers. Keeping these * in commonMain prevents the two platforms from drifting on page size, margins, or type scale. * - * All measurements are in PDF points (1/72 inch). + * Unless noted otherwise, all measurements are in PDF points (1/72 inch). */ internal object PdfConfig { - const val PAGE_WIDTH = 595 // A4 page width - const val PAGE_HEIGHT = 842 // A4 page height + /** Page width in points (A4 portrait, 210mm). */ + const val PAGE_WIDTH = 595 + + /** Page height in points (A4 portrait, 297mm). */ + const val PAGE_HEIGHT = 842 + + /** Page margin applied to all four edges. */ const val MARGIN = 40 + + /** Font size for title text. */ const val TITLE_SIZE = 11f + + /** Font size body and table-cell text. */ const val BODY_SIZE = 11f + + /** Font size for captions and metadata (header/footer) text. */ const val CAPTION_SIZE = 9f + + /** Vertical spacing added between lines of text. */ const val LINE_SPACING = 4f + + /** Target size of the QR code block. */ const val QR_SIZE = 200 + + /** Horizontal gap between the two columns of the page header. */ const val HEADER_COLUMN_GAP = 16 - const val TABLE_TASK_LABEL_RATIO = 0.35f + + /** Inner padding between a table cell's border and its text. */ const val CELL_PADDING = 6 + + /** Stroke width used for table and cell borders. */ const val BORDER_WIDTH = 0.5f - const val PHOTO_MAX_HEIGHT_RATIO = 0.35f + + /** Vertical gap below the header before body content begins. */ const val HEADER_BOTTOM_GAP = 28f + + /** Vertical gap above the footer separating it from body content. */ const val FOOTER_TOP_GAP = 28f + + /** Maximum number of lines rendered for each header value. */ const val MAX_HEADER_VALUE_LINES = 1 + + /** Maximum number of lines rendered for the footer text. */ const val MAX_FOOTER_LINES = 1 + + /** Resolution in dots-per-inch used when rasterizing images for the PDF. */ const val IMAGE_RENDER_DPI = 300f + + /** Content width between the left and right margins. */ const val USABLE_WIDTH = PAGE_WIDTH - 2 * MARGIN + + /** Fraction of [USABLE_WIDTH] allotted to the task-label column of a table row. */ + private const val TABLE_TASK_LABEL_RATIO = 0.35f + + /** Width of the task-label column. */ const val TABLE_TASK_COLUMN_WIDTH = (USABLE_WIDTH * TABLE_TASK_LABEL_RATIO).toInt() + + /** Width for task-label text (the task column minus its padding). */ const val TABLE_TASK_TEXT_WIDTH = TABLE_TASK_COLUMN_WIDTH - 2 * CELL_PADDING + + /** Width for answer text (the remaining table width minus its padding). */ const val TABLE_ANSWER_TEXT_WIDTH = USABLE_WIDTH - TABLE_TASK_COLUMN_WIDTH - 2 * CELL_PADDING + + /** Fraction of the usable page height a single photo may occupy before being scaled down. */ + private const val PHOTO_MAX_HEIGHT_RATIO = 0.35f + + /** Maximum table photo height. */ const val PHOTO_MAX_HEIGHT = ((PAGE_HEIGHT - 2 * MARGIN) * PHOTO_MAX_HEIGHT_RATIO).toInt() + + /** Width reserved on the footer's right side for the page-number label. */ const val PAGE_NUMBER_BAND_WIDTH = 60 - const val FOOTER_PAGE_NUMBER_GAP = 8 + + /** Minimum horizontal gap between the footer text and the page-number. */ + private const val FOOTER_PAGE_NUMBER_GAP = 8 + + /** Maximum width for footer text (usable width minus the page-number and its gap) */ const val FOOTER_TEXT_MAX_WIDTH = USABLE_WIDTH - PAGE_NUMBER_BAND_WIDTH - FOOTER_PAGE_NUMBER_GAP } From 212ef46c3a4219e554039095b8e871ad0978e757 Mon Sep 17 00:00:00 2001 From: andreia Date: Thu, 11 Jun 2026 14:53:48 +0200 Subject: [PATCH 5/8] convert QR_SIZE to float and remove unneeded params --- .../groundplatform/feature/pdf/render/PdfConfig.kt | 2 +- .../pdf/render/components/PageFooterLayout.kt | 3 --- .../feature/pdf/render/components/QrBlockLayout.kt | 7 ++----- .../pdf/render/components/QrBlockLayoutTest.kt | 13 +++---------- 4 files changed, 6 insertions(+), 19 deletions(-) diff --git a/feature/pdf/src/commonMain/kotlin/org/groundplatform/feature/pdf/render/PdfConfig.kt b/feature/pdf/src/commonMain/kotlin/org/groundplatform/feature/pdf/render/PdfConfig.kt index 8a8640f64f..72a2023d9c 100644 --- a/feature/pdf/src/commonMain/kotlin/org/groundplatform/feature/pdf/render/PdfConfig.kt +++ b/feature/pdf/src/commonMain/kotlin/org/groundplatform/feature/pdf/render/PdfConfig.kt @@ -44,7 +44,7 @@ internal object PdfConfig { const val LINE_SPACING = 4f /** Target size of the QR code block. */ - const val QR_SIZE = 200 + const val QR_SIZE = 200f /** Horizontal gap between the two columns of the page header. */ const val HEADER_COLUMN_GAP = 16 diff --git a/feature/pdf/src/commonMain/kotlin/org/groundplatform/feature/pdf/render/components/PageFooterLayout.kt b/feature/pdf/src/commonMain/kotlin/org/groundplatform/feature/pdf/render/components/PageFooterLayout.kt index 9abf5498de..db3c56cf6d 100644 --- a/feature/pdf/src/commonMain/kotlin/org/groundplatform/feature/pdf/render/components/PageFooterLayout.kt +++ b/feature/pdf/src/commonMain/kotlin/org/groundplatform/feature/pdf/render/components/PageFooterLayout.kt @@ -27,13 +27,11 @@ import org.groundplatform.feature.pdf.render.PdfOffset * Pre-computed layout for the page footer with separate left and right slots. * * @param footerTextOffset The top-left position where the footer text begins. - * @param footerMaxWidth The maximum width available for the footer text. * @param pageNumberOffset The top-left position where the page number begins. * @param pageNumberMaxWidth The maximum width available for the page number */ internal data class PageFooterLayout( val footerTextOffset: PdfOffset, - val footerMaxWidth: Int, val pageNumberOffset: PdfOffset, val pageNumberMaxWidth: Int, ) { @@ -51,7 +49,6 @@ internal data class PageFooterLayout( val pageNumberLeft = left + USABLE_WIDTH - PAGE_NUMBER_BAND_WIDTH return PageFooterLayout( footerTextOffset = PdfOffset(left, top), - footerMaxWidth = FOOTER_TEXT_MAX_WIDTH, pageNumberOffset = PdfOffset(pageNumberLeft, top), pageNumberMaxWidth = PAGE_NUMBER_BAND_WIDTH, ) diff --git a/feature/pdf/src/commonMain/kotlin/org/groundplatform/feature/pdf/render/components/QrBlockLayout.kt b/feature/pdf/src/commonMain/kotlin/org/groundplatform/feature/pdf/render/components/QrBlockLayout.kt index aafb3b964a..c3ec764ddd 100644 --- a/feature/pdf/src/commonMain/kotlin/org/groundplatform/feature/pdf/render/components/QrBlockLayout.kt +++ b/feature/pdf/src/commonMain/kotlin/org/groundplatform/feature/pdf/render/components/QrBlockLayout.kt @@ -28,23 +28,20 @@ import org.groundplatform.feature.pdf.render.PdfRect * * @param qrFrame Position and size of the QR image. * @param captionOffset Top-left position of the caption text (centered under the QR). - * @param captionMaxWidth Maximum width for the caption. * @param nextCursorY Cursor Y position after this block. */ internal data class QrBlockLayout( val qrFrame: PdfRect, val captionOffset: PdfOffset, - val captionMaxWidth: Int, val nextCursorY: Float, ) { companion object { fun compute(top: Float, captionHeight: Float): QrBlockLayout { - val x = (PAGE_WIDTH - MARGIN - QR_SIZE).toFloat() + val x = (PAGE_WIDTH - MARGIN - QR_SIZE) val captionTop = top + QR_SIZE + LINE_SPACING return QrBlockLayout( - qrFrame = PdfRect(x, top, QR_SIZE.toFloat(), QR_SIZE.toFloat()), + qrFrame = PdfRect(x, top, QR_SIZE, QR_SIZE), captionOffset = PdfOffset(x, captionTop), - captionMaxWidth = QR_SIZE, nextCursorY = captionTop + captionHeight + LINE_SPACING * 2, ) } diff --git a/feature/pdf/src/commonTest/kotlin/org/groundplatform/feature/pdf/render/components/QrBlockLayoutTest.kt b/feature/pdf/src/commonTest/kotlin/org/groundplatform/feature/pdf/render/components/QrBlockLayoutTest.kt index befaa6a137..8c9deabc9a 100644 --- a/feature/pdf/src/commonTest/kotlin/org/groundplatform/feature/pdf/render/components/QrBlockLayoutTest.kt +++ b/feature/pdf/src/commonTest/kotlin/org/groundplatform/feature/pdf/render/components/QrBlockLayoutTest.kt @@ -26,7 +26,7 @@ class QrBlockLayoutTest { private val qrSize = PdfConfig.QR_SIZE private val lineSpacing = PdfConfig.LINE_SPACING - private val expectedX = (pageWidth - margin - qrSize).toFloat() + private val expectedX = (pageWidth - margin - qrSize) @Test fun `QR frame is a square anchored at the right margin`() { @@ -34,8 +34,8 @@ class QrBlockLayoutTest { assertEquals(expectedX, layout.qrFrame.x) assertEquals(0f, layout.qrFrame.y) - assertEquals(qrSize.toFloat(), layout.qrFrame.width) - assertEquals(qrSize.toFloat(), layout.qrFrame.height) + assertEquals(qrSize, layout.qrFrame.width) + assertEquals(qrSize, layout.qrFrame.height) assertEquals((pageWidth - margin).toFloat(), layout.qrFrame.right) } @@ -48,13 +48,6 @@ class QrBlockLayoutTest { assertEquals(top + qrSize + lineSpacing, layout.captionOffset.y) } - @Test - fun `caption maxWidth equals QR size so it stays under the QR image`() { - val layout = QrBlockLayout.compute(top = 0f, captionHeight = 10f) - - assertEquals(qrSize, layout.captionMaxWidth) - } - @Test fun `caption shares its X with the QR frame`() { val layout = QrBlockLayout.compute(top = 0f, captionHeight = 10f) From ab15c5361a2e0bb883e6ceb0a58813ce57a0fc60 Mon Sep 17 00:00:00 2001 From: andreia Date: Thu, 11 Jun 2026 14:57:38 +0200 Subject: [PATCH 6/8] move layout constants from PdfConfig to the specific layout files --- .../feature/pdf/render/PdfConfig.kt | 55 +------------------ .../pdf/render/components/PageFooterLayout.kt | 24 +++++--- .../pdf/render/components/PageHeaderLayout.kt | 17 ++++-- .../pdf/render/components/QrBlockLayout.kt | 4 +- .../pdf/render/components/TableLayout.kt | 29 +++++++++- .../render/components/PageFooterLayoutTest.kt | 11 ++-- .../render/components/PageHeaderLayoutTest.kt | 6 +- .../render/components/QrBlockLayoutTest.kt | 3 +- .../pdf/render/components/TableLayoutTest.kt | 6 +- 9 files changed, 74 insertions(+), 81 deletions(-) diff --git a/feature/pdf/src/commonMain/kotlin/org/groundplatform/feature/pdf/render/PdfConfig.kt b/feature/pdf/src/commonMain/kotlin/org/groundplatform/feature/pdf/render/PdfConfig.kt index 72a2023d9c..773a65a214 100644 --- a/feature/pdf/src/commonMain/kotlin/org/groundplatform/feature/pdf/render/PdfConfig.kt +++ b/feature/pdf/src/commonMain/kotlin/org/groundplatform/feature/pdf/render/PdfConfig.kt @@ -43,60 +43,9 @@ internal object PdfConfig { /** Vertical spacing added between lines of text. */ const val LINE_SPACING = 4f - /** Target size of the QR code block. */ - const val QR_SIZE = 200f - - /** Horizontal gap between the two columns of the page header. */ - const val HEADER_COLUMN_GAP = 16 - - /** Inner padding between a table cell's border and its text. */ - const val CELL_PADDING = 6 - - /** Stroke width used for table and cell borders. */ - const val BORDER_WIDTH = 0.5f - - /** Vertical gap below the header before body content begins. */ - const val HEADER_BOTTOM_GAP = 28f - - /** Vertical gap above the footer separating it from body content. */ - const val FOOTER_TOP_GAP = 28f - - /** Maximum number of lines rendered for each header value. */ - const val MAX_HEADER_VALUE_LINES = 1 - - /** Maximum number of lines rendered for the footer text. */ - const val MAX_FOOTER_LINES = 1 - - /** Resolution in dots-per-inch used when rasterizing images for the PDF. */ - const val IMAGE_RENDER_DPI = 300f - /** Content width between the left and right margins. */ const val USABLE_WIDTH = PAGE_WIDTH - 2 * MARGIN - /** Fraction of [USABLE_WIDTH] allotted to the task-label column of a table row. */ - private const val TABLE_TASK_LABEL_RATIO = 0.35f - - /** Width of the task-label column. */ - const val TABLE_TASK_COLUMN_WIDTH = (USABLE_WIDTH * TABLE_TASK_LABEL_RATIO).toInt() - - /** Width for task-label text (the task column minus its padding). */ - const val TABLE_TASK_TEXT_WIDTH = TABLE_TASK_COLUMN_WIDTH - 2 * CELL_PADDING - - /** Width for answer text (the remaining table width minus its padding). */ - const val TABLE_ANSWER_TEXT_WIDTH = USABLE_WIDTH - TABLE_TASK_COLUMN_WIDTH - 2 * CELL_PADDING - - /** Fraction of the usable page height a single photo may occupy before being scaled down. */ - private const val PHOTO_MAX_HEIGHT_RATIO = 0.35f - - /** Maximum table photo height. */ - const val PHOTO_MAX_HEIGHT = ((PAGE_HEIGHT - 2 * MARGIN) * PHOTO_MAX_HEIGHT_RATIO).toInt() - - /** Width reserved on the footer's right side for the page-number label. */ - const val PAGE_NUMBER_BAND_WIDTH = 60 - - /** Minimum horizontal gap between the footer text and the page-number. */ - private const val FOOTER_PAGE_NUMBER_GAP = 8 - - /** Maximum width for footer text (usable width minus the page-number and its gap) */ - const val FOOTER_TEXT_MAX_WIDTH = USABLE_WIDTH - PAGE_NUMBER_BAND_WIDTH - FOOTER_PAGE_NUMBER_GAP + /** Resolution in dots-per-inch used when rasterizing images for the PDF. */ + const val IMAGE_RENDER_DPI = 300f } diff --git a/feature/pdf/src/commonMain/kotlin/org/groundplatform/feature/pdf/render/components/PageFooterLayout.kt b/feature/pdf/src/commonMain/kotlin/org/groundplatform/feature/pdf/render/components/PageFooterLayout.kt index db3c56cf6d..915638e51b 100644 --- a/feature/pdf/src/commonMain/kotlin/org/groundplatform/feature/pdf/render/components/PageFooterLayout.kt +++ b/feature/pdf/src/commonMain/kotlin/org/groundplatform/feature/pdf/render/components/PageFooterLayout.kt @@ -15,11 +15,8 @@ */ package org.groundplatform.feature.pdf.render.components -import org.groundplatform.feature.pdf.render.PdfConfig.FOOTER_TEXT_MAX_WIDTH -import org.groundplatform.feature.pdf.render.PdfConfig.FOOTER_TOP_GAP import org.groundplatform.feature.pdf.render.PdfConfig.MARGIN import org.groundplatform.feature.pdf.render.PdfConfig.PAGE_HEIGHT -import org.groundplatform.feature.pdf.render.PdfConfig.PAGE_NUMBER_BAND_WIDTH import org.groundplatform.feature.pdf.render.PdfConfig.USABLE_WIDTH import org.groundplatform.feature.pdf.render.PdfOffset @@ -36,12 +33,25 @@ internal data class PageFooterLayout( val pageNumberMaxWidth: Int, ) { companion object { + /** Vertical gap above the footer separating it from body content. */ + const val TOP_GAP = 28f + + /** Width reserved on the footer's right side for the page-number label. */ + const val PAGE_NUMBER_BAND_WIDTH = 60 + + /** Minimum horizontal gap between the footer text and the page number. */ + const val PAGE_NUMBER_GAP = 8 + + /** Maximum width for footer text (usable width minus the page-number band and its gap). */ + const val TEXT_MAX_WIDTH = USABLE_WIDTH - PAGE_NUMBER_BAND_WIDTH - PAGE_NUMBER_GAP + + /** Maximum number of lines rendered for the footer text. */ + const val MAX_LINES = 1 + /** - * Vertical space the footer occupies, including the [FOOTER_TOP_GAP] separating it from page - * content. Feed this to [org.groundplatform.feature.pdf.render.PdfCursor] so pagination keeps - * the footer clear of content. + * Vertical space the footer occupies, including the [TOP_GAP] separating it from page content. */ - fun reserve(footerHeight: Float): Float = footerHeight + FOOTER_TOP_GAP + fun reserve(footerHeight: Float): Float = footerHeight + TOP_GAP fun compute(footerHeight: Float): PageFooterLayout { val top = PAGE_HEIGHT - MARGIN - footerHeight diff --git a/feature/pdf/src/commonMain/kotlin/org/groundplatform/feature/pdf/render/components/PageHeaderLayout.kt b/feature/pdf/src/commonMain/kotlin/org/groundplatform/feature/pdf/render/components/PageHeaderLayout.kt index c02bbb6025..7fef084998 100644 --- a/feature/pdf/src/commonMain/kotlin/org/groundplatform/feature/pdf/render/components/PageHeaderLayout.kt +++ b/feature/pdf/src/commonMain/kotlin/org/groundplatform/feature/pdf/render/components/PageHeaderLayout.kt @@ -15,8 +15,6 @@ */ package org.groundplatform.feature.pdf.render.components -import org.groundplatform.feature.pdf.render.PdfConfig.HEADER_BOTTOM_GAP -import org.groundplatform.feature.pdf.render.PdfConfig.HEADER_COLUMN_GAP import org.groundplatform.feature.pdf.render.PdfConfig.LINE_SPACING import org.groundplatform.feature.pdf.render.PdfConfig.MARGIN import org.groundplatform.feature.pdf.render.PdfConfig.USABLE_WIDTH @@ -38,10 +36,17 @@ internal data class PageHeaderLayout( val nextCursorY: Float, ) { companion object { - const val COLUMN_WIDTH: Int = (USABLE_WIDTH - 2 * HEADER_COLUMN_GAP) / 3 + /** Horizontal gap between the two columns of the page header. */ + const val COLUMN_GAP = 16 + /** Vertical gap below the header before body content begins. */ + const val BOTTOM_GAP = 28f + + /** Maximum number of lines rendered for each header value. */ + const val MAX_LINES = 1 + const val COLUMN_WIDTH: Int = (USABLE_WIDTH - 2 * COLUMN_GAP) / 3 const val LEFT_X: Float = MARGIN.toFloat() - const val CENTER_X: Float = LEFT_X + COLUMN_WIDTH + HEADER_COLUMN_GAP - const val RIGHT_X: Float = LEFT_X + 2 * (COLUMN_WIDTH + HEADER_COLUMN_GAP) + const val CENTER_X: Float = LEFT_X + COLUMN_WIDTH + COLUMN_GAP + const val RIGHT_X: Float = LEFT_X + 2 * (COLUMN_WIDTH + COLUMN_GAP) fun compute(top: Float, labelHeight: Float, valueHeight: Float): PageHeaderLayout { val columnBottom = top + labelHeight + LINE_SPACING + valueHeight @@ -49,7 +54,7 @@ internal data class PageHeaderLayout( leftColumn = column(LEFT_X, top, labelHeight), centerColumn = column(CENTER_X, top, labelHeight), rightTextOffset = PdfOffset(RIGHT_X, top), - nextCursorY = columnBottom + HEADER_BOTTOM_GAP, + nextCursorY = columnBottom + BOTTOM_GAP, ) } diff --git a/feature/pdf/src/commonMain/kotlin/org/groundplatform/feature/pdf/render/components/QrBlockLayout.kt b/feature/pdf/src/commonMain/kotlin/org/groundplatform/feature/pdf/render/components/QrBlockLayout.kt index c3ec764ddd..57de444128 100644 --- a/feature/pdf/src/commonMain/kotlin/org/groundplatform/feature/pdf/render/components/QrBlockLayout.kt +++ b/feature/pdf/src/commonMain/kotlin/org/groundplatform/feature/pdf/render/components/QrBlockLayout.kt @@ -18,7 +18,6 @@ package org.groundplatform.feature.pdf.render.components import org.groundplatform.feature.pdf.render.PdfConfig.LINE_SPACING import org.groundplatform.feature.pdf.render.PdfConfig.MARGIN import org.groundplatform.feature.pdf.render.PdfConfig.PAGE_WIDTH -import org.groundplatform.feature.pdf.render.PdfConfig.QR_SIZE import org.groundplatform.feature.pdf.render.PdfOffset import org.groundplatform.feature.pdf.render.PdfRect @@ -36,6 +35,9 @@ internal data class QrBlockLayout( val nextCursorY: Float, ) { companion object { + /** Target size of the QR code block. */ + const val QR_SIZE = 200f + fun compute(top: Float, captionHeight: Float): QrBlockLayout { val x = (PAGE_WIDTH - MARGIN - QR_SIZE) val captionTop = top + QR_SIZE + LINE_SPACING diff --git a/feature/pdf/src/commonMain/kotlin/org/groundplatform/feature/pdf/render/components/TableLayout.kt b/feature/pdf/src/commonMain/kotlin/org/groundplatform/feature/pdf/render/components/TableLayout.kt index 84d3a7e3af..ae5a2d3eb9 100644 --- a/feature/pdf/src/commonMain/kotlin/org/groundplatform/feature/pdf/render/components/TableLayout.kt +++ b/feature/pdf/src/commonMain/kotlin/org/groundplatform/feature/pdf/render/components/TableLayout.kt @@ -15,10 +15,9 @@ */ package org.groundplatform.feature.pdf.render.components -import org.groundplatform.feature.pdf.render.PdfConfig.CELL_PADDING import org.groundplatform.feature.pdf.render.PdfConfig.LINE_SPACING import org.groundplatform.feature.pdf.render.PdfConfig.MARGIN -import org.groundplatform.feature.pdf.render.PdfConfig.TABLE_TASK_COLUMN_WIDTH +import org.groundplatform.feature.pdf.render.PdfConfig.PAGE_HEIGHT import org.groundplatform.feature.pdf.render.PdfConfig.USABLE_WIDTH import org.groundplatform.feature.pdf.render.PdfItemSize import org.groundplatform.feature.pdf.render.PdfLine @@ -26,6 +25,30 @@ import org.groundplatform.feature.pdf.render.PdfOffset import org.groundplatform.feature.pdf.render.PdfRect internal object TableLayout { + /** Inner padding between a table cell's border and its text. */ + const val CELL_PADDING = 6 + + /** Stroke width used for table and cell borders. */ + const val BORDER_WIDTH = 0.5f + + /** Fraction of [USABLE_WIDTH] allotted to the task-label column of a table row. */ + private const val TASK_LABEL_RATIO = 0.35f + + /** Width of the task-label column. */ + const val TASK_COLUMN_WIDTH = (USABLE_WIDTH * TASK_LABEL_RATIO).toInt() + + /** Width for task-label text (the task column minus its padding). */ + const val TASK_TEXT_WIDTH = TASK_COLUMN_WIDTH - 2 * CELL_PADDING + + /** Width for answer text (the remaining table width minus its padding). */ + const val ANSWER_TEXT_WIDTH = USABLE_WIDTH - TASK_COLUMN_WIDTH - 2 * CELL_PADDING + + /** Fraction of the usable page height a single photo may occupy before being scaled down. */ + private const val PHOTO_MAX_HEIGHT_RATIO = 0.35f + + /** Maximum table photo height. */ + const val PHOTO_MAX_HEIGHT = ((PAGE_HEIGHT - 2 * MARGIN) * PHOTO_MAX_HEIGHT_RATIO).toInt() + /** * Layout of a single two-column table row. Left cell holds a single text block; right cell may * contain either text or an image. @@ -92,7 +115,7 @@ internal object TableLayout { val left = MARGIN.toFloat() val right = left + USABLE_WIDTH - val midX = left + TABLE_TASK_COLUMN_WIDTH + val midX = left + TASK_COLUMN_WIDTH val rowBottom = rowTop + totalHeight val contentTop = rowTop + CELL_PADDING val rightCellLeft = midX + CELL_PADDING diff --git a/feature/pdf/src/commonTest/kotlin/org/groundplatform/feature/pdf/render/components/PageFooterLayoutTest.kt b/feature/pdf/src/commonTest/kotlin/org/groundplatform/feature/pdf/render/components/PageFooterLayoutTest.kt index 8b56b1b98b..80f600f9f3 100644 --- a/feature/pdf/src/commonTest/kotlin/org/groundplatform/feature/pdf/render/components/PageFooterLayoutTest.kt +++ b/feature/pdf/src/commonTest/kotlin/org/groundplatform/feature/pdf/render/components/PageFooterLayoutTest.kt @@ -25,8 +25,8 @@ class PageFooterLayoutTest { private val margin = PdfConfig.MARGIN.toFloat() private val pageHeight = PdfConfig.PAGE_HEIGHT private val usableWidth = PdfConfig.USABLE_WIDTH - private val pageNumberBand = PdfConfig.PAGE_NUMBER_BAND_WIDTH - private val footerTextMaxWidth = PdfConfig.FOOTER_TEXT_MAX_WIDTH + private val pageNumberBand = PageFooterLayout.PAGE_NUMBER_BAND_WIDTH + private val footerTextMaxWidth = PageFooterLayout.TEXT_MAX_WIDTH @Test fun `footer text anchors against the bottom margin`() { @@ -46,10 +46,9 @@ class PageFooterLayoutTest { } @Test - fun `slot widths match their respective configuration constants`() { + fun `page number slot width matches its configuration constant`() { val layout = PageFooterLayout.compute(footerHeight = 12f) - assertEquals(footerTextMaxWidth, layout.footerMaxWidth) assertEquals(pageNumberBand, layout.pageNumberMaxWidth) } @@ -57,7 +56,7 @@ class PageFooterLayoutTest { fun `footer and page number slots do not overlap`() { val layout = PageFooterLayout.compute(footerHeight = 12f) - val footerRight = layout.footerTextOffset.x + layout.footerMaxWidth + val footerRight = layout.footerTextOffset.x + footerTextMaxWidth assertTrue(footerRight <= layout.pageNumberOffset.x) } @@ -93,6 +92,6 @@ class PageFooterLayoutTest { fun `reserve adds the top gap to the footer height`() { val footerHeight = 12f - assertEquals(footerHeight + PdfConfig.FOOTER_TOP_GAP, PageFooterLayout.reserve(footerHeight)) + assertEquals(footerHeight + PageFooterLayout.TOP_GAP, PageFooterLayout.reserve(footerHeight)) } } diff --git a/feature/pdf/src/commonTest/kotlin/org/groundplatform/feature/pdf/render/components/PageHeaderLayoutTest.kt b/feature/pdf/src/commonTest/kotlin/org/groundplatform/feature/pdf/render/components/PageHeaderLayoutTest.kt index b15573a95a..417c0f6662 100644 --- a/feature/pdf/src/commonTest/kotlin/org/groundplatform/feature/pdf/render/components/PageHeaderLayoutTest.kt +++ b/feature/pdf/src/commonTest/kotlin/org/groundplatform/feature/pdf/render/components/PageHeaderLayoutTest.kt @@ -21,12 +21,14 @@ import kotlin.test.assertTrue import kotlin.text.compareTo import org.groundplatform.feature.pdf.render.PdfConfig import org.groundplatform.feature.pdf.render.PdfOffset +import org.groundplatform.feature.pdf.render.components.PageHeaderLayout.Companion.BOTTOM_GAP +import org.groundplatform.feature.pdf.render.components.PageHeaderLayout.Companion.COLUMN_GAP class PageHeaderLayoutTest { private val lineSpacing = PdfConfig.LINE_SPACING - private val headerBottomGap = PdfConfig.HEADER_BOTTOM_GAP - private val headerColumnGap = PdfConfig.HEADER_COLUMN_GAP + private val headerBottomGap = BOTTOM_GAP + private val headerColumnGap = COLUMN_GAP private val usableWidth = PdfConfig.USABLE_WIDTH @Test diff --git a/feature/pdf/src/commonTest/kotlin/org/groundplatform/feature/pdf/render/components/QrBlockLayoutTest.kt b/feature/pdf/src/commonTest/kotlin/org/groundplatform/feature/pdf/render/components/QrBlockLayoutTest.kt index 8c9deabc9a..e7c54ccd60 100644 --- a/feature/pdf/src/commonTest/kotlin/org/groundplatform/feature/pdf/render/components/QrBlockLayoutTest.kt +++ b/feature/pdf/src/commonTest/kotlin/org/groundplatform/feature/pdf/render/components/QrBlockLayoutTest.kt @@ -18,12 +18,13 @@ package org.groundplatform.feature.pdf.render.components import kotlin.test.Test import kotlin.test.assertEquals import org.groundplatform.feature.pdf.render.PdfConfig +import org.groundplatform.feature.pdf.render.components.QrBlockLayout.Companion.QR_SIZE class QrBlockLayoutTest { private val margin = PdfConfig.MARGIN private val pageWidth = PdfConfig.PAGE_WIDTH - private val qrSize = PdfConfig.QR_SIZE + private val qrSize = QR_SIZE private val lineSpacing = PdfConfig.LINE_SPACING private val expectedX = (pageWidth - margin - qrSize) diff --git a/feature/pdf/src/commonTest/kotlin/org/groundplatform/feature/pdf/render/components/TableLayoutTest.kt b/feature/pdf/src/commonTest/kotlin/org/groundplatform/feature/pdf/render/components/TableLayoutTest.kt index c6a48aa924..c8a483223a 100644 --- a/feature/pdf/src/commonTest/kotlin/org/groundplatform/feature/pdf/render/components/TableLayoutTest.kt +++ b/feature/pdf/src/commonTest/kotlin/org/groundplatform/feature/pdf/render/components/TableLayoutTest.kt @@ -24,14 +24,16 @@ import org.groundplatform.feature.pdf.render.PdfConfig import org.groundplatform.feature.pdf.render.PdfItemSize import org.groundplatform.feature.pdf.render.PdfLine import org.groundplatform.feature.pdf.render.PdfOffset +import org.groundplatform.feature.pdf.render.components.TableLayout.CELL_PADDING +import org.groundplatform.feature.pdf.render.components.TableLayout.TASK_COLUMN_WIDTH class TableLayoutTest { - private val cellPadding = PdfConfig.CELL_PADDING.toFloat() + private val cellPadding = CELL_PADDING.toFloat() private val lineSpacing = PdfConfig.LINE_SPACING private val margin = PdfConfig.MARGIN.toFloat() private val usableWidth = PdfConfig.USABLE_WIDTH - private val taskColumnWidth = PdfConfig.TABLE_TASK_COLUMN_WIDTH + private val taskColumnWidth = TASK_COLUMN_WIDTH @Test fun `label sits below a top gap at the left margin`() { From a468c82dee861cd3160a62c9a69a3f87b8bbceef Mon Sep 17 00:00:00 2001 From: andreia Date: Thu, 11 Jun 2026 15:41:57 +0200 Subject: [PATCH 7/8] fix code style warnings --- .../feature/pdf/render/components/QrBlockLayout.kt | 2 +- .../feature/pdf/render/components/QrBlockLayoutTest.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/feature/pdf/src/commonMain/kotlin/org/groundplatform/feature/pdf/render/components/QrBlockLayout.kt b/feature/pdf/src/commonMain/kotlin/org/groundplatform/feature/pdf/render/components/QrBlockLayout.kt index 57de444128..9019edcc5b 100644 --- a/feature/pdf/src/commonMain/kotlin/org/groundplatform/feature/pdf/render/components/QrBlockLayout.kt +++ b/feature/pdf/src/commonMain/kotlin/org/groundplatform/feature/pdf/render/components/QrBlockLayout.kt @@ -39,7 +39,7 @@ internal data class QrBlockLayout( const val QR_SIZE = 200f fun compute(top: Float, captionHeight: Float): QrBlockLayout { - val x = (PAGE_WIDTH - MARGIN - QR_SIZE) + val x = PAGE_WIDTH - MARGIN - QR_SIZE val captionTop = top + QR_SIZE + LINE_SPACING return QrBlockLayout( qrFrame = PdfRect(x, top, QR_SIZE, QR_SIZE), diff --git a/feature/pdf/src/commonTest/kotlin/org/groundplatform/feature/pdf/render/components/QrBlockLayoutTest.kt b/feature/pdf/src/commonTest/kotlin/org/groundplatform/feature/pdf/render/components/QrBlockLayoutTest.kt index e7c54ccd60..17606c975e 100644 --- a/feature/pdf/src/commonTest/kotlin/org/groundplatform/feature/pdf/render/components/QrBlockLayoutTest.kt +++ b/feature/pdf/src/commonTest/kotlin/org/groundplatform/feature/pdf/render/components/QrBlockLayoutTest.kt @@ -27,7 +27,7 @@ class QrBlockLayoutTest { private val qrSize = QR_SIZE private val lineSpacing = PdfConfig.LINE_SPACING - private val expectedX = (pageWidth - margin - qrSize) + private val expectedX = pageWidth - margin - qrSize @Test fun `QR frame is a square anchored at the right margin`() { From 0c7574d000a7ceaa9f969b956bee2f32e9b1f75f Mon Sep 17 00:00:00 2001 From: andreia Date: Thu, 11 Jun 2026 15:56:36 +0200 Subject: [PATCH 8/8] rename components package to layout --- .../pdf/render/{components => layout}/PageFooterLayout.kt | 2 +- .../pdf/render/{components => layout}/PageHeaderLayout.kt | 2 +- .../pdf/render/{components => layout}/QrBlockLayout.kt | 2 +- .../pdf/render/{components => layout}/TableLayout.kt | 2 +- .../render/{components => layout}/PageFooterLayoutTest.kt | 2 +- .../render/{components => layout}/PageHeaderLayoutTest.kt | 7 +++---- .../pdf/render/{components => layout}/QrBlockLayoutTest.kt | 4 ++-- .../pdf/render/{components => layout}/TableLayoutTest.kt | 6 +++--- 8 files changed, 13 insertions(+), 14 deletions(-) rename feature/pdf/src/commonMain/kotlin/org/groundplatform/feature/pdf/render/{components => layout}/PageFooterLayout.kt (97%) rename feature/pdf/src/commonMain/kotlin/org/groundplatform/feature/pdf/render/{components => layout}/PageHeaderLayout.kt (97%) rename feature/pdf/src/commonMain/kotlin/org/groundplatform/feature/pdf/render/{components => layout}/QrBlockLayout.kt (97%) rename feature/pdf/src/commonMain/kotlin/org/groundplatform/feature/pdf/render/{components => layout}/TableLayout.kt (99%) rename feature/pdf/src/commonTest/kotlin/org/groundplatform/feature/pdf/render/{components => layout}/PageFooterLayoutTest.kt (98%) rename feature/pdf/src/commonTest/kotlin/org/groundplatform/feature/pdf/render/{components => layout}/PageHeaderLayoutTest.kt (93%) rename feature/pdf/src/commonTest/kotlin/org/groundplatform/feature/pdf/render/{components => layout}/QrBlockLayoutTest.kt (93%) rename feature/pdf/src/commonTest/kotlin/org/groundplatform/feature/pdf/render/{components => layout}/TableLayoutTest.kt (97%) diff --git a/feature/pdf/src/commonMain/kotlin/org/groundplatform/feature/pdf/render/components/PageFooterLayout.kt b/feature/pdf/src/commonMain/kotlin/org/groundplatform/feature/pdf/render/layout/PageFooterLayout.kt similarity index 97% rename from feature/pdf/src/commonMain/kotlin/org/groundplatform/feature/pdf/render/components/PageFooterLayout.kt rename to feature/pdf/src/commonMain/kotlin/org/groundplatform/feature/pdf/render/layout/PageFooterLayout.kt index 915638e51b..ff67c28bcc 100644 --- a/feature/pdf/src/commonMain/kotlin/org/groundplatform/feature/pdf/render/components/PageFooterLayout.kt +++ b/feature/pdf/src/commonMain/kotlin/org/groundplatform/feature/pdf/render/layout/PageFooterLayout.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.groundplatform.feature.pdf.render.components +package org.groundplatform.feature.pdf.render.layout import org.groundplatform.feature.pdf.render.PdfConfig.MARGIN import org.groundplatform.feature.pdf.render.PdfConfig.PAGE_HEIGHT diff --git a/feature/pdf/src/commonMain/kotlin/org/groundplatform/feature/pdf/render/components/PageHeaderLayout.kt b/feature/pdf/src/commonMain/kotlin/org/groundplatform/feature/pdf/render/layout/PageHeaderLayout.kt similarity index 97% rename from feature/pdf/src/commonMain/kotlin/org/groundplatform/feature/pdf/render/components/PageHeaderLayout.kt rename to feature/pdf/src/commonMain/kotlin/org/groundplatform/feature/pdf/render/layout/PageHeaderLayout.kt index 7fef084998..9e061827a7 100644 --- a/feature/pdf/src/commonMain/kotlin/org/groundplatform/feature/pdf/render/components/PageHeaderLayout.kt +++ b/feature/pdf/src/commonMain/kotlin/org/groundplatform/feature/pdf/render/layout/PageHeaderLayout.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.groundplatform.feature.pdf.render.components +package org.groundplatform.feature.pdf.render.layout import org.groundplatform.feature.pdf.render.PdfConfig.LINE_SPACING import org.groundplatform.feature.pdf.render.PdfConfig.MARGIN diff --git a/feature/pdf/src/commonMain/kotlin/org/groundplatform/feature/pdf/render/components/QrBlockLayout.kt b/feature/pdf/src/commonMain/kotlin/org/groundplatform/feature/pdf/render/layout/QrBlockLayout.kt similarity index 97% rename from feature/pdf/src/commonMain/kotlin/org/groundplatform/feature/pdf/render/components/QrBlockLayout.kt rename to feature/pdf/src/commonMain/kotlin/org/groundplatform/feature/pdf/render/layout/QrBlockLayout.kt index 9019edcc5b..edf77eb30f 100644 --- a/feature/pdf/src/commonMain/kotlin/org/groundplatform/feature/pdf/render/components/QrBlockLayout.kt +++ b/feature/pdf/src/commonMain/kotlin/org/groundplatform/feature/pdf/render/layout/QrBlockLayout.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.groundplatform.feature.pdf.render.components +package org.groundplatform.feature.pdf.render.layout import org.groundplatform.feature.pdf.render.PdfConfig.LINE_SPACING import org.groundplatform.feature.pdf.render.PdfConfig.MARGIN diff --git a/feature/pdf/src/commonMain/kotlin/org/groundplatform/feature/pdf/render/components/TableLayout.kt b/feature/pdf/src/commonMain/kotlin/org/groundplatform/feature/pdf/render/layout/TableLayout.kt similarity index 99% rename from feature/pdf/src/commonMain/kotlin/org/groundplatform/feature/pdf/render/components/TableLayout.kt rename to feature/pdf/src/commonMain/kotlin/org/groundplatform/feature/pdf/render/layout/TableLayout.kt index ae5a2d3eb9..8d044b3e34 100644 --- a/feature/pdf/src/commonMain/kotlin/org/groundplatform/feature/pdf/render/components/TableLayout.kt +++ b/feature/pdf/src/commonMain/kotlin/org/groundplatform/feature/pdf/render/layout/TableLayout.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.groundplatform.feature.pdf.render.components +package org.groundplatform.feature.pdf.render.layout import org.groundplatform.feature.pdf.render.PdfConfig.LINE_SPACING import org.groundplatform.feature.pdf.render.PdfConfig.MARGIN diff --git a/feature/pdf/src/commonTest/kotlin/org/groundplatform/feature/pdf/render/components/PageFooterLayoutTest.kt b/feature/pdf/src/commonTest/kotlin/org/groundplatform/feature/pdf/render/layout/PageFooterLayoutTest.kt similarity index 98% rename from feature/pdf/src/commonTest/kotlin/org/groundplatform/feature/pdf/render/components/PageFooterLayoutTest.kt rename to feature/pdf/src/commonTest/kotlin/org/groundplatform/feature/pdf/render/layout/PageFooterLayoutTest.kt index 80f600f9f3..2624d774dd 100644 --- a/feature/pdf/src/commonTest/kotlin/org/groundplatform/feature/pdf/render/components/PageFooterLayoutTest.kt +++ b/feature/pdf/src/commonTest/kotlin/org/groundplatform/feature/pdf/render/layout/PageFooterLayoutTest.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.groundplatform.feature.pdf.render.components +package org.groundplatform.feature.pdf.render.layout import kotlin.test.Test import kotlin.test.assertEquals diff --git a/feature/pdf/src/commonTest/kotlin/org/groundplatform/feature/pdf/render/components/PageHeaderLayoutTest.kt b/feature/pdf/src/commonTest/kotlin/org/groundplatform/feature/pdf/render/layout/PageHeaderLayoutTest.kt similarity index 93% rename from feature/pdf/src/commonTest/kotlin/org/groundplatform/feature/pdf/render/components/PageHeaderLayoutTest.kt rename to feature/pdf/src/commonTest/kotlin/org/groundplatform/feature/pdf/render/layout/PageHeaderLayoutTest.kt index 417c0f6662..0d8f290a69 100644 --- a/feature/pdf/src/commonTest/kotlin/org/groundplatform/feature/pdf/render/components/PageHeaderLayoutTest.kt +++ b/feature/pdf/src/commonTest/kotlin/org/groundplatform/feature/pdf/render/layout/PageHeaderLayoutTest.kt @@ -13,16 +13,15 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.groundplatform.feature.pdf.render.components +package org.groundplatform.feature.pdf.render.layout import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertTrue -import kotlin.text.compareTo import org.groundplatform.feature.pdf.render.PdfConfig import org.groundplatform.feature.pdf.render.PdfOffset -import org.groundplatform.feature.pdf.render.components.PageHeaderLayout.Companion.BOTTOM_GAP -import org.groundplatform.feature.pdf.render.components.PageHeaderLayout.Companion.COLUMN_GAP +import org.groundplatform.feature.pdf.render.layout.PageHeaderLayout.Companion.BOTTOM_GAP +import org.groundplatform.feature.pdf.render.layout.PageHeaderLayout.Companion.COLUMN_GAP class PageHeaderLayoutTest { diff --git a/feature/pdf/src/commonTest/kotlin/org/groundplatform/feature/pdf/render/components/QrBlockLayoutTest.kt b/feature/pdf/src/commonTest/kotlin/org/groundplatform/feature/pdf/render/layout/QrBlockLayoutTest.kt similarity index 93% rename from feature/pdf/src/commonTest/kotlin/org/groundplatform/feature/pdf/render/components/QrBlockLayoutTest.kt rename to feature/pdf/src/commonTest/kotlin/org/groundplatform/feature/pdf/render/layout/QrBlockLayoutTest.kt index 17606c975e..eabb71ddc0 100644 --- a/feature/pdf/src/commonTest/kotlin/org/groundplatform/feature/pdf/render/components/QrBlockLayoutTest.kt +++ b/feature/pdf/src/commonTest/kotlin/org/groundplatform/feature/pdf/render/layout/QrBlockLayoutTest.kt @@ -13,12 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.groundplatform.feature.pdf.render.components +package org.groundplatform.feature.pdf.render.layout import kotlin.test.Test import kotlin.test.assertEquals import org.groundplatform.feature.pdf.render.PdfConfig -import org.groundplatform.feature.pdf.render.components.QrBlockLayout.Companion.QR_SIZE +import org.groundplatform.feature.pdf.render.layout.QrBlockLayout.Companion.QR_SIZE class QrBlockLayoutTest { diff --git a/feature/pdf/src/commonTest/kotlin/org/groundplatform/feature/pdf/render/components/TableLayoutTest.kt b/feature/pdf/src/commonTest/kotlin/org/groundplatform/feature/pdf/render/layout/TableLayoutTest.kt similarity index 97% rename from feature/pdf/src/commonTest/kotlin/org/groundplatform/feature/pdf/render/components/TableLayoutTest.kt rename to feature/pdf/src/commonTest/kotlin/org/groundplatform/feature/pdf/render/layout/TableLayoutTest.kt index c8a483223a..822bb6d537 100644 --- a/feature/pdf/src/commonTest/kotlin/org/groundplatform/feature/pdf/render/components/TableLayoutTest.kt +++ b/feature/pdf/src/commonTest/kotlin/org/groundplatform/feature/pdf/render/layout/TableLayoutTest.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.groundplatform.feature.pdf.render.components +package org.groundplatform.feature.pdf.render.layout import kotlin.test.Test import kotlin.test.assertEquals @@ -24,8 +24,8 @@ import org.groundplatform.feature.pdf.render.PdfConfig import org.groundplatform.feature.pdf.render.PdfItemSize import org.groundplatform.feature.pdf.render.PdfLine import org.groundplatform.feature.pdf.render.PdfOffset -import org.groundplatform.feature.pdf.render.components.TableLayout.CELL_PADDING -import org.groundplatform.feature.pdf.render.components.TableLayout.TASK_COLUMN_WIDTH +import org.groundplatform.feature.pdf.render.layout.TableLayout.CELL_PADDING +import org.groundplatform.feature.pdf.render.layout.TableLayout.TASK_COLUMN_WIDTH class TableLayoutTest {