Initial working implementation
This commit is contained in:
parent
ad67ab3f84
commit
2cce84eb49
|
@ -0,0 +1,31 @@
|
|||
# Fontconfig_actor
|
||||
|
||||
A [Syndicate actor](https://syndicate-lang.org) for querying [Fontconfig](https://www.freedesktop.org/software/fontconfig/).
|
||||
|
||||
The actor responds to observations of font properties by asserting what it determines to be the most appropriate font.
|
||||
|
||||
The assertion format is `<fontconfig {pattern…} {attributes…}>` (see [protocol.prs](./protocol.prs)). The `pattern` field is a dictionary of properties to match fonts against and the `attributes` field is a dictionary of the properties of a font selected by Fontconfig. An application observes a `pattern` and takes the `file` and `index` fields of a corresponding assertion and reads font data from the file-system. In the case that `pattern` does not match any fonts an assertion will still be made, so it is possible that all of the properties in `pattern` will be contradicted in `attributes`.
|
||||
|
||||
For a list of possibly supported properties see the [Fontconfig documentation](https://www.freedesktop.org/software/fontconfig/fontconfig-devel/x19.html).
|
||||
|
||||
Example [Syndicate server](https://git.syndicate-lang.org/syndicate-lang/syndicate-rs) configuration:
|
||||
```
|
||||
<require-service <daemon fontconfig_actor>>
|
||||
<daemon fontconfig_actor {
|
||||
argv: [ "/home/emery/src/bin/fontconfig_actor" ]
|
||||
protocol: application/syndicate
|
||||
}>
|
||||
|
||||
let ?fontspace = dataspace
|
||||
<fontspace $fontspace>
|
||||
|
||||
? <service-object <daemon fontconfig_actor> ?cap> [
|
||||
$cap <serve $fontspace>
|
||||
]
|
||||
|
||||
$fontspace [
|
||||
? <fontconfig {family: "Gentium Book Plus"} ?attributes> [
|
||||
$log ! <log "-" { line: <fontconfig $attributes> }>
|
||||
]
|
||||
]
|
||||
```
|
|
@ -1,6 +1,6 @@
|
|||
# Package
|
||||
|
||||
version = "20230326"
|
||||
version = "20230329"
|
||||
author = "Emery Hemingway"
|
||||
description = "Syndicate actor for asserting Fontconfig information"
|
||||
license = "Unlicense"
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
version 1 .
|
||||
Serve = <serve @cap #!any> .
|
||||
FontAssertion = <fontconfig @properties {symbol: any ...:...} @file string @index int> .
|
||||
Properties = {symbol: any ...:...} .
|
||||
FontAssertion = <fontconfig @pattern Properties @attributes Properties> .
|
||||
|
|
|
@ -1,25 +1,18 @@
|
|||
# SPDX-FileCopyrightText: ☭ Emery Hemingway
|
||||
# SPDX-License-Identifier: Unlicense
|
||||
|
||||
import std/[asyncdispatch, os, osproc, strutils]
|
||||
import std/[asyncdispatch, options, os, strutils, tables]
|
||||
import preserves, syndicate, syndicate/[patterns]
|
||||
import ./protocol
|
||||
|
||||
proc logError(args: varargs[string, `$`]) = writeLine(stderr, args)
|
||||
|
||||
{.passC: staticExec("pkg-config --cflags fontconfig").}
|
||||
{.passL: staticExec("pkg-config --libs fontconfig").}
|
||||
|
||||
{.pragma: fcHeader, header: "<fontconfig/fontconfig.h>".}
|
||||
{.pragma: importFc, fcHeader, importc: "Fc$1".}
|
||||
|
||||
const
|
||||
FcFalse = cint 0
|
||||
|
||||
FC_FAMILY = "family"
|
||||
FC_STYLE = "style"
|
||||
FC_WEIGHT = "weight"
|
||||
FC_SIZE = "size"
|
||||
FC_FILE = "file"
|
||||
|
||||
type
|
||||
FcChar8* = uint8
|
||||
FcBool* = cint
|
||||
|
@ -29,9 +22,34 @@ type
|
|||
FcResultOutOfMemory
|
||||
|
||||
FcPattern = distinct pointer
|
||||
FcPatternIter {.importc.} = object
|
||||
|
||||
FcValue {.importc.} = object
|
||||
`type`: FcType
|
||||
u: FcValueUnion
|
||||
FcType {.importc} = enum
|
||||
FcTypeUnknown = -1,
|
||||
FcTypeVoid,
|
||||
FcTypeInteger,
|
||||
FcTypeDouble,
|
||||
FcTypeString,
|
||||
FcTypeBool,
|
||||
FcTypeMatrix,
|
||||
FcTypeCharSet,
|
||||
FcTypeFTFace,
|
||||
FcTypeLangSet,
|
||||
FcTypeRange
|
||||
FcValueUnion {.importc.} = object
|
||||
s: cstring
|
||||
i: cint
|
||||
b: FcBool
|
||||
d: cdouble
|
||||
|
||||
FcConfig = distinct pointer
|
||||
|
||||
const
|
||||
(FcFalse, FcTrue) = (cint 0, cint 1)
|
||||
|
||||
proc Init(): FcBool {.importFc.} ## Initialize fontconfig library
|
||||
proc Fini() {.importFc.} ## Finalize fontconfig library.
|
||||
|
||||
|
@ -40,72 +58,103 @@ proc PatternDestroy(p: FcPattern) {.importFc.}
|
|||
|
||||
proc PatternAddInteger(p: FcPattern; obj: cstring; i: cint): FcBool {.importFc.}
|
||||
proc PatternAddDouble(p: FcPattern; obj: cstring; d: cdouble): FcBool {.importFc.}
|
||||
proc PatternAddString(p: FcPattern; obj, s: cstring): FcBool {.importFc.}
|
||||
proc PatternAddString(p: FcPattern; obj: cstring; s: cstring): FcBool {.importFc.}
|
||||
proc PatternAddBool(p: FcPattern; obj: cstring; b: bool): FcBool {.importFc.}
|
||||
proc DefaultSubstitute(p: FcPattern) {.importFc.}
|
||||
proc PatternPrint(p: FcPattern) {.importFc.}
|
||||
|
||||
proc FontMatch(cfg: FcConfig; p: FcPattern; r: var FcResult): FcPattern {.importFc.}
|
||||
|
||||
proc PatternGetString(p: FcPattern; obj: cstring; n: cint;
|
||||
s: ptr cstring): FcResult {.importFc.}
|
||||
proc PatternIterStart(p: FcPattern; iter: ptr FcPatternIter) {.importFc.}
|
||||
proc PatternIterNext(p: FcPattern; iter: ptr FcPatternIter): FcBool {.importFc.}
|
||||
proc PatternIterValueCount(p: FcPattern; iter: ptr FcPatternIter): cint {.importFc.}
|
||||
proc PatternIterIsValid(p: FcPattern; iter: ptr FcPatternIter): FcBool {.importFc.}
|
||||
proc PatternIterGetObject(p: FcPattern; iter: ptr FcPatternIter): cstring {.importFc.}
|
||||
proc PatternIterGetValue(p: FcPattern; iter: ptr FcPatternIter; id: cint; v: ptr FcValue; b: pointer): FcResult {.importFc.}
|
||||
|
||||
proc findSystemTypeface*(family = ""; style = ""; weight = 0; size = 0.0): string =
|
||||
## Find a path to an appropriate system typeface for the given parameters.
|
||||
## This proc always returns a path to a typeface file, results may vary.
|
||||
# TODO: only return font in supported formats
|
||||
proc getValue(pat: FcPattern; iter: ptr FcPatternIter; i: cint): Option[Preserve[void]] =
|
||||
var fcVal: FcValue
|
||||
if PatternIterGetValue(pat, iter, i, addr fcVal, nil) == FcResultMatch:
|
||||
case fcVal.`type`
|
||||
of FcTypeInteger:
|
||||
result = some fcVal.u.i.toPreserve
|
||||
of FcTypeDouble:
|
||||
result = some fcVal.u.d.toPreserve
|
||||
of FcTypeString:
|
||||
result = some ($fcVal.u.s).toPreserve
|
||||
of FcTypeBool:
|
||||
result = some (fcVal.u.b == FcTrue).toPreserve
|
||||
else: discard
|
||||
|
||||
proc fillProperties(pat: FcPattern): Properties =
|
||||
var iter: FcPatternIter
|
||||
PatternIterStart(pat, addr iter)
|
||||
while PatternIterIsValid(pat, addr iter) == FcTrue:
|
||||
let vc = PatternIterValueCount(pat, addr iter)
|
||||
if vc > 0:
|
||||
var key = Symbol $PatternIterGetObject(pat, addr iter)
|
||||
if vc == 1:
|
||||
var val = getValue(pat, addr iter, 0)
|
||||
if val.isSome: result[key] = get val
|
||||
else:
|
||||
var val = initSequenceOfCap(vc)
|
||||
for i in 0..<vc:
|
||||
let opt = getValue(pat, addr iter, i)
|
||||
if opt.isSome: add(val.sequence, get(opt))
|
||||
if val.sequence.len > 0:
|
||||
result[key] = val
|
||||
if PatternIterNext(pat, addr iter) != FcTrue:
|
||||
break
|
||||
|
||||
proc fillPattern(properties: Properties): FcPattern =
|
||||
result = PatternCreate()
|
||||
for sym, val in properties.pairs:
|
||||
let key = sym.string
|
||||
let recognized =
|
||||
case val.kind
|
||||
of pkBoolean: PatternAddBool(result, key, val.bool)
|
||||
of pkFloat: PatternAddDouble(result, key, val.float)
|
||||
of pkDouble: PatternAddDouble(result, key, val.double)
|
||||
of pkSignedInteger: PatternAddInteger(result, key, cint val.int)
|
||||
of pkString:
|
||||
PatternAddString(result, key, val.string)
|
||||
else:
|
||||
logError("invalid property: ", val)
|
||||
FcFalse
|
||||
if recognized == FcFalse:
|
||||
logError("unrecognized property: ", key.escape)
|
||||
DefaultSubstitute(result)
|
||||
|
||||
template withFontconfig(body: untyped): untyped =
|
||||
## Initialize and deinitialize the Fontconfig library for the scope of `body`.
|
||||
if Init() == FcFalse:
|
||||
raise newException(IOError, "Failed to initialize FontConfig")
|
||||
|
||||
var pat = PatternCreate()
|
||||
DefaultSubstitute(pat)
|
||||
if family != "":
|
||||
discard PatternAddString(pat, FC_FAMILY, family);
|
||||
if style != "":
|
||||
discard PatternAddString(pat, FC_STYLE, style);
|
||||
if weight != 0:
|
||||
discard PatternAddInteger(pat, FC_WEIGHT, cint weight);
|
||||
if size != 0.0:
|
||||
discard PatternAddDouble(pat, FC_SIZE, size);
|
||||
|
||||
var
|
||||
res = FcResultNoMatch
|
||||
font = FontMatch(nil, pat, res)
|
||||
if res == FcResultMatch:
|
||||
# PatternPrint(font);
|
||||
var path: cstring
|
||||
if PatternGetString(font, FC_FILE, 0, addr path) == FcResultMatch:
|
||||
result = $path
|
||||
PatternDestroy(font)
|
||||
|
||||
PatternDestroy(pat)
|
||||
quit"Failed to initialize FontConfig"
|
||||
else:
|
||||
body
|
||||
Fini()
|
||||
if result == "":
|
||||
raise newException(IOError, "Failed to find a system typeface")
|
||||
|
||||
proc serve(ds: Ref; turn: var Turn) =
|
||||
let observation = ?Observe(pattern: !FontAssertion) ?? {0: grabDict()}
|
||||
during(turn, ds, observation) do (properties: Assertion):
|
||||
stderr.writeLine "looking for ", properties
|
||||
|
||||
#[
|
||||
let propPat = { toSymbol("family", Ref) : ?"Foo" }.dictionaryPattern
|
||||
let testPat = FontAssertion ? { 0: propPat, 1: grab(), 2: grab() }
|
||||
|
||||
onPublish(turn, ds, testPat) do (file: string, index: int):
|
||||
stderr.writeLine "found file ", file, " with index ", index
|
||||
|
||||
during(turn, ds, Observe ? { 0: grab() }) do (pat: Assertion):
|
||||
stderr.writeLine "observed request for ", pat
|
||||
|
||||
during(turn, ds, Observe ? { 0: ?(FontAssertion ? { 0: drop()}) }) do:
|
||||
stderr.writeLine "fontconfig observe found! "
|
||||
]#
|
||||
|
||||
during(turn, ds, observation) do (properties: Preserve[void]):
|
||||
var fontAssert: FontAssertion
|
||||
for key, val in properties.pairs:
|
||||
if key.isSymbol and val.isRecord("lit", 1):
|
||||
fontAssert.pattern[key.symbol] = val[0]
|
||||
withFontconfig:
|
||||
var
|
||||
res = FcResultNoMatch
|
||||
pat = fillPattern(fontAssert.pattern)
|
||||
font = FontMatch(nil, pat, res)
|
||||
if res != FcResultMatch:
|
||||
logError "no font matched for ", fontAssert.pattern
|
||||
else:
|
||||
fontAssert.attributes = fillProperties(font)
|
||||
PatternDestroy(font)
|
||||
PatternDestroy(pat)
|
||||
discard publish(turn, ds, fontAssert)
|
||||
|
||||
bootDataspace("main") do (root: Ref; turn: var Turn):
|
||||
connectStdio(root, turn)
|
||||
if Init() == FcFalse:
|
||||
quit"Failed to initialize FontConfig"
|
||||
during(turn, root, ?Serve) do (ds: Ref):
|
||||
serve(ds, turn)
|
||||
|
||||
|
|
|
@ -7,12 +7,12 @@ type
|
|||
`cap`* {.preservesEmbedded.}: Preserve[void]
|
||||
|
||||
FontAssertion* {.preservesRecord: "fontconfig".} = object
|
||||
`properties`*: Table[Symbol, Preserve[void]]
|
||||
`file`*: string
|
||||
`index`*: BiggestInt
|
||||
`pattern`*: Properties
|
||||
`attributes`*: Properties
|
||||
|
||||
proc `$`*(x: Serve | FontAssertion): string =
|
||||
Properties* = Table[Symbol, Preserve[void]]
|
||||
proc `$`*(x: Serve | FontAssertion | Properties): string =
|
||||
`$`(toPreserve(x))
|
||||
|
||||
proc encode*(x: Serve | FontAssertion): seq[byte] =
|
||||
proc encode*(x: Serve | FontAssertion | Properties): seq[byte] =
|
||||
encode(toPreserve(x))
|
||||
|
|
Loading…
Reference in New Issue