Initial working implementation

This commit is contained in:
Emery Hemingway 2023-03-29 17:53:37 -05:00
parent ad67ab3f84
commit 2cce84eb49
5 changed files with 150 additions and 69 deletions

31
README.md Normal file
View File

@ -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> }>
]
]
```

View File

@ -1,6 +1,6 @@
# Package
version = "20230326"
version = "20230329"
author = "Emery Hemingway"
description = "Syndicate actor for asserting Fontconfig information"
license = "Unlicense"

View File

@ -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> .

View File

@ -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)

View File

@ -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))