Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a thin wrapper around Skia's PDF backend #775

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ fun skiaHeadersDirs(skiaDir: File): List<File> =
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"),
Expand Down
80 changes: 80 additions & 0 deletions skiko/src/commonMain/kotlin/org/jetbrains/skia/Document.kt
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)
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 skiko/src/commonMain/kotlin/org/jetbrains/skia/pdf/PDFDateTime.kt
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 skiko/src/commonMain/kotlin/org/jetbrains/skia/pdf/PDFDocument.kt
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 skiko/src/commonMain/kotlin/org/jetbrains/skia/pdf/PDFMetadata.kt
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
)
26 changes: 26 additions & 0 deletions skiko/src/jvmMain/cpp/common/Document.cc
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();
}
58 changes: 58 additions & 0 deletions skiko/src/jvmMain/cpp/common/pdf/PDFDocument.cc
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);
}
Loading
Loading