Snap to standard formats: A4, Letter...

This commit is contained in:
Pierre-Yves Nicolas
2026-05-23 20:10:51 +02:00
parent 9394b19731
commit c3bd144681
6 changed files with 179 additions and 10 deletions

View File

@@ -39,7 +39,7 @@ data class PageToExport(
val quad = metadata.normalizedQuad.scaledTo(1.0, 1.0, size.width, size.height)
val realDimensions = estimateRealDimensions(
quad, size.width.toInt(), size.height.toInt(), metadata.opticalMeasures
)
).snapToStandardFormat()
return realDimensions.applyRotation(metadata.baseRotation)
}
}

View File

@@ -24,6 +24,7 @@ import org.fairscan.app.BuildConfig
import org.fairscan.app.data.PdfWriter
import org.fairscan.app.domain.PageToExport
import org.fairscan.imageprocessing.EstimatedDimensions
import org.fairscan.imageprocessing.PaperFormats
import java.io.OutputStream
import java.util.Calendar
@@ -43,17 +44,18 @@ class AndroidPdfWriter : PdfWriter {
val heightPx = image.height.toFloat()
val dimensions = page.estimatedDimensions()
val (widthPoints, heightPoints) = when (dimensions) {
is EstimatedDimensions.Physical -> {
dimensions.widthMm.toFloat() * pointsPerMm to dimensions.heightMm.toFloat() * pointsPerMm
}
val (widthMm, heightMm) = when (dimensions) {
is EstimatedDimensions.Physical ->
clipToMaxFormat(dimensions.widthMm, dimensions.heightMm)
else -> {
// No physical dimensions available: approximate using US Letter max dimension
val maxDimInMm = 279.4f
val scalePxToMm = maxDimInMm / maxOf(widthPx, heightPx)
widthPx * scalePxToMm * pointsPerMm to heightPx * scalePxToMm * pointsPerMm
// No physical dimensions available
val maxDimMm = PaperFormats.A4.heightMm
val scalePxToMm = maxDimMm / maxOf(widthPx, heightPx)
clipToMaxFormat(widthPx * scalePxToMm, heightPx * scalePxToMm)
}
}
val widthPoints = widthMm.toFloat() * pointsPerMm
val heightPoints = heightMm.toFloat() * pointsPerMm
val page = PDPage(PDRectangle(widthPoints, heightPoints))
document.addPage(page)
@@ -68,3 +70,14 @@ class AndroidPdfWriter : PdfWriter {
return doc.numberOfPages
}
}
fun clipToMaxFormat(widthMm: Double, heightMm: Double): Pair<Double, Double> {
// Normalize to portrait for comparison
val (w, h) = if (widthMm <= heightMm) widthMm to heightMm else heightMm to widthMm
val portrait = widthMm <= heightMm
val maxFormat = PaperFormats.A4
val scale = minOf(maxFormat.widthMm / w, maxFormat.heightMm / h, 1.0)
val clipped = w * scale to h * scale
return if (portrait) clipped else clipped.second to clipped.first
}

View File

@@ -0,0 +1,58 @@
/*
* Copyright 2025-2026 The FairScan authors
*
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation, either version 3 of the License, or (at your option)
* any later version.
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.fairscan.app.platform
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.offset
import org.junit.Test
class AndroidPdfWriterTest {
@Test fun `portrait smaller than A4 is unchanged`() {
val (w, h) = clipToMaxFormat(100.0, 150.0)
assertThat(w).isEqualTo(100.0)
assertThat(h).isEqualTo(150.0)
}
@Test fun `portrait taller than A4 is clipped preserving ratio`() {
val (w, h) = clipToMaxFormat(210.0, 400.0)
assertThat(h).isCloseTo(297.0, offset(0.1))
assertThat(w / h).isCloseTo(210.0 / 400.0, offset(0.001))
}
@Test fun `landscape wider than A4 is clipped preserving ratio`() {
val (w, h) = clipToMaxFormat(300.0, 200.0)
assertThat(w).isCloseTo(297.0, offset(0.1))
assertThat(w / h).isCloseTo(300.0 / 200.0, offset(0.001))
}
@Test fun `exactly A4 is unchanged`() {
val (w, h) = clipToMaxFormat(210.0, 297.0)
assertThat(w).isCloseTo(210.0, offset(0.001))
assertThat(h).isCloseTo(297.0, offset(0.001))
}
@Test fun `landscape orientation is preserved after clip`() {
val (w, h) = clipToMaxFormat(400.0, 250.0)
assertThat(w).isGreaterThan(h)
}
@Test fun `landscape smaller than A4 is unchanged`() {
val (w, h) = clipToMaxFormat(297.0, 150.0)
assertThat(w).isCloseTo(297.0, offset(0.001))
assertThat(h).isCloseTo(150.0, offset(0.001))
}
}