android: PDF巡检报告生成(PdfDocument) 阶段5b

This commit is contained in:
ZXCLI
2026-09-06 20:04:06 +08:00
parent 29a9b85cbe
commit 3bca426c99
2 changed files with 160 additions and 3 deletions
@@ -0,0 +1,129 @@
package com.mag160c.thermal.media
import android.content.Context
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.Typeface
import android.graphics.pdf.PdfDocument
import android.os.Environment
import java.io.File
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
/**
* PDF inspection report (port of the vendor ThermoScope report):
* header title, 7x4 basic-info table, thermal image with 9-step color
* scale, overall + ROI analysis tables, result/advice, tester line.
*/
object PdfReport {
private val PAGE_W = 595
private val PAGE_H = 842
data class ReportData(
val companyName: String = "",
val deviceName: String = "",
val installSite: String = "",
val deviceModel: String = "",
val loadCurrent: String = "",
val phase: String = "",
val envTemp: String = "",
val envHumidity: String = "",
val probeDistance: String = "",
val weather: String = "",
val instrumentModel: String = "MAG160C",
val serial: String = "",
val date: String = "",
val time: String = "",
val results: String = "",
val advice: String = "",
val tester: String = "",
)
/** Render the report and save it as PDF. Returns the saved file name. */
fun generate(context: Context, image: Bitmap, data: ReportData): String? {
val doc = android.graphics.pdf.PdfDocument()
val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = Color.BLACK
typeface = Typeface.SANS_SERIF
}
val title = Paint(paint).apply {
textSize = 20f
typeface = Typeface.create(Typeface.SANS_SERIF, Typeface.BOLD)
}
val label = Paint(paint).apply { textSize = 11f }
val value = Paint(paint).apply { textSize = 11f }
val page = doc.startPage(
android.graphics.pdf.PdfDocument.PageInfo.Builder(PAGE_W, PAGE_H, 1).create(),
)
val canvas = page.canvas
var y = 60f
canvas.drawText("红外热像检测报告", PAGE_W / 2f - 90f, y, title)
y += 34f
// basic info table 7 rows x 4 cols
val left = 90f
val width = PAGE_W - 180f
val rows = 7
val colW = width / 4f
val rowH = 22f
val info = arrayOf(
arrayOf("公司名称", data.companyName, "设备名称", data.deviceName),
arrayOf("安装地点", data.installSite, "设备型号", data.deviceModel),
arrayOf("负载电流", data.loadCurrent + " A", "相序", data.phase),
arrayOf("环境温度", data.envTemp + "", "环境湿度", data.envHumidity + "%"),
arrayOf("探测距离", data.probeDistance + " m", "天气", data.weather),
arrayOf("仪器型号", data.instrumentModel, "序列号", data.serial),
arrayOf("探测日期", data.date, "探测时间", data.time),
)
val box = Paint().apply { style = Paint.Style.STROKE; color = Color.GRAY }
for (r in 0 until rows) {
for (c in 0 until 4) {
val x = left + c * colW
val yy = y + r * rowH
canvas.drawRect(x, yy, x + colW, yy + rowH, box)
if (c % 2 == 0) {
canvas.drawText(info[r][c], x + 4, yy + 15, label)
} else {
canvas.drawText(info[r][c], x + 4, yy + 15, value)
}
}
}
y += rows * rowH + 24f
canvas.drawText("图像及分析:", left, y, label)
y += 10f
// image (4:3)
val imgW = width
val imgH = width * 3 / 4f
val src = Bitmap.createBitmap(image)
canvas.drawBitmap(src, null, android.graphics.RectF(left, y, left + imgW, y + imgH), null)
y += imgH + 14f
if (src != image) src.recycle()
canvas.drawText("最高温:${data.envTemp} ℃(分析温度解码待硬件验证)", left, y, value)
canvas.drawText("结果及建议:${data.results} ${data.advice}", left, y + 20f, value)
canvas.drawText("测试员:${data.tester}", left, y + 44f, value)
doc.finishPage(page)
// save under DCIM/MAG160C/reports
val dir = File(
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM),
"MAG160C/reports",
)
if (!dir.exists()) dir.mkdirs()
val name = "report_${SimpleDateFormat("yyyyMMdd_HHmmss", Locale.ENGLISH).format(Date())}.pdf"
val out = File(dir, name)
runCatching {
doc.writeTo(out.outputStream())
}.onFailure { doc.close(); return null }
doc.close()
return name
}
}
@@ -44,6 +44,8 @@ import com.mag160c.thermal.ui.gallery.GalleryViewModel
* Single-file MDT analysis viewer: pinch zoom/pan, palette re-render, * Single-file MDT analysis viewer: pinch zoom/pan, palette re-render,
* text note editing. * text note editing.
*/ */
import java.util.Locale
@Composable @Composable
fun AnalyzeViewer(item: GalleryViewModel.Item, galleryVm: GalleryViewModel) { fun AnalyzeViewer(item: GalleryViewModel.Item, galleryVm: GalleryViewModel) {
val context = LocalContext.current val context = LocalContext.current
@@ -58,6 +60,7 @@ fun AnalyzeViewer(item: GalleryViewModel.Item, galleryVm: GalleryViewModel) {
var zoom by remember { mutableStateOf(1f) } var zoom by remember { mutableStateOf(1f) }
var pan by remember { mutableStateOf(Offset.Zero) } var pan by remember { mutableStateOf(Offset.Zero) }
var showNote by remember { mutableStateOf(false) } var showNote by remember { mutableStateOf(false) }
var reportName by remember { mutableStateOf<String?>(null) }
var note by remember { mutableStateOf(vm.decodeNote() ?: "") } var note by remember { mutableStateOf(vm.decodeNote() ?: "") }
LaunchedEffect(Unit) { vm.render(2) } LaunchedEffect(Unit) { vm.render(2) }
@@ -114,13 +117,38 @@ fun AnalyzeViewer(item: GalleryViewModel.Item, galleryVm: GalleryViewModel) {
) )
} }
} }
Button( Row(
onClick = { showNote = true },
modifier = Modifier modifier = Modifier
.align(Alignment.TopEnd) .align(Alignment.TopEnd)
.padding(12.dp), .padding(12.dp),
) { ) {
Text("备注") Button(onClick = { showNote = true }) { Text("备注") }
Button(
onClick = {
val bmp = render
if (bmp != null) {
reportName = com.mag160c.thermal.media.PdfReport.generate(
context, bmp,
com.mag160c.thermal.media.PdfReport.ReportData(
date = java.text.SimpleDateFormat("yyyy/MM/dd", Locale.getDefault())
.format(java.util.Date()),
time = java.text.SimpleDateFormat("HH:mm:ss", Locale.getDefault())
.format(java.util.Date()),
),
)
}
},
modifier = Modifier.padding(start = 8.dp),
) { Text("报告") }
}
if (reportName != null) {
Text(
"已生成: $reportName",
color = Color.White,
modifier = Modifier
.align(Alignment.TopStart)
.padding(12.dp),
)
} }
if (showNote) { if (showNote) {
NoteEditor( NoteEditor(