-
Notifications
You must be signed in to change notification settings - Fork 120
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add a thin wrapper around Skia's PDF backend
- Loading branch information
1 parent
b73b374
commit 49650f1
Showing
11 changed files
with
517 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
80 changes: 80 additions & 0 deletions
80
skiko/src/commonMain/kotlin/org/jetbrains/skia/Document.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) |
9 changes: 9 additions & 0 deletions
9
skiko/src/commonMain/kotlin/org/jetbrains/skia/pdf/PDFCompressionLevel.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); | ||
} |
38 changes: 38 additions & 0 deletions
38
skiko/src/commonMain/kotlin/org/jetbrains/skia/pdf/PDFDateTime.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) | ||
|
||
} |
73 changes: 73 additions & 0 deletions
73
skiko/src/commonMain/kotlin/org/jetbrains/skia/pdf/PDFDocument.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
48 changes: 48 additions & 0 deletions
48
skiko/src/commonMain/kotlin/org/jetbrains/skia/pdf/PDFMetadata.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
#include <jni.h> | ||
#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<SkDocument*>(static_cast<uintptr_t>(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<jlong>(canvas); | ||
} | ||
|
||
extern "C" JNIEXPORT void JNICALL Java_org_jetbrains_skia_DocumentKt__1nEndPage | ||
(JNIEnv* env, jclass jclass, jlong ptr) { | ||
SkDocument* instance = reinterpret_cast<SkDocument*>(static_cast<uintptr_t>(ptr)); | ||
instance->endPage(); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
#include <jni.h> | ||
#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<SkPDF::Metadata::CompressionLevel>(compressionLevel); | ||
SkWStream* wstream = reinterpret_cast<SkWStream*>(static_cast<uintptr_t>(wstreamPtr)); | ||
SkDocument* instance = SkPDF::MakeDocument(wstream, metadata).release(); | ||
return reinterpret_cast<jlong>(instance); | ||
} |
Oops, something went wrong.