Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,17 @@ import android.text.StaticLayout
import org.groundplatform.feature.pdf.render.image.PdfImage

internal class FakePdfCanvas : PdfCanvas {
val startedPages = mutableListOf<Int>()
val startedPageNumbers = mutableListOf<Int>()
val startedPageSizes = mutableListOf<PdfPageSize>()
var finishedPages = 0
val drawnText = mutableListOf<String>()
val drawnImages = mutableListOf<PdfImage>()
val drawnLines = mutableListOf<PdfLine>()
val drawnOverlays = mutableListOf<Pair<MapOverlay, Boolean>>()

override fun startPage(pageNumber: Int) {
startedPages += pageNumber
override fun startPage(pageNumber: Int, pageSize: PdfPageSize) {
startedPageNumbers += pageNumber
startedPageSizes += pageSize
}

override fun finishPage() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ class PdfCanvasTest {
val layout = StaticLayout.Builder.obtain("body", 0, "body".length, TextPaint(), 100).build()
val image = PdfImage(Bitmap.createBitmap(10, 10, Bitmap.Config.ARGB_8888))
with(MeasurementPdfCanvas) {
startPage(pageNumber = 1)
startPage(pageNumber = 1, pageSize = PdfConfig.REPORT_PAGE_SIZE)
drawStaticLayout(layout, x = 0f, y = 0f)
drawImage(image, RectF(0f, 0f, 10f, 10f), smoothScaling = true)
drawLine(0f, 0f, 10f, 10f)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,16 +46,43 @@ class PdfWriterTest {

newPdfWriter(EMPTY_DOCUMENT, PdfImageSet(emptyMap()), canvas).drawDocument(EMPTY_DOCUMENT)

assertEquals(0, canvas.startedPages.size)
assertEquals(0, canvas.startedPageNumbers.size)
assertEquals(0, canvas.finishedPages)
}

@Test
fun `opens and closes exactly one page for a single-page document`() {
fun `opens and closes a qr page and one content page for a single-page document`() {
val canvas = renderDocument(SINGLE_PAGE_DOCUMENT)

assertEquals(listOf(1), canvas.startedPages)
assertEquals(1, canvas.finishedPages)
assertEquals(listOf(1, 2), canvas.startedPageNumbers)
assertEquals(2, canvas.finishedPages)
}

@Test
fun `draws the qr on an A7 page ahead of the A4 content pages`() {
val canvas = renderDocument(SINGLE_PAGE_DOCUMENT)

assertEquals(
listOf(PdfConfig.QR_PAGE_SIZE, PdfConfig.REPORT_PAGE_SIZE),
canvas.startedPageSizes,
)
}

@Test
fun `does not open a qr page when no qr image is provided`() {
val canvas = renderDocument(SINGLE_PAGE_DOCUMENT, pdfImageSet(qr = null))

assertEquals(listOf(PdfConfig.REPORT_PAGE_SIZE), canvas.startedPageSizes)
}

@Test
fun `draws no header or footer on the qr page`() {
val tableless =
SINGLE_PAGE_DOCUMENT.copy(table = SINGLE_PAGE_DOCUMENT.table.copy(rows = emptyList()))

val canvas = renderDocument(tableless, pdfImageSet(qr = pdfImage()))

assertEquals(listOf(QR_BLOCK.submissionName, QR_BLOCK.scanCaption), canvas.drawnText)
}

@Test
Expand Down Expand Up @@ -86,23 +113,24 @@ class PdfWriterTest {

pdfWriter.drawDocument(TEST_PDF_DOCUMENT)

assertTrue(pdfWriter.pageCount > 1)
assertEquals(pdfWriter.pageCount, canvas.drawnText.count { it == HEADER.surveyName })
assertTrue(pdfWriter.bodyPageCount > 1)
assertEquals(pdfWriter.bodyPageCount, canvas.drawnText.count { it == HEADER.surveyName })
assertEquals(
pdfWriter.pageCount,
pdfWriter.bodyPageCount,
canvas.drawnText.count {
it == "${FOOTER.dataCollectorLabel}: ${FOOTER.dataCollectorName}, ${FOOTER.userEmail}"
},
)
}

@Test
fun `draws the qr image and caption when a qr image is provided`() {
fun `draws the qr image, its submission title and caption when a qr image is provided`() {
val qr = pdfImage()
val canvas = renderDocument(SINGLE_PAGE_DOCUMENT, pdfImageSet(qr = qr))

assertTrue(canvas.drawnImages.any { it.bitmap === qr.bitmap })
assertTrue(canvas.drawnText.contains(QR_BLOCK.scanCaption))
assertTrue(canvas.drawnText.contains(QR_BLOCK.submissionName))
}

@Test
Expand Down Expand Up @@ -144,7 +172,7 @@ class PdfWriterTest {
}

@Test
fun `includes the page number in the footer when totalPages is set`() {
fun `numbers the content pages in the footer, leaving the qr page out of the count`() {
val canvas = renderDocument(SINGLE_PAGE_DOCUMENT, totalPages = 1)

assertTrue(canvas.drawnText.contains("1/1"))
Expand Down Expand Up @@ -175,8 +203,8 @@ class PdfWriterTest {
pdfWriter.drawDocument(TEST_PDF_DOCUMENT)

// Every page resets the flag, so each page's first row draws exactly 1 top border.
assertTrue(pdfWriter.pageCount > 1)
assertEquals(pdfWriter.pageCount, canvas.topBorderCount())
assertTrue(pdfWriter.bodyPageCount > 1)
assertEquals(pdfWriter.bodyPageCount, canvas.topBorderCount())
}

@Test
Expand All @@ -186,7 +214,7 @@ class PdfWriterTest {

val canvas = renderDocument(tableless, pdfImageSet(qr = pdfImage()))

assertEquals(listOf(1), canvas.startedPages)
assertEquals(listOf(1), canvas.startedPageNumbers)
assertFalse(canvas.drawnText.contains(TABLE.submissionLabel))
}

Expand All @@ -200,7 +228,7 @@ class PdfWriterTest {
private fun renderPageCount(totalPages: Int?): Int =
newPdfWriter(TEST_PDF_DOCUMENT, PdfImageSet(emptyMap()), MeasurementPdfCanvas, totalPages)
.apply { drawDocument(TEST_PDF_DOCUMENT) }
.pageCount
.bodyPageCount

private fun newPdfWriter(
document: SubmissionPdfDocument,
Expand Down Expand Up @@ -252,7 +280,7 @@ class PdfWriterTest {
userEmail = "user@gmail.com",
)

val QR_BLOCK = SubmissionPdfDocument.QrBlock(scanCaption = "Scan")
val QR_BLOCK = SubmissionPdfDocument.QrBlock(submissionName = "Plot 42", scanCaption = "Scan")

val TABLE =
SubmissionPdfDocument.Table(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ import kotlinx.coroutines.coroutineScope
import org.groundplatform.feature.pdf.render.fitInside
import org.groundplatform.feature.pdf.render.image.PdfImage
import org.groundplatform.feature.pdf.render.image.PdfImageSet
import org.groundplatform.feature.pdf.render.layout.QrBlockLayout
import org.groundplatform.feature.pdf.render.layout.QrPageLayout
import org.groundplatform.feature.pdf.render.layout.TableLayout
import org.groundplatform.feature.pdf.render.pointsToRenderPixels
import org.groundplatform.ui.components.qrcode.PDF_LOGO_SIZE_FRACTION
Expand All @@ -56,7 +56,7 @@ class AndroidPdfImageProvider(
@DrawableRes private val logoDrawableRes: Int,
) : PdfImageProvider {

private val qrMaxPx = pointsToRenderPixels(QrBlockLayout.QR_SIZE)
private val qrMaxPx = pointsToRenderPixels(QrPageLayout.QR_SIZE)
private val photoMaxWidthPx = pointsToRenderPixels(TableLayout.ANSWER_TEXT_WIDTH.toFloat())
private val photoMaxHeightPx = pointsToRenderPixels(TableLayout.PHOTO_MAX_HEIGHT.toFloat())

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ class AndroidPdfRenderer(private val ioDispatcher: CoroutineDispatcher) : PdfRen
private fun measurePageCount(document: SubmissionPdfDocument, images: PdfImageSet): Int =
writer(document, images, MeasurementPdfCanvas, totalPages = null)
.apply { drawDocument(document) }
.pageCount
.bodyPageCount

private fun writer(
document: SubmissionPdfDocument,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,8 @@ internal class DocumentPdfCanvas(private val pdf: PdfDocument) : PdfCanvas {

private val mapOverlayTextPaint = PdfTextPaints().mapOverlay

override fun startPage(pageNumber: Int) {
val info =
PdfDocument.PageInfo.Builder(PdfConfig.PAGE_WIDTH, PdfConfig.PAGE_HEIGHT, pageNumber).create()
override fun startPage(pageNumber: Int, pageSize: PdfPageSize) {
val info = PdfDocument.PageInfo.Builder(pageSize.width, pageSize.height, pageNumber).create()
currentPage = pdf.startPage(info)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import org.groundplatform.feature.pdf.render.image.PdfImage

/** Abstraction for drawing onto a PDF page. */
internal interface PdfCanvas {
fun startPage(pageNumber: Int)
fun startPage(pageNumber: Int, pageSize: PdfPageSize)

fun finishPage()

Expand All @@ -36,7 +36,7 @@ internal interface PdfCanvas {

/** Used during the page-counting phase. Drops every drawing call. */
internal object MeasurementPdfCanvas : PdfCanvas {
override fun startPage(pageNumber: Int) = Unit
override fun startPage(pageNumber: Int, pageSize: PdfPageSize) = Unit

override fun finishPage() = Unit

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import org.groundplatform.feature.pdf.render.PdfConfig.TITLE_SIZE
internal class PdfTextPaints {
val title: TextPaint = textPaint(TITLE_SIZE, bold = false)
val body: TextPaint = textPaint(BODY_SIZE, bold = false)
val bodyLabel: TextPaint = textPaint(BODY_SIZE, bold = true)
val metaLabel: TextPaint = textPaint(CAPTION_SIZE, bold = true, textColor = Color.GRAY)
val meta: TextPaint = textPaint(CAPTION_SIZE, bold = false, textColor = Color.GRAY)
val caption: TextPaint = textPaint(CAPTION_SIZE, bold = false)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ import org.groundplatform.feature.pdf.render.image.PdfImage
import org.groundplatform.feature.pdf.render.image.PdfImageSet
import org.groundplatform.feature.pdf.render.layout.PageFooterLayout
import org.groundplatform.feature.pdf.render.layout.PageHeaderLayout
import org.groundplatform.feature.pdf.render.layout.QrBlockLayout
import org.groundplatform.feature.pdf.render.layout.QrPageLayout
import org.groundplatform.feature.pdf.render.layout.TableLayout

/**
Expand All @@ -53,16 +53,19 @@ internal class PdfWriter(
) : PdfPageController.PageLifecycle {
private val paints = PdfTextPaints()

private val coverImage = images[PdfImageSet.ImageRef.Qr]

private val footerLayout: StaticLayout = buildFooterLayout(footer)
private val cursor =
PdfCursor(footerReserve = PageFooterLayout.reserve(footerLayout.height.toFloat()))
private val pageController = PdfPageController(cursor, this)
private val pageController =
PdfPageController(cursor, this, coverPages = if (coverImage == null) 0 else 1)

val pageCount: Int
get() = pageController.pageCount
val bodyPageCount: Int
get() = pageController.bodyPageCount

override fun onPageStarted(pageNumber: Int) {
pdfCanvas.startPage(pageNumber)
pdfCanvas.startPage(pageNumber, PdfConfig.REPORT_PAGE_SIZE)
drawPageHeader()
}

Expand All @@ -72,26 +75,34 @@ internal class PdfWriter(
}

fun drawDocument(document: SubmissionPdfDocument) {
drawQrBlock(document.qrBlock)
drawQrPage(document.qrBlock)
drawTable(document.table)
finalizePage()
}

private fun drawQrBlock(block: QrBlock) {
val qr = images[PdfImageSet.ImageRef.Qr] ?: return
pageController.ensurePage()
val captionLayout =
private fun drawQrPage(block: QrBlock) {
val qr = coverImage ?: return
val blockWidth = QrPageLayout.QR_SIZE.toInt()
val titleLayout =
staticLayout(
block.scanCaption,
paints.caption,
QrBlockLayout.QR_SIZE.toInt(),
Layout.Alignment.ALIGN_CENTER,
text = block.submissionName,
paint = paints.bodyLabel,
maxWidth = blockWidth,
alignment = Layout.Alignment.ALIGN_CENTER,
maxLines = QrPageLayout.TITLE_MAX_LINES,
)
val captionLayout =
staticLayout(block.scanCaption, paints.caption, blockWidth, Layout.Alignment.ALIGN_CENTER)
val layout =
QrBlockLayout.compute(top = cursor.y, captionHeight = captionLayout.height.toFloat())
QrPageLayout.compute(
titleHeight = titleLayout.height.toFloat(),
captionHeight = captionLayout.height.toFloat(),
)
pdfCanvas.startPage(COVER_PAGE_NUMBER, PdfConfig.QR_PAGE_SIZE)
drawStaticLayoutAt(titleLayout, layout.titleOffset)
drawImage(qr, layout.qrFrame, smoothScaling = false)
drawStaticLayoutAt(captionLayout, layout.captionOffset)
cursor.moveTo(layout.nextCursorY)
pdfCanvas.finishPage()
}

private fun drawTable(table: SubmissionPdfDocument.Table) {
Expand Down Expand Up @@ -183,7 +194,7 @@ internal class PdfWriter(
totalPages?.let { total ->
val pageNumber =
staticLayout(
"${pageController.pageCount}/$total",
"${pageController.bodyPageCount}/$total",
paints.meta,
layout.pageNumberMaxWidth,
alignment = Layout.Alignment.ALIGN_OPPOSITE,
Expand Down Expand Up @@ -238,19 +249,19 @@ internal class PdfWriter(
private fun drawLine(line: PdfLine) =
pdfCanvas.drawLine(line.startX, line.startY, line.endX, line.endY)

private fun buildFooterLayout(footer: Footer): StaticLayout {
val footerLabel = footer.dataCollectorLabel
val footerText =
SpannableString("$footerLabel: ${footer.dataCollectorName}, ${footer.userEmail}").apply {
setSpan(StyleSpan(Typeface.BOLD), 0, footerLabel.length, Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
}
return staticLayout(
footerText,
private fun buildFooterLayout(footer: Footer): StaticLayout =
staticLayout(
labeled(footer.dataCollectorLabel, "${footer.dataCollectorName}, ${footer.userEmail}"),
paints.meta,
PageFooterLayout.TEXT_MAX_WIDTH,
maxLines = PageFooterLayout.MAX_LINES,
)
}

/** Builds a `"<label>: <value>"` string with the label in bold. */
private fun labeled(label: String, value: String): SpannableString =
SpannableString("$label: $value").apply {
setSpan(StyleSpan(Typeface.BOLD), 0, label.length, Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
}

/**
* Lays out [text] wrapped to [maxWidth]. When [maxLines] is set, overflow is ellipsized so a
Expand All @@ -273,4 +284,12 @@ internal class PdfWriter(
}
}
.build()

private companion object {
/**
* The QR page opens the document, so the body pages start at 2 whenever there is this cover
* page.
*/
const val COVER_PAGE_NUMBER = 1
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ class LoiReportMapper(
val document =
SubmissionPdfDocument(
header = buildHeader(details, submission),
qrBlock = buildQrBlock(),
qrBlock = buildQrBlock(loiReport.loiName),
footer = buildFooter(details),
table =
Table(
Expand Down Expand Up @@ -93,8 +93,11 @@ class LoiReportMapper(
dateFormatter.formatTime(submission.lastModified.clientTimestamp),
)

private suspend fun buildQrBlock(): QrBlock =
QrBlock(scanCaption = strings.resolve(Res.string.scan_this_qr_to_download_geojson))
private suspend fun buildQrBlock(loiName: String): QrBlock =
QrBlock(
submissionName = loiName,
scanCaption = strings.resolve(Res.string.scan_this_qr_to_download_geojson),
)

private suspend fun buildFooter(details: LoiReport.SubmissionDetails): Footer =
Footer(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ data class SubmissionPdfDocument(
val timestamp: String,
)

data class QrBlock(val scanCaption: String)
data class QrBlock(val submissionName: String, val scanCaption: String)

data class Table(val submissionLabel: String, val loiName: String, val rows: List<Row>)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,18 @@ internal object PdfConfig {
/** Page height in points (A4 portrait, 297mm). */
const val PAGE_HEIGHT = 842

/** QR page width in points (A7 portrait, 74mm). */
const val QR_PAGE_WIDTH = 210

/** QR page height in points (A7 portrait, 105mm). */
const val QR_PAGE_HEIGHT = 298

/** Size of the report body pages. */
val REPORT_PAGE_SIZE = PdfPageSize(PAGE_WIDTH, PAGE_HEIGHT)

/** Size of the standalone QR page. */
val QR_PAGE_SIZE = PdfPageSize(QR_PAGE_WIDTH, QR_PAGE_HEIGHT)

/** Page margin applied to all four edges. */
const val MARGIN = 40

Expand Down
Loading
Loading