Unverified Commit 2cc4b1c9 authored by Nikolay Igotti's avatar Nikolay Igotti Committed by GitHub

AWT fonts interop support (#448)

parent c8bdf96a
...@@ -308,6 +308,9 @@ kotlin { ...@@ -308,6 +308,9 @@ kotlin {
val awtTest by getting { val awtTest by getting {
dependsOn(jvmTest) dependsOn(jvmTest)
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-swing:$coroutinesVersion")
}
} }
if (supportAndroid) { if (supportAndroid) {
...@@ -1031,6 +1034,8 @@ tasks.withType<Test>().configureEach { ...@@ -1031,6 +1034,8 @@ tasks.withType<Test>().configureEach {
systemProperty("skiko.jar.path", jar.absolutePath) systemProperty("skiko.jar.path", jar.absolutePath)
systemProperty("skiko.test.screenshots.dir", File(project.projectDir, "src/jvmTest/screenshots").absolutePath) systemProperty("skiko.test.screenshots.dir", File(project.projectDir, "src/jvmTest/screenshots").absolutePath)
systemProperty("skiko.test.font.dir", File(project.projectDir, "src/commonTest/resources/fonts").absolutePath)
systemProperty("skiko.test.ui.enabled", System.getProperty("skiko.test.ui.enabled", "false")) systemProperty("skiko.test.ui.enabled", System.getProperty("skiko.test.ui.enabled", "false"))
systemProperty("skiko.test.ui.renderApi", System.getProperty("skiko.test.ui.renderApi", "all")) systemProperty("skiko.test.ui.renderApi", System.getProperty("skiko.test.ui.renderApi", "all"))
......
package org.jetbrains.skiko package org.jetbrains.skiko
import org.jetbrains.skia.Bitmap import org.jetbrains.skia.*
import org.jetbrains.skia.ColorAlphaType
import org.jetbrains.skia.ColorType
import org.jetbrains.skia.ImageInfo
import org.jetbrains.skia.Image
import org.jetbrains.skia.impl.BufferUtil import org.jetbrains.skia.impl.BufferUtil
import java.awt.Transparency import java.awt.Transparency
import java.awt.color.ColorSpace import java.awt.color.ColorSpace
...@@ -210,3 +206,9 @@ private fun toSkikoKey(event: KeyEvent): Int { ...@@ -210,3 +206,9 @@ private fun toSkikoKey(event: KeyEvent): Int {
} }
return key return key
} }
suspend fun java.awt.Font.toSkikoTypeface(): Typeface? {
val file = AwtFontManager.DEFAULT.findFontFile(this) ?: return null
val data = file.readBytes()
return Typeface.makeFromData(Data.makeFromBytes(data))
}
\ No newline at end of file
package org.jetbrains.skiko
import kotlinx.coroutines.*
import java.awt.Font
import java.awt.FontFormatException
import java.io.File
import java.io.FileInputStream
import java.io.IOException
import java.util.concurrent.ConcurrentHashMap
class AwtFontManager(fontPaths: Array<String> = emptyArray()) {
private var fontsMap = ConcurrentHashMap<String, File>()
@Volatile
private var allFontsCachedImpl = false
private val waitChannel = RendezvousBroadcastChannel<Int>()
private var customFontPaths = mutableListOf(*fontPaths)
private var cacheJob: Job? = null
init {
invalidate()
}
private fun systemFontsPaths(): List<String> {
return when (hostOs) {
OS.Windows -> {
val winPath = System.getenv("WINDIR")
val localAppPath = System.getenv("LOCALAPPDATA")
listOf(
"$winPath\\Fonts",
"$localAppPath\\Microsoft\\Windows\\Fonts"
)
}
OS.MacOS -> {
listOf(
System.getProperty("user.home") + File.separator + "Library/Fonts",
"/Library/Fonts",
"/System/Library/Fonts"
)
}
OS.Linux -> {
val pathsToCheck = arrayOf(
System.getProperty("user.home") + File.separator + ".fonts",
"/usr/share/fonts/truetype",
"/usr/share/fonts/TTF"
)
val resultList = ArrayList<String>()
for (i in pathsToCheck.indices.reversed()) {
val path = pathsToCheck[i]
val tmp = File(path)
if (tmp.exists() && tmp.isDirectory && tmp.canRead()) {
resultList.add(path)
}
}
resultList
}
else -> {
throw RuntimeException("Unknown OS: $hostOs")
}
}
}
private fun findFontFiles(paths: List<String>): List<File> {
val files = mutableListOf<File>()
paths.forEach { path ->
val fontDirectory = File(path)
if (fontDirectory.exists()) {
fontDirectory.walk().filter { it.isFile && it.extension.lowercase() == "ttf" }.forEach {
files.add(it)
}
}
}
return files
}
private suspend fun cacheAllFonts() {
fontsMap.clear()
val fontFiles = findFontFiles(customFontPaths) + findFontFiles(systemFontsPaths())
for (file in fontFiles) {
try {
val f = FileInputStream(file.absolutePath).use {
Font.createFont(Font.TRUETYPE_FONT, it)
}
val name = f.family
fontsMap[name] = file.absoluteFile
yield()
} catch (e: FontFormatException) {
} catch (e: IOException) {
}
}
}
/**
* Find font file path from an AWT font.
* As font finding is long IO-intensive process, this operation checks if given font
* is already known to the font manager.
*
* If you want stable and predictable result it's better use [findFontFile] or check [allFontsCached].
*
* @param font - AWT font for which we need to know the path
* @return path to font, if known
*/
@DelicateSkikoApi
fun findAvailableFontFile(font: Font): File? {
return fontsMap[font.family]
}
/**
* Show all fonts currently known to AWT font manager. As font indexing could take time,
* may have not all elements.
*
* If you want stable and predictable result it's better use [listFontFiles] or check [allFontsCached].
*
* @return list of currently known fonts
*/
@DelicateSkikoApi
fun listAvailableFontFiles(): List<File> {
return fontsMap.values.toList()
}
/**
* Show all fonts known to AWT font manager.
* @return list of known fonts
*/
suspend fun listFontFiles(): List<File> {
waitAllFontsCached()
return fontsMap.values.toList()
}
/**
* Find font file path from an AWT font.
* As font finding is long IO-intensive process, this operation may suspend for pretty long time.
* @param font - which AWT font to look for
* @return path to the font file or null, if not found
*/
suspend fun findFontFile(font: Font): File? {
waitAllFontsCached()
return fontsMap[font.family]
}
/**
* Find font file path from the family name.
* As font finding is long IO-intensive process, this operation may suspend for pretty long time.
* @param family - which AWT font to look for
* @return path to the font file or null, if not found
*/
suspend fun findFontFamilyFile(family: String): File? {
waitAllFontsCached()
return fontsMap[family]
}
/**
* Invalidate cache and start caching again. Maybe useful to re-read fonts
* when changed.
*/
fun invalidate() {
cacheJob?.let {
it.cancel()
}
allFontsCachedImpl = false
cacheJob = GlobalScope.launch(Dispatchers.IO) {
cacheAllFonts()
allFontsCachedImpl = true
waitChannel.sendAll(1)
}
}
/**
* Add custom directory to font search paths. Call [invalidate]
* for operation to take effect.
*/
fun addCustomPath(path: String) {
customFontPaths += path
}
/**
* If all AWT fonts were cached. Check this property before using non-suspend version
* of font conversion APIs.
*/
@DelicateSkikoApi
val allFontsCached: Boolean
get() = allFontsCachedImpl
/**
* Call continuation only when all AWT fonts are cached.
* Please avoid this API and prefer suspend operations.
*/
@DelicateSkikoApi
fun whenAllFontsCachedBlocking(continuation: () -> Unit) {
// TODO: avoid busy loop
while (!allFontsCachedImpl) {}
continuation()
}
/**
* Suspend until all AWT fonts were cached.
*/
private suspend fun waitAllFontsCached() {
if (!allFontsCachedImpl) {
waitChannel.receive()
}
}
companion object {
val DEFAULT by lazy { AwtFontManager() }
}
}
package org.jetbrains.skiko
import org.jetbrains.skiko.tests.runTest
import org.junit.Assert.assertTrue
import org.junit.Assume
import org.junit.Test
import java.awt.Font
import java.awt.GraphicsEnvironment
class AwtFontInterop {
private val fontManager = AwtFontManager()
private fun assumeOk() {
Assume.assumeFalse(GraphicsEnvironment.isHeadless())
Assume.assumeTrue(hostOs != OS.Linux)
}
@OptIn(DelicateSkikoApi::class)
@Test
fun canFindAvailableFont() = fontManager.whenAllFontsCachedBlocking {
assumeOk()
val font = Font("Verdana", Font.BOLD, 12)
val path = fontManager.findAvailableFontFile(font)
assertTrue("Font must be found", path != null)
path!!
assertTrue("Font must be file", path.exists() && path.isFile)
}
@Test
fun canFindFont() {
runTest {
assumeOk()
val font = Font("Verdana", Font.BOLD, 12)
val path = fontManager.findFontFile(font)
assertTrue("Font must be found", path != null)
path!!
assertTrue("Font must be file", path.exists() && path.isFile)
}
}
@Test
fun canFindFamily() {
runTest {
assumeOk()
val path = fontManager.findFontFamilyFile("Verdana")
assertTrue("Font must be found", path != null)
path!!
assertTrue("Font must be file", path.exists() && path.isFile)
}
}
@OptIn(DelicateSkikoApi::class)
@Test
fun nonExistentFont() = fontManager.whenAllFontsCachedBlocking {
assumeOk()
val font = Font("XXXYYY745", Font.BOLD, 12)
val path = fontManager.findAvailableFontFile(font)
assertTrue("Font must not be found", path == null)
}
@Test
fun makeSkikoTypeface() {
runTest {
assumeOk()
Assume.assumeFalse(GraphicsEnvironment.isHeadless())
val font = Font("Verdana", Font.BOLD, 12)
val skikoTypeface = font.toSkikoTypeface()
assertTrue("Skiko typeface must work", skikoTypeface != null)
skikoTypeface!!
assertTrue("Skiko typeface name is incorrect: ${skikoTypeface.familyName}", skikoTypeface.familyName == "Verdana")
}
}
@Test
fun listAllFonts() {
runTest {
assumeOk()
val fontFiles = fontManager.listFontFiles()
assertTrue("There must be fonts", fontFiles.isNotEmpty())
}
}
@Test
fun addCustomPath() {
runTest {
assumeOk()
val resDir = System.getProperty("skiko.test.font.dir")!!
fontManager.addCustomPath(resDir)
fontManager.invalidate()
val path = fontManager.findFontFamilyFile("JetBrains Mono")
assertTrue("Custom font must be found", path != null)
path!!
assertTrue("Font must be file", path.exists() && path.isFile)
}
}
}
\ No newline at end of file
package org.jetbrains.skiko
/**
* Marks declarations that are **delicate** &mdash;
* they have limited use-case and shall be used with care in general code.
* Any use of a delicate declaration has to be carefully reviewed to make sure it is
* properly used and does not create problems like concurrency issues, memory and resource leaks.
* Carefully read documentation of any declaration marked as `DelicateSkikoApi`.
*/
@MustBeDocumented
@Retention(value = AnnotationRetention.BINARY)
@RequiresOptIn(
level = RequiresOptIn.Level.WARNING,
message = "This is a delicate API and its use requires care." +
" Make sure you fully read and understand documentation of the declaration that is marked as a delicate API."
)
annotation class DelicateSkikoApi
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment