From 3bca426c99039af14225384d264f32c09d9ab68f Mon Sep 17 00:00:00 2001 From: ZXCLI Date: Sun, 6 Sep 2026 20:04:06 +0800 Subject: [PATCH] =?UTF-8?q?android:=20PDF=E5=B7=A1=E6=A3=80=E6=8A=A5?= =?UTF-8?q?=E5=91=8A=E7=94=9F=E6=88=90(PdfDocument)=20=E9=98=B6=E6=AE=B55b?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/mag160c/thermal/media/PdfReport.kt | 129 ++++++++++++++++++ .../thermal/ui/analyze/AnalyzeViewer.kt | 34 ++++- 2 files changed, 160 insertions(+), 3 deletions(-) create mode 100644 android/app/src/main/kotlin/com/mag160c/thermal/media/PdfReport.kt diff --git a/android/app/src/main/kotlin/com/mag160c/thermal/media/PdfReport.kt b/android/app/src/main/kotlin/com/mag160c/thermal/media/PdfReport.kt new file mode 100644 index 0000000..891b257 --- /dev/null +++ b/android/app/src/main/kotlin/com/mag160c/thermal/media/PdfReport.kt @@ -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 + } +} diff --git a/android/app/src/main/kotlin/com/mag160c/thermal/ui/analyze/AnalyzeViewer.kt b/android/app/src/main/kotlin/com/mag160c/thermal/ui/analyze/AnalyzeViewer.kt index c11749c..1665ad7 100644 --- a/android/app/src/main/kotlin/com/mag160c/thermal/ui/analyze/AnalyzeViewer.kt +++ b/android/app/src/main/kotlin/com/mag160c/thermal/ui/analyze/AnalyzeViewer.kt @@ -44,6 +44,8 @@ import com.mag160c.thermal.ui.gallery.GalleryViewModel * Single-file MDT analysis viewer: pinch zoom/pan, palette re-render, * text note editing. */ +import java.util.Locale + @Composable fun AnalyzeViewer(item: GalleryViewModel.Item, galleryVm: GalleryViewModel) { val context = LocalContext.current @@ -58,6 +60,7 @@ fun AnalyzeViewer(item: GalleryViewModel.Item, galleryVm: GalleryViewModel) { var zoom by remember { mutableStateOf(1f) } var pan by remember { mutableStateOf(Offset.Zero) } var showNote by remember { mutableStateOf(false) } + var reportName by remember { mutableStateOf(null) } var note by remember { mutableStateOf(vm.decodeNote() ?: "") } LaunchedEffect(Unit) { vm.render(2) } @@ -114,13 +117,38 @@ fun AnalyzeViewer(item: GalleryViewModel.Item, galleryVm: GalleryViewModel) { ) } } - Button( - onClick = { showNote = true }, + Row( modifier = Modifier .align(Alignment.TopEnd) .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) { NoteEditor(