Skip to content

Commit

Permalink
Add a thin wrapper around Skia's PDF backend
Browse files Browse the repository at this point in the history
  • Loading branch information
LoadingByte committed Oct 20, 2024
1 parent b73b374 commit 97cbaee
Show file tree
Hide file tree
Showing 11 changed files with 513 additions and 0 deletions.
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
76 changes: 76 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,76 @@
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, specifying a stream to store the output.
* 2. For each "page" of content:
* 1. canvas = doc.beginPage(...)
* 2. draw_my_content(canvas);
* 3. 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")
private external fun _nBeginPage(
ptr: NativePointer, width: Float, height: Float, content: InteropPointer
): NativePointer

@ExternalSymbolName("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)

}
71 changes: 71 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,71 @@
package org.jetbrains.skia.pdf

import org.jetbrains.skia.Document
import org.jetbrains.skia.ExternalSymbolName
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()
try {
val ptr = 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
)
}
require(ptr != NullPointer) { "PDF document was created with invalid arguments." }
return Document(ptr, out)
} finally {
reachabilityBarrier(out)
}
}

}

@ExternalSymbolName("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
50 changes: 50 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,50 @@
package org.jetbrains.skia.pdf

import org.jetbrains.skiko.Version

/**
* 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 ${Version.skia}",
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

0 comments on commit 97cbaee

Please sign in to comment.