From 49650f19ec78bfaa4915a426047801bf77882fd6 Mon Sep 17 00:00:00 2001 From: LoadingByte Date: Mon, 21 Oct 2024 01:47:51 +0200 Subject: [PATCH] Add a thin wrapper around Skia's PDF backend --- .../configuration/CommonTasksConfiguration.kt | 1 + .../kotlin/org/jetbrains/skia/Document.kt | 80 +++++++++++++ .../jetbrains/skia/pdf/PDFCompressionLevel.kt | 9 ++ .../org/jetbrains/skia/pdf/PDFDateTime.kt | 38 ++++++ .../org/jetbrains/skia/pdf/PDFDocument.kt | 73 ++++++++++++ .../org/jetbrains/skia/pdf/PDFMetadata.kt | 48 ++++++++ skiko/src/jvmMain/cpp/common/Document.cc | 26 +++++ .../src/jvmMain/cpp/common/pdf/PDFDocument.cc | 58 +++++++++ .../org/jetbrains/skia/pdf/PDFDocumentTest.kt | 110 ++++++++++++++++++ skiko/src/nativeJsMain/cpp/Document.cc | 21 ++++ skiko/src/nativeJsMain/cpp/pdf/PDFDocument.cc | 53 +++++++++ 11 files changed, 517 insertions(+) create mode 100644 skiko/src/commonMain/kotlin/org/jetbrains/skia/Document.kt create mode 100644 skiko/src/commonMain/kotlin/org/jetbrains/skia/pdf/PDFCompressionLevel.kt create mode 100644 skiko/src/commonMain/kotlin/org/jetbrains/skia/pdf/PDFDateTime.kt create mode 100644 skiko/src/commonMain/kotlin/org/jetbrains/skia/pdf/PDFDocument.kt create mode 100644 skiko/src/commonMain/kotlin/org/jetbrains/skia/pdf/PDFMetadata.kt create mode 100644 skiko/src/jvmMain/cpp/common/Document.cc create mode 100644 skiko/src/jvmMain/cpp/common/pdf/PDFDocument.cc create mode 100644 skiko/src/jvmTest/kotlin/org/jetbrains/skia/pdf/PDFDocumentTest.kt create mode 100644 skiko/src/nativeJsMain/cpp/Document.cc create mode 100644 skiko/src/nativeJsMain/cpp/pdf/PDFDocument.cc diff --git a/skiko/buildSrc/src/main/kotlin/tasks/configuration/CommonTasksConfiguration.kt b/skiko/buildSrc/src/main/kotlin/tasks/configuration/CommonTasksConfiguration.kt index ee1ca75e7..5cbe8b09b 100644 --- a/skiko/buildSrc/src/main/kotlin/tasks/configuration/CommonTasksConfiguration.kt +++ b/skiko/buildSrc/src/main/kotlin/tasks/configuration/CommonTasksConfiguration.kt @@ -27,6 +27,7 @@ fun skiaHeadersDirs(skiaDir: File): List = skiaDir.resolve("include/utils"), skiaDir.resolve("include/codec"), skiaDir.resolve("include/svg"), + skiaDir.resolve("include/docs"), skiaDir.resolve("modules/skottie/include"), skiaDir.resolve("modules/skparagraph/include"), skiaDir.resolve("modules/skshaper/include"), diff --git a/skiko/src/commonMain/kotlin/org/jetbrains/skia/Document.kt b/skiko/src/commonMain/kotlin/org/jetbrains/skia/Document.kt new file mode 100644 index 000000000..327a70382 --- /dev/null +++ b/skiko/src/commonMain/kotlin/org/jetbrains/skia/Document.kt @@ -0,0 +1,80 @@ +package org.jetbrains.skia + +import org.jetbrains.skia.impl.* +import org.jetbrains.skia.impl.Library.Companion.staticLoad + +/** + * High-level API for creating a document-based canvas. To use: + * + * 1. Create a document, e.g., via `PDFDocument.make(...)`. + * 2. For each page of content: + * ``` + * canvas = doc.beginPage(...) + * draw_my_content(canvas) + * doc.endPage() + * ``` + * 3. Close the document with `doc.close()`. + */ +class Document internal constructor(ptr: NativePointer, internal val _owner: Any) : RefCnt(ptr) { + + companion object { + init { + staticLoad() + } + } + + /** + * Begins a new page for the document, returning the canvas that will draw + * into the page. The document owns this canvas, and it will go out of + * scope when endPage() or close() is called, or the document is deleted. + * + * @throws IllegalArgumentException If no page can be created with the supplied arguments. + */ + fun beginPage(width: Float, height: Float, content: Rect? = null): Canvas { + Stats.onNativeCall() + try { + val ptr = interopScope { + _nBeginPage(_ptr, width, height, toInterop(content?.serializeToFloatArray())) + } + require(ptr != NullPointer) { "Document page was created with invalid arguments." } + return Canvas(ptr, false, this) + } finally { + reachabilityBarrier(this) + } + } + + /** + * Call endPage() when the content for the current page has been drawn + * (into the canvas returned by beginPage()). After this call the canvas + * returned by beginPage() will be out-of-scope. + */ + fun endPage() { + Stats.onNativeCall() + try { + _nEndPage(_ptr) + } finally { + reachabilityBarrier(this) + } + } + + /** + * Call close() when all pages have been drawn. This will close the file + * or stream holding the document's contents. After close() the document + * can no longer add new pages. + */ + // Deleting the document (which super.close() does) will automatically invoke SkDocument::close. + override fun close() { + super.close() + } + +} + +@ExternalSymbolName("org_jetbrains_skia_Document__1nBeginPage") +@ModuleImport("./skiko.mjs", "org_jetbrains_skia_Document__1nBeginPage") +private external fun _nBeginPage( + ptr: NativePointer, width: Float, height: Float, content: InteropPointer +): NativePointer + +@ExternalSymbolName("org_jetbrains_skia_Document__1nEndPage") +@ModuleImport("./skiko.mjs", "org_jetbrains_skia_Document__1nEndPage") +private external fun _nEndPage(ptr: NativePointer) diff --git a/skiko/src/commonMain/kotlin/org/jetbrains/skia/pdf/PDFCompressionLevel.kt b/skiko/src/commonMain/kotlin/org/jetbrains/skia/pdf/PDFCompressionLevel.kt new file mode 100644 index 000000000..a8ab3368e --- /dev/null +++ b/skiko/src/commonMain/kotlin/org/jetbrains/skia/pdf/PDFCompressionLevel.kt @@ -0,0 +1,9 @@ +package org.jetbrains.skia.pdf + +enum class PDFCompressionLevel(internal val skiaRepresentation: Int) { + DEFAULT(-1), + NONE(0), + LOW_BUT_FAST(1), + AVERAGE(6), + HIGH_BUT_SLOW(9); +} diff --git a/skiko/src/commonMain/kotlin/org/jetbrains/skia/pdf/PDFDateTime.kt b/skiko/src/commonMain/kotlin/org/jetbrains/skia/pdf/PDFDateTime.kt new file mode 100644 index 000000000..dba7d3514 --- /dev/null +++ b/skiko/src/commonMain/kotlin/org/jetbrains/skia/pdf/PDFDateTime.kt @@ -0,0 +1,38 @@ +package org.jetbrains.skia.pdf + +/* + * This class mirrors SkPDF::DateTime, but as Skia uses it only in the PDF backend, + * we've moved it into the PDF package to not pollute the main namespace. + * + * Notice that we have omitted the dayOfWeek field, as it is unused in the PDF backend. + */ +/** + * @property year Year, e.g., 2023. + * @property month Month between 1 and 12. + * @property day Day between 1 and 31. + * @property hour Hour between 0 and 23. + * @property minute Minute between 0 and 59. + * @property second Second between 0 and 59. + * @property timeZoneMinutes The number of minutes that the time zone is ahead of or behind UTC. + */ +data class PDFDateTime( + val year: Int, + val month: Int, + val day: Int, + val hour: Int, + val minute: Int, + val second: Int, + val timeZoneMinutes: Int = 0 +) { + + init { + require(month in 1..12) { "Month must be between 1 and 12." } + require(day in 1..31) { "Day must be between 1 and 31." } + require(hour in 0..23) { "Hour must be between 0 and 23." } + require(minute in 0..59) { "Minute must be between 0 and 59." } + require(second in 0..59) { "Second must be between 0 and 59." } + } + + internal fun asArray() = intArrayOf(year, month, day, hour, minute, second, timeZoneMinutes) + +} diff --git a/skiko/src/commonMain/kotlin/org/jetbrains/skia/pdf/PDFDocument.kt b/skiko/src/commonMain/kotlin/org/jetbrains/skia/pdf/PDFDocument.kt new file mode 100644 index 000000000..fa3c7ebce --- /dev/null +++ b/skiko/src/commonMain/kotlin/org/jetbrains/skia/pdf/PDFDocument.kt @@ -0,0 +1,73 @@ +package org.jetbrains.skia.pdf + +import org.jetbrains.skia.Document +import org.jetbrains.skia.ExternalSymbolName +import org.jetbrains.skia.ModuleImport +import org.jetbrains.skia.WStream +import org.jetbrains.skia.impl.* +import org.jetbrains.skia.impl.Library.Companion.staticLoad +import org.jetbrains.skia.impl.Native.Companion.NullPointer + +object PDFDocument { + + init { + staticLoad() + } + + /** + * Creates a PDF-backed document, writing the results into a WStream. + * + * PDF pages are sized in point units. 1 pt == 1/72 inch == 127/360 mm. + * + * @param out A PDF document will be written to this stream. The document may write + * to the stream at anytime during its lifetime, until either close() is + * called or the document is deleted. + * @param metadata A PDFMetadata object. Any fields may be left empty. + * @throws IllegalArgumentException If no PDF document can be created with the supplied arguments. + */ + fun make(out: WStream, metadata: PDFMetadata = PDFMetadata()): Document { + Stats.onNativeCall() + val ptr = try { + interopScope { + _nMakeDocument( + getPtr(out), + toInterop(metadata.title), + toInterop(metadata.author), + toInterop(metadata.subject), + toInterop(metadata.keywords), + toInterop(metadata.creator), + toInterop(metadata.producer), + toInterop(metadata.creation?.asArray()), + toInterop(metadata.modified?.asArray()), + metadata.rasterDPI, + metadata.pdfA, + metadata.encodingQuality, + metadata.compressionLevel.skiaRepresentation + ) + } + } finally { + reachabilityBarrier(out) + } + require(ptr != NullPointer) { "PDF document was created with invalid arguments." } + return Document(ptr, out) + } + +} + +@ExternalSymbolName("org_jetbrains_skia_pdf_PDFDocument__1nMakeDocument") +@ModuleImport("./skiko.mjs", "org_jetbrains_skia_pdf_PDFDocument__1nMakeDocument") +private external fun _nMakeDocument( + wstreamPtr: NativePointer, + title: InteropPointer, + author: InteropPointer, + subject: InteropPointer, + keywords: InteropPointer, + creator: InteropPointer, + producer: InteropPointer, + creation: InteropPointer, + modified: InteropPointer, + rasterDPI: Float, + pdfA: Boolean, + encodingQuality: Int, + compressionLevel: Int +): NativePointer diff --git a/skiko/src/commonMain/kotlin/org/jetbrains/skia/pdf/PDFMetadata.kt b/skiko/src/commonMain/kotlin/org/jetbrains/skia/pdf/PDFMetadata.kt new file mode 100644 index 000000000..145b58ca3 --- /dev/null +++ b/skiko/src/commonMain/kotlin/org/jetbrains/skia/pdf/PDFMetadata.kt @@ -0,0 +1,48 @@ +package org.jetbrains.skia.pdf + +/** + * Optional metadata to be passed into the PDF factory function. + * + * @property title The document's title. + * @property author The name of the person who created the document. + * @property subject The subject of the document. + * @property keywords Keywords associated with the document. + * Commas may be used to delineate keywords within the string. + * @property creator If the document was converted to PDF from another format, + * the name of the conforming product that created the + * original document from which it was converted. + * @property producer The product that is converting this document to PDF. + * @property creation The date and time the document was created. + * The zero default value represents an unknown/unset time. + * @property modified The date and time the document was most recently modified. + * The zero default value represents an unknown/unset time. + * @property rasterDPI The DPI (pixels-per-inch) at which features without native PDF support + * will be rasterized (e.g. draw image with perspective, draw text with + * perspective, ...). A larger DPI would create a PDF that reflects the + * original intent with better fidelity, but it can make for larger PDF + * files too, which would use more memory while rendering, and it would be + * slower to be processed or sent online or to printer. + * @property pdfA If true, include XMP metadata, a document UUID, and sRGB output intent + * information. This adds length to the document and makes it + * non-reproducible, but are necessary features for PDF/A-2b conformance + * @property encodingQuality Encoding quality controls the trade-off between size and quality. By + * default this is set to 101 percent, which corresponds to lossless + * encoding. If this value is set to a value <= 100, and the image is + * opaque, it will be encoded (using JPEG) with that quality setting. + * @property compressionLevel PDF streams may be compressed to save space. + * Use this to specify the desired compression vs time tradeoff. + */ +data class PDFMetadata( + val title: String? = null, + val author: String? = null, + val subject: String? = null, + val keywords: String? = null, + val creator: String? = null, + val producer: String? = "Skia/PDF", + val creation: PDFDateTime? = null, + val modified: PDFDateTime? = null, + val rasterDPI: Float = 72f, + val pdfA: Boolean = false, + val encodingQuality: Int = 101, + val compressionLevel: PDFCompressionLevel = PDFCompressionLevel.DEFAULT +) diff --git a/skiko/src/jvmMain/cpp/common/Document.cc b/skiko/src/jvmMain/cpp/common/Document.cc new file mode 100644 index 000000000..0766b6bf5 --- /dev/null +++ b/skiko/src/jvmMain/cpp/common/Document.cc @@ -0,0 +1,26 @@ +#include +#include "SkDocument.h" +#include "interop.hh" + +extern "C" JNIEXPORT jlong JNICALL Java_org_jetbrains_skia_DocumentKt__1nBeginPage + (JNIEnv* env, jclass jclass, jlong ptr, jfloat width, jfloat height, jfloatArray jcontentArr) { + SkDocument* instance = reinterpret_cast(static_cast(ptr)); + jfloat* contentArr; + SkRect content; + SkRect* contentPtr = nullptr; + if (jcontentArr != nullptr) { + contentArr = env->GetFloatArrayElements(jcontentArr, 0); + content = { contentArr[0], contentArr[1], contentArr[2], contentArr[3] }; + contentPtr = &content; + } + SkCanvas* canvas = instance->beginPage(width, height, contentPtr); + if (jcontentArr != nullptr) + env->ReleaseFloatArrayElements(jcontentArr, contentArr, 0); + return reinterpret_cast(canvas); +} + +extern "C" JNIEXPORT void JNICALL Java_org_jetbrains_skia_DocumentKt__1nEndPage + (JNIEnv* env, jclass jclass, jlong ptr) { + SkDocument* instance = reinterpret_cast(static_cast(ptr)); + instance->endPage(); +} diff --git a/skiko/src/jvmMain/cpp/common/pdf/PDFDocument.cc b/skiko/src/jvmMain/cpp/common/pdf/PDFDocument.cc new file mode 100644 index 000000000..c43bb987a --- /dev/null +++ b/skiko/src/jvmMain/cpp/common/pdf/PDFDocument.cc @@ -0,0 +1,58 @@ +#include +#include "SkPDFDocument.h" +#include "../interop.hh" + +static void copyJIntArrayToDateTime(JNIEnv* env, jintArray& jarr, SkPDF::DateTime* result) { + if (jarr == nullptr) { + *result = {}; + } else { + jint* arr = env->GetIntArrayElements(jarr, 0); + result->fTimeZoneMinutes = arr[6]; + result->fYear = arr[0]; + result->fMonth = arr[1]; + result->fDayOfWeek = -1; + result->fDay = arr[2]; + result->fHour = arr[3]; + result->fMinute = arr[4]; + result->fSecond = arr[5]; + env->ReleaseIntArrayElements(jarr, arr, 0); + } +} + +extern "C" JNIEXPORT jlong JNICALL Java_org_jetbrains_skia_pdf_PDFDocumentKt__1nMakeDocument( + JNIEnv* env, + jclass jclass, + jlong wstreamPtr, + jstring jtitle, + jstring jauthor, + jstring jsubject, + jstring jkeywords, + jstring jcreator, + jstring jproducer, + jintArray jcreation, + jintArray jmodified, + jfloat rasterDPI, + jboolean pdfA, + jint encodingQuality, + jint compressionLevel +) { + SkPDF::DateTime creation, modified; + copyJIntArrayToDateTime(env, jcreation, &creation); + copyJIntArrayToDateTime(env, jmodified, &modified); + SkPDF::Metadata metadata; + metadata.fTitle = skString(env, jtitle); + metadata.fAuthor = skString(env, jauthor); + metadata.fSubject = skString(env, jsubject); + metadata.fKeywords = skString(env, jkeywords); + metadata.fCreator = skString(env, jcreator); + metadata.fProducer = skString(env, jproducer); + metadata.fCreation = creation; + metadata.fModified = modified; + metadata.fRasterDPI = rasterDPI; + metadata.fPDFA = pdfA; + metadata.fEncodingQuality = encodingQuality; + metadata.fCompressionLevel = static_cast(compressionLevel); + SkWStream* wstream = reinterpret_cast(static_cast(wstreamPtr)); + SkDocument* instance = SkPDF::MakeDocument(wstream, metadata).release(); + return reinterpret_cast(instance); +} diff --git a/skiko/src/jvmTest/kotlin/org/jetbrains/skia/pdf/PDFDocumentTest.kt b/skiko/src/jvmTest/kotlin/org/jetbrains/skia/pdf/PDFDocumentTest.kt new file mode 100644 index 000000000..2c39079e4 --- /dev/null +++ b/skiko/src/jvmTest/kotlin/org/jetbrains/skia/pdf/PDFDocumentTest.kt @@ -0,0 +1,110 @@ +package org.jetbrains.skiko.tests.org.jetbrains.skia.pdf + +import org.jetbrains.skia.Color +import org.jetbrains.skia.OutputWStream +import org.jetbrains.skia.Paint +import org.jetbrains.skia.Rect +import org.jetbrains.skia.pdf.PDFCompressionLevel +import org.jetbrains.skia.pdf.PDFDateTime +import org.jetbrains.skia.pdf.PDFDocument +import org.jetbrains.skia.pdf.PDFMetadata +import java.io.ByteArrayOutputStream +import kotlin.test.* + +class PDFDocumentTest { + + @Test + fun makeWithNullMetadata() { + val metadata = PDFMetadata(producer = null, compressionLevel = PDFCompressionLevel.NONE) + val baos = ByteArrayOutputStream() + PDFDocument.make(OutputWStream(baos), metadata).use { doc -> + assertNotNull(doc.beginPage(100f, 250f), "Canvas is null.") + doc.endPage() + } + val pdf = baos.toString(Charsets.UTF_8) + assertDoesNotContain(pdf, "/Title") + assertDoesNotContain(pdf, "/Author") + assertDoesNotContain(pdf, "/Subject") + assertDoesNotContain(pdf, "/Keywords") + assertDoesNotContain(pdf, "/Creator") + assertDoesNotContain(pdf, "/Producer") + assertDoesNotContain(pdf, "/CreationDate") + assertDoesNotContain(pdf, "/ModDate") + } + + @Test + fun makeWithNonNullMetadata() { + val metadata = PDFMetadata( + title = "My Novel", + author = "Johann Wolfgang von Goethe", + subject = "Literature", + keywords = "Some,Important,Keywords", + creator = "Skiko Test Suite", + producer = "Skia", + creation = PDFDateTime(2023, 7, 26, 13, 37, 42), + modified = PDFDateTime(2024, 5, 12, 10, 20, 30, 150), + compressionLevel = PDFCompressionLevel.NONE + ) + val baos = ByteArrayOutputStream() + PDFDocument.make(OutputWStream(baos), metadata).use { doc -> + assertNotNull(doc.beginPage(100f, 250f), "Canvas is null.") + doc.endPage() + } + val pdf = baos.toString(Charsets.UTF_8) + assertContains(pdf, "/Title (${metadata.title})") + assertContains(pdf, "/Author (${metadata.author})") + assertContains(pdf, "/Subject (${metadata.subject})") + assertContains(pdf, "/Keywords (${metadata.keywords})") + assertContains(pdf, "/Creator (${metadata.creator})") + assertContains(pdf, "/Producer (${metadata.producer})") + assertContains(pdf, "/CreationDate (D:20230726133742+00'00')") + assertContains(pdf, "/ModDate (D:20240512102030+02'30')") + } + + @Test + fun draw() { + val metadata = PDFMetadata(compressionLevel = PDFCompressionLevel.NONE) + val baos = ByteArrayOutputStream() + PDFDocument.make(OutputWStream(baos), metadata).use { doc -> + val canvas = assertNotNull(doc.beginPage(100f, 250f), "Canvas is null.") + canvas.drawRect(Rect(10f, 20f, 35f, 50f), Paint().apply { color = Color.RED }) + doc.endPage() + } + val pdf = baos.toString(Charsets.UTF_8) + assertContains(pdf, "/MediaBox [0 0 100 250]") + // Assert that the PDF contains some operations we would expect for our red rect drawing operation. + assertContains(pdf, "1 0 0 rg") + assertContains(pdf, "10 20 25 30 re") + } + + @Test + fun drawWithContentRect() { + val metadata = PDFMetadata(compressionLevel = PDFCompressionLevel.NONE) + val baos = ByteArrayOutputStream() + PDFDocument.make(OutputWStream(baos), metadata).use { doc -> + val canvas = assertNotNull(doc.beginPage(100f, 250f, Rect(60f, 40f, 90f, 220f)), "Canvas is null.") + canvas.drawRect(Rect(10f, 20f, 35f, 50f), Paint().apply { color = Color.RED }) + doc.endPage() + } + val pdf = baos.toString(Charsets.UTF_8) + // Assert that the PDF contains the content rect somewhere. + assertContains(pdf, "60 40 30 180 re") + } + + @Test + fun beginInvalidPage() { + val doc = PDFDocument.make(OutputWStream(ByteArrayOutputStream())) + assertFailsWith { + doc.beginPage(-10f, -20f) + } + } + + private fun assertDoesNotContain(charSequence: CharSequence, other: CharSequence) { + assertTrue( + other !in charSequence, + "Expected the char sequence to not contain the substring.\n" + + "CharSequence <$charSequence>, substring <$other>." + ) + } + +} diff --git a/skiko/src/nativeJsMain/cpp/Document.cc b/skiko/src/nativeJsMain/cpp/Document.cc new file mode 100644 index 000000000..a141f0b5b --- /dev/null +++ b/skiko/src/nativeJsMain/cpp/Document.cc @@ -0,0 +1,21 @@ +#include "SkDocument.h" +#include "common.h" + +SKIKO_EXPORT KNativePointer org_jetbrains_skia_Document__1nBeginPage + (KNativePointer ptr, KFloat width, KFloat height, KFloat* contentArr) { + SkDocument* instance = reinterpret_cast((ptr)); + SkRect content; + SkRect* contentPtr = nullptr; + if (contentArr != nullptr) { + content = { contentArr[0], contentArr[1], contentArr[2], contentArr[3] }; + contentPtr = &content; + } + SkCanvas* canvas = instance->beginPage(width, height, contentPtr); + return reinterpret_cast(canvas); +} + +SKIKO_EXPORT void org_jetbrains_skia_Document__1nEndPage + (KNativePointer ptr) { + SkDocument* instance = reinterpret_cast((ptr)); + instance->endPage(); +} diff --git a/skiko/src/nativeJsMain/cpp/pdf/PDFDocument.cc b/skiko/src/nativeJsMain/cpp/pdf/PDFDocument.cc new file mode 100644 index 000000000..f3d240c71 --- /dev/null +++ b/skiko/src/nativeJsMain/cpp/pdf/PDFDocument.cc @@ -0,0 +1,53 @@ +#include "SkPDFDocument.h" +#include "common.h" + +static void copyKIntArrayToDateTime(KInt* arr, SkPDF::DateTime* result) { + if (arr == nullptr) { + *result = {}; + } else { + result->fTimeZoneMinutes = arr[6]; + result->fYear = arr[0]; + result->fMonth = arr[1]; + result->fDayOfWeek = -1; + result->fDay = arr[2]; + result->fHour = arr[3]; + result->fMinute = arr[4]; + result->fSecond = arr[5]; + } +} + +SKIKO_EXPORT KNativePointer org_jetbrains_skia_pdf_PDFDocument__1nMakeDocument( + KNativePointer wstreamPtr, + KInteropPointer ktitle, + KInteropPointer kauthor, + KInteropPointer ksubject, + KInteropPointer kkeywords, + KInteropPointer kcreator, + KInteropPointer kproducer, + KInt* kcreation, + KInt* kmodified, + KFloat rasterDPI, + KBoolean pdfA, + KInt encodingQuality, + KInt compressionLevel +) { + SkPDF::DateTime creation, modified; + copyKIntArrayToDateTime(kcreation, &creation); + copyKIntArrayToDateTime(kmodified, &modified); + SkPDF::Metadata metadata; + metadata.fTitle = skString(ktitle); + metadata.fAuthor = skString(kauthor); + metadata.fSubject = skString(ksubject); + metadata.fKeywords = skString(kkeywords); + metadata.fCreator = skString(kcreator); + metadata.fProducer = skString(kproducer); + metadata.fCreation = creation; + metadata.fModified = modified; + metadata.fRasterDPI = rasterDPI; + metadata.fPDFA = pdfA; + metadata.fEncodingQuality = encodingQuality; + metadata.fCompressionLevel = static_cast(compressionLevel); + SkWStream* wstream = reinterpret_cast((wstreamPtr)); + SkDocument* instance = SkPDF::MakeDocument(wstream, metadata).release(); + return reinterpret_cast(instance); +}