Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# EditorConfig is awesome: http://EditorConfig.org

# top-most EditorConfig file
root = true

[*]
end_of_line = lf
insert_final_newline = true
charset = utf-8
indent_style = space
indent_size = 4

[Makefile]
indent_style = tab

[*.yml]
indent_size = 2
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,8 @@
*.egg-info/
Pipfile.lock
.pytest_cache
build
dist
.vscode
.idea/
*__pycache__/
12 changes: 12 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
language: python
python:
- '2.6'
- '2.7'
- '3.5'
- '3.6'
install:
- pip install pipenv
- make install-dev
- make install-package-in-venv
script:
- make test
1 change: 1 addition & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
Copyright (c) 2014 Thiago de Arruda
Copyright (c) 2020 Yoan Tournade <y@yoantournade.com>

Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
Expand Down
23 changes: 23 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
install:
pipenv install

install-dev:
pipenv install --dev

test:
pipenv run pytest -vv

install-package-in-venv:
pipenv uninstall fclist
pipenv run python setup.py install

test-package-in-venv: install-package-in-venv test

format:
pipenv run black .

release:
pipenv run python setup.py sdist
pipenv run python setup.py bdist_wheel --universal
pipenv run twine check dist/*
pipenv run twine upload dist/*
15 changes: 15 additions & 0 deletions Pipfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
[[source]]
url = "https://pypi.org/simple"
verify_ssl = true
name = "pypi"

[packages]
cffi = "*"

[dev-packages]
pytest = "*"
black = "*"
twine = "*"

[requires]
python_version = "3"
25 changes: 19 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
###Python cffi bridge to fontconfig's FcFontList/FcFontMatch
### Python cffi bridge to fontconfig's FcFontList/FcFontMatch

Useful for python programs that need to query information about fonts installed
in the system(use this instead of parsing fc-list output). Requires the
fontconfig shared library installed in a directory that the cffi module can
find.
Note: this is [a plublished](https://pypi.org/project/fclist-cffi/) fork of tarruda [`python-fclist`](https://github.com/tarruda/python-fclist).

###Usage
Useful for python programs that need to query information about fonts installed in the system(use this instead of parsing fc-list output).
Requires the fontconfig shared library installed in a directory that the [cffi module] can find.

### Installing

```sh
pip install fclist-cffi
```

### Usage

```python
from fclist import fclist, fcmatch
Expand All @@ -24,3 +30,10 @@ fontconfig.h.

`fcmatch` receives the same pattern that is normally passed to `fc-match`, but
it doesn't support the --all/--sort options(only returns a single font object).

### Tests

Tests are written to use [pytest].

[cffi module]: https://cffi.readthedocs.io/en/latest/
[pytest]: https://docs.pytest.org
145 changes: 83 additions & 62 deletions fclist.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@
import cffi


__all__ = ('fclist', 'fcmatch',)
__all__ = (
"fclist",
"fcmatch",
)


API = '''
API = """
typedef enum {
FcResultMatch, FcResultNoMatch, FcResultTypeMismatch, FcResultNoId,
FcResultOutOfMemory
Expand Down Expand Up @@ -42,30 +45,30 @@
void FcObjectSetDestroy(void *os);
void FcFontSetDestroy(void *fs);
void free(void *ptr);
'''
"""


ffi = cffi.FFI()
ffi.cdef(API)

fc = ffi.dlopen('fontconfig')
fc = ffi.dlopen("fontconfig")
keys = {}


def get_bool(font, key, ffi_key, data, ptr):
ptr = ffi.new('int *')
ptr = ffi.new("int *")
if fc.FcPatternGetBool(font, ffi_key, 0, ptr) == fc.FcResultMatch:
data[key] = bool(ptr[0])


def get_double(font, key, ffi_key, data, ptr):
ptr = ffi.new('double *')
ptr = ffi.new("double *")
if fc.FcPatternGetDouble(font, ffi_key, 0, ptr) == fc.FcResultMatch:
data[key] = ptr[0]


def get_int(font, key, ffi_key, data, ptr):
ptr = ffi.new('int *')
ptr = ffi.new("int *")
if fc.FcPatternGetInteger(font, ffi_key, 0, ptr) == fc.FcResultMatch:
data[key] = ptr[0]

Expand All @@ -77,62 +80,74 @@ def get_string(font, key, ffi_key, data, ptr):

def build_fc_key_table():
bool_keys = [
'antialias',
'hinting',
'verticallayout',
'autohint',
'globaladvance',
'outline',
'scalable',
'minspace',
'embolden',
'embeddedbitmap',
'decorative',
"antialias",
"hinting",
"verticallayout",
"autohint",
"globaladvance",
"outline",
"scalable",
"minspace",
"embolden",
"embeddedbitmap",
"decorative",
]

int_keys = [
'spacing',
'hintstyle',
'width',
'index',
'rgba',
'fontversion',
'lcdfilter',
"spacing",
"hintstyle",
"width",
"index",
"rgba",
"fontversion",
"lcdfilter",
]

double_keys = [
'size',
'aspect',
'pixelsize',
'scale',
'dpi',
"size",
"aspect",
"pixelsize",
"scale",
"dpi",
]

string_keys = [
'family',
'style',
'aspect',
'foundry',
'file',
'rasterizer',
'charset',
'lang',
'fullname',
'familylang',
'stylelang',
'fullnamelang',
'capability',
'fontformat',
"family",
"style",
"aspect",
"foundry",
"file",
"rasterizer",
"charset",
"lang",
"fullname",
"familylang",
"stylelang",
"fullnamelang",
"capability",
"fontformat",
]

for key in bool_keys:
keys[key] = (ffi.new('char[]', str.encode(key)), get_bool,)
keys[key] = (
ffi.new("char[]", str.encode(key)),
get_bool,
)
for key in int_keys:
keys[key] = (ffi.new('char[]', str.encode(key)), get_int,)
keys[key] = (
ffi.new("char[]", str.encode(key)),
get_int,
)
for key in double_keys:
keys[key] = (ffi.new('char[]', str.encode(key)), get_double,)
keys[key] = (
ffi.new("char[]", str.encode(key)),
get_double,
)
for key in string_keys:
keys[key] = (ffi.new('char[]', str.encode(key)), get_string,)
keys[key] = (
ffi.new("char[]", str.encode(key)),
get_string,
)


build_fc_key_table()
Expand All @@ -146,10 +161,11 @@ def __init__(self, data):
if isinstance(value, bytes):
value = value.decode()
setattr(self, key, value)
self.style = set(self.style.split())
if self.style is not None:
self.style = set(self.style.split())

def __repr__(self):
return 'family: {family}, style: {style}'.format(**self.__dict__)
return "family: {family}, style: {style}".format(**self.__dict__)


def fclist(**query):
Expand All @@ -161,18 +177,18 @@ def fclist(**query):
the fontconfig.h header.
"""
config = ffi.gc(fc.FcInitLoadConfigAndFonts(), fc.FcConfigDestroy)
pat_str = ''.join([':{0}={1}'.format(k, v) for k, v in query.items()])
pat_str = "".join([":{0}={1}".format(k, v) for k, v in query.items()])
pat = ffi.gc(fc.FcNameParse(str.encode(pat_str)), fc.FcPatternDestroy)
pat_str = ffi.string(ffi.gc(fc.FcNameUnparse(pat), fc.free))
if pat_str == '' and len(query):
raise Exception('Invalid search query')
if pat_str == "" and len(query):
raise Exception("Invalid search query")
os = ffi.gc(fc.FcObjectSetBuild(*osb_args), fc.FcObjectSetDestroy)
fs = ffi.gc(fc.FcFontList(config, pat, os), fc.FcFontSetDestroy)
ptrs = {
get_bool: ffi.new('int *'),
get_int: ffi.new('int *'),
get_double: ffi.new('double *'),
get_string: ffi.new('char **'),
get_bool: ffi.new("int *"),
get_int: ffi.new("int *"),
get_double: ffi.new("double *"),
get_string: ffi.new("char **"),
}
for i in range(fs.nfont):
data = {}
Expand All @@ -187,15 +203,20 @@ def fcmatch(pat_str):
pat = ffi.gc(fc.FcNameParse(str.encode(pat_str)), fc.FcPatternDestroy)
fc.FcConfigSubstitute(config, pat, fc.FcMatchPattern)
fc.FcDefaultSubstitute(pat)
res = ffi.new('FcResult *')
res = ffi.new("FcResult *")
font = ffi.gc(fc.FcFontMatch(config, pat, res), fc.FcPatternDestroy)
ptrs = {
get_bool: ffi.new('int *'),
get_int: ffi.new('int *'),
get_double: ffi.new('double *'),
get_string: ffi.new('char **'),
get_bool: ffi.new("int *"),
get_int: ffi.new("int *"),
get_double: ffi.new("double *"),
get_string: ffi.new("char **"),
}
data = {}
for key, (ffi_key, extract,) in keys.items():
extract(font, key, ffi_key, data, ptrs[extract])
return Font(data)


if __name__ == "__main__":
for font in fclist():
print(font.family, font.style, font.file)
35 changes: 18 additions & 17 deletions setup.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,22 @@
import os
from setuptools import setup

description="Python cffi bridge to fontconfig's FcFontList/FcFontMatch",
if os.path.exists('README.md'):
long_description = open('README.md').read()
else:
long_description = description
readme_markdown = None
with open("README.md") as f:
readme_markdown = f.read()

setup(name='fclist',
version='1.1.1',
description=description,
long_description=long_description,
url='http://github.com/tarruda/python-fclist',
download_url='https://github.com/tarruda/python-fclist/archive/1.1.1.tar.gz',
author='Thiago de Arruda',
author_email='tpadilha84@gmail.com',
license='MIT',
py_modules=['fclist'],
install_requires=['cffi'],
zip_safe=False)
setup(
name="fclist-cffi",
version="1.1.2",
description="Python cffi bridge to fontconfig's FcFontList/FcFontMatch",
long_description=readme_markdown,
long_description_content_type="text/markdown",
url="https://github.com/MonsieurV/python-fclist",
download_url="https://github.com/MonsieurV/python-fclist/archive/1.1.2.tar.gz",
author="Yoan Tournade",
author_email="y@yoantournade.com",
license="MIT",
py_modules=["fclist"],
install_requires=["cffi"],
zip_safe=False,
)
Loading