Implement Phase 9: PDF export via share intents
- PdfExporter: creates PdfDocument, scales 300 DPI canonical coords to 72 DPI PDF points, renders strokes without grid - Share via Intent.ACTION_SEND + FileProvider - PDF button in editor toolbar exports current page Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -14,8 +14,10 @@ import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import net.metacircular.engpad.data.db.EngPadDatabase
|
||||
import net.metacircular.engpad.data.db.toFloatArray
|
||||
import net.metacircular.engpad.data.model.Page
|
||||
import net.metacircular.engpad.data.model.PageSize
|
||||
import net.metacircular.engpad.data.repository.PageRepository
|
||||
import net.metacircular.engpad.ui.export.PdfExporter
|
||||
|
||||
@Composable
|
||||
fun EditorScreen(
|
||||
@@ -97,6 +99,16 @@ fun EditorScreen(
|
||||
viewModel.copySelection()
|
||||
canvasView.clearSelection()
|
||||
},
|
||||
onExport = {
|
||||
val page = Page(id = pageId, notebookId = 0, pageNumber = 1, createdAt = 0)
|
||||
val file = PdfExporter.exportPages(
|
||||
context = context,
|
||||
notebookTitle = "eng-pad-export",
|
||||
pageSize = pageSize,
|
||||
pages = listOf(page to strokes),
|
||||
)
|
||||
PdfExporter.shareFile(context, file)
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
AndroidView(
|
||||
|
||||
@@ -23,6 +23,7 @@ fun EditorToolbar(
|
||||
hasSelection: Boolean,
|
||||
onDeleteSelection: () -> Unit,
|
||||
onCopySelection: () -> Unit,
|
||||
onExport: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Row(
|
||||
@@ -58,6 +59,7 @@ fun EditorToolbar(
|
||||
TextButton(onClick = onCopySelection) { Text("Copy") }
|
||||
}
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
TextButton(onClick = onExport) { Text("PDF") }
|
||||
TextButton(onClick = onUndo, enabled = canUndo) {
|
||||
Text("Undo")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
package net.metacircular.engpad.ui.export
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Color
|
||||
import android.graphics.Paint
|
||||
import android.graphics.Path
|
||||
import android.graphics.pdf.PdfDocument
|
||||
import androidx.core.content.FileProvider
|
||||
import net.metacircular.engpad.data.db.toFloatArray
|
||||
import net.metacircular.engpad.data.model.Page
|
||||
import net.metacircular.engpad.data.model.PageSize
|
||||
import net.metacircular.engpad.data.model.Stroke
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* Exports pages to PDF. Coordinates are in 300 DPI canonical space and
|
||||
* scaled to 72 DPI (PDF points) during export via a scale factor of 0.24.
|
||||
*/
|
||||
object PdfExporter {
|
||||
|
||||
private const val CANONICAL_TO_PDF = 72f / 300f // 0.24
|
||||
|
||||
fun exportPages(
|
||||
context: Context,
|
||||
notebookTitle: String,
|
||||
pageSize: PageSize,
|
||||
pages: List<Pair<Page, List<Stroke>>>,
|
||||
): File {
|
||||
val pdfDoc = PdfDocument()
|
||||
|
||||
val pdfWidth = (pageSize.widthPt * CANONICAL_TO_PDF).toInt()
|
||||
val pdfHeight = (pageSize.heightPt * CANONICAL_TO_PDF).toInt()
|
||||
|
||||
for ((index, pair) in pages.withIndex()) {
|
||||
val (_, strokes) = pair
|
||||
val pageInfo = PdfDocument.PageInfo.Builder(pdfWidth, pdfHeight, index + 1).create()
|
||||
val pdfPage = pdfDoc.startPage(pageInfo)
|
||||
val canvas = pdfPage.canvas
|
||||
|
||||
// Scale from canonical (300 DPI) to PDF (72 DPI)
|
||||
canvas.scale(CANONICAL_TO_PDF, CANONICAL_TO_PDF)
|
||||
|
||||
// Draw strokes (no grid)
|
||||
for (stroke in strokes) {
|
||||
val points = stroke.pointData.toFloatArray()
|
||||
val path = buildPath(points)
|
||||
val paint = buildPaint(stroke.penSize, stroke.color)
|
||||
canvas.drawPath(path, paint)
|
||||
}
|
||||
|
||||
pdfDoc.finishPage(pdfPage)
|
||||
}
|
||||
|
||||
val exportDir = File(context.cacheDir, "exports")
|
||||
exportDir.mkdirs()
|
||||
val sanitizedTitle = notebookTitle.replace(Regex("[^a-zA-Z0-9._-]"), "_")
|
||||
val file = File(exportDir, "$sanitizedTitle.pdf")
|
||||
file.outputStream().use { pdfDoc.writeTo(it) }
|
||||
pdfDoc.close()
|
||||
|
||||
return file
|
||||
}
|
||||
|
||||
fun shareFile(context: Context, file: File) {
|
||||
val uri = FileProvider.getUriForFile(
|
||||
context,
|
||||
"${context.packageName}.fileprovider",
|
||||
file,
|
||||
)
|
||||
val intent = Intent(Intent.ACTION_SEND).apply {
|
||||
type = "application/pdf"
|
||||
putExtra(Intent.EXTRA_STREAM, uri)
|
||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
}
|
||||
context.startActivity(Intent.createChooser(intent, "Export PDF"))
|
||||
}
|
||||
|
||||
private fun buildPath(points: FloatArray): Path {
|
||||
val path = Path()
|
||||
if (points.size < 2) return path
|
||||
path.moveTo(points[0], points[1])
|
||||
var i = 2
|
||||
while (i < points.size - 1) {
|
||||
path.lineTo(points[i], points[i + 1])
|
||||
i += 2
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
private fun buildPaint(penSize: Float, color: Int): Paint {
|
||||
return Paint().apply {
|
||||
this.color = color
|
||||
strokeWidth = penSize
|
||||
style = Paint.Style.STROKE
|
||||
strokeCap = Paint.Cap.ROUND
|
||||
strokeJoin = Paint.Join.ROUND
|
||||
isAntiAlias = true
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user