├── requirements.txt ├── .dockerignore ├── .env.example ├── Dockerfile ├── docker-compose.yml ├── LICENSE ├── README.md ├── .gitignore ├── helpers.py └── server.py /requirements.txt: -------------------------------------------------------------------------------- 1 | Flask==2.3.2 2 | flask-cors -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | *.pyc 3 | *.pyo 4 | .git -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | COMPOSE_PROJECT_NAME=FIGMA_LINUX_FONT_HELPER 2 | #FONT_FOLDER=/home/my-user/.local/share/fonts -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:3.14 2 | 3 | RUN apk add --no-cache python3 py3-pip fontconfig 4 | 5 | WORKDIR /app 6 | 7 | COPY . . 8 | 9 | RUN pip install -r requirements.txt 10 | 11 | EXPOSE 18412 12 | EXPOSE 44950 13 | EXPOSE 7335 14 | 15 | CMD ["python3", "server.py", "0.0.0.0"] 16 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.7' 2 | services: 3 | server: 4 | build: . 5 | volumes: 6 | - ${FONT_FOLDER:-/usr/share/fonts}:/usr/share/fonts 7 | # Uncomment the lines bellow and adjust them to your need if you need more fonts paths 8 | # - /home/youruser/anotherfontfolder:/usr/share/fonts/anotherfontfolder 9 | # - /tmp/yetanotherfontfolder:/usr/share/fonts/yetanotherfontfolder 10 | ports: 11 | - "18412:18412" 12 | - "7335:7335" 13 | - "44950:18412" # New versions of Figma also look for this 44950 port 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Vin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # figma-linux-font-helper 2 | Figma Linux Font Helper 3 | 4 | # Why 5 | 6 | A fellow friend of mine was switching to Linux, and needed the Local fonts support for Figma 7 | 8 | # How 9 | 10 | This project was a reverse engineer from the local font helper from Figma for App, it uses fc-list, and fc-cache for the fonts lists, and python for the webserver 11 | 12 | # How to use it 13 | 14 | * Install Python3 and Pip3 (Pip for Python3) 15 | * Run `sudo pip3 install -r requirements.txt` 16 | * Run `python3 server.py` 17 | * Navigate to figma.com and try to use the local fonts. 18 | 19 | 20 | # Docker-compose 21 | 22 | You can also use `docker-compose` in order to run it as a container, simply run `docker-compose up` and let docker do it's magic 23 | 24 | There's also a environment variable in the compose file, called `FONTS_FOLDER`, use this variable if your font folder is mapped somewhere else, for example, on OSX you might want to use `FONT_FOLDER=~/Library/Fonts docker-compose up` 25 | 26 | Rename `.env.example` to `.env` to define a custom value to `FONTS_FOLDER` before build your containers with `docker-compose`. 27 | 28 | 29 | # Big Thanks 30 | 31 | Big Thanks to the following contributors for improving this project! (NOT sorted by order of importance) 32 | 33 | * [arpanetus](https://github.com/arpanetus) 34 | * [aanpilov](https://github.com/aanpilov) 35 | * [marcosfreitas](https://github.com/marcosfreitas) 36 | * Wayne Steidley 37 | * [misotrnka](https://github.com/misotrnka) 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | -------------------------------------------------------------------------------- /helpers.py: -------------------------------------------------------------------------------- 1 | # Figma Font Helper 2 | # Maintainer Vin 3 | # Copyright 2019 Vin 4 | # MIT License 5 | 6 | import os 7 | import re 8 | import subprocess 9 | import sys 10 | 11 | 12 | def is_valid_origin(origin): 13 | if origin is None: 14 | return True 15 | 16 | if origin.endswith("/"): 17 | origin = origin[:-1] 18 | 19 | return re.match( 20 | ( 21 | '^https?:\\/\\/(?:(?:\\w+\\.)?figma.com|localhost|' 22 | '127\\.0\\.0\\.1)(?::\\d+)?$' 23 | ), 24 | origin 25 | ) 26 | 27 | 28 | def __split_if_comma__(value): 29 | return value.split(",", 1)[0] if value.find(",") > -1 else value 30 | 31 | 32 | def get_font_list(): 33 | font_list = {} 34 | 35 | allowed_extensions = ('\\.ttf', '\\.ttc', '\\.otf') 36 | 37 | font_shell_command = ["fc-list --format '%%{file} | %%{family} | %%{weight} | %%{style} | %%{postscriptname}\\n' | sort | grep -i -e '%s'" % ( 38 | "\\|".join(allowed_extensions), 39 | )] 40 | 41 | new_env = dict(os.environ) 42 | new_env['LC_ALL'] = 'C' 43 | 44 | font_shell_return = subprocess.run( 45 | font_shell_command, shell=True, env=new_env, capture_output=True) 46 | 47 | if font_shell_return.returncode == 0: 48 | stdout_encoding = sys.stdout.encoding 49 | 50 | for font_line in str(font_shell_return.stdout.decode(stdout_encoding)).split("\n"): 51 | details = font_line.split(" | ") 52 | if len(details) == 5: 53 | if details[0] not in font_list: 54 | font_list[details[0].strip()] = [{ 55 | "localizedFamily": __split_if_comma__(details[1]).strip(), 56 | "postscript": details[4].strip(), 57 | "style": __split_if_comma__(details[3]).strip(), 58 | "weight": details[2], 59 | "stretch": 5, 60 | "italic": True if re.match("Italic|Oblique", details[3]) else False, 61 | "family": __split_if_comma__(details[1]).strip(), 62 | "localizedStyle": __split_if_comma__(details[3]).strip() 63 | }] 64 | 65 | return font_list 66 | 67 | 68 | if __name__ == "__main__": 69 | print("Font List:") 70 | print(get_font_list()) 71 | -------------------------------------------------------------------------------- /server.py: -------------------------------------------------------------------------------- 1 | # Figma Font Helper 2 | # Maintainer Vin 3 | # Copyright 2019 Vin 4 | # MIT License 5 | 6 | import io 7 | import os 8 | import sys 9 | 10 | from flask import Flask, jsonify, make_response, request, send_file 11 | from flask_cors import CORS 12 | 13 | from helpers import get_font_list, is_valid_origin 14 | 15 | app = Flask(__name__) 16 | CORS(app, resources={r"/figma/*": {"origins": "*"}}) 17 | 18 | HTTP_PORT = 18412 19 | HTTPS_PORT = 7335 20 | PROTOCOL_VERSION = 17 21 | FONT_FILES = get_font_list() 22 | 23 | # Figma now returns a 403 by default 24 | @app.errorhandler(404) 25 | def answers_with_403(e = None): 26 | return ('Unauthorized', 403) 27 | 28 | 29 | @app.route("/figma/version") 30 | def version(): 31 | if is_valid_origin(request.referrer): 32 | response = make_response(jsonify({ 33 | "version": PROTOCOL_VERSION 34 | })) 35 | 36 | response.headers['Content-Type'] = 'application/json' 37 | 38 | return response 39 | else: 40 | return answers_with_403() 41 | 42 | 43 | @app.route("/figma/font-files") 44 | def font_files(): 45 | if is_valid_origin(request.referrer): 46 | response = make_response(jsonify({ 47 | "version": PROTOCOL_VERSION, 48 | "fontFiles": FONT_FILES 49 | })) 50 | 51 | response.headers['Content-Type'] = 'application/json' 52 | 53 | return response 54 | else: 55 | return answers_with_403() 56 | 57 | 58 | @app.route("/figma/font-file") 59 | def font_file(): 60 | file_name = request.args.get("file") 61 | 62 | if file_name: 63 | if file_name in FONT_FILES: 64 | with open(file_name, 'rb') as bites: 65 | response = make_response(send_file( 66 | io.BytesIO(bites.read()), 67 | attachment_filename=os.path.basename(file_name), 68 | mimetype='application/octet-stream' 69 | )) 70 | 71 | response.headers['Content-Type'] = 'application/json' 72 | 73 | return response 74 | 75 | return ('', 404) 76 | 77 | 78 | @app.route("/figma/update") 79 | def need_update(): 80 | if is_valid_origin(request.referrer): 81 | response = make_response(jsonify({ 82 | "version": PROTOCOL_VERSION 83 | })) 84 | 85 | response.headers['Content-Type'] = 'application/json' 86 | 87 | return response 88 | else: 89 | return answers_with_403() 90 | 91 | 92 | if __name__ == '__main__': 93 | if len(sys.argv) > 1: 94 | if sys.argv[1] == "docker-mode": 95 | hostname = "0.0.0.0" 96 | else: 97 | hostname = sys.argv[1] 98 | else: 99 | hostname = "127.0.0.1" 100 | 101 | app.run(host=hostname, port=HTTP_PORT) 102 | --------------------------------------------------------------------------------