Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
S
skiko
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
liuqiang
skiko
Commits
2cc4b1c9
Unverified
Commit
2cc4b1c9
authored
Jan 12, 2022
by
Nikolay Igotti
Committed by
GitHub
Jan 12, 2022
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
AWT fonts interop support (#448)
parent
c8bdf96a
Changes
5
Hide whitespace changes
Inline
Side-by-side
Showing
5 changed files
with
331 additions
and
5 deletions
+331
-5
build.gradle.kts
skiko/build.gradle.kts
+5
-0
Convertors.jvm.kt
.../src/awtMain/kotlin/org/jetbrains/skiko/Convertors.jvm.kt
+7
-5
FontInterop.awt.kt
...src/awtMain/kotlin/org/jetbrains/skiko/FontInterop.awt.kt
+206
-0
AwtFontInterop.kt
.../src/awtTest/kotlin/org/jetbrains/skiko/AwtFontInterop.kt
+96
-0
Annotations.kt
.../src/commonMain/kotlin/org/jetbrains/skiko/Annotations.kt
+17
-0
No files found.
skiko/build.gradle.kts
View file @
2cc4b1c9
...
@@ -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"
))
...
...
skiko/src/awtMain/kotlin/org/jetbrains/skiko/Convertors.jvm.kt
View file @
2cc4b1c9
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
...
@@ -209,4 +205,10 @@ private fun toSkikoKey(event: KeyEvent): Int {
...
@@ -209,4 +205,10 @@ 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
skiko/src/awtMain/kotlin/org/jetbrains/skiko/FontInterop.awt.kt
0 → 100644
View file @
2cc4b1c9
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
()
}
}
}
skiko/src/awtTest/kotlin/org/jetbrains/skiko/AwtFontInterop.kt
0 → 100644
View file @
2cc4b1c9
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
skiko/src/commonMain/kotlin/org/jetbrains/skiko/Annotations.kt
0 → 100644
View file @
2cc4b1c9
package
org.jetbrains.skiko
/**
* Marks declarations that are **delicate** —
* 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
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment