├── .env.example ├── .gitignore ├── LICENSE.md ├── Pipfile ├── Pipfile.lock ├── Procfile ├── README.md ├── app.py ├── config.py ├── fonts ├── GMK_font.ttf ├── SA_font.ttf ├── fa2unicode.json └── kbd-webfont2unicode.json ├── help ├── help1.md ├── help1_1.png ├── help1_2.png ├── help1_3.png ├── help2.md ├── help2_1.png └── help2_2.png ├── images ├── GMK_BASE1.png ├── GMK_BASE2.png ├── GMK_BASE3.png ├── GMK_BASE4.png ├── GMK_BASE5.png ├── GMK_BIGENTER1.png ├── GMK_BIGENTER2.png ├── GMK_BIGENTER3.png ├── GMK_BIGENTER4.png ├── GMK_BIGENTER5.png ├── GMK_ISO1.png ├── GMK_ISO2.png ├── GMK_ISO3.png ├── GMK_ISO4.png ├── GMK_ISO5.png ├── GMK_SPACE1.png ├── GMK_SPACE2.png ├── GMK_SPACE3.png ├── GMK_SPACE4.png ├── GMK_SPACE5.png ├── GMK_STEP1.png ├── GMK_STEP2.png ├── GMK_STEP3.png ├── GMK_STEP4.png ├── GMK_STEP5.png ├── SA_BASE1.png ├── SA_BASE2.png ├── SA_BASE3.png ├── SA_BASE4.png ├── SA_BASE5.png ├── SA_BIGENTER1.png ├── SA_BIGENTER2.png ├── SA_BIGENTER3.png ├── SA_BIGENTER4.png ├── SA_BIGENTER5.png ├── SA_ISO1.png ├── SA_ISO2.png ├── SA_ISO3.png ├── SA_ISO4.png ├── SA_ISO5.png ├── SA_SPACE1.png ├── SA_SPACE2.png ├── SA_SPACE3.png ├── SA_SPACE4.png ├── SA_SPACE5.png ├── SA_STEP1.png ├── SA_STEP2.png ├── SA_STEP3.png ├── SA_STEP4.png ├── SA_STEP5.png └── script.py ├── key.py ├── keyboard.py ├── render_output.png ├── static ├── favicon-16x16.png ├── favicon-32x32.png ├── favicon.ico ├── k.png ├── ladda-themeless.min.css ├── ladda.min.js └── spin.min.js ├── templates └── index.html └── test.py /.env.example: -------------------------------------------------------------------------------- 1 | SECRET_KEY='flask-secret-key' 2 | API_TOKEN='github-api-token' 3 | -------------------------------------------------------------------------------- /.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 | env/ 12 | build/ 13 | bin/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | include/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | wheels/ 26 | *.egg-info/ 27 | .installed.cfg 28 | *.egg 29 | pip-selfcheck.json 30 | 31 | # PyInstaller 32 | # Usually these files are written by a python script from a template 33 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 34 | *.manifest 35 | *.spec 36 | 37 | # Installer logs 38 | pip-log.txt 39 | pip-delete-this-directory.txt 40 | 41 | # Unit test / coverage reports 42 | htmlcov/ 43 | .tox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *,cover 50 | .hypothesis/ 51 | 52 | # Translations 53 | *.mo 54 | *.pot 55 | 56 | # Django stuff: 57 | *.log 58 | local_settings.py 59 | 60 | # Flask stuff: 61 | instance/ 62 | .webassets-cache 63 | 64 | # Scrapy stuff: 65 | .scrapy 66 | 67 | # Sphinx documentation 68 | docs/_build/ 69 | 70 | # PyBuilder 71 | target/ 72 | 73 | # Jupyter Notebook 74 | .ipynb_checkpoints 75 | 76 | # pyenv 77 | .python-version 78 | 79 | # celery beat schedule file 80 | celerybeat-schedule 81 | 82 | # SageMath parsed files 83 | *.sage.py 84 | 85 | # dotenv 86 | .env 87 | 88 | # virtualenv 89 | .venv 90 | venv/ 91 | ENV/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | 96 | # Rope project settings 97 | .ropeproject 98 | 99 | # mkdocs documentation 100 | /site 101 | 102 | # Mac OS X custom attribute files 103 | .DS_Store 104 | 105 | # redis dumps 106 | *.rdb 107 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Alexander Zhang 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 | 23 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [packages] 7 | pygithub = "*" 8 | flask-wtf = "*" 9 | flask-cors = "*" 10 | flask-restx = "*" 11 | lxml = "*" 12 | pillow = "*" 13 | colormath = "*" 14 | gunicorn = "*" 15 | tinycss2 = "*" 16 | 17 | [dev-packages] 18 | 19 | [requires] 20 | python_version = "3.10" 21 | -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "735be4f44897e1fdbf63c6caf57c3345b3fb458095b9fdf5af5723c6eadf6430" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.10" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "aniso8601": { 20 | "hashes": [ 21 | "sha256:1d2b7ef82963909e93c4f24ce48d4de9e66009a21bf1c1e1c85bdd0812fe412f", 22 | "sha256:72e3117667eedf66951bb2d93f4296a56b94b078a8a95905a052611fb3f1b973" 23 | ], 24 | "markers": "python_version >= '3.5'", 25 | "version": "==9.0.1" 26 | }, 27 | "attrs": { 28 | "hashes": [ 29 | "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4", 30 | "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd" 31 | ], 32 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", 33 | "version": "==21.4.0" 34 | }, 35 | "certifi": { 36 | "hashes": [ 37 | "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872", 38 | "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569" 39 | ], 40 | "version": "==2021.10.8" 41 | }, 42 | "cffi": { 43 | "hashes": [ 44 | "sha256:00c878c90cb53ccfaae6b8bc18ad05d2036553e6d9d1d9dbcf323bbe83854ca3", 45 | "sha256:0104fb5ae2391d46a4cb082abdd5c69ea4eab79d8d44eaaf79f1b1fd806ee4c2", 46 | "sha256:06c48159c1abed75c2e721b1715c379fa3200c7784271b3c46df01383b593636", 47 | "sha256:0808014eb713677ec1292301ea4c81ad277b6cdf2fdd90fd540af98c0b101d20", 48 | "sha256:10dffb601ccfb65262a27233ac273d552ddc4d8ae1bf93b21c94b8511bffe728", 49 | "sha256:14cd121ea63ecdae71efa69c15c5543a4b5fbcd0bbe2aad864baca0063cecf27", 50 | "sha256:17771976e82e9f94976180f76468546834d22a7cc404b17c22df2a2c81db0c66", 51 | "sha256:181dee03b1170ff1969489acf1c26533710231c58f95534e3edac87fff06c443", 52 | "sha256:23cfe892bd5dd8941608f93348c0737e369e51c100d03718f108bf1add7bd6d0", 53 | "sha256:263cc3d821c4ab2213cbe8cd8b355a7f72a8324577dc865ef98487c1aeee2bc7", 54 | "sha256:2756c88cbb94231c7a147402476be2c4df2f6078099a6f4a480d239a8817ae39", 55 | "sha256:27c219baf94952ae9d50ec19651a687b826792055353d07648a5695413e0c605", 56 | "sha256:2a23af14f408d53d5e6cd4e3d9a24ff9e05906ad574822a10563efcef137979a", 57 | "sha256:31fb708d9d7c3f49a60f04cf5b119aeefe5644daba1cd2a0fe389b674fd1de37", 58 | "sha256:3415c89f9204ee60cd09b235810be700e993e343a408693e80ce7f6a40108029", 59 | "sha256:3773c4d81e6e818df2efbc7dd77325ca0dcb688116050fb2b3011218eda36139", 60 | "sha256:3b96a311ac60a3f6be21d2572e46ce67f09abcf4d09344c49274eb9e0bf345fc", 61 | "sha256:3f7d084648d77af029acb79a0ff49a0ad7e9d09057a9bf46596dac9514dc07df", 62 | "sha256:41d45de54cd277a7878919867c0f08b0cf817605e4eb94093e7516505d3c8d14", 63 | "sha256:4238e6dab5d6a8ba812de994bbb0a79bddbdf80994e4ce802b6f6f3142fcc880", 64 | "sha256:45db3a33139e9c8f7c09234b5784a5e33d31fd6907800b316decad50af323ff2", 65 | "sha256:45e8636704eacc432a206ac7345a5d3d2c62d95a507ec70d62f23cd91770482a", 66 | "sha256:4958391dbd6249d7ad855b9ca88fae690783a6be9e86df65865058ed81fc860e", 67 | "sha256:4a306fa632e8f0928956a41fa8e1d6243c71e7eb59ffbd165fc0b41e316b2474", 68 | "sha256:57e9ac9ccc3101fac9d6014fba037473e4358ef4e89f8e181f8951a2c0162024", 69 | "sha256:59888172256cac5629e60e72e86598027aca6bf01fa2465bdb676d37636573e8", 70 | "sha256:5e069f72d497312b24fcc02073d70cb989045d1c91cbd53979366077959933e0", 71 | "sha256:64d4ec9f448dfe041705426000cc13e34e6e5bb13736e9fd62e34a0b0c41566e", 72 | "sha256:6dc2737a3674b3e344847c8686cf29e500584ccad76204efea14f451d4cc669a", 73 | "sha256:74fdfdbfdc48d3f47148976f49fab3251e550a8720bebc99bf1483f5bfb5db3e", 74 | "sha256:75e4024375654472cc27e91cbe9eaa08567f7fbdf822638be2814ce059f58032", 75 | "sha256:786902fb9ba7433aae840e0ed609f45c7bcd4e225ebb9c753aa39725bb3e6ad6", 76 | "sha256:8b6c2ea03845c9f501ed1313e78de148cd3f6cad741a75d43a29b43da27f2e1e", 77 | "sha256:91d77d2a782be4274da750752bb1650a97bfd8f291022b379bb8e01c66b4e96b", 78 | "sha256:91ec59c33514b7c7559a6acda53bbfe1b283949c34fe7440bcf917f96ac0723e", 79 | "sha256:920f0d66a896c2d99f0adbb391f990a84091179542c205fa53ce5787aff87954", 80 | "sha256:a5263e363c27b653a90078143adb3d076c1a748ec9ecc78ea2fb916f9b861962", 81 | "sha256:abb9a20a72ac4e0fdb50dae135ba5e77880518e742077ced47eb1499e29a443c", 82 | "sha256:c2051981a968d7de9dd2d7b87bcb9c939c74a34626a6e2f8181455dd49ed69e4", 83 | "sha256:c21c9e3896c23007803a875460fb786118f0cdd4434359577ea25eb556e34c55", 84 | "sha256:c2502a1a03b6312837279c8c1bd3ebedf6c12c4228ddbad40912d671ccc8a962", 85 | "sha256:d4d692a89c5cf08a8557fdeb329b82e7bf609aadfaed6c0d79f5a449a3c7c023", 86 | "sha256:da5db4e883f1ce37f55c667e5c0de439df76ac4cb55964655906306918e7363c", 87 | "sha256:e7022a66d9b55e93e1a845d8c9eba2a1bebd4966cd8bfc25d9cd07d515b33fa6", 88 | "sha256:ef1f279350da2c586a69d32fc8733092fd32cc8ac95139a00377841f59a3f8d8", 89 | "sha256:f54a64f8b0c8ff0b64d18aa76675262e1700f3995182267998c31ae974fbc382", 90 | "sha256:f5c7150ad32ba43a07c4479f40241756145a1f03b43480e058cfd862bf5041c7", 91 | "sha256:f6f824dc3bce0edab5f427efcfb1d63ee75b6fcb7282900ccaf925be84efb0fc", 92 | "sha256:fd8a250edc26254fe5b33be00402e6d287f562b6a5b2152dec302fa15bb3e997", 93 | "sha256:ffaa5c925128e29efbde7301d8ecaf35c8c60ffbcd6a1ffd3a552177c8e5e796" 94 | ], 95 | "version": "==1.15.0" 96 | }, 97 | "charset-normalizer": { 98 | "hashes": [ 99 | "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597", 100 | "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df" 101 | ], 102 | "markers": "python_version >= '3'", 103 | "version": "==2.0.12" 104 | }, 105 | "click": { 106 | "hashes": [ 107 | "sha256:24e1a4a9ec5bf6299411369b208c1df2188d9eb8d916302fe6bf03faed227f1e", 108 | "sha256:479707fe14d9ec9a0757618b7a100a0ae4c4e236fac5b7f80ca68028141a1a72" 109 | ], 110 | "markers": "python_version >= '3.7'", 111 | "version": "==8.1.2" 112 | }, 113 | "colormath": { 114 | "hashes": [ 115 | "sha256:3d4605af344527da0e4f9f504fad7ddbebda35322c566a6c72e28edb1ff31217" 116 | ], 117 | "index": "pypi", 118 | "version": "==3.0.0" 119 | }, 120 | "deprecated": { 121 | "hashes": [ 122 | "sha256:43ac5335da90c31c24ba028af536a91d41d53f9e6901ddb021bcc572ce44e38d", 123 | "sha256:64756e3e14c8c5eea9795d93c524551432a0be75629f8f29e67ab8caf076c76d" 124 | ], 125 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 126 | "version": "==1.2.13" 127 | }, 128 | "flask": { 129 | "hashes": [ 130 | "sha256:8a4cf32d904cf5621db9f0c9fbcd7efabf3003f22a04e4d0ce790c7137ec5264", 131 | "sha256:a8c9bd3e558ec99646d177a9739c41df1ded0629480b4c8d2975412f3c9519c8" 132 | ], 133 | "markers": "python_version >= '3.7'", 134 | "version": "==2.1.1" 135 | }, 136 | "flask-cors": { 137 | "hashes": [ 138 | "sha256:74efc975af1194fc7891ff5cd85b0f7478be4f7f59fe158102e91abb72bb4438", 139 | "sha256:b60839393f3b84a0f3746f6cdca56c1ad7426aa738b70d6c61375857823181de" 140 | ], 141 | "index": "pypi", 142 | "version": "==3.0.10" 143 | }, 144 | "flask-restx": { 145 | "hashes": [ 146 | "sha256:63c69a61999a34f1774eaccc6fc8c7f504b1aad7d56a8ec672264e52d9ac05f4", 147 | "sha256:96157547acaa8892adcefd8c60abf9040212ac2a8634937a82946e07b46147fd" 148 | ], 149 | "index": "pypi", 150 | "version": "==0.5.1" 151 | }, 152 | "flask-wtf": { 153 | "hashes": [ 154 | "sha256:34fe5c6fee0f69b50e30f81a3b7ea16aa1492a771fe9ad0974d164610c09a6c9", 155 | "sha256:9d733658c80be551ce7d5bc13c7a7ac0d80df509be1e23827c847d9520f4359a" 156 | ], 157 | "index": "pypi", 158 | "version": "==1.0.1" 159 | }, 160 | "gunicorn": { 161 | "hashes": [ 162 | "sha256:9dcc4547dbb1cb284accfb15ab5667a0e5d1881cc443e0677b4882a4067a807e", 163 | "sha256:e0a968b5ba15f8a328fdfd7ab1fcb5af4470c28aaf7e55df02a99bc13138e6e8" 164 | ], 165 | "index": "pypi", 166 | "version": "==20.1.0" 167 | }, 168 | "idna": { 169 | "hashes": [ 170 | "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff", 171 | "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d" 172 | ], 173 | "markers": "python_version >= '3'", 174 | "version": "==3.3" 175 | }, 176 | "itsdangerous": { 177 | "hashes": [ 178 | "sha256:2c2349112351b88699d8d4b6b075022c0808887cb7ad10069318a8b0bc88db44", 179 | "sha256:5dbbc68b317e5e42f327f9021763545dc3fc3bfe22e6deb96aaf1fc38874156a" 180 | ], 181 | "markers": "python_version >= '3.7'", 182 | "version": "==2.1.2" 183 | }, 184 | "jinja2": { 185 | "hashes": [ 186 | "sha256:539835f51a74a69f41b848a9645dbdc35b4f20a3b601e2d9a7e22947b15ff119", 187 | "sha256:640bed4bb501cbd17194b3cace1dc2126f5b619cf068a726b98192a0fde74ae9" 188 | ], 189 | "markers": "python_version >= '3.7'", 190 | "version": "==3.1.1" 191 | }, 192 | "jsonschema": { 193 | "hashes": [ 194 | "sha256:636694eb41b3535ed608fe04129f26542b59ed99808b4f688aa32dcf55317a83", 195 | "sha256:77281a1f71684953ee8b3d488371b162419767973789272434bbc3f29d9c8823" 196 | ], 197 | "markers": "python_version >= '3.7'", 198 | "version": "==4.4.0" 199 | }, 200 | "lxml": { 201 | "hashes": [ 202 | "sha256:078306d19a33920004addeb5f4630781aaeabb6a8d01398045fcde085091a169", 203 | "sha256:0c1978ff1fd81ed9dcbba4f91cf09faf1f8082c9d72eb122e92294716c605428", 204 | "sha256:1010042bfcac2b2dc6098260a2ed022968dbdfaf285fc65a3acf8e4eb1ffd1bc", 205 | "sha256:1d650812b52d98679ed6c6b3b55cbb8fe5a5460a0aef29aeb08dc0b44577df85", 206 | "sha256:20b8a746a026017acf07da39fdb10aa80ad9877046c9182442bf80c84a1c4696", 207 | "sha256:2403a6d6fb61c285969b71f4a3527873fe93fd0abe0832d858a17fe68c8fa507", 208 | "sha256:24f5c5ae618395ed871b3d8ebfcbb36e3f1091fd847bf54c4de623f9107942f3", 209 | "sha256:28d1af847786f68bec57961f31221125c29d6f52d9187c01cd34dc14e2b29430", 210 | "sha256:31499847fc5f73ee17dbe1b8e24c6dafc4e8d5b48803d17d22988976b0171f03", 211 | "sha256:31ba2cbc64516dcdd6c24418daa7abff989ddf3ba6d3ea6f6ce6f2ed6e754ec9", 212 | "sha256:330bff92c26d4aee79c5bc4d9967858bdbe73fdbdbacb5daf623a03a914fe05b", 213 | "sha256:5045ee1ccd45a89c4daec1160217d363fcd23811e26734688007c26f28c9e9e7", 214 | "sha256:52cbf2ff155b19dc4d4100f7442f6a697938bf4493f8d3b0c51d45568d5666b5", 215 | "sha256:530f278849031b0eb12f46cca0e5db01cfe5177ab13bd6878c6e739319bae654", 216 | "sha256:545bd39c9481f2e3f2727c78c169425efbfb3fbba6e7db4f46a80ebb249819ca", 217 | "sha256:5804e04feb4e61babf3911c2a974a5b86f66ee227cc5006230b00ac6d285b3a9", 218 | "sha256:5a58d0b12f5053e270510bf12f753a76aaf3d74c453c00942ed7d2c804ca845c", 219 | "sha256:5f148b0c6133fb928503cfcdfdba395010f997aa44bcf6474fcdd0c5398d9b63", 220 | "sha256:5f7d7d9afc7b293147e2d506a4596641d60181a35279ef3aa5778d0d9d9123fe", 221 | "sha256:60d2f60bd5a2a979df28ab309352cdcf8181bda0cca4529769a945f09aba06f9", 222 | "sha256:6259b511b0f2527e6d55ad87acc1c07b3cbffc3d5e050d7e7bcfa151b8202df9", 223 | "sha256:6268e27873a3d191849204d00d03f65c0e343b3bcb518a6eaae05677c95621d1", 224 | "sha256:627e79894770783c129cc5e89b947e52aa26e8e0557c7e205368a809da4b7939", 225 | "sha256:62f93eac69ec0f4be98d1b96f4d6b964855b8255c345c17ff12c20b93f247b68", 226 | "sha256:6d6483b1229470e1d8835e52e0ff3c6973b9b97b24cd1c116dca90b57a2cc613", 227 | "sha256:6f7b82934c08e28a2d537d870293236b1000d94d0b4583825ab9649aef7ddf63", 228 | "sha256:6fe4ef4402df0250b75ba876c3795510d782def5c1e63890bde02d622570d39e", 229 | "sha256:719544565c2937c21a6f76d520e6e52b726d132815adb3447ccffbe9f44203c4", 230 | "sha256:730766072fd5dcb219dd2b95c4c49752a54f00157f322bc6d71f7d2a31fecd79", 231 | "sha256:74eb65ec61e3c7c019d7169387d1b6ffcfea1b9ec5894d116a9a903636e4a0b1", 232 | "sha256:7993232bd4044392c47779a3c7e8889fea6883be46281d45a81451acfd704d7e", 233 | "sha256:80bbaddf2baab7e6de4bc47405e34948e694a9efe0861c61cdc23aa774fcb141", 234 | "sha256:86545e351e879d0b72b620db6a3b96346921fa87b3d366d6c074e5a9a0b8dadb", 235 | "sha256:891dc8f522d7059ff0024cd3ae79fd224752676447f9c678f2a5c14b84d9a939", 236 | "sha256:8a31f24e2a0b6317f33aafbb2f0895c0bce772980ae60c2c640d82caac49628a", 237 | "sha256:8b99ec73073b37f9ebe8caf399001848fced9c08064effdbfc4da2b5a8d07b93", 238 | "sha256:986b7a96228c9b4942ec420eff37556c5777bfba6758edcb95421e4a614b57f9", 239 | "sha256:a1547ff4b8a833511eeaceacbcd17b043214fcdb385148f9c1bc5556ca9623e2", 240 | "sha256:a2bfc7e2a0601b475477c954bf167dee6d0f55cb167e3f3e7cefad906e7759f6", 241 | "sha256:a3c5f1a719aa11866ffc530d54ad965063a8cbbecae6515acbd5f0fae8f48eaa", 242 | "sha256:a9f1c3489736ff8e1c7652e9dc39f80cff820f23624f23d9eab6e122ac99b150", 243 | "sha256:aa0cf4922da7a3c905d000b35065df6184c0dc1d866dd3b86fd961905bbad2ea", 244 | "sha256:ad4332a532e2d5acb231a2e5d33f943750091ee435daffca3fec0a53224e7e33", 245 | "sha256:b2582b238e1658c4061ebe1b4df53c435190d22457642377fd0cb30685cdfb76", 246 | "sha256:b6fc2e2fb6f532cf48b5fed57567ef286addcef38c28874458a41b7837a57807", 247 | "sha256:b92d40121dcbd74831b690a75533da703750f7041b4bf951befc657c37e5695a", 248 | "sha256:bbab6faf6568484707acc052f4dfc3802bdb0cafe079383fbaa23f1cdae9ecd4", 249 | "sha256:c0b88ed1ae66777a798dc54f627e32d3b81c8009967c63993c450ee4cbcbec15", 250 | "sha256:ce13d6291a5f47c1c8dbd375baa78551053bc6b5e5c0e9bb8e39c0a8359fd52f", 251 | "sha256:db3535733f59e5605a88a706824dfcb9bd06725e709ecb017e165fc1d6e7d429", 252 | "sha256:dd10383f1d6b7edf247d0960a3db274c07e96cf3a3fc7c41c8448f93eac3fb1c", 253 | "sha256:e01f9531ba5420838c801c21c1b0f45dbc9607cb22ea2cf132844453bec863a5", 254 | "sha256:e11527dc23d5ef44d76fef11213215c34f36af1608074561fcc561d983aeb870", 255 | "sha256:e1ab2fac607842ac36864e358c42feb0960ae62c34aa4caaf12ada0a1fb5d99b", 256 | "sha256:e1fd7d2fe11f1cb63d3336d147c852f6d07de0d0020d704c6031b46a30b02ca8", 257 | "sha256:e9f84ed9f4d50b74fbc77298ee5c870f67cb7e91dcdc1a6915cb1ff6a317476c", 258 | "sha256:ec4b4e75fc68da9dc0ed73dcdb431c25c57775383fec325d23a770a64e7ebc87", 259 | "sha256:f10ce66fcdeb3543df51d423ede7e238be98412232fca5daec3e54bcd16b8da0", 260 | "sha256:f63f62fc60e6228a4ca9abae28228f35e1bd3ce675013d1dfb828688d50c6e23", 261 | "sha256:fa56bb08b3dd8eac3a8c5b7d075c94e74f755fd9d8a04543ae8d37b1612dd170", 262 | "sha256:fa9b7c450be85bfc6cd39f6df8c5b8cbd76b5d6fc1f69efec80203f9894b885f" 263 | ], 264 | "index": "pypi", 265 | "version": "==4.8.0" 266 | }, 267 | "markupsafe": { 268 | "hashes": [ 269 | "sha256:0212a68688482dc52b2d45013df70d169f542b7394fc744c02a57374a4207003", 270 | "sha256:089cf3dbf0cd6c100f02945abeb18484bd1ee57a079aefd52cffd17fba910b88", 271 | "sha256:10c1bfff05d95783da83491be968e8fe789263689c02724e0c691933c52994f5", 272 | "sha256:33b74d289bd2f5e527beadcaa3f401e0df0a89927c1559c8566c066fa4248ab7", 273 | "sha256:3799351e2336dc91ea70b034983ee71cf2f9533cdff7c14c90ea126bfd95d65a", 274 | "sha256:3ce11ee3f23f79dbd06fb3d63e2f6af7b12db1d46932fe7bd8afa259a5996603", 275 | "sha256:421be9fbf0ffe9ffd7a378aafebbf6f4602d564d34be190fc19a193232fd12b1", 276 | "sha256:43093fb83d8343aac0b1baa75516da6092f58f41200907ef92448ecab8825135", 277 | "sha256:46d00d6cfecdde84d40e572d63735ef81423ad31184100411e6e3388d405e247", 278 | "sha256:4a33dea2b688b3190ee12bd7cfa29d39c9ed176bda40bfa11099a3ce5d3a7ac6", 279 | "sha256:4b9fe39a2ccc108a4accc2676e77da025ce383c108593d65cc909add5c3bd601", 280 | "sha256:56442863ed2b06d19c37f94d999035e15ee982988920e12a5b4ba29b62ad1f77", 281 | "sha256:671cd1187ed5e62818414afe79ed29da836dde67166a9fac6d435873c44fdd02", 282 | "sha256:694deca8d702d5db21ec83983ce0bb4b26a578e71fbdbd4fdcd387daa90e4d5e", 283 | "sha256:6a074d34ee7a5ce3effbc526b7083ec9731bb3cbf921bbe1d3005d4d2bdb3a63", 284 | "sha256:6d0072fea50feec76a4c418096652f2c3238eaa014b2f94aeb1d56a66b41403f", 285 | "sha256:6fbf47b5d3728c6aea2abb0589b5d30459e369baa772e0f37a0320185e87c980", 286 | "sha256:7f91197cc9e48f989d12e4e6fbc46495c446636dfc81b9ccf50bb0ec74b91d4b", 287 | "sha256:86b1f75c4e7c2ac2ccdaec2b9022845dbb81880ca318bb7a0a01fbf7813e3812", 288 | "sha256:8dc1c72a69aa7e082593c4a203dcf94ddb74bb5c8a731e4e1eb68d031e8498ff", 289 | "sha256:8e3dcf21f367459434c18e71b2a9532d96547aef8a871872a5bd69a715c15f96", 290 | "sha256:8e576a51ad59e4bfaac456023a78f6b5e6e7651dcd383bcc3e18d06f9b55d6d1", 291 | "sha256:96e37a3dc86e80bf81758c152fe66dbf60ed5eca3d26305edf01892257049925", 292 | "sha256:97a68e6ada378df82bc9f16b800ab77cbf4b2fada0081794318520138c088e4a", 293 | "sha256:99a2a507ed3ac881b975a2976d59f38c19386d128e7a9a18b7df6fff1fd4c1d6", 294 | "sha256:a49907dd8420c5685cfa064a1335b6754b74541bbb3706c259c02ed65b644b3e", 295 | "sha256:b09bf97215625a311f669476f44b8b318b075847b49316d3e28c08e41a7a573f", 296 | "sha256:b7bd98b796e2b6553da7225aeb61f447f80a1ca64f41d83612e6139ca5213aa4", 297 | "sha256:b87db4360013327109564f0e591bd2a3b318547bcef31b468a92ee504d07ae4f", 298 | "sha256:bcb3ed405ed3222f9904899563d6fc492ff75cce56cba05e32eff40e6acbeaa3", 299 | "sha256:d4306c36ca495956b6d568d276ac11fdd9c30a36f1b6eb928070dc5360b22e1c", 300 | "sha256:d5ee4f386140395a2c818d149221149c54849dfcfcb9f1debfe07a8b8bd63f9a", 301 | "sha256:dda30ba7e87fbbb7eab1ec9f58678558fd9a6b8b853530e176eabd064da81417", 302 | "sha256:e04e26803c9c3851c931eac40c695602c6295b8d432cbe78609649ad9bd2da8a", 303 | "sha256:e1c0b87e09fa55a220f058d1d49d3fb8df88fbfab58558f1198e08c1e1de842a", 304 | "sha256:e72591e9ecd94d7feb70c1cbd7be7b3ebea3f548870aa91e2732960fa4d57a37", 305 | "sha256:e8c843bbcda3a2f1e3c2ab25913c80a3c5376cd00c6e8c4a86a89a28c8dc5452", 306 | "sha256:efc1913fd2ca4f334418481c7e595c00aad186563bbc1ec76067848c7ca0a933", 307 | "sha256:f121a1420d4e173a5d96e47e9a0c0dcff965afdf1626d28de1460815f7c4ee7a", 308 | "sha256:fc7b548b17d238737688817ab67deebb30e8073c95749d55538ed473130ec0c7" 309 | ], 310 | "markers": "python_version >= '3.7'", 311 | "version": "==2.1.1" 312 | }, 313 | "networkx": { 314 | "hashes": [ 315 | "sha256:011e85d277c89681e8fa661cf5ff0743443445049b0b68789ad55ef09340c6e0", 316 | "sha256:d1194ba753e5eed07cdecd1d23c5cd7a3c772099bd8dbd2fea366788cf4de7ba" 317 | ], 318 | "markers": "python_version >= '3.8'", 319 | "version": "==2.7.1" 320 | }, 321 | "numpy": { 322 | "hashes": [ 323 | "sha256:07a8c89a04997625236c5ecb7afe35a02af3896c8aa01890a849913a2309c676", 324 | "sha256:08d9b008d0156c70dc392bb3ab3abb6e7a711383c3247b410b39962263576cd4", 325 | "sha256:201b4d0552831f7250a08d3b38de0d989d6f6e4658b709a02a73c524ccc6ffce", 326 | "sha256:2c10a93606e0b4b95c9b04b77dc349b398fdfbda382d2a39ba5a822f669a0123", 327 | "sha256:3ca688e1b9b95d80250bca34b11a05e389b1420d00e87a0d12dc45f131f704a1", 328 | "sha256:48a3aecd3b997bf452a2dedb11f4e79bc5bfd21a1d4cc760e703c31d57c84b3e", 329 | "sha256:568dfd16224abddafb1cbcce2ff14f522abe037268514dd7e42c6776a1c3f8e5", 330 | "sha256:5bfb1bb598e8229c2d5d48db1860bcf4311337864ea3efdbe1171fb0c5da515d", 331 | "sha256:639b54cdf6aa4f82fe37ebf70401bbb74b8508fddcf4797f9fe59615b8c5813a", 332 | "sha256:8251ed96f38b47b4295b1ae51631de7ffa8260b5b087808ef09a39a9d66c97ab", 333 | "sha256:92bfa69cfbdf7dfc3040978ad09a48091143cffb778ec3b03fa170c494118d75", 334 | "sha256:97098b95aa4e418529099c26558eeb8486e66bd1e53a6b606d684d0c3616b168", 335 | "sha256:a3bae1a2ed00e90b3ba5f7bd0a7c7999b55d609e0c54ceb2b076a25e345fa9f4", 336 | "sha256:c34ea7e9d13a70bf2ab64a2532fe149a9aced424cd05a2c4ba662fd989e3e45f", 337 | "sha256:dbc7601a3b7472d559dc7b933b18b4b66f9aa7452c120e87dfb33d02008c8a18", 338 | "sha256:e7927a589df200c5e23c57970bafbd0cd322459aa7b1ff73b7c2e84d6e3eae62", 339 | "sha256:f8c1f39caad2c896bc0018f699882b345b2a63708008be29b1f355ebf6f933fe", 340 | "sha256:f950f8845b480cffe522913d35567e29dd381b0dc7e4ce6a4a9f9156417d2430", 341 | "sha256:fade0d4f4d292b6f39951b6836d7a3c7ef5b2347f3c420cd9820a1d90d794802", 342 | "sha256:fdf3c08bce27132395d3c3ba1503cac12e17282358cb4bddc25cc46b0aca07aa" 343 | ], 344 | "markers": "python_version >= '3.8'", 345 | "version": "==1.22.3" 346 | }, 347 | "pillow": { 348 | "hashes": [ 349 | "sha256:01ce45deec9df310cbbee11104bae1a2a43308dd9c317f99235b6d3080ddd66e", 350 | "sha256:0c51cb9edac8a5abd069fd0758ac0a8bfe52c261ee0e330f363548aca6893595", 351 | "sha256:17869489de2fce6c36690a0c721bd3db176194af5f39249c1ac56d0bb0fcc512", 352 | "sha256:21dee8466b42912335151d24c1665fcf44dc2ee47e021d233a40c3ca5adae59c", 353 | "sha256:25023a6209a4d7c42154073144608c9a71d3512b648a2f5d4465182cb93d3477", 354 | "sha256:255c9d69754a4c90b0ee484967fc8818c7ff8311c6dddcc43a4340e10cd1636a", 355 | "sha256:35be4a9f65441d9982240e6966c1eaa1c654c4e5e931eaf580130409e31804d4", 356 | "sha256:3f42364485bfdab19c1373b5cd62f7c5ab7cc052e19644862ec8f15bb8af289e", 357 | "sha256:3fddcdb619ba04491e8f771636583a7cc5a5051cd193ff1aa1ee8616d2a692c5", 358 | "sha256:463acf531f5d0925ca55904fa668bb3461c3ef6bc779e1d6d8a488092bdee378", 359 | "sha256:4fe29a070de394e449fd88ebe1624d1e2d7ddeed4c12e0b31624561b58948d9a", 360 | "sha256:55dd1cf09a1fd7c7b78425967aacae9b0d70125f7d3ab973fadc7b5abc3de652", 361 | "sha256:5a3ecc026ea0e14d0ad7cd990ea7f48bfcb3eb4271034657dc9d06933c6629a7", 362 | "sha256:5cfca31ab4c13552a0f354c87fbd7f162a4fafd25e6b521bba93a57fe6a3700a", 363 | "sha256:66822d01e82506a19407d1afc104c3fcea3b81d5eb11485e593ad6b8492f995a", 364 | "sha256:69e5ddc609230d4408277af135c5b5c8fe7a54b2bdb8ad7c5100b86b3aab04c6", 365 | "sha256:6b6d4050b208c8ff886fd3db6690bf04f9a48749d78b41b7a5bf24c236ab0165", 366 | "sha256:7a053bd4d65a3294b153bdd7724dce864a1d548416a5ef61f6d03bf149205160", 367 | "sha256:82283af99c1c3a5ba1da44c67296d5aad19f11c535b551a5ae55328a317ce331", 368 | "sha256:8782189c796eff29dbb37dd87afa4ad4d40fc90b2742704f94812851b725964b", 369 | "sha256:8d79c6f468215d1a8415aa53d9868a6b40c4682165b8cb62a221b1baa47db458", 370 | "sha256:97bda660702a856c2c9e12ec26fc6d187631ddfd896ff685814ab21ef0597033", 371 | "sha256:a325ac71914c5c043fa50441b36606e64a10cd262de12f7a179620f579752ff8", 372 | "sha256:a336a4f74baf67e26f3acc4d61c913e378e931817cd1e2ef4dfb79d3e051b481", 373 | "sha256:a598d8830f6ef5501002ae85c7dbfcd9c27cc4efc02a1989369303ba85573e58", 374 | "sha256:a5eaf3b42df2bcda61c53a742ee2c6e63f777d0e085bbc6b2ab7ed57deb13db7", 375 | "sha256:aea7ce61328e15943d7b9eaca87e81f7c62ff90f669116f857262e9da4057ba3", 376 | "sha256:af79d3fde1fc2e33561166d62e3b63f0cc3e47b5a3a2e5fea40d4917754734ea", 377 | "sha256:c24f718f9dd73bb2b31a6201e6db5ea4a61fdd1d1c200f43ee585fc6dcd21b34", 378 | "sha256:c5b0ff59785d93b3437c3703e3c64c178aabada51dea2a7f2c5eccf1bcf565a3", 379 | "sha256:c7110ec1701b0bf8df569a7592a196c9d07c764a0a74f65471ea56816f10e2c8", 380 | "sha256:c870193cce4b76713a2b29be5d8327c8ccbe0d4a49bc22968aa1e680930f5581", 381 | "sha256:c9efef876c21788366ea1f50ecb39d5d6f65febe25ad1d4c0b8dff98843ac244", 382 | "sha256:de344bcf6e2463bb25179d74d6e7989e375f906bcec8cb86edb8b12acbc7dfef", 383 | "sha256:eb1b89b11256b5b6cad5e7593f9061ac4624f7651f7a8eb4dfa37caa1dfaa4d0", 384 | "sha256:ed742214068efa95e9844c2d9129e209ed63f61baa4d54dbf4cf8b5e2d30ccf2", 385 | "sha256:f401ed2bbb155e1ade150ccc63db1a4f6c1909d3d378f7d1235a44e90d75fb97", 386 | "sha256:fb89397013cf302f282f0fc998bb7abf11d49dcff72c8ecb320f76ea6e2c5717" 387 | ], 388 | "index": "pypi", 389 | "version": "==9.1.0" 390 | }, 391 | "pycparser": { 392 | "hashes": [ 393 | "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9", 394 | "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206" 395 | ], 396 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 397 | "version": "==2.21" 398 | }, 399 | "pygithub": { 400 | "hashes": [ 401 | "sha256:1bbfff9372047ff3f21d5cd8e07720f3dbfdaf6462fcaed9d815f528f1ba7283", 402 | "sha256:2caf0054ea079b71e539741ae56c5a95e073b81fa472ce222e81667381b9601b" 403 | ], 404 | "index": "pypi", 405 | "version": "==1.55" 406 | }, 407 | "pyjwt": { 408 | "hashes": [ 409 | "sha256:b888b4d56f06f6dcd777210c334e69c737be74755d3e5e9ee3fe67dc18a0ee41", 410 | "sha256:e0c4bb8d9f0af0c7f5b1ec4c5036309617d03d56932877f2f7a0beeb5318322f" 411 | ], 412 | "markers": "python_version >= '3.6'", 413 | "version": "==2.3.0" 414 | }, 415 | "pynacl": { 416 | "hashes": [ 417 | "sha256:06b8f6fa7f5de8d5d2f7573fe8c863c051225a27b61e6860fd047b1775807858", 418 | "sha256:0c84947a22519e013607c9be43706dd42513f9e6ae5d39d3613ca1e142fba44d", 419 | "sha256:20f42270d27e1b6a29f54032090b972d97f0a1b0948cc52392041ef7831fee93", 420 | "sha256:401002a4aaa07c9414132aaed7f6836ff98f59277a234704ff66878c2ee4a0d1", 421 | "sha256:52cb72a79269189d4e0dc537556f4740f7f0a9ec41c1322598799b0bdad4ef92", 422 | "sha256:61f642bf2378713e2c2e1de73444a3778e5f0a38be6fee0fe532fe30060282ff", 423 | "sha256:8ac7448f09ab85811607bdd21ec2464495ac8b7c66d146bf545b0f08fb9220ba", 424 | "sha256:a36d4a9dda1f19ce6e03c9a784a2921a4b726b02e1c736600ca9c22029474394", 425 | "sha256:a422368fc821589c228f4c49438a368831cb5bbc0eab5ebe1d7fac9dded6567b", 426 | "sha256:e46dae94e34b085175f8abb3b0aaa7da40767865ac82c928eeb9e57e1ea8a543" 427 | ], 428 | "markers": "python_version >= '3.6'", 429 | "version": "==1.5.0" 430 | }, 431 | "pyrsistent": { 432 | "hashes": [ 433 | "sha256:0e3e1fcc45199df76053026a51cc59ab2ea3fc7c094c6627e93b7b44cdae2c8c", 434 | "sha256:1b34eedd6812bf4d33814fca1b66005805d3640ce53140ab8bbb1e2651b0d9bc", 435 | "sha256:4ed6784ceac462a7d6fcb7e9b663e93b9a6fb373b7f43594f9ff68875788e01e", 436 | "sha256:5d45866ececf4a5fff8742c25722da6d4c9e180daa7b405dc0a2a2790d668c26", 437 | "sha256:636ce2dc235046ccd3d8c56a7ad54e99d5c1cd0ef07d9ae847306c91d11b5fec", 438 | "sha256:6455fc599df93d1f60e1c5c4fe471499f08d190d57eca040c0ea182301321286", 439 | "sha256:6bc66318fb7ee012071b2792024564973ecc80e9522842eb4e17743604b5e045", 440 | "sha256:7bfe2388663fd18bd8ce7db2c91c7400bf3e1a9e8bd7d63bf7e77d39051b85ec", 441 | "sha256:7ec335fc998faa4febe75cc5268a9eac0478b3f681602c1f27befaf2a1abe1d8", 442 | "sha256:914474c9f1d93080338ace89cb2acee74f4f666fb0424896fcfb8d86058bf17c", 443 | "sha256:b568f35ad53a7b07ed9b1b2bae09eb15cdd671a5ba5d2c66caee40dbf91c68ca", 444 | "sha256:cdfd2c361b8a8e5d9499b9082b501c452ade8bbf42aef97ea04854f4a3f43b22", 445 | "sha256:d1b96547410f76078eaf66d282ddca2e4baae8964364abb4f4dcdde855cd123a", 446 | "sha256:d4d61f8b993a7255ba714df3aca52700f8125289f84f704cf80916517c46eb96", 447 | "sha256:d7a096646eab884bf8bed965bad63ea327e0d0c38989fc83c5ea7b8a87037bfc", 448 | "sha256:df46c854f490f81210870e509818b729db4488e1f30f2a1ce1698b2295a878d1", 449 | "sha256:e24a828f57e0c337c8d8bb9f6b12f09dfdf0273da25fda9e314f0b684b415a07", 450 | "sha256:e4f3149fd5eb9b285d6bfb54d2e5173f6a116fe19172686797c056672689daf6", 451 | "sha256:e92a52c166426efbe0d1ec1332ee9119b6d32fc1f0bbfd55d5c1088070e7fc1b", 452 | "sha256:f87cc2863ef33c709e237d4b5f4502a62a00fab450c9e020892e8e2ede5847f5", 453 | "sha256:fd8da6d0124efa2f67d86fa70c851022f87c98e205f0594e1fae044e7119a5a6" 454 | ], 455 | "markers": "python_version >= '3.7'", 456 | "version": "==0.18.1" 457 | }, 458 | "pytz": { 459 | "hashes": [ 460 | "sha256:1e760e2fe6a8163bc0b3d9a19c4f84342afa0a2affebfaa84b01b978a02ecaa7", 461 | "sha256:e68985985296d9a66a881eb3193b0906246245294a881e7c8afe623866ac6a5c" 462 | ], 463 | "version": "==2022.1" 464 | }, 465 | "requests": { 466 | "hashes": [ 467 | "sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61", 468 | "sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d" 469 | ], 470 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", 471 | "version": "==2.27.1" 472 | }, 473 | "setuptools": { 474 | "hashes": [ 475 | "sha256:41aface2e85b517c3a466b4689b8055c02cd2e623461f09af7d93f3da65c4709", 476 | "sha256:88fafba4abc2f047e08a188fd4bbc10b0e464592c37b514c19f8f8f88d94450b" 477 | ], 478 | "markers": "python_version >= '3.7'", 479 | "version": "==61.3.1" 480 | }, 481 | "six": { 482 | "hashes": [ 483 | "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", 484 | "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" 485 | ], 486 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 487 | "version": "==1.16.0" 488 | }, 489 | "tinycss2": { 490 | "hashes": [ 491 | "sha256:b2e44dd8883c360c35dd0d1b5aad0b610e5156c2cb3b33434634e539ead9d8bf", 492 | "sha256:fe794ceaadfe3cf3e686b22155d0da5780dd0e273471a51846d0a02bc204fec8" 493 | ], 494 | "index": "pypi", 495 | "version": "==1.1.1" 496 | }, 497 | "urllib3": { 498 | "hashes": [ 499 | "sha256:44ece4d53fb1706f667c9bd1c648f5469a2ec925fcf3a776667042d645472c14", 500 | "sha256:aabaf16477806a5e1dd19aa41f8c2b7950dd3c746362d7e3223dbe6de6ac448e" 501 | ], 502 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", 503 | "version": "==1.26.9" 504 | }, 505 | "webencodings": { 506 | "hashes": [ 507 | "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78", 508 | "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923" 509 | ], 510 | "version": "==0.5.1" 511 | }, 512 | "werkzeug": { 513 | "hashes": [ 514 | "sha256:3c5493ece8268fecdcdc9c0b112211acd006354723b280d643ec732b6d4063d6", 515 | "sha256:f8e89a20aeabbe8a893c24a461d3ee5dad2123b05cc6abd73ceed01d39c3ae74" 516 | ], 517 | "markers": "python_version >= '3.7'", 518 | "version": "==2.1.1" 519 | }, 520 | "wrapt": { 521 | "hashes": [ 522 | "sha256:00108411e0f34c52ce16f81f1d308a571df7784932cc7491d1e94be2ee93374b", 523 | "sha256:01f799def9b96a8ec1ef6b9c1bbaf2bbc859b87545efbecc4a78faea13d0e3a0", 524 | "sha256:09d16ae7a13cff43660155383a2372b4aa09109c7127aa3f24c3cf99b891c330", 525 | "sha256:14e7e2c5f5fca67e9a6d5f753d21f138398cad2b1159913ec9e9a67745f09ba3", 526 | "sha256:167e4793dc987f77fd476862d32fa404d42b71f6a85d3b38cbce711dba5e6b68", 527 | "sha256:1807054aa7b61ad8d8103b3b30c9764de2e9d0c0978e9d3fc337e4e74bf25faa", 528 | "sha256:1f83e9c21cd5275991076b2ba1cd35418af3504667affb4745b48937e214bafe", 529 | "sha256:21b1106bff6ece8cb203ef45b4f5778d7226c941c83aaaa1e1f0f4f32cc148cd", 530 | "sha256:22626dca56fd7f55a0733e604f1027277eb0f4f3d95ff28f15d27ac25a45f71b", 531 | "sha256:23f96134a3aa24cc50614920cc087e22f87439053d886e474638c68c8d15dc80", 532 | "sha256:2498762814dd7dd2a1d0248eda2afbc3dd9c11537bc8200a4b21789b6df6cd38", 533 | "sha256:28c659878f684365d53cf59dc9a1929ea2eecd7ac65da762be8b1ba193f7e84f", 534 | "sha256:2eca15d6b947cfff51ed76b2d60fd172c6ecd418ddab1c5126032d27f74bc350", 535 | "sha256:354d9fc6b1e44750e2a67b4b108841f5f5ea08853453ecbf44c81fdc2e0d50bd", 536 | "sha256:36a76a7527df8583112b24adc01748cd51a2d14e905b337a6fefa8b96fc708fb", 537 | "sha256:3a0a4ca02752ced5f37498827e49c414d694ad7cf451ee850e3ff160f2bee9d3", 538 | "sha256:3a71dbd792cc7a3d772ef8cd08d3048593f13d6f40a11f3427c000cf0a5b36a0", 539 | "sha256:3a88254881e8a8c4784ecc9cb2249ff757fd94b911d5df9a5984961b96113fff", 540 | "sha256:47045ed35481e857918ae78b54891fac0c1d197f22c95778e66302668309336c", 541 | "sha256:4775a574e9d84e0212f5b18886cace049a42e13e12009bb0491562a48bb2b758", 542 | "sha256:493da1f8b1bb8a623c16552fb4a1e164c0200447eb83d3f68b44315ead3f9036", 543 | "sha256:4b847029e2d5e11fd536c9ac3136ddc3f54bc9488a75ef7d040a3900406a91eb", 544 | "sha256:59d7d92cee84a547d91267f0fea381c363121d70fe90b12cd88241bd9b0e1763", 545 | "sha256:5a0898a640559dec00f3614ffb11d97a2666ee9a2a6bad1259c9facd01a1d4d9", 546 | "sha256:5a9a1889cc01ed2ed5f34574c90745fab1dd06ec2eee663e8ebeefe363e8efd7", 547 | "sha256:5b835b86bd5a1bdbe257d610eecab07bf685b1af2a7563093e0e69180c1d4af1", 548 | "sha256:5f24ca7953f2643d59a9c87d6e272d8adddd4a53bb62b9208f36db408d7aafc7", 549 | "sha256:61e1a064906ccba038aa3c4a5a82f6199749efbbb3cef0804ae5c37f550eded0", 550 | "sha256:65bf3eb34721bf18b5a021a1ad7aa05947a1767d1aa272b725728014475ea7d5", 551 | "sha256:6807bcee549a8cb2f38f73f469703a1d8d5d990815c3004f21ddb68a567385ce", 552 | "sha256:68aeefac31c1f73949662ba8affaf9950b9938b712fb9d428fa2a07e40ee57f8", 553 | "sha256:6915682f9a9bc4cf2908e83caf5895a685da1fbd20b6d485dafb8e218a338279", 554 | "sha256:6d9810d4f697d58fd66039ab959e6d37e63ab377008ef1d63904df25956c7db0", 555 | "sha256:729d5e96566f44fccac6c4447ec2332636b4fe273f03da128fff8d5559782b06", 556 | "sha256:748df39ed634851350efa87690c2237a678ed794fe9ede3f0d79f071ee042561", 557 | "sha256:763a73ab377390e2af26042f685a26787c402390f682443727b847e9496e4a2a", 558 | "sha256:8323a43bd9c91f62bb7d4be74cc9ff10090e7ef820e27bfe8815c57e68261311", 559 | "sha256:8529b07b49b2d89d6917cfa157d3ea1dfb4d319d51e23030664a827fe5fd2131", 560 | "sha256:87fa943e8bbe40c8c1ba4086971a6fefbf75e9991217c55ed1bcb2f1985bd3d4", 561 | "sha256:88236b90dda77f0394f878324cfbae05ae6fde8a84d548cfe73a75278d760291", 562 | "sha256:891c353e95bb11abb548ca95c8b98050f3620a7378332eb90d6acdef35b401d4", 563 | "sha256:89ba3d548ee1e6291a20f3c7380c92f71e358ce8b9e48161401e087e0bc740f8", 564 | "sha256:8c6be72eac3c14baa473620e04f74186c5d8f45d80f8f2b4eda6e1d18af808e8", 565 | "sha256:9a242871b3d8eecc56d350e5e03ea1854de47b17f040446da0e47dc3e0b9ad4d", 566 | "sha256:9a3ff5fb015f6feb78340143584d9f8a0b91b6293d6b5cf4295b3e95d179b88c", 567 | "sha256:9a5a544861b21e0e7575b6023adebe7a8c6321127bb1d238eb40d99803a0e8bd", 568 | "sha256:9d57677238a0c5411c76097b8b93bdebb02eb845814c90f0b01727527a179e4d", 569 | "sha256:9d8c68c4145041b4eeae96239802cfdfd9ef927754a5be3f50505f09f309d8c6", 570 | "sha256:9d9fcd06c952efa4b6b95f3d788a819b7f33d11bea377be6b8980c95e7d10775", 571 | "sha256:a0057b5435a65b933cbf5d859cd4956624df37b8bf0917c71756e4b3d9958b9e", 572 | "sha256:a65bffd24409454b889af33b6c49d0d9bcd1a219b972fba975ac935f17bdf627", 573 | "sha256:b0ed6ad6c9640671689c2dbe6244680fe8b897c08fd1fab2228429b66c518e5e", 574 | "sha256:b21650fa6907e523869e0396c5bd591cc326e5c1dd594dcdccac089561cacfb8", 575 | "sha256:b3f7e671fb19734c872566e57ce7fc235fa953d7c181bb4ef138e17d607dc8a1", 576 | "sha256:b77159d9862374da213f741af0c361720200ab7ad21b9f12556e0eb95912cd48", 577 | "sha256:bb36fbb48b22985d13a6b496ea5fb9bb2a076fea943831643836c9f6febbcfdc", 578 | "sha256:d066ffc5ed0be00cd0352c95800a519cf9e4b5dd34a028d301bdc7177c72daf3", 579 | "sha256:d332eecf307fca852d02b63f35a7872de32d5ba8b4ec32da82f45df986b39ff6", 580 | "sha256:d808a5a5411982a09fef6b49aac62986274ab050e9d3e9817ad65b2791ed1425", 581 | "sha256:d9bdfa74d369256e4218000a629978590fd7cb6cf6893251dad13d051090436d", 582 | "sha256:db6a0ddc1282ceb9032e41853e659c9b638789be38e5b8ad7498caac00231c23", 583 | "sha256:debaf04f813ada978d7d16c7dfa16f3c9c2ec9adf4656efdc4defdf841fc2f0c", 584 | "sha256:f0408e2dbad9e82b4c960274214af533f856a199c9274bd4aff55d4634dedc33", 585 | "sha256:f2f3bc7cd9c9fcd39143f11342eb5963317bd54ecc98e3650ca22704b69d9653" 586 | ], 587 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", 588 | "version": "==1.14.0" 589 | }, 590 | "wtforms": { 591 | "hashes": [ 592 | "sha256:6b351bbb12dd58af57ffef05bc78425d08d1914e0fd68ee14143b7ade023c5bc", 593 | "sha256:837f2f0e0ca79481b92884962b914eba4e72b7a2daaf1f939c890ed0124b834b" 594 | ], 595 | "markers": "python_version >= '3.7'", 596 | "version": "==3.0.1" 597 | } 598 | }, 599 | "develop": {} 600 | } 601 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: gunicorn app:app --preload 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # KLE-Render 2 | > Get prettier images of Keyboard Layout Editor designs 3 | 4 | This project uses Python, Pillow, and Flask to serve up more realistic visualizations of custom mechanical keyboard layouts. It works by stretching, tinting, and tiling base images of a 1u keycap. Check it out at [kle-render.herokuapp.com](http://kle-render.herokuapp.com/). You can also see a [sample render](#sample-image) of Nantucket Selectric below. 5 | 6 | ## Frequently Asked Questions 7 | ### What layouts are supported? 8 | KLE-Render should support any layout created with keyboard-layout-editor though some may not render exactly as expected. Specifically, certain very uncommon unicode glyphs may not be displayed. Custom legend images take up the full width of the keycap and only one per key can be used. The base images are only of SA and GMK keycaps, but most layouts in DSA, DCS, OEM, or even beamspring profiles should still look pretty close. Sculpted row profiles are not supported; everything is assumed to be uniform row 3. 9 | 10 | ### How do I include icons and novelties in my layout? 11 | Many common icons and symbols are part of [Unicode](https://unicode-table.com), and can be rendered simply by pasting the appropriate character into the legend text boxes in keyboard-layout-editor. To match the default GMK icons, you can copy characters from the [GMK Icons Template](http://www.keyboard-layout-editor.com/#/gists/afc75b1f6ebee743ff0e2f589b0fc68d). Less common icons are available as part of the Font Awesome or Keyboard-Layout-Editor icon sets under the Character Picker dropdown. 12 | 13 | For truly custom legends though, you'll have to use an html image tag, like this one ``. The src parameter here should point to a PNG image with your legend on a transparent background. Note that KLE-Render does not support combining these custom legend images with regular text on the same key, and ignores any sizing or position info - the image is always resized to cover the entire top of the keycap. For reference see the SA and GMK kitting templates below. 14 | 15 | ### Are custom fonts supported? 16 | There is limited support for custom fonts, but this should be considered an advanced feature. Most layouts won't work without some changes to the custom styling. For more details on how and why see [issue #7](https://github.com/CQCumbers/kle_render/issues/7#issuecomment-880827473). 17 | 18 | ### What do I do if I get an error page when trying to render? 19 | If you get an internal server error when attempting to render a layout, first make sure that your JSON input is downloaded properly or that your gist url actually exists. If the error persists, please contact me with the gist link or JSON that is causing the problem and I may be able to fix it. I am CQ\_Cumbers on reddit and geekhack. 20 | 21 | ### Why don't my renders look like the ones on Massdrop? 22 | This tool can generate more realistic renders of arbitrary keyboard layouts, but because it works by stretching and tinting grayscale images, there are many limitations on realism as compared to actual 3D renders. If you're looking for fast and photorealistic visualizations [kbrenders.com](http://www.kbrenders.com) can be a useful resource. For certain custom work, however, you may still have to do post-processing in photoshop or commission a professional like thesiscamper to work with you. 23 | 24 | ### How do I turn my set design into a group buy? 25 | If you're looking to create a keycap set for a group buy livingspeedbump (creator of SA Jukebox) has a nice [guide](https://www.keychatter.com/2015/10/10/how-to-create-a-keycap-set-for-a-group-buy/) up on keychatter. 26 | 27 | 28 | ## Templates 29 | The following templates have their legend sizes and keycap profiles pre-configured for accurate rendering. Use them as a starting point for your own designs! 30 | 31 | - Example Kitting - [SA](http://www.keyboard-layout-editor.com/#/gists/6331e126fa6340711e53a0806d57cde5)/[GMK](http://www.keyboard-layout-editor.com/#/gists/a3a9791b1068f1100b151c33debf660f) 32 | - Mech Mini 2 (40%) - [SA](http://www.keyboard-layout-editor.com/#/gists/ea2a231112ffceae047494ac9a93e706)/[GMK](http://www.keyboard-layout-editor.com/#/gists/eed1f1854dda3999bcdd730f0143c627) 33 | - Klippe (60%) - [SA](http://www.keyboard-layout-editor.com/#/gists/f8369e8d6ae12c6d30bbf6db9731bca5)/[GMK](http://www.keyboard-layout-editor.com/#/gists/c2aedbf20e6a1ee5320a0f89b114d6da) 34 | - J-02 (HHKB) - [SA](http://www.keyboard-layout-editor.com/#/gists/1e01f5c46bcc3ba388f84d3a26f2e2eb)/[GMK](http://www.keyboard-layout-editor.com/#/gists/d5ef16b69b4ea15569d7a319bbf90a8e) 35 | - RAMA M65 (65%) - [SA](http://www.keyboard-layout-editor.com/#/gists/3ca3649e1d048134ddd0e835d1dd735b)/[GMK](http://www.keyboard-layout-editor.com/#/gists/4319599274157d2a0dd0e38328b76878) 36 | - GMMK Pro (75%) - [SA](http://www.keyboard-layout-editor.com/#/gists/c1a1d76bfcd236bc36e1c04c1e86a0d8)/[GMK](http://www.keyboard-layout-editor.com/#/gists/8ab0de3dd5dc804ecb052924a1c45be5) 37 | - JP01 (Arisu) - [SA](http://www.keyboard-layout-editor.com/#/gists/4f06c7adcce33046a463084af34aae60)/[GMK](http://www.keyboard-layout-editor.com/#/gists/de533ff9b29225bb65a6155151030673) 38 | - Mech27 (TKL) - [SA](http://www.keyboard-layout-editor.com/#/gists/10629d008a99d8d6eb6f8c59414b5dd8)/[GMK](http://www.keyboard-layout-editor.com/#/gists/6e6692825b348f40c040ca9750e469a8) 39 | - Espectro (96%) - [SA](http://www.keyboard-layout-editor.com/#/gists/6b996bea3ebf8a85866ddea606e25de4)/[GMK](http://www.keyboard-layout-editor.com/#/gists/6a03012a82e7bbca14db635142913a7) 40 | - Cypher (1800-like) - [SA](http://www.keyboard-layout-editor.com/#/gists/9b5535a779ae9f095da3b8a73a39a3cf)/[GMK](http://www.keyboard-layout-editor.com/#/gists/27bc8c126110952cc77c69ef972a7d0d) 41 | - Triangle (Full-size) - [SA](http://www.keyboard-layout-editor.com/#/gists/b86a688e6502fcc910d4b32ca2fa642e)/[GMK](http://www.keyboard-layout-editor.com/#/gists/11f7fc1a19c7f2210f560a93c8ab82a2) 42 | - Modifier Icons - [GMK](http://www.keyboard-layout-editor.com/#/gists/afc75b1f6ebee743ff0e2f589b0fc68d) 43 | 44 | ## Sample Image 45 | Nantucket Selectric ([JSON](http://www.keyboard-layout-editor.com/#/gists/4de8adb88cb4c45c2f43)) 46 | 47 | ![Sample Render](render_output.png) 48 | -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | import json, github, io, flask_wtf, flask_wtf.file, wtforms, flask_cors 2 | from flask import Flask, Blueprint, send_file, render_template, flash, Markup 3 | from flask_restx import Api, Resource 4 | from keyboard import Keyboard 5 | 6 | 7 | app = Flask(__name__) 8 | app.config.from_object('config') 9 | flask_cors.CORS(app) 10 | github_api = github.Github(app.config['API_TOKEN']) 11 | blueprint = Blueprint('api', __name__, url_prefix='/api') 12 | api = Api( 13 | blueprint, version='1.0', title='KLE-Render API', 14 | description='Prettier images of Keyboard Layout Editor designs. URLs relative to this page' 15 | ) 16 | kle_parser = api.parser() 17 | kle_parser.add_argument( 18 | 'data', required=True, location='json', 19 | help='Downloaded strict JSON from Raw Data tab of Keyboard Layout Editor' 20 | ) 21 | app.register_blueprint(blueprint) 22 | 23 | 24 | def serve_pil_image(pil_img): 25 | img_io = io.BytesIO() 26 | pil_img.save(img_io, 'PNG', compress_level=3) 27 | img_io.seek(0) 28 | return send_file(img_io, mimetype='image/png') 29 | 30 | 31 | @api.route('/') 32 | @api.param('id', 'Copy from keyboard-layout-editor.com/#/gists/') 33 | class FromGist(Resource): 34 | def get(self, id): 35 | files = github_api.get_gist(id).files 36 | layout = next(v for k, v in files.items() if k.endswith('.kbd.json')) 37 | img = Keyboard(json.loads(layout.content)).render() 38 | return serve_pil_image(img) 39 | 40 | 41 | @api.route('/') 42 | @api.expect(kle_parser) 43 | class FromJSON(Resource): 44 | def post(self): 45 | img = Keyboard(api.payload).render() 46 | return serve_pil_image(img) 47 | 48 | 49 | @api.errorhandler(github.GithubException) 50 | def not_found(error): 51 | return {'message': error.message}, 404 52 | 53 | 54 | class InputForm(flask_wtf.FlaskForm): 55 | url = wtforms.StringField('Copy the URL of a saved layout:') 56 | valid = [flask_wtf.file.FileAllowed(['json'], 'Upload must be JSON')] 57 | json = flask_wtf.file.FileField('Or upload raw JSON:', validators=valid) 58 | 59 | 60 | def flash_errors(form): 61 | for field, errors in form.errors.items(): 62 | for error in errors: 63 | flash(error) 64 | 65 | 66 | @app.route('/', methods=['GET', 'POST']) 67 | @app.route('/index', methods=['GET', 'POST']) 68 | def index(): 69 | form = InputForm() 70 | if form.validate_on_submit(): 71 | if len(form.url.data) > 0: 72 | try: 73 | files = github_api.get_gist(form.url.data.split('gists/', 1)[1]).files 74 | layout = next(v for k, v in files.items() if k.endswith('.kbd.json')) 75 | img = Keyboard(json.loads(layout.content)).render() 76 | return serve_pil_image(img) 77 | except (IndexError, github.GithubException): 78 | flash('Not a valid Keyboard Layout Editor gist') 79 | elif form.json.data: 80 | try: 81 | content = json.loads(form.json.data.read().decode('utf-8')) 82 | img = Keyboard(content).render() 83 | return serve_pil_image(img) 84 | except ValueError: 85 | flash(Markup('Invalid JSON input - see (?) for help')) 86 | flash_errors(form) 87 | return render_template('index.html', form=form) 88 | 89 | 90 | if __name__ == '__main__': 91 | app.run(debug=True) 92 | -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | SWAGGER_UI_DOC_EXPANSION = 'full' 4 | SWAGGER_UI_JSONEDITOR = True 5 | DEBUG = False 6 | CORS_HEADERS = 'Content-Type' 7 | CORS_RESOURCES = r'/api/*' 8 | WTF_CSRF_ENABLED = False 9 | SECRET_KEY = os.environ.get('SECRET_KEY') 10 | API_TOKEN = os.environ.get('API_TOKEN') 11 | -------------------------------------------------------------------------------- /fonts/GMK_font.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CQCumbers/kle_render/a14ccea8e46286ba01c10e3380eca0a963e21d5d/fonts/GMK_font.ttf -------------------------------------------------------------------------------- /fonts/SA_font.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CQCumbers/kle_render/a14ccea8e46286ba01c10e3380eca0a963e21d5d/fonts/SA_font.ttf -------------------------------------------------------------------------------- /fonts/fa2unicode.json: -------------------------------------------------------------------------------- 1 | { 2 | "fa-glass": "f000", 3 | "fa-music": "f001", 4 | "fa-search": "f002", 5 | "fa-envelope-o": "f003", 6 | "fa-heart": "f004", 7 | "fa-star": "f005", 8 | "fa-star-o": "f006", 9 | "fa-user": "f007", 10 | "fa-film": "f008", 11 | "fa-th-large": "f009", 12 | "fa-th": "f00a", 13 | "fa-th-list": "f00b", 14 | "fa-check": "f00c", 15 | "fa-remove": "f00d", 16 | "fa-close": "f00d", 17 | "fa-times": "f00d", 18 | "fa-search-plus": "f00e", 19 | "fa-search-minus": "f010", 20 | "fa-power-off": "f011", 21 | "fa-signal": "f012", 22 | "fa-gear": "f013", 23 | "fa-cog": "f013", 24 | "fa-trash-o": "f014", 25 | "fa-home": "f015", 26 | "fa-file-o": "f016", 27 | "fa-clock-o": "f017", 28 | "fa-road": "f018", 29 | "fa-download": "f019", 30 | "fa-arrow-circle-o-down": "f01a", 31 | "fa-arrow-circle-o-up": "f01b", 32 | "fa-inbox": "f01c", 33 | "fa-play-circle-o": "f01d", 34 | "fa-rotate-right": "f01e", 35 | "fa-repeat": "f01e", 36 | "fa-refresh": "f021", 37 | "fa-list-alt": "f022", 38 | "fa-lock": "f023", 39 | "fa-flag": "f024", 40 | "fa-headphones": "f025", 41 | "fa-volume-off": "f026", 42 | "fa-volume-down": "f027", 43 | "fa-volume-up": "f028", 44 | "fa-qrcode": "f029", 45 | "fa-barcode": "f02a", 46 | "fa-tag": "f02b", 47 | "fa-tags": "f02c", 48 | "fa-book": "f02d", 49 | "fa-bookmark": "f02e", 50 | "fa-print": "f02f", 51 | "fa-camera": "f030", 52 | "fa-font": "f031", 53 | "fa-bold": "f032", 54 | "fa-italic": "f033", 55 | "fa-text-height": "f034", 56 | "fa-text-width": "f035", 57 | "fa-align-left": "f036", 58 | "fa-align-center": "f037", 59 | "fa-align-right": "f038", 60 | "fa-align-justify": "f039", 61 | "fa-list": "f03a", 62 | "fa-dedent": "f03b", 63 | "fa-outdent": "f03b", 64 | "fa-indent": "f03c", 65 | "fa-video-camera": "f03d", 66 | "fa-photo": "f03e", 67 | "fa-image": "f03e", 68 | "fa-picture-o": "f03e", 69 | "fa-pencil": "f040", 70 | "fa-map-marker": "f041", 71 | "fa-adjust": "f042", 72 | "fa-tint": "f043", 73 | "fa-edit": "f044", 74 | "fa-pencil-square-o": "f044", 75 | "fa-share-square-o": "f045", 76 | "fa-check-square-o": "f046", 77 | "fa-arrows": "f047", 78 | "fa-step-backward": "f048", 79 | "fa-fast-backward": "f049", 80 | "fa-backward": "f04a", 81 | "fa-play": "f04b", 82 | "fa-pause": "f04c", 83 | "fa-stop": "f04d", 84 | "fa-forward": "f04e", 85 | "fa-fast-forward": "f050", 86 | "fa-step-forward": "f051", 87 | "fa-eject": "f052", 88 | "fa-chevron-left": "f053", 89 | "fa-chevron-right": "f054", 90 | "fa-plus-circle": "f055", 91 | "fa-minus-circle": "f056", 92 | "fa-times-circle": "f057", 93 | "fa-check-circle": "f058", 94 | "fa-question-circle": "f059", 95 | "fa-info-circle": "f05a", 96 | "fa-crosshairs": "f05b", 97 | "fa-times-circle-o": "f05c", 98 | "fa-check-circle-o": "f05d", 99 | "fa-ban": "f05e", 100 | "fa-arrow-left": "f060", 101 | "fa-arrow-right": "f061", 102 | "fa-arrow-up": "f062", 103 | "fa-arrow-down": "f063", 104 | "fa-mail-forward": "f064", 105 | "fa-share": "f064", 106 | "fa-expand": "f065", 107 | "fa-compress": "f066", 108 | "fa-plus": "f067", 109 | "fa-minus": "f068", 110 | "fa-asterisk": "f069", 111 | "fa-exclamation-circle": "f06a", 112 | "fa-gift": "f06b", 113 | "fa-leaf": "f06c", 114 | "fa-fire": "f06d", 115 | "fa-eye": "f06e", 116 | "fa-eye-slash": "f070", 117 | "fa-warning": "f071", 118 | "fa-exclamation-triangle": "f071", 119 | "fa-plane": "f072", 120 | "fa-calendar": "f073", 121 | "fa-random": "f074", 122 | "fa-comment": "f075", 123 | "fa-magnet": "f076", 124 | "fa-chevron-up": "f077", 125 | "fa-chevron-down": "f078", 126 | "fa-retweet": "f079", 127 | "fa-shopping-cart": "f07a", 128 | "fa-folder": "f07b", 129 | "fa-folder-open": "f07c", 130 | "fa-arrows-v": "f07d", 131 | "fa-arrows-h": "f07e", 132 | "fa-bar-chart-o": "f080", 133 | "fa-bar-chart": "f080", 134 | "fa-twitter-square": "f081", 135 | "fa-facebook-square": "f082", 136 | "fa-camera-retro": "f083", 137 | "fa-key": "f084", 138 | "fa-gears": "f085", 139 | "fa-cogs": "f085", 140 | "fa-comments": "f086", 141 | "fa-thumbs-o-up": "f087", 142 | "fa-thumbs-o-down": "f088", 143 | "fa-star-half": "f089", 144 | "fa-heart-o": "f08a", 145 | "fa-sign-out": "f08b", 146 | "fa-linkedin-square": "f08c", 147 | "fa-thumb-tack": "f08d", 148 | "fa-external-link": "f08e", 149 | "fa-sign-in": "f090", 150 | "fa-trophy": "f091", 151 | "fa-github-square": "f092", 152 | "fa-upload": "f093", 153 | "fa-lemon-o": "f094", 154 | "fa-phone": "f095", 155 | "fa-square-o": "f096", 156 | "fa-bookmark-o": "f097", 157 | "fa-phone-square": "f098", 158 | "fa-twitter": "f099", 159 | "fa-facebook-f": "f09a", 160 | "fa-facebook": "f09a", 161 | "fa-github": "f09b", 162 | "fa-unlock": "f09c", 163 | "fa-credit-card": "f09d", 164 | "fa-rss": "f09e", 165 | "fa-hdd-o": "f0a0", 166 | "fa-bullhorn": "f0a1", 167 | "fa-bell": "f0f3", 168 | "fa-certificate": "f0a3", 169 | "fa-hand-o-right": "f0a4", 170 | "fa-hand-o-left": "f0a5", 171 | "fa-hand-o-up": "f0a6", 172 | "fa-hand-o-down": "f0a7", 173 | "fa-arrow-circle-left": "f0a8", 174 | "fa-arrow-circle-right": "f0a9", 175 | "fa-arrow-circle-up": "f0aa", 176 | "fa-arrow-circle-down": "f0ab", 177 | "fa-globe": "f0ac", 178 | "fa-wrench": "f0ad", 179 | "fa-tasks": "f0ae", 180 | "fa-filter": "f0b0", 181 | "fa-briefcase": "f0b1", 182 | "fa-arrows-alt": "f0b2", 183 | "fa-group": "f0c0", 184 | "fa-users": "f0c0", 185 | "fa-chain": "f0c1", 186 | "fa-link": "f0c1", 187 | "fa-cloud": "f0c2", 188 | "fa-flask": "f0c3", 189 | "fa-cut": "f0c4", 190 | "fa-scissors": "f0c4", 191 | "fa-copy": "f0c5", 192 | "fa-files-o": "f0c5", 193 | "fa-paperclip": "f0c6", 194 | "fa-save": "f0c7", 195 | "fa-floppy-o": "f0c7", 196 | "fa-square": "f0c8", 197 | "fa-navicon": "f0c9", 198 | "fa-reorder": "f0c9", 199 | "fa-bars": "f0c9", 200 | "fa-list-ul": "f0ca", 201 | "fa-list-ol": "f0cb", 202 | "fa-strikethrough": "f0cc", 203 | "fa-underline": "f0cd", 204 | "fa-table": "f0ce", 205 | "fa-magic": "f0d0", 206 | "fa-truck": "f0d1", 207 | "fa-pinterest": "f0d2", 208 | "fa-pinterest-square": "f0d3", 209 | "fa-google-plus-square": "f0d4", 210 | "fa-google-plus": "f0d5", 211 | "fa-money": "f0d6", 212 | "fa-caret-down": "f0d7", 213 | "fa-caret-up": "f0d8", 214 | "fa-caret-left": "f0d9", 215 | "fa-caret-right": "f0da", 216 | "fa-columns": "f0db", 217 | "fa-unsorted": "f0dc", 218 | "fa-sort": "f0dc", 219 | "fa-sort-down": "f0dd", 220 | "fa-sort-desc": "f0dd", 221 | "fa-sort-up": "f0de", 222 | "fa-sort-asc": "f0de", 223 | "fa-envelope": "f0e0", 224 | "fa-linkedin": "f0e1", 225 | "fa-rotate-left": "f0e2", 226 | "fa-undo": "f0e2", 227 | "fa-legal": "f0e3", 228 | "fa-gavel": "f0e3", 229 | "fa-dashboard": "f0e4", 230 | "fa-tachometer": "f0e4", 231 | "fa-comment-o": "f0e5", 232 | "fa-comments-o": "f0e6", 233 | "fa-flash": "f0e7", 234 | "fa-bolt": "f0e7", 235 | "fa-sitemap": "f0e8", 236 | "fa-umbrella": "f0e9", 237 | "fa-paste": "f0ea", 238 | "fa-clipboard": "f0ea", 239 | "fa-lightbulb-o": "f0eb", 240 | "fa-exchange": "f0ec", 241 | "fa-cloud-download": "f0ed", 242 | "fa-cloud-upload": "f0ee", 243 | "fa-user-md": "f0f0", 244 | "fa-stethoscope": "f0f1", 245 | "fa-suitcase": "f0f2", 246 | "fa-bell-o": "f0a2", 247 | "fa-coffee": "f0f4", 248 | "fa-cutlery": "f0f5", 249 | "fa-file-text-o": "f0f6", 250 | "fa-building-o": "f0f7", 251 | "fa-hospital-o": "f0f8", 252 | "fa-ambulance": "f0f9", 253 | "fa-medkit": "f0fa", 254 | "fa-fighter-jet": "f0fb", 255 | "fa-beer": "f0fc", 256 | "fa-h-square": "f0fd", 257 | "fa-plus-square": "f0fe", 258 | "fa-angle-double-left": "f100", 259 | "fa-angle-double-right": "f101", 260 | "fa-angle-double-up": "f102", 261 | "fa-angle-double-down": "f103", 262 | "fa-angle-left": "f104", 263 | "fa-angle-right": "f105", 264 | "fa-angle-up": "f106", 265 | "fa-angle-down": "f107", 266 | "fa-desktop": "f108", 267 | "fa-laptop": "f109", 268 | "fa-tablet": "f10a", 269 | "fa-mobile-phone": "f10b", 270 | "fa-mobile": "f10b", 271 | "fa-circle-o": "f10c", 272 | "fa-quote-left": "f10d", 273 | "fa-quote-right": "f10e", 274 | "fa-spinner": "f110", 275 | "fa-circle": "f111", 276 | "fa-mail-reply": "f112", 277 | "fa-reply": "f112", 278 | "fa-github-alt": "f113", 279 | "fa-folder-o": "f114", 280 | "fa-folder-open-o": "f115", 281 | "fa-smile-o": "f118", 282 | "fa-frown-o": "f119", 283 | "fa-meh-o": "f11a", 284 | "fa-gamepad": "f11b", 285 | "fa-keyboard-o": "f11c", 286 | "fa-flag-o": "f11d", 287 | "fa-flag-checkered": "f11e", 288 | "fa-terminal": "f120", 289 | "fa-code": "f121", 290 | "fa-mail-reply-all": "f122", 291 | "fa-reply-all": "f122", 292 | "fa-star-half-empty": "f123", 293 | "fa-star-half-full": "f123", 294 | "fa-star-half-o": "f123", 295 | "fa-location-arrow": "f124", 296 | "fa-crop": "f125", 297 | "fa-code-fork": "f126", 298 | "fa-unlink": "f127", 299 | "fa-chain-broken": "f127", 300 | "fa-question": "f128", 301 | "fa-info": "f129", 302 | "fa-exclamation": "f12a", 303 | "fa-superscript": "f12b", 304 | "fa-subscript": "f12c", 305 | "fa-eraser": "f12d", 306 | "fa-puzzle-piece": "f12e", 307 | "fa-microphone": "f130", 308 | "fa-microphone-slash": "f131", 309 | "fa-shield": "f132", 310 | "fa-calendar-o": "f133", 311 | "fa-fire-extinguisher": "f134", 312 | "fa-rocket": "f135", 313 | "fa-maxcdn": "f136", 314 | "fa-chevron-circle-left": "f137", 315 | "fa-chevron-circle-right": "f138", 316 | "fa-chevron-circle-up": "f139", 317 | "fa-chevron-circle-down": "f13a", 318 | "fa-html5": "f13b", 319 | "fa-css3": "f13c", 320 | "fa-anchor": "f13d", 321 | "fa-unlock-alt": "f13e", 322 | "fa-bullseye": "f140", 323 | "fa-ellipsis-h": "f141", 324 | "fa-ellipsis-v": "f142", 325 | "fa-rss-square": "f143", 326 | "fa-play-circle": "f144", 327 | "fa-ticket": "f145", 328 | "fa-minus-square": "f146", 329 | "fa-minus-square-o": "f147", 330 | "fa-level-up": "f148", 331 | "fa-level-down": "f149", 332 | "fa-check-square": "f14a", 333 | "fa-pencil-square": "f14b", 334 | "fa-external-link-square": "f14c", 335 | "fa-share-square": "f14d", 336 | "fa-compass": "f14e", 337 | "fa-toggle-down": "f150", 338 | "fa-caret-square-o-down": "f150", 339 | "fa-toggle-up": "f151", 340 | "fa-caret-square-o-up": "f151", 341 | "fa-toggle-right": "f152", 342 | "fa-caret-square-o-right": "f152", 343 | "fa-euro": "f153", 344 | "fa-eur": "f153", 345 | "fa-gbp": "f154", 346 | "fa-dollar": "f155", 347 | "fa-usd": "f155", 348 | "fa-rupee": "f156", 349 | "fa-inr": "f156", 350 | "fa-cny": "f157", 351 | "fa-rmb": "f157", 352 | "fa-yen": "f157", 353 | "fa-jpy": "f157", 354 | "fa-ruble": "f158", 355 | "fa-rouble": "f158", 356 | "fa-rub": "f158", 357 | "fa-won": "f159", 358 | "fa-krw": "f159", 359 | "fa-bitcoin": "f15a", 360 | "fa-btc": "f15a", 361 | "fa-file": "f15b", 362 | "fa-file-text": "f15c", 363 | "fa-sort-alpha-asc": "f15d", 364 | "fa-sort-alpha-desc": "f15e", 365 | "fa-sort-amount-asc": "f160", 366 | "fa-sort-amount-desc": "f161", 367 | "fa-sort-numeric-asc": "f162", 368 | "fa-sort-numeric-desc": "f163", 369 | "fa-thumbs-up": "f164", 370 | "fa-thumbs-down": "f165", 371 | "fa-youtube-square": "f166", 372 | "fa-youtube": "f167", 373 | "fa-xing": "f168", 374 | "fa-xing-square": "f169", 375 | "fa-youtube-play": "f16a", 376 | "fa-dropbox": "f16b", 377 | "fa-stack-overflow": "f16c", 378 | "fa-instagram": "f16d", 379 | "fa-flickr": "f16e", 380 | "fa-adn": "f170", 381 | "fa-bitbucket": "f171", 382 | "fa-bitbucket-square": "f172", 383 | "fa-tumblr": "f173", 384 | "fa-tumblr-square": "f174", 385 | "fa-long-arrow-down": "f175", 386 | "fa-long-arrow-up": "f176", 387 | "fa-long-arrow-left": "f177", 388 | "fa-long-arrow-right": "f178", 389 | "fa-apple": "f179", 390 | "fa-windows": "f17a", 391 | "fa-android": "f17b", 392 | "fa-linux": "f17c", 393 | "fa-dribbble": "f17d", 394 | "fa-skype": "f17e", 395 | "fa-foursquare": "f180", 396 | "fa-trello": "f181", 397 | "fa-female": "f182", 398 | "fa-male": "f183", 399 | "fa-gittip": "f184", 400 | "fa-gratipay": "f184", 401 | "fa-sun-o": "f185", 402 | "fa-moon-o": "f186", 403 | "fa-archive": "f187", 404 | "fa-bug": "f188", 405 | "fa-vk": "f189", 406 | "fa-weibo": "f18a", 407 | "fa-renren": "f18b", 408 | "fa-pagelines": "f18c", 409 | "fa-stack-exchange": "f18d", 410 | "fa-arrow-circle-o-right": "f18e", 411 | "fa-arrow-circle-o-left": "f190", 412 | "fa-toggle-left": "f191", 413 | "fa-caret-square-o-left": "f191", 414 | "fa-dot-circle-o": "f192", 415 | "fa-wheelchair": "f193", 416 | "fa-vimeo-square": "f194", 417 | "fa-turkish-lira": "f195", 418 | "fa-try": "f195", 419 | "fa-plus-square-o": "f196", 420 | "fa-space-shuttle": "f197", 421 | "fa-slack": "f198", 422 | "fa-envelope-square": "f199", 423 | "fa-wordpress": "f19a", 424 | "fa-openid": "f19b", 425 | "fa-institution": "f19c", 426 | "fa-bank": "f19c", 427 | "fa-university": "f19c", 428 | "fa-mortar-board": "f19d", 429 | "fa-graduation-cap": "f19d", 430 | "fa-yahoo": "f19e", 431 | "fa-google": "f1a0", 432 | "fa-reddit": "f1a1", 433 | "fa-reddit-square": "f1a2", 434 | "fa-stumbleupon-circle": "f1a3", 435 | "fa-stumbleupon": "f1a4", 436 | "fa-delicious": "f1a5", 437 | "fa-digg": "f1a6", 438 | "fa-pied-piper": "f1a7", 439 | "fa-pied-piper-alt": "f1a8", 440 | "fa-drupal": "f1a9", 441 | "fa-joomla": "f1aa", 442 | "fa-language": "f1ab", 443 | "fa-fax": "f1ac", 444 | "fa-building": "f1ad", 445 | "fa-child": "f1ae", 446 | "fa-paw": "f1b0", 447 | "fa-spoon": "f1b1", 448 | "fa-cube": "f1b2", 449 | "fa-cubes": "f1b3", 450 | "fa-behance": "f1b4", 451 | "fa-behance-square": "f1b5", 452 | "fa-steam": "f1b6", 453 | "fa-steam-square": "f1b7", 454 | "fa-recycle": "f1b8", 455 | "fa-automobile": "f1b9", 456 | "fa-car": "f1b9", 457 | "fa-cab": "f1ba", 458 | "fa-taxi": "f1ba", 459 | "fa-tree": "f1bb", 460 | "fa-spotify": "f1bc", 461 | "fa-deviantart": "f1bd", 462 | "fa-soundcloud": "f1be", 463 | "fa-database": "f1c0", 464 | "fa-file-pdf-o": "f1c1", 465 | "fa-file-word-o": "f1c2", 466 | "fa-file-excel-o": "f1c3", 467 | "fa-file-powerpoint-o": "f1c4", 468 | "fa-file-photo-o": "f1c5", 469 | "fa-file-picture-o": "f1c5", 470 | "fa-file-image-o": "f1c5", 471 | "fa-file-zip-o": "f1c6", 472 | "fa-file-archive-o": "f1c6", 473 | "fa-file-sound-o": "f1c7", 474 | "fa-file-audio-o": "f1c7", 475 | "fa-file-movie-o": "f1c8", 476 | "fa-file-video-o": "f1c8", 477 | "fa-file-code-o": "f1c9", 478 | "fa-vine": "f1ca", 479 | "fa-codepen": "f1cb", 480 | "fa-jsfiddle": "f1cc", 481 | "fa-life-bouy": "f1cd", 482 | "fa-life-buoy": "f1cd", 483 | "fa-life-saver": "f1cd", 484 | "fa-support": "f1cd", 485 | "fa-life-ring": "f1cd", 486 | "fa-circle-o-notch": "f1ce", 487 | "fa-ra": "f1d0", 488 | "fa-rebel": "f1d0", 489 | "fa-ge": "f1d1", 490 | "fa-empire": "f1d1", 491 | "fa-git-square": "f1d2", 492 | "fa-git": "f1d3", 493 | "fa-hacker-news": "f1d4", 494 | "fa-tencent-weibo": "f1d5", 495 | "fa-qq": "f1d6", 496 | "fa-wechat": "f1d7", 497 | "fa-weixin": "f1d7", 498 | "fa-send": "f1d8", 499 | "fa-paper-plane": "f1d8", 500 | "fa-send-o": "f1d9", 501 | "fa-paper-plane-o": "f1d9", 502 | "fa-history": "f1da", 503 | "fa-genderless": "f1db", 504 | "fa-circle-thin": "f1db", 505 | "fa-header": "f1dc", 506 | "fa-paragraph": "f1dd", 507 | "fa-sliders": "f1de", 508 | "fa-share-alt": "f1e0", 509 | "fa-share-alt-square": "f1e1", 510 | "fa-bomb": "f1e2", 511 | "fa-soccer-ball-o": "f1e3", 512 | "fa-futbol-o": "f1e3", 513 | "fa-tty": "f1e4", 514 | "fa-binoculars": "f1e5", 515 | "fa-plug": "f1e6", 516 | "fa-slideshare": "f1e7", 517 | "fa-twitch": "f1e8", 518 | "fa-yelp": "f1e9", 519 | "fa-newspaper-o": "f1ea", 520 | "fa-wifi": "f1eb", 521 | "fa-calculator": "f1ec", 522 | "fa-paypal": "f1ed", 523 | "fa-google-wallet": "f1ee", 524 | "fa-cc-visa": "f1f0", 525 | "fa-cc-mastercard": "f1f1", 526 | "fa-cc-discover": "f1f2", 527 | "fa-cc-amex": "f1f3", 528 | "fa-cc-paypal": "f1f4", 529 | "fa-cc-stripe": "f1f5", 530 | "fa-bell-slash": "f1f6", 531 | "fa-bell-slash-o": "f1f7", 532 | "fa-trash": "f1f8", 533 | "fa-copyright": "f1f9", 534 | "fa-at": "f1fa", 535 | "fa-eyedropper": "f1fb", 536 | "fa-paint-brush": "f1fc", 537 | "fa-birthday-cake": "f1fd", 538 | "fa-area-chart": "f1fe", 539 | "fa-pie-chart": "f200", 540 | "fa-line-chart": "f201", 541 | "fa-lastfm": "f202", 542 | "fa-lastfm-square": "f203", 543 | "fa-toggle-off": "f204", 544 | "fa-toggle-on": "f205", 545 | "fa-bicycle": "f206", 546 | "fa-bus": "f207", 547 | "fa-ioxhost": "f208", 548 | "fa-angellist": "f209", 549 | "fa-cc": "f20a", 550 | "fa-shekel": "f20b", 551 | "fa-sheqel": "f20b", 552 | "fa-ils": "f20b", 553 | "fa-meanpath": "f20c", 554 | "fa-buysellads": "f20d", 555 | "fa-connectdevelop": "f20e", 556 | "fa-dashcube": "f210", 557 | "fa-forumbee": "f211", 558 | "fa-leanpub": "f212", 559 | "fa-sellsy": "f213", 560 | "fa-shirtsinbulk": "f214", 561 | "fa-simplybuilt": "f215", 562 | "fa-skyatlas": "f216", 563 | "fa-cart-plus": "f217", 564 | "fa-cart-arrow-down": "f218", 565 | "fa-diamond": "f219", 566 | "fa-ship": "f21a", 567 | "fa-user-secret": "f21b", 568 | "fa-motorcycle": "f21c", 569 | "fa-street-view": "f21d", 570 | "fa-heartbeat": "f21e", 571 | "fa-venus": "f221", 572 | "fa-mars": "f222", 573 | "fa-mercury": "f223", 574 | "fa-transgender": "f224", 575 | "fa-transgender-alt": "f225", 576 | "fa-venus-double": "f226", 577 | "fa-mars-double": "f227", 578 | "fa-venus-mars": "f228", 579 | "fa-mars-stroke": "f229", 580 | "fa-mars-stroke-v": "f22a", 581 | "fa-mars-stroke-h": "f22b", 582 | "fa-neuter": "f22c", 583 | "fa-facebook-official": "f230", 584 | "fa-pinterest-p": "f231", 585 | "fa-whatsapp": "f232", 586 | "fa-server": "f233", 587 | "fa-user-plus": "f234", 588 | "fa-user-times": "f235", 589 | "fa-hotel": "f236", 590 | "fa-bed": "f236", 591 | "fa-viacoin": "f237", 592 | "fa-train": "f238", 593 | "fa-subway": "f239", 594 | "fa-medium": "f23a" 595 | } 596 | -------------------------------------------------------------------------------- /fonts/kbd-webfont2unicode.json: -------------------------------------------------------------------------------- 1 | { 2 | "kb-Multimedia-Mute-2": "e85e", 3 | "kb-Multimedia-Down": "e86d", 4 | "kb-logo-commodore": "e605", 5 | "kb-Unicode-DeleteRight-Small": "e81b", 6 | "kb-Multimedia-Rewind": "e866", 7 | "kb-Line-Start": "e853", 8 | "kb-Arrows-Right": "e84f", 9 | "kb-Multimedia-Eject": "e85c", 10 | "kb-Unicode-BackSpace-DeleteLeft-Big": "e819", 11 | "kb-logo-linux-tux-ibm": "e60a", 12 | "kb-Return-1": "e815", 13 | "kb-Unicode-Lock-Open-2": "e82f", 14 | "kb-Unicode-DeleteRight-Big": "e81a", 15 | "kb-Arrows-Up": "e84e", 16 | "kb-logo-ubuntu_cof-circle": "e608", 17 | "kb-logo-linux-archlinux": "e60d", 18 | "kb-logo-linux-knoppix": "e616", 19 | "kb-Unicode-Control-1": "e810", 20 | "kb-Unicode-Clock": "e82b", 21 | "kb-Hamburger-Menu": "e827", 22 | "kb-logo-linux-redhat": "e617", 23 | "kb-Multimedia-Rewind-Start": "e868", 24 | "kb-logo-gnu": "e615", 25 | "kb-logo-android": "e619", 26 | "kb-Arrows-Bottom-2": "e844", 27 | "kb-Symbol-Peace": "e8a1", 28 | "kb-logo-linux-tux": "e609", 29 | "kb-Unicode-Pause-2": "e803", 30 | "kb-Arrows-Up-Right": "e852", 31 | "kb-Undo-3": "e837", 32 | "kb-Multimedia-FastForward-End": "e867", 33 | "kb-Unicode-Stopwatch": "e82a", 34 | "kb-1-Round-Filled-2": "e820", 35 | "kb-batman": "e704", 36 | "kb-Search-1": "e830", 37 | "kb-Symbol-Skull-Bones-1": "e8a3", 38 | "kb-Multimedia-Pause": "e86e", 39 | "kb-logo-linux-fedora": "e613", 40 | "kb-community-hapster": "e700", 41 | "kb-Unicode-BackSpace-DeleteLeft-Small": "e81c", 42 | "kb-Unicode-Enter-1": "e813", 43 | "kb-Unicode-Lock-Closed-2": "e82e", 44 | "kb-Unicode-Page-Up-2": "e848", 45 | "kb-Unicode-Alternate-2": "e80b", 46 | "kb-Unicode-Page-Up-3": "e849", 47 | "kb-Undo-1": "e835", 48 | "kb-A-Round-Filled-SanSerif": "e823", 49 | "kb-Arrows-Top-3": "e841", 50 | "kb-community-awesome": "e701", 51 | "kb-Unicode-Page-Down-3": "e84c", 52 | "kb-Unicode-Option-1": "e80c", 53 | "kb-Multimedia-FastForwar": "e865", 54 | "kb-Unicode-Screen-Dim": "e83d", 55 | "kb-Unicode-ClearScreen-1": "e808", 56 | "kb-Unicode-Page-Down-2": "e84b", 57 | "kb-Scissors-2": "e833", 58 | "kb-Arrows-Bottom-3": "e845", 59 | "kb-Unicode-Alternate-1": "e80a", 60 | "kb-logo-apple": "e602", 61 | "kb-logo-winlin-cygwin": "e610", 62 | "kb-Arrows-Down-Circle-Filled": "e85b", 63 | "kb-Unicode-Insert-1": "e81d", 64 | "kb-Symbol-Ankh": "e8a0", 65 | "kb-Multimedia-Up": "e86c", 66 | "kb-Unicode-PrintScreen-1": "e806", 67 | "kb-Undo-2": "e836", 68 | "kb-Multimedia-Volume-Down-2": "e862", 69 | "kb-logo-vim": "e604", 70 | "kb-Arrows-Left": "e84d", 71 | "kb-logo-windows-8": "e601", 72 | "kb-Unicode-Break-2": "e805", 73 | "kb-Multimedia-Back": "e86a", 74 | "kb-Multimedia-Play-Pause": "e869", 75 | "kb-Return-4": "e818", 76 | "kb-Search-2": "e831", 77 | "kb-logo-linux-tux-ibm-invert": "e60b", 78 | "kb-Multimedia-Mute-3": "e85f", 79 | "kb-Unicode-Pause-1": "e802", 80 | "kb-logo-linux-debian": "e611", 81 | "kb-logo-ubuntu_cof": "e607", 82 | "kb-Multimedia-Record": "e870", 83 | "kb-Unicode-Scroll-2": "e83a", 84 | "kb-Line-End": "e854", 85 | "kb-logo-bsd-freebsd": "e60e", 86 | "kb-Multimedia-Mute-4": "e860", 87 | "kb-Unicode-Escape-1": "e800", 88 | "kb-Arrows-Up-Circle-Filled": "e859", 89 | "kb-Unicode-Insert-2": "e81e", 90 | "kb-Unicode-Decimal-Separator-1": "e83b", 91 | "kb-Unicode-Option-2": "e80d", 92 | "kb-Return-3": "e817", 93 | "kb-Unicode-Lock-Open-1": "e82d", 94 | "kb-community-awesome-invert": "e702", 95 | "kb-logo-linux-gentoo": "e614", 96 | "kb-A-Round-SanSerif": "e826", 97 | "kb-Arrows-Bottom-1": "e843", 98 | "kb-Arrows-Up-Left": "e851", 99 | "kb-logo-atari": "e60c", 100 | "kb-Multimedia-Volume-Down-1": "e861", 101 | "kb-Arrows-Left-Circle-Filled": "e858", 102 | "kb-logo-linux-opensuse": "e618", 103 | "kb-Unicode-Enter-2": "e814", 104 | "kb-Unicode-Lock-Closed-1": "e82c", 105 | "kb-Unicode-Command-3": "e80f", 106 | "kb-logo-linux-centos": "e60f", 107 | "kb-A-Round-Filled-Serif": "e822", 108 | "kb-Arrows-Right-Circle-Filled": "e85a", 109 | "kb-logo-linux-edubuntu": "e612", 110 | "kb-1-Round": "e821", 111 | "kb-Arrows-Bottom-4": "e846", 112 | "kb-Unicode-Scroll-1": "e839", 113 | "kb-Scissors-1": "e832", 114 | "kb-Unicode-Page-Up-1": "e847", 115 | "kb-Tab-2": "e857", 116 | "kb-Arrows-Top-4": "e842", 117 | "kb-1-Round-Filled-1": "e81f", 118 | "kb-Arrows-Down": "e850", 119 | "kb-Unicode-Hourglass-1": "e828", 120 | "kb-Unicode-PrintScreen-2": "e807", 121 | "kb-Symbol-YinYang": "e8a6", 122 | "kb-Unicode-Escape-2": "e801", 123 | "kb-Multimedia-Volume-Up-2": "e864", 124 | "kb-Arrows-Top-1": "e83f", 125 | "kb-Symbol-Alien": "e8a2", 126 | "kb-Multimedia-Mute-1": "e85d", 127 | "kb-Multimedia-Volume-Up-1": "e863", 128 | "kb-Unicode-Hourglass-2": "e829", 129 | "kb-Unicode-Decimal-Separator-2": "e83c", 130 | "kb-Unicode-Screen-Bright": "e83e", 131 | "kb-Tab-1": "e856", 132 | "kb-Unicode-Control-3": "e812", 133 | "kb-copyleft": "e703", 134 | "kb-Symbol-Keyboard": "e8a5", 135 | "kb-Multimedia-Stop": "e86f", 136 | "kb-Return-2": "e816", 137 | "kb-Unicode-Break-1": "e804", 138 | "kb-Multimedia-Play": "e86b", 139 | "kb-logo-amiga": "e606", 140 | "kb-A-Square-Filled-SanSerif": "e825", 141 | "kb-logo-windows-7": "e600", 142 | "kb-Arrows-Top-2": "e840", 143 | "kb-A-Square-Filled-Serif": "e824", 144 | "kb-Unicode-Control-2": "e811", 145 | "kb-Unicode-Command-1": "e80e", 146 | "kb-Redo-1": "e838", 147 | "kb-Scissors-3": "e834", 148 | "kb-Unicode-Page-Down-1": "e84a", 149 | "kb-Unicode-ClearScreen-2": "e809", 150 | "kb-Symbol-Skull-Bones-2": "e8a4", 151 | "kb-logo-apple-outline": "e603", 152 | "kb-Line-Start-End": "e855" 153 | } 154 | -------------------------------------------------------------------------------- /help/help1.md: -------------------------------------------------------------------------------- 1 | # How To Get a Gist URL 2 | 3 | ![Step 1: Sign in to Keyboard Layout Editor with Github](help1_1.png) 4 | 5 | ![Step 2: Save your layout to a Gist](help1_2.png) 6 | 7 | ![Step 3: Copy your new URL](help1_3.png) 8 | -------------------------------------------------------------------------------- /help/help1_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CQCumbers/kle_render/a14ccea8e46286ba01c10e3380eca0a963e21d5d/help/help1_1.png -------------------------------------------------------------------------------- /help/help1_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CQCumbers/kle_render/a14ccea8e46286ba01c10e3380eca0a963e21d5d/help/help1_2.png -------------------------------------------------------------------------------- /help/help1_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CQCumbers/kle_render/a14ccea8e46286ba01c10e3380eca0a963e21d5d/help/help1_3.png -------------------------------------------------------------------------------- /help/help2.md: -------------------------------------------------------------------------------- 1 | # How To Download JSON 2 | 3 | ![Step 1: Click on the "Raw Data" tab](help2_1.png) 4 | 5 | ![Step 2: Download the JSON file](help2_2.png) 6 | -------------------------------------------------------------------------------- /help/help2_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CQCumbers/kle_render/a14ccea8e46286ba01c10e3380eca0a963e21d5d/help/help2_1.png -------------------------------------------------------------------------------- /help/help2_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CQCumbers/kle_render/a14ccea8e46286ba01c10e3380eca0a963e21d5d/help/help2_2.png -------------------------------------------------------------------------------- /images/GMK_BASE1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CQCumbers/kle_render/a14ccea8e46286ba01c10e3380eca0a963e21d5d/images/GMK_BASE1.png -------------------------------------------------------------------------------- /images/GMK_BASE2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CQCumbers/kle_render/a14ccea8e46286ba01c10e3380eca0a963e21d5d/images/GMK_BASE2.png -------------------------------------------------------------------------------- /images/GMK_BASE3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CQCumbers/kle_render/a14ccea8e46286ba01c10e3380eca0a963e21d5d/images/GMK_BASE3.png -------------------------------------------------------------------------------- /images/GMK_BASE4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CQCumbers/kle_render/a14ccea8e46286ba01c10e3380eca0a963e21d5d/images/GMK_BASE4.png -------------------------------------------------------------------------------- /images/GMK_BASE5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CQCumbers/kle_render/a14ccea8e46286ba01c10e3380eca0a963e21d5d/images/GMK_BASE5.png -------------------------------------------------------------------------------- /images/GMK_BIGENTER1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CQCumbers/kle_render/a14ccea8e46286ba01c10e3380eca0a963e21d5d/images/GMK_BIGENTER1.png -------------------------------------------------------------------------------- /images/GMK_BIGENTER2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CQCumbers/kle_render/a14ccea8e46286ba01c10e3380eca0a963e21d5d/images/GMK_BIGENTER2.png -------------------------------------------------------------------------------- /images/GMK_BIGENTER3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CQCumbers/kle_render/a14ccea8e46286ba01c10e3380eca0a963e21d5d/images/GMK_BIGENTER3.png -------------------------------------------------------------------------------- /images/GMK_BIGENTER4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CQCumbers/kle_render/a14ccea8e46286ba01c10e3380eca0a963e21d5d/images/GMK_BIGENTER4.png -------------------------------------------------------------------------------- /images/GMK_BIGENTER5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CQCumbers/kle_render/a14ccea8e46286ba01c10e3380eca0a963e21d5d/images/GMK_BIGENTER5.png -------------------------------------------------------------------------------- /images/GMK_ISO1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CQCumbers/kle_render/a14ccea8e46286ba01c10e3380eca0a963e21d5d/images/GMK_ISO1.png -------------------------------------------------------------------------------- /images/GMK_ISO2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CQCumbers/kle_render/a14ccea8e46286ba01c10e3380eca0a963e21d5d/images/GMK_ISO2.png -------------------------------------------------------------------------------- /images/GMK_ISO3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CQCumbers/kle_render/a14ccea8e46286ba01c10e3380eca0a963e21d5d/images/GMK_ISO3.png -------------------------------------------------------------------------------- /images/GMK_ISO4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CQCumbers/kle_render/a14ccea8e46286ba01c10e3380eca0a963e21d5d/images/GMK_ISO4.png -------------------------------------------------------------------------------- /images/GMK_ISO5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CQCumbers/kle_render/a14ccea8e46286ba01c10e3380eca0a963e21d5d/images/GMK_ISO5.png -------------------------------------------------------------------------------- /images/GMK_SPACE1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CQCumbers/kle_render/a14ccea8e46286ba01c10e3380eca0a963e21d5d/images/GMK_SPACE1.png -------------------------------------------------------------------------------- /images/GMK_SPACE2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CQCumbers/kle_render/a14ccea8e46286ba01c10e3380eca0a963e21d5d/images/GMK_SPACE2.png -------------------------------------------------------------------------------- /images/GMK_SPACE3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CQCumbers/kle_render/a14ccea8e46286ba01c10e3380eca0a963e21d5d/images/GMK_SPACE3.png -------------------------------------------------------------------------------- /images/GMK_SPACE4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CQCumbers/kle_render/a14ccea8e46286ba01c10e3380eca0a963e21d5d/images/GMK_SPACE4.png -------------------------------------------------------------------------------- /images/GMK_SPACE5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CQCumbers/kle_render/a14ccea8e46286ba01c10e3380eca0a963e21d5d/images/GMK_SPACE5.png -------------------------------------------------------------------------------- /images/GMK_STEP1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CQCumbers/kle_render/a14ccea8e46286ba01c10e3380eca0a963e21d5d/images/GMK_STEP1.png -------------------------------------------------------------------------------- /images/GMK_STEP2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CQCumbers/kle_render/a14ccea8e46286ba01c10e3380eca0a963e21d5d/images/GMK_STEP2.png -------------------------------------------------------------------------------- /images/GMK_STEP3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CQCumbers/kle_render/a14ccea8e46286ba01c10e3380eca0a963e21d5d/images/GMK_STEP3.png -------------------------------------------------------------------------------- /images/GMK_STEP4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CQCumbers/kle_render/a14ccea8e46286ba01c10e3380eca0a963e21d5d/images/GMK_STEP4.png -------------------------------------------------------------------------------- /images/GMK_STEP5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CQCumbers/kle_render/a14ccea8e46286ba01c10e3380eca0a963e21d5d/images/GMK_STEP5.png -------------------------------------------------------------------------------- /images/SA_BASE1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CQCumbers/kle_render/a14ccea8e46286ba01c10e3380eca0a963e21d5d/images/SA_BASE1.png -------------------------------------------------------------------------------- /images/SA_BASE2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CQCumbers/kle_render/a14ccea8e46286ba01c10e3380eca0a963e21d5d/images/SA_BASE2.png -------------------------------------------------------------------------------- /images/SA_BASE3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CQCumbers/kle_render/a14ccea8e46286ba01c10e3380eca0a963e21d5d/images/SA_BASE3.png -------------------------------------------------------------------------------- /images/SA_BASE4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CQCumbers/kle_render/a14ccea8e46286ba01c10e3380eca0a963e21d5d/images/SA_BASE4.png -------------------------------------------------------------------------------- /images/SA_BASE5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CQCumbers/kle_render/a14ccea8e46286ba01c10e3380eca0a963e21d5d/images/SA_BASE5.png -------------------------------------------------------------------------------- /images/SA_BIGENTER1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CQCumbers/kle_render/a14ccea8e46286ba01c10e3380eca0a963e21d5d/images/SA_BIGENTER1.png -------------------------------------------------------------------------------- /images/SA_BIGENTER2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CQCumbers/kle_render/a14ccea8e46286ba01c10e3380eca0a963e21d5d/images/SA_BIGENTER2.png -------------------------------------------------------------------------------- /images/SA_BIGENTER3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CQCumbers/kle_render/a14ccea8e46286ba01c10e3380eca0a963e21d5d/images/SA_BIGENTER3.png -------------------------------------------------------------------------------- /images/SA_BIGENTER4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CQCumbers/kle_render/a14ccea8e46286ba01c10e3380eca0a963e21d5d/images/SA_BIGENTER4.png -------------------------------------------------------------------------------- /images/SA_BIGENTER5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CQCumbers/kle_render/a14ccea8e46286ba01c10e3380eca0a963e21d5d/images/SA_BIGENTER5.png -------------------------------------------------------------------------------- /images/SA_ISO1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CQCumbers/kle_render/a14ccea8e46286ba01c10e3380eca0a963e21d5d/images/SA_ISO1.png -------------------------------------------------------------------------------- /images/SA_ISO2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CQCumbers/kle_render/a14ccea8e46286ba01c10e3380eca0a963e21d5d/images/SA_ISO2.png -------------------------------------------------------------------------------- /images/SA_ISO3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CQCumbers/kle_render/a14ccea8e46286ba01c10e3380eca0a963e21d5d/images/SA_ISO3.png -------------------------------------------------------------------------------- /images/SA_ISO4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CQCumbers/kle_render/a14ccea8e46286ba01c10e3380eca0a963e21d5d/images/SA_ISO4.png -------------------------------------------------------------------------------- /images/SA_ISO5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CQCumbers/kle_render/a14ccea8e46286ba01c10e3380eca0a963e21d5d/images/SA_ISO5.png -------------------------------------------------------------------------------- /images/SA_SPACE1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CQCumbers/kle_render/a14ccea8e46286ba01c10e3380eca0a963e21d5d/images/SA_SPACE1.png -------------------------------------------------------------------------------- /images/SA_SPACE2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CQCumbers/kle_render/a14ccea8e46286ba01c10e3380eca0a963e21d5d/images/SA_SPACE2.png -------------------------------------------------------------------------------- /images/SA_SPACE3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CQCumbers/kle_render/a14ccea8e46286ba01c10e3380eca0a963e21d5d/images/SA_SPACE3.png -------------------------------------------------------------------------------- /images/SA_SPACE4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CQCumbers/kle_render/a14ccea8e46286ba01c10e3380eca0a963e21d5d/images/SA_SPACE4.png -------------------------------------------------------------------------------- /images/SA_SPACE5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CQCumbers/kle_render/a14ccea8e46286ba01c10e3380eca0a963e21d5d/images/SA_SPACE5.png -------------------------------------------------------------------------------- /images/SA_STEP1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CQCumbers/kle_render/a14ccea8e46286ba01c10e3380eca0a963e21d5d/images/SA_STEP1.png -------------------------------------------------------------------------------- /images/SA_STEP2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CQCumbers/kle_render/a14ccea8e46286ba01c10e3380eca0a963e21d5d/images/SA_STEP2.png -------------------------------------------------------------------------------- /images/SA_STEP3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CQCumbers/kle_render/a14ccea8e46286ba01c10e3380eca0a963e21d5d/images/SA_STEP3.png -------------------------------------------------------------------------------- /images/SA_STEP4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CQCumbers/kle_render/a14ccea8e46286ba01c10e3380eca0a963e21d5d/images/SA_STEP4.png -------------------------------------------------------------------------------- /images/SA_STEP5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CQCumbers/kle_render/a14ccea8e46286ba01c10e3380eca0a963e21d5d/images/SA_STEP5.png -------------------------------------------------------------------------------- /images/script.py: -------------------------------------------------------------------------------- 1 | from PIL import Image, ImageDraw 2 | import os 3 | 4 | u = 200 5 | transparent_area = (0,u,int(0.25*u),int(2*u)) 6 | # transparent_area = (0, 0, int(0.75*u), u) 7 | 8 | for fn in os.listdir('.'): 9 | # if os.path.isfile(fn) and fn.startswith('GMK_ISO'): 10 | # if os.path.isfile(fn) and fn.startswith('GMK_BIGENTER'): 11 | if os.path.isfile(fn) and fn.endswith('crunch.png'): 12 | os.rename(fn, fn[:-11]+'.png') 13 | 14 | # im = Image.open(fn) 15 | # mask=Image.new('L', im.size, color=255) 16 | # draw=ImageDraw.Draw(mask) 17 | # draw.rectangle(transparent_area, fill=0) 18 | # im.putalpha(mask) 19 | # im.save(fn) 20 | -------------------------------------------------------------------------------- /key.py: -------------------------------------------------------------------------------- 1 | import functools, io, math, requests 2 | from PIL import Image, ImageMath, ImageColor, ImageCms 3 | from PIL import ImageDraw, ImageFont, ImageOps 4 | from colormath import color_objects, color_conversions 5 | 6 | 7 | class Key: 8 | __slots__ = [ 9 | 'x', 'y', 'width', 'height', 'x2', 'y2', 'width2', 'height2', 10 | 'rotation_angle', 'rotation_x', 'rotation_y', 'fonts', 11 | 'res', 'flat', 'str_profile', 'decal', 'step', 'ghost', 'pic', 'color', 12 | 'align', 'labels', 'label_sizes', 'label_colors', 'model_res', 13 | ] 14 | 15 | 16 | def __init__(self): 17 | self.x = self.y = 0.0 18 | self.width = self.height = 1.0 19 | self.x2 = self.y2 = 0.0 20 | self.width2 = self.height2 = 0.0 21 | 22 | self.rotation_angle = 0.0 23 | self.rotation_x = self.rotation_y = 0.0 24 | 25 | self.res = 200 26 | self.str_profile = 'GMK' 27 | self.fonts = [None] * 12 28 | self.flat = self.decal = self.step = False 29 | self.ghost = self.pic = False 30 | self.color = '#EEEEEE' 31 | 32 | self.align = None 33 | self.labels = [] 34 | self.label_sizes = [3.0] * 12 35 | self.label_colors = ['#000000'] * 12 36 | self.model_res = 0.01905 37 | 38 | 39 | @functools.lru_cache() 40 | def get_full_profile(self): 41 | # only GMK and SA base images 42 | full_profile = self.str_profile.upper().split(' ') 43 | profile = 'SA' if full_profile[0] in ('SA', 'DSA') else 'GMK' 44 | 45 | # row profile used to specify keys with special base images 46 | props = (self.width, self.height, self.x2, self.y2, self.width2, self.height2) 47 | special_keys = { 48 | (1.5, 1.0, 0.25, 0.0, 1.25, 2.0): 'ISO', 49 | (1.25, 2.0, -0.25, 0.0, 1.5, 1.0): 'ISO', 50 | (1.5, 2.0, -0.75, 1.0, 2.25, 1.0): 'BIGENTER' 51 | } 52 | if props in special_keys: 53 | row_profile = special_keys[props] 54 | elif self.step and self.height == self.height2: 55 | row_profile = 'STEP' 56 | elif (self.width >= 6.0 and self.height == 1.0) or (full_profile[-1] == 'SPACE'): 57 | row_profile = 'SPACE' 58 | else: 59 | row_profile = 'BASE' 60 | 61 | return (profile, row_profile) 62 | 63 | 64 | @functools.lru_cache() 65 | def get_font(self, i, size, symbol): 66 | path = 'fonts/{}_font.ttf'.format(self.get_full_profile()[0]) 67 | if symbol: return ImageFont.truetype(path, size) 68 | try: return ImageFont.truetype(io.BytesIO(self.fonts[i]), size) 69 | except Exception: return ImageFont.truetype(path, size) 70 | 71 | 72 | @functools.lru_cache() 73 | def get_base_color(self): 74 | # calculate perceptual gray of key color 75 | color = ImageColor.getrgb(self.color) 76 | bright = 0.3 * color[0] + 0.59 * color[1] + 0.11 * color[2] 77 | 78 | # get corresponding base image's average color 79 | if (bright > 0xB0): 80 | return 0xE0 # 224 81 | elif (bright > 0x80): 82 | return 0xB0 # 176 83 | elif (bright > 0x50): 84 | return 0x80 # 128 85 | elif (bright > 0x20): 86 | return 0x50 # 80 87 | else: 88 | return 0x20 # 32 89 | 90 | 91 | def get_label_props(self): 92 | if self.decal: 93 | props = {'margin_x': .22, 'margin_top': .2, 'margin_bottom': .2, 'line_spacing': .08} 94 | elif self.get_full_profile()[0] == 'GMK': 95 | props = {'margin_x': .22, 'margin_top': .14, 'margin_bottom': .34, 'line_spacing': .06} 96 | if len(self.labels) and self.label_sizes[0] == 8: props['margin_top'] -= 0.05; 97 | else: 98 | props = {'margin_x': .22, 'margin_top': .16, 'margin_bottom': .29, 'line_spacing': .08} 99 | props = {k: int(v * self.res) for k, v in props.items()} 100 | 101 | # center SA and decal labels if not explicitly aligned 102 | align = self.align if self.align else 0 103 | if self.align == None and (self.decal or self.get_full_profile()[0] != 'GMK'): 104 | align = 7 if len(self.labels) == 1 else 5 if len(self.labels) <= 3 else 0 105 | 106 | # calculate row/column and font size of each label 107 | pattern = [0, 8, 2, 6, 9, 7, 1, 10, 3, 4, 11, 5] 108 | center_front, center_row, center_col = [digit == '1' for digit in '{0:03b}'.format(align)] 109 | props.update({'font_sizes': [], 'positions': []}) 110 | for i in range(min(len(self.labels), 12)): 111 | row, col = (int(pattern.index(i) / 3), pattern.index(i) % 3) 112 | col = (1 if col < 1 else -1) if (center_col and row < 3) or (center_front and row > 2) else col 113 | row = (1 if row < 1 else -1) if (center_row and row < 3) else row 114 | label_size = self.label_sizes[i] if row != None and row < 3 else 3.0 115 | props['font_sizes'].append(int(.09 * self.res + .03 * self.res * label_size)) 116 | props['positions'].append((row, col)) 117 | 118 | return props 119 | 120 | 121 | def get_location(self, key_img): 122 | # get pixel location of key as (left, upper, right, lower) 123 | u = self.res 124 | x, y = min(self.x, self.x + self.x2), min(self.y, self.y + self.y2) 125 | 126 | if self.rotation_angle != 0 or self.rotation_x != 0 or self.rotation_y != 0: 127 | # center about which to rotate key 128 | rx, ry, a = self.rotation_x, self.rotation_y, math.radians(self.rotation_angle) 129 | x2, y2 = x * math.cos(a) - y * math.sin(a), y * math.cos(a) + x * math.sin(a) 130 | 131 | left, top = -self.width / 2, -self.height / 2 132 | left2, top2 = left * math.cos(a) - top * math.sin(a), top * math.cos(a) + left * math.sin(a) 133 | 134 | x, y = rx + x2 - key_img.width / u / 2 - left2, ry + y2 - key_img.height / u / 2 - top2 135 | return (int(i) for i in (x * u, y * u, x * u + key_img.width, y * u + key_img.height)) 136 | 137 | 138 | def get_model_location(self): 139 | # get bounding box of model in x/y plane 140 | x, y, res = min(self.x, self.x + self.x2), min(self.y, self.y + self.y2), self.model_res 141 | width = max(self.width2 + abs(self.x2), self.width) 142 | height = max(self.height2 + abs(self.y2), self.height) 143 | if self.rotation_angle != 0 or self.rotation_x != 0 or self.rotation_y != 0: 144 | rx, ry, a = self.rotation_x, self.rotation_y, math.radians(self.rotation_angle) 145 | x, y = rx + x * math.cos(a) - y * math.sin(a), ry + y * math.cos(a) + x * math.sin(a) 146 | width, height = width * math.cos(a) - height * math.sin(a), height * math.cos(a) + width * math.sin(a) 147 | return (x, y, x + width, y + height) 148 | 149 | 150 | def get_base_img(self, full_profile): 151 | if self.flat: 152 | res, color, row, sizes = self.res, self.color, full_profile[1], {'ISO': (1.5, 2), 'BIGENTER': (2.25, 2)} 153 | return Image.new('RGBA', [int(res * x) for x in sizes.get(row, (1, 1))], color=ImageColor.getrgb(color)) 154 | return open_base_img(full_profile, self.res, self.get_base_color(), self.color) 155 | 156 | 157 | def get_base_model(self, full_profile, scene): 158 | return copy_model('{0}_{1}'.format(*full_profile), scene) 159 | 160 | 161 | def stretch_img(self, base_img, width, height): 162 | w, h = base_img.size 163 | new_img = Image.new('RGBA', (width, height)) 164 | new_img.paste(base_img, (0, 0, base_img.width, base_img.height)) 165 | 166 | # stretch or crop base image horizontally 167 | if width > w: 168 | center_part = base_img.crop((int(w / 2), 0, int(w / 2) + 10, h)) 169 | right_part = base_img.crop((int(w / 2) + 1, 0, w, h)) 170 | for i in range(1, width - w + 1, 10): 171 | new_img.paste(center_part, (int(w / 2) + i, 0, int(w / 2) + i + 10, h)) 172 | new_img.paste(right_part, (width - right_part.width, 0, width, h)) 173 | elif width < w: 174 | right_part = base_img.crop((w - int(width / 2), 0, w, h)) 175 | new_img.paste(right_part, (width - right_part.width, 0, width, h)) 176 | 177 | # stretch or crop base image vertically 178 | if height > h: 179 | middle_part = new_img.crop((0, int(h / 2), width, int(h / 2) + 10)) 180 | bottom_part = new_img.crop((0, int(h / 2) + 1, width, h)) 181 | for i in range(1, height - h + 1, 10): 182 | new_img.paste(middle_part, (0, int(h / 2) + i, new_img.width, int(h / 2) + i + 10)) 183 | new_img.paste(bottom_part, (0, height - bottom_part.height, width, height)) 184 | elif height < h: 185 | bottom_part = new_img.crop((0, h - int(height / 2), width, h)) 186 | new_img.paste(bottom_part, (0, height - bottom_part.height, width, height)) 187 | 188 | return new_img 189 | 190 | 191 | def stretch_model(self, model, width, height): 192 | if width > 1: 193 | # shift right section right 194 | for v in model.data.vertices: 195 | v.co[0] -= (width - 1) * self.model_res if v.co[0] < -self.model_res / 2 else 0 196 | elif width < 1: 197 | # keep left section, compress middle section, shift right section left 198 | res, mid = self.model_res, width * self.model_res / 2 199 | for v in model.data.vertices: 200 | v.co[0] = v.co[0] if v.co[0] > -mid else (-mid if v.co[0] > -res + mid else v.co[0] + res - mid * 2) 201 | for p in model.data.polygons: p.use_smooth = False 202 | 203 | if height > 1: 204 | # shift bottom section down 205 | for v in model.data.vertices: 206 | v.co[1] += (height - 1) * self.model_res if v.co[1] > self.model_res / 2 else 0 207 | elif height < 1: 208 | # keep top section, compress middle section, shift bottom section up 209 | res, mid = self.model_res, height * self.model_res / 2 210 | for v in model.data.vertices: 211 | v.co[1] = v.co[1] if v.co[1] < mid else (mid if v.co[1] < res - mid else v.co[1] - res + mid * 2) 212 | for p in model.data.polygons: p.use_smooth = False 213 | 214 | 215 | def create_key(self): 216 | profile, row_profile = self.get_full_profile() 217 | if self.decal: 218 | return Image.new('RGBA', (int(self.width * self.res + 1), int(self.height * self.res))) 219 | elif row_profile in ('ISO', 'BIGENTER'): 220 | return self.get_base_img((profile, row_profile)).copy() 221 | elif self.width2 == 0.0 and self.height2 == 0.0: 222 | base_img = self.get_base_img((profile, row_profile)) 223 | return self.stretch_img(base_img, int(self.width * self.res + 1), int(self.height * self.res)) 224 | else: 225 | # calculate total width of keycap 226 | u, x2, y2 = self.res, self.x2, self.y2 227 | width = max(self.width2 + x2, self.width) if x2 >= 0 else max(self.width - x2, self.width2) 228 | height = max(self.height2 + y2, self.height) if y2 >= 0 else max(self.height - y2, self.height2) 229 | # create touch surface 230 | key_img = Image.new('RGBA', (int(width * u + 1), int(height * u))) 231 | base_img = self.get_base_img((profile, 'BASE')) 232 | touch_surface = self.stretch_img(base_img, int(self.width * u + 1), int(self.height * u)) 233 | key_img.paste(touch_surface, (max(int(-x2 * u), 0), max(int(-y2 * u), 0))) 234 | 235 | if row_profile == 'STEP': 236 | overlap = int(0.3 * self.res) 237 | # add left step 238 | if x2 < 0: 239 | left_img = self.get_base_img((profile, row_profile)).copy().transpose(Image.FLIP_LEFT_RIGHT) 240 | left_step = self.stretch_img(left_img, int(-x2 * u + overlap + 1), int(height * u)) 241 | key_img.paste(left_step, (0, 0)) 242 | # add right step 243 | if max(-x2, 0) + self.width < width: 244 | right_img = self.get_base_img((profile, row_profile)) 245 | img_width = int((width - self.width - max(-x2, 0)) * u + overlap + 1) 246 | right_step = self.stretch_img(right_img, img_width, int(height * u)) 247 | img_x = int((max(-x2, 0) + self.width) * u - overlap) 248 | key_img.paste(right_step, (img_x, 0)) 249 | else: 250 | # handle arbitrary second surface 251 | extra_img = self.get_base_img((profile, row_profile)) 252 | extra_surface = self.stretch_img(extra_img, int(self.width2 * u + 1), int(self.height2 * u)) 253 | key_img.paste(extra_surface, (max(int(x2 * u), 0), max(int(y2 * u), 0))) 254 | 255 | return key_img 256 | 257 | 258 | def create_model(self, scene): 259 | profile, row_profile = self.get_full_profile() 260 | if self.str_profile.startswith('DSA'): profile = 'DSA' 261 | 262 | if self.decal: 263 | model = copy_model('DECAL', scene) 264 | self.stretch_model(model, self.width, self.height) 265 | return model 266 | elif row_profile in ('ISO', 'BIGENTER'): 267 | return self.get_base_model((profile, row_profile), scene) 268 | elif self.width2 == 0.0 and self.height2 == 0.0: 269 | model = self.get_base_model((profile, row_profile), scene) 270 | self.stretch_model(model, self.width, self.height) 271 | return model 272 | else: 273 | # calculate total width of keycap 274 | model_res, x2, y2 = self.model_res, self.x2, self.y2 275 | width = max(self.width2 + x2, self.width) if x2 >= 0 else max(self.width - x2, self.width2) 276 | height = max(self.height2 + y2, self.height) if y2 >= 0 else max(self.height - y2, self.height2) 277 | # create touch surface 278 | key_model = self.get_base_model((profile, 'BASE'), scene) 279 | self.stretch_model(key_model, self.width, self.height) 280 | # move touch surface mesh relative to object origin 281 | for v in key_model.data.vertices: 282 | v.co[0] -= max(-x2, 0) * model_res 283 | v.co[1] += max(-y2, 0) * model_res 284 | 285 | if row_profile == 'STEP': 286 | # add left step 287 | if x2 < 0: 288 | left_step = self.get_base_model((profile, row_profile), scene) 289 | for v in left_step.data.vertices: v.co[0] = -model_res * 0.97 - v.co[0] 290 | self.stretch_model(left_step, -x2 + 0.333, height) 291 | left_step.parent = key_model 292 | # add right step 293 | if max(-x2, 0) + self.width < width: 294 | right_step = self.get_base_model((profile, row_profile), scene) 295 | self.stretch_model(right_step, width - self.width - max(-x2, 0) + 0.333, height) 296 | right_step.location = (-(max(-x2, 0) + self.width - 0.333) * model_res, 0, 0) 297 | right_step.parent = key_model 298 | else: 299 | # handle arbitrary second surface 300 | extra_model = self.get_base_model((profile, row_profile), scene) 301 | self.stretch_model(extra_model, self.width2, self.height2) 302 | extra_model.location = (-max(x2 * model_res, 0), max(y2 * model_res, 0), 0) 303 | extra_model.parent = key_model 304 | 305 | return key_model 306 | 307 | 308 | def pic_key(self, key_img): 309 | try: 310 | props = self.get_label_props() 311 | width, height = int(self.width * self.res), int(self.height * self.res) 312 | size = (width - props['margin_x'] * 2, height - props['margin_top'] - props['margin_bottom']) 313 | with Image.open(requests.get(self.labels[0], stream=True).raw) as label_img: 314 | label_img = label_img.convert('RGBA') 315 | pic_img = ImageOps.pad(label_img, size, method=Image.BILINEAR) 316 | key_img.paste(pic_img, (props['margin_x'], props['margin_top']), mask=pic_img) 317 | return key_img 318 | except Exception: 319 | return key_img 320 | 321 | 322 | def label_key(self, key_img): 323 | # if blank, exit immediately 324 | if len(self.labels) < 1: return key_img 325 | if self.pic: return self.pic_key(key_img) 326 | 327 | props = self.get_label_props() 328 | width, height = int(self.width * self.res), int(self.height * self.res) 329 | x_offset, y_offset = max(int(-self.x2 * self.res), 0), max(int(-self.y2 * self.res), 0) 330 | col2x = [ 331 | lambda w: props['margin_x'] + x_offset, 332 | lambda w: (width - w) / 2 + x_offset, 333 | lambda w: width - props['margin_x'] - w + x_offset 334 | ] 335 | row2y = [ 336 | lambda h: props['margin_top'] + y_offset, 337 | lambda h: (height - props['margin_bottom'] + props['margin_top'] - h) / 2 + y_offset, 338 | lambda h: height - props['margin_bottom'] - h + y_offset, 339 | lambda h: props['margin_top'], 340 | ] 341 | aligns = ['left', 'center', 'right'] 342 | # seperate surface for front printed labels 343 | top_draw = ImageDraw.Draw(key_img) 344 | front_plane = Image.new('RGBA', (width, max(height - props['margin_bottom'] * 2, 1))) 345 | front_draw = ImageDraw.Draw(front_plane) 346 | 347 | for i in range(min(len(self.labels), 12)): 348 | (row, col), text = props['positions'][i], self.labels[i] 349 | if not text or row == None: continue 350 | 351 | # load font and calculate text dimensions 352 | symbol = any(0x2190 <= ord(c) <= 0x26ff for c in text) 353 | font = self.get_font(row * 3 + col, props['font_sizes'][i], symbol) 354 | text = break_text(text, font, width - props['margin_x'] * 2) if not self.decal else text 355 | text = text.upper() if self.get_full_profile()[0] != 'GMK' and not self.decal else text 356 | text_width, text_height = font.getsize_multiline(text, spacing=props['line_spacing']) 357 | # retrieve label color and lighten to simulate reflectivity 358 | color = ImageColor.getrgb(self.label_colors[i]) 359 | color = color if self.flat else tuple(band + 0x26 for band in color) 360 | 361 | # draw labels accordings to row/col of props 362 | (front_draw if row == 3 else top_draw).multiline_text( 363 | (col2x[col](text_width), row2y[row](text_height)), text, font=font, 364 | fill=color, spacing=props['line_spacing'], align=aligns[col] 365 | ) 366 | 367 | # compress front printed labels vertically 368 | front_plane = front_plane.resize((width, props['margin_bottom']), resample=Image.BILINEAR) 369 | key_img.paste(front_plane, (x_offset, height - props['margin_bottom'] + y_offset), mask=front_plane) 370 | return key_img 371 | 372 | 373 | def render(self, scale, flat): 374 | self.res, self.flat = int(self.res / scale), flat 375 | # create key, then tint key, then label key 376 | key_img = self.label_key(self.create_key()) 377 | if self.ghost: key_img.putalpha(Image.new('L', key_img.size, color=64)) 378 | if not flat: key_img = key_img.rotate(-self.rotation_angle, resample=Image.BILINEAR, expand=1) 379 | return key_img 380 | 381 | 382 | def model(self, scene): 383 | # create model, then rotate and place 384 | model = self.create_model(scene) 385 | location, res = self.get_model_location(), self.model_res 386 | model.rotation_euler[2] = math.radians(-self.rotation_angle) 387 | model.location[0] -= location[0] * res 388 | model.location[1] += location[1] * res * math.cos(model.rotation_euler[0]) 389 | model.location[2] += location[1] * res * math.sin(model.rotation_euler[0]) 390 | return model 391 | 392 | 393 | srgb_profile, lab_profile = ImageCms.createProfile('sRGB'), ImageCms.createProfile('LAB', colorTemp=5000) 394 | rgb2lab_transform = ImageCms.buildTransformFromOpenProfiles(srgb_profile, lab_profile, 'RGB', 'LAB') 395 | lab2rgb_transform = ImageCms.buildTransformFromOpenProfiles(lab_profile, srgb_profile, 'LAB', 'RGB') 396 | 397 | 398 | @functools.lru_cache() 399 | def open_base_img(full_profile, res, base_color, color): 400 | # get base image according to profile and perceptual gray of key color 401 | base_num = str([0xE0, 0xB0, 0x80, 0x50, 0x20].index(base_color) + 1) 402 | 403 | # open image and convert to Lab 404 | with Image.open('images/{0}_{1}{2}.png'.format(*full_profile, base_num)) as img: 405 | key_img = img.resize((int(s * res / 200) for s in img.size), resample=Image.BILINEAR).convert('RGBA') 406 | if full_profile[1] in ('ISO', 'BIGENTER'): alpha = key_img.split()[-1] 407 | l, a, b = ImageCms.applyTransform(key_img, rgb2lab_transform).split() 408 | 409 | # convert key color to Lab 410 | # a and b should be scaled by 128/100, but desaturation looks more natural 411 | rgb_color = color_objects.sRGBColor(*ImageColor.getrgb(color), is_upscaled=True) 412 | lab_color = color_conversions.convert_color(rgb_color, color_objects.LabColor) 413 | l1, a1, b1 = lab_color.get_value_tuple() 414 | l1, a1, b1 = int(l1 * 256 / 100), int(a1 + 128), int(b1 + 128) 415 | 416 | # change Lab of base image to match that of key color 417 | l = ImageMath.eval('convert(l + l1 - l_avg, "L")', l=l, l1=l1, l_avg=base_color) 418 | a = ImageMath.eval('convert(a + a1 - a, "L")', a=a, a1=a1) 419 | b = ImageMath.eval('convert(b + b1 - b, "L")', b=b, b1=b1) 420 | 421 | key_img = ImageCms.applyTransform(Image.merge('LAB', (l, a, b)), lab2rgb_transform).convert('RGBA') 422 | if full_profile[1] in ('ISO', 'BIGENTER'): key_img.putalpha(alpha) 423 | return key_img 424 | 425 | 426 | @functools.lru_cache() 427 | def break_text(text, font, limit): 428 | if not ' ' in text: return text 429 | words, lines = text.split(' '), [''] 430 | while words: 431 | word = words.pop(0) 432 | if font.getsize(lines[-1] + word)[0] + 1 < limit or len(lines[-1]) < 1: 433 | lines[-1] += word + ' ' 434 | else: 435 | lines.append(word + ' ') 436 | return '\n'.join([line[:-1] for line in lines]) 437 | 438 | 439 | def copy_model(name, scene): 440 | # duplicate object properties and data, link to scene 441 | original_model = scene.objects.get(name) 442 | model = original_model.copy() 443 | model.data = original_model.data.copy() 444 | scene.collection.objects.link(model) 445 | return model 446 | -------------------------------------------------------------------------------- /keyboard.py: -------------------------------------------------------------------------------- 1 | import copy, html, lxml.html, re, json, requests, tinycss2 2 | from PIL import Image, ImageColor, ImageDraw, ImageFont 3 | from key import Key 4 | 5 | 6 | class Keyboard: 7 | __slots__ = ['keys', 'keyboard', 'max_size', 'color'] 8 | 9 | 10 | def __init__(self, json): 11 | # parse keyboard-layout-editor JSON format 12 | data = deserialise(json) 13 | self.keys, self.color = data[0], ImageColor.getrgb(data[1]) 14 | self.keyboard = Image.new('RGB', (1000, 1000), color=self.color) 15 | self.max_size = (0, 0) 16 | 17 | 18 | def render(self): 19 | # choose size and scale of canvas depending on number of keys 20 | scale, border = min(int(len(self.keys) / 160 + 1), 5), 24 21 | 22 | # render each key 23 | for key in self.keys: self.render_key(key, scale, border) 24 | 25 | # watermark and crop the image 26 | self.max_size = [size + int(border / scale) for size in self.max_size] 27 | self.watermark_keyboard('Made with kle-render.herokuapp.com', scale) 28 | self.keyboard = self.keyboard.crop((0, 0, self.max_size[0], self.max_size[1])) 29 | return self.keyboard 30 | 31 | 32 | def render_key(self, key, scale, border): 33 | # render key and scale resulting image for subpixel accuracy 34 | key_img = key.render(scale, False) 35 | 36 | # paste in proper location and update max_size 37 | location = [coord + border for coord in key.get_location(key_img)] 38 | self.max_size = [max(location[2], self.max_size[0]), max(location[3], self.max_size[1])] 39 | self.expand_keyboard(scale) 40 | self.keyboard.paste(key_img, (location[0], location[1]), mask=key_img) 41 | 42 | 43 | def expand_keyboard(self, scale): 44 | if all(self.max_size[i] < self.keyboard.size[i] for i in range(2)): return 45 | new_size = tuple(int(size + 1000 / scale) for size in self.max_size) 46 | new_keyboard = Image.new('RGB', new_size, color=self.color) 47 | new_keyboard.paste(self.keyboard, (0, 0)) 48 | self.keyboard = new_keyboard 49 | 50 | 51 | def watermark_keyboard(self, text, scale): 52 | # config margin size and watermark colors 53 | margin = int(18 / scale) 54 | background_color = ImageColor.getrgb('#202020') 55 | text_color = ImageColor.getrgb('#E0E0E0') 56 | 57 | # calculate size of watermark 58 | draw = ImageDraw.Draw(self.keyboard) 59 | font = ImageFont.truetype('fonts/SA_font.ttf', int(36 / scale)) 60 | w, h = font.getsize(text) 61 | self.max_size = size = [max(w, self.max_size[0]), (self.max_size[1] + h + margin * 2)] 62 | 63 | # draw watermark bar below image 64 | draw.rectangle((0, size[1] - h - margin * 2, size[0] + 1, size[1] + 1), fill=background_color) 65 | draw.text((margin, size[1] - h - margin), text, font=font, fill=text_color) 66 | 67 | 68 | def get_labels(key, fa_subs, kb_subs): 69 | # split into labels for each part of key 70 | labels = key.split('\n') 71 | for i, label in enumerate(labels): 72 | tree = lxml.html.fragment_fromstring(label, create_parent=True) 73 | # set key.pic to true and make label url of image 74 | if tree.xpath('//img[1]/@src'): 75 | return (tree.xpath('//img[1]/@src'), True) 76 | 77 | # replace icons with unicode characters 78 | for fa_icon in tree.find_class('fa'): 79 | fa_class = re.search(r'fa-\S+', fa_icon.get('class')) 80 | if fa_class and fa_class.group(0) in fa_subs: 81 | fa_icon.text = chr(int(fa_subs[fa_class.group(0)], 16)) 82 | for kb_icon in tree.find_class('kb'): 83 | kb_class = re.search(r'kb-\S+', kb_icon.get('class')) 84 | if kb_class and kb_class.group(0) in kb_subs: 85 | kb_icon.text = chr(int(kb_subs[kb_class.group(0)], 16)) 86 | 87 | # replace breaks with newlines and remove html entities 88 | for br in tree.xpath('//br'): br.text = '\n' 89 | labels[i] = html.unescape(tree.text_content()) 90 | return (labels, False) 91 | 92 | 93 | def decl_name(decls, name, pred): 94 | # extract declaration with name from list 95 | for node in decls: 96 | if node.type != 'declaration': continue 97 | if node.lower_name != name: continue 98 | return next((x.value for x in node.value if pred(x)), '') 99 | 100 | 101 | def get_fonts(css): 102 | # define helper functions 103 | def is_str(x): return x.type == 'string' or x.type == 'ident' 104 | def is_ttf(x): return x.type == 'url' and '.ttf' in x.value 105 | 106 | fonts, ttfs = [None] * 12, {} 107 | for rule in tinycss2.parse_stylesheet(css): 108 | # set fonts using keylabel styles 109 | if rule.type == 'qualified-rule': 110 | decls = tinycss2.parse_declaration_list(rule.content) 111 | font = ttfs.get(decl_name(decls, 'font-family', is_str)) 112 | for sel in rule.prelude: 113 | for i in range(12): 114 | if sel == '*': fonts[i] = fonts[i] or font 115 | if sel.type != 'ident': continue 116 | ids = ('keylabels', 'keylabel', f'keylabel{i}') 117 | if sel.lower_value in ids: fonts[i] = font 118 | 119 | # download ttfs from @font-face 120 | if rule.type == 'at-rule' and rule.lower_at_keyword == 'font-face': 121 | decls = tinycss2.parse_declaration_list(rule.content) 122 | name = decl_name(decls, 'font-family', is_str) 123 | url = decl_name(decls, 'src', is_ttf) 124 | if (url): ttfs[name] = requests.get(url).content 125 | return fonts 126 | 127 | 128 | def deserialise(rows): 129 | # Initialize with defaults 130 | keys, backcolor, current = [], '#EEEEEE', Key() 131 | color_format = re.compile(r'^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$') 132 | default_size = current.label_sizes[0] 133 | with open('fonts/fa2unicode.json') as fa, open('fonts/kbd-webfont2unicode.json') as kb: 134 | fa_subs, kb_subs = json.load(fa), json.load(kb) 135 | 136 | for row in rows: 137 | if isinstance(row, list): 138 | for key in row: 139 | if isinstance(key, str): 140 | newKey = copy.copy(current) 141 | newKey.labels, newKey.pic = get_labels(key, fa_subs, kb_subs) 142 | keys.append(newKey) 143 | 144 | # Set up for the next key 145 | current.x += current.width 146 | current.width = current.height = 1.0 147 | current.x2 = current.y2 = current.width2 = current.height2 = 0.0 148 | current.pic = current.step = current.decal = False 149 | else: 150 | if 'r' in key: 151 | current.rotation_angle = key['r'] 152 | if 'rx' in key: 153 | current.rotation_x = key['rx'] 154 | current.x = current.y = 0 155 | if 'ry' in key: 156 | current.rotation_y = key['ry'] 157 | current.y = current.y = 0 158 | if 'a' in key: 159 | current.align = int(key['a']) 160 | if 'f' in key: 161 | default_size = float(key['f']) 162 | current.label_sizes = [default_size] * 12 163 | if 'f2' in key: 164 | current.label_sizes = [float(key['f2'])] * 12 165 | if 'fa' in key: 166 | label_sizes = [float(size) if size > 0 else default_size for size in key['fa']] 167 | current.label_sizes = label_sizes[:12] + [default_size] * (12 - len(label_sizes)) 168 | if 'p' in key: 169 | current.str_profile = key['p'] 170 | if 'c' in key: 171 | color = key['c'].replace(';', '') 172 | current.color = color if color_format.match(color) else current.color 173 | if 't' in key: 174 | colors = [line.replace(';', '') for line in key['t'].splitlines()] 175 | default_color = colors[0] if colors and color_format.match(colors[0]) else '#000' 176 | colors = [color if color_format.match(color) else default_color for color in colors] 177 | current.label_colors = colors[:12] + [default_color] * (12 - len(colors)) 178 | if 'x' in key: 179 | current.x += float(key['x']) 180 | if 'y' in key: 181 | current.y += float(key['y']) 182 | if 'w' in key: 183 | current.width = float(key['w']) 184 | if 'h' in key: 185 | current.height = float(key['h']) 186 | if 'x2' in key: 187 | current.x2 = float(key['x2']) 188 | if 'y2' in key: 189 | current.y2 = float(key['y2']) 190 | if 'w2' in key: 191 | current.width2 = float(key['w2']) 192 | current.height2 = current.height 193 | if 'h2' in key: 194 | current.height2 = float(key['h2']) 195 | current.width2 = current.width if current.width2 == 0.0 else current.width2 196 | if 'l' in key: 197 | current.step = key['l'] 198 | if 'g' in key: 199 | current.ghost = key['g'] 200 | if 'd' in key: 201 | current.decal = key['d'] 202 | # End of the row 203 | current.y += 1.0 204 | current.x = 0 205 | else: 206 | # Parse global properties 207 | if 'backcolor' in row: 208 | new_color = row['backcolor'].replace(';', '') 209 | backcolor = new_color if color_format.match(new_color) else backcolor 210 | if 'css' in row: 211 | try: current.fonts = get_fonts(row['css']) 212 | except Exception: pass 213 | return keys, backcolor 214 | -------------------------------------------------------------------------------- /render_output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CQCumbers/kle_render/a14ccea8e46286ba01c10e3380eca0a963e21d5d/render_output.png -------------------------------------------------------------------------------- /static/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CQCumbers/kle_render/a14ccea8e46286ba01c10e3380eca0a963e21d5d/static/favicon-16x16.png -------------------------------------------------------------------------------- /static/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CQCumbers/kle_render/a14ccea8e46286ba01c10e3380eca0a963e21d5d/static/favicon-32x32.png -------------------------------------------------------------------------------- /static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CQCumbers/kle_render/a14ccea8e46286ba01c10e3380eca0a963e21d5d/static/favicon.ico -------------------------------------------------------------------------------- /static/k.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CQCumbers/kle_render/a14ccea8e46286ba01c10e3380eca0a963e21d5d/static/k.png -------------------------------------------------------------------------------- /static/ladda-themeless.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Ladda 3 | * http://lab.hakim.se/ladda 4 | * MIT licensed 5 | * 6 | * Copyright (C) 2016 Hakim El Hattab, http://hakim.se 7 | */.ladda-button{position:relative}.ladda-button .ladda-spinner{position:absolute;z-index:2;display:inline-block;width:32px;height:32px;top:50%;margin-top:0;opacity:0;pointer-events:none}.ladda-button .ladda-label{position:relative;z-index:3}.ladda-button .ladda-progress{position:absolute;width:0;height:100%;left:0;top:0;background:rgba(0,0,0,0.2);visibility:hidden;opacity:0;-webkit-transition:0.1s linear all !important;-moz-transition:0.1s linear all !important;-ms-transition:0.1s linear all !important;-o-transition:0.1s linear all !important;transition:0.1s linear all !important}.ladda-button[data-loading] .ladda-progress{opacity:1;visibility:visible}.ladda-button,.ladda-button .ladda-spinner,.ladda-button .ladda-label{-webkit-transition:0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275) all !important;-moz-transition:0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275) all !important;-ms-transition:0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275) all !important;-o-transition:0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275) all !important;transition:0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275) all !important}.ladda-button[data-style=zoom-in],.ladda-button[data-style=zoom-in] .ladda-spinner,.ladda-button[data-style=zoom-in] .ladda-label,.ladda-button[data-style=zoom-out],.ladda-button[data-style=zoom-out] .ladda-spinner,.ladda-button[data-style=zoom-out] .ladda-label{-webkit-transition:0.3s ease all !important;-moz-transition:0.3s ease all !important;-ms-transition:0.3s ease all !important;-o-transition:0.3s ease all !important;transition:0.3s ease all !important}.ladda-button[data-style=expand-right] .ladda-spinner{right:-6px}.ladda-button[data-style=expand-right][data-size="s"] .ladda-spinner,.ladda-button[data-style=expand-right][data-size="xs"] .ladda-spinner{right:-12px}.ladda-button[data-style=expand-right][data-loading]{padding-right:56px}.ladda-button[data-style=expand-right][data-loading] .ladda-spinner{opacity:1}.ladda-button[data-style=expand-right][data-loading][data-size="s"],.ladda-button[data-style=expand-right][data-loading][data-size="xs"]{padding-right:40px}.ladda-button[data-style=expand-left] .ladda-spinner{left:26px}.ladda-button[data-style=expand-left][data-size="s"] .ladda-spinner,.ladda-button[data-style=expand-left][data-size="xs"] .ladda-spinner{left:4px}.ladda-button[data-style=expand-left][data-loading]{padding-left:56px}.ladda-button[data-style=expand-left][data-loading] .ladda-spinner{opacity:1}.ladda-button[data-style=expand-left][data-loading][data-size="s"],.ladda-button[data-style=expand-left][data-loading][data-size="xs"]{padding-left:40px}.ladda-button[data-style=expand-up]{overflow:hidden}.ladda-button[data-style=expand-up] .ladda-spinner{top:-32px;left:50%;margin-left:0}.ladda-button[data-style=expand-up][data-loading]{padding-top:54px}.ladda-button[data-style=expand-up][data-loading] .ladda-spinner{opacity:1;top:26px;margin-top:0}.ladda-button[data-style=expand-up][data-loading][data-size="s"],.ladda-button[data-style=expand-up][data-loading][data-size="xs"]{padding-top:32px}.ladda-button[data-style=expand-up][data-loading][data-size="s"] .ladda-spinner,.ladda-button[data-style=expand-up][data-loading][data-size="xs"] .ladda-spinner{top:4px}.ladda-button[data-style=expand-down]{overflow:hidden}.ladda-button[data-style=expand-down] .ladda-spinner{top:62px;left:50%;margin-left:0}.ladda-button[data-style=expand-down][data-size="s"] .ladda-spinner,.ladda-button[data-style=expand-down][data-size="xs"] .ladda-spinner{top:40px}.ladda-button[data-style=expand-down][data-loading]{padding-bottom:54px}.ladda-button[data-style=expand-down][data-loading] .ladda-spinner{opacity:1}.ladda-button[data-style=expand-down][data-loading][data-size="s"],.ladda-button[data-style=expand-down][data-loading][data-size="xs"]{padding-bottom:32px}.ladda-button[data-style=slide-left]{overflow:hidden}.ladda-button[data-style=slide-left] .ladda-label{position:relative}.ladda-button[data-style=slide-left] .ladda-spinner{left:100%;margin-left:0}.ladda-button[data-style=slide-left][data-loading] .ladda-label{opacity:0;left:-100%}.ladda-button[data-style=slide-left][data-loading] .ladda-spinner{opacity:1;left:50%}.ladda-button[data-style=slide-right]{overflow:hidden}.ladda-button[data-style=slide-right] .ladda-label{position:relative}.ladda-button[data-style=slide-right] .ladda-spinner{right:100%;margin-left:0;left:16px}.ladda-button[data-style=slide-right][data-loading] .ladda-label{opacity:0;left:100%}.ladda-button[data-style=slide-right][data-loading] .ladda-spinner{opacity:1;left:50%}.ladda-button[data-style=slide-up]{overflow:hidden}.ladda-button[data-style=slide-up] .ladda-label{position:relative}.ladda-button[data-style=slide-up] .ladda-spinner{left:50%;margin-left:0;margin-top:1em}.ladda-button[data-style=slide-up][data-loading] .ladda-label{opacity:0;top:-1em}.ladda-button[data-style=slide-up][data-loading] .ladda-spinner{opacity:1;margin-top:0}.ladda-button[data-style=slide-down]{overflow:hidden}.ladda-button[data-style=slide-down] .ladda-label{position:relative}.ladda-button[data-style=slide-down] .ladda-spinner{left:50%;margin-left:0;margin-top:-2em}.ladda-button[data-style=slide-down][data-loading] .ladda-label{opacity:0;top:1em}.ladda-button[data-style=slide-down][data-loading] .ladda-spinner{opacity:1;margin-top:0}.ladda-button[data-style=zoom-out]{overflow:hidden}.ladda-button[data-style=zoom-out] .ladda-spinner{left:50%;margin-left:32px;-webkit-transform:scale(2.5);-moz-transform:scale(2.5);-ms-transform:scale(2.5);-o-transform:scale(2.5);transform:scale(2.5)}.ladda-button[data-style=zoom-out] .ladda-label{position:relative;display:inline-block}.ladda-button[data-style=zoom-out][data-loading] .ladda-label{opacity:0;-webkit-transform:scale(0.5);-moz-transform:scale(0.5);-ms-transform:scale(0.5);-o-transform:scale(0.5);transform:scale(0.5)}.ladda-button[data-style=zoom-out][data-loading] .ladda-spinner{opacity:1;margin-left:0;-webkit-transform:none;-moz-transform:none;-ms-transform:none;-o-transform:none;transform:none}.ladda-button[data-style=zoom-in]{overflow:hidden}.ladda-button[data-style=zoom-in] .ladda-spinner{left:50%;margin-left:-16px;-webkit-transform:scale(0.2);-moz-transform:scale(0.2);-ms-transform:scale(0.2);-o-transform:scale(0.2);transform:scale(0.2)}.ladda-button[data-style=zoom-in] .ladda-label{position:relative;display:inline-block}.ladda-button[data-style=zoom-in][data-loading] .ladda-label{opacity:0;-webkit-transform:scale(2.2);-moz-transform:scale(2.2);-ms-transform:scale(2.2);-o-transform:scale(2.2);transform:scale(2.2)}.ladda-button[data-style=zoom-in][data-loading] .ladda-spinner{opacity:1;margin-left:0;-webkit-transform:none;-moz-transform:none;-ms-transform:none;-o-transform:none;transform:none}.ladda-button[data-style=contract]{overflow:hidden;width:100px}.ladda-button[data-style=contract] .ladda-spinner{left:50%;margin-left:0}.ladda-button[data-style=contract][data-loading]{border-radius:50%;width:52px}.ladda-button[data-style=contract][data-loading] .ladda-label{opacity:0}.ladda-button[data-style=contract][data-loading] .ladda-spinner{opacity:1}.ladda-button[data-style=contract-overlay]{overflow:hidden;width:100px;box-shadow:0px 0px 0px 2000px transparent}.ladda-button[data-style=contract-overlay] .ladda-spinner{left:50%;margin-left:0}.ladda-button[data-style=contract-overlay][data-loading]{border-radius:50%;width:52px;box-shadow:0px 0px 0px 2000px rgba(0,0,0,0.8)}.ladda-button[data-style=contract-overlay][data-loading] .ladda-label{opacity:0}.ladda-button[data-style=contract-overlay][data-loading] .ladda-spinner{opacity:1} 8 | -------------------------------------------------------------------------------- /static/ladda.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Ladda 1.0.0 (2016-03-08, 09:31) 3 | * http://lab.hakim.se/ladda 4 | * MIT licensed 5 | * 6 | * Copyright (C) 2016 Hakim El Hattab, http://hakim.se 7 | */ 8 | !function(a,b){"object"==typeof exports?module.exports=b(require("spin.js")):"function"==typeof define&&define.amd?define(["spin"],b):a.Ladda=b(a.Spinner)}(this,function(a){"use strict";function b(a){if("undefined"==typeof a)return void console.warn("Ladda button target must be defined.");if(/ladda-button/i.test(a.className)||(a.className+=" ladda-button"),a.hasAttribute("data-style")||a.setAttribute("data-style","expand-right"),!a.querySelector(".ladda-label")){var b=document.createElement("span");b.className="ladda-label",i(a,b)}var c,d=a.querySelector(".ladda-spinner");d||(d=document.createElement("span"),d.className="ladda-spinner"),a.appendChild(d);var e,f={start:function(){return c||(c=g(a)),a.setAttribute("disabled",""),a.setAttribute("data-loading",""),clearTimeout(e),c.spin(d),this.setProgress(0),this},startAfter:function(a){return clearTimeout(e),e=setTimeout(function(){f.start()},a),this},stop:function(){return a.removeAttribute("disabled"),a.removeAttribute("data-loading"),clearTimeout(e),c&&(e=setTimeout(function(){c.stop()},1e3)),this},toggle:function(){return this.isLoading()?this.stop():this.start(),this},setProgress:function(b){b=Math.max(Math.min(b,1),0);var c=a.querySelector(".ladda-progress");0===b&&c&&c.parentNode?c.parentNode.removeChild(c):(c||(c=document.createElement("div"),c.className="ladda-progress",a.appendChild(c)),c.style.width=(b||0)*a.offsetWidth+"px")},enable:function(){return this.stop(),this},disable:function(){return this.stop(),a.setAttribute("disabled",""),this},isLoading:function(){return a.hasAttribute("data-loading")},remove:function(){clearTimeout(e),a.removeAttribute("disabled",""),a.removeAttribute("data-loading",""),c&&(c.stop(),c=null);for(var b=0,d=j.length;d>b;b++)if(f===j[b]){j.splice(b,1);break}}};return j.push(f),f}function c(a,b){for(;a.parentNode&&a.tagName!==b;)a=a.parentNode;return b===a.tagName?a:void 0}function d(a){for(var b=["input","textarea","select"],c=[],d=0;dg;g++)!function(){var a=f[g];if("function"==typeof a.addEventListener){var h=b(a),i=-1;a.addEventListener("click",function(b){var f=!0,g=c(a,"FORM");if("undefined"!=typeof g)if("function"==typeof g.checkValidity)f=g.checkValidity();else for(var j=d(g),k=0;ka;a++)j[a].stop()}function g(b){var c,d,e=b.offsetHeight;0===e&&(e=parseFloat(window.getComputedStyle(b).height)),e>32&&(e*=.8),b.hasAttribute("data-spinner-size")&&(e=parseInt(b.getAttribute("data-spinner-size"),10)),b.hasAttribute("data-spinner-color")&&(c=b.getAttribute("data-spinner-color")),b.hasAttribute("data-spinner-lines")&&(d=parseInt(b.getAttribute("data-spinner-lines"),10));var f=.2*e,g=.6*f,h=7>f?2:3;return new a({color:c||"#fff",lines:d||12,radius:f,length:g,width:h,zIndex:"auto",top:"auto",left:"auto",className:""})}function h(a){for(var b=[],c=0;cb;b++)a.appendChild(arguments[b]);return a}function c(a,b,c,d){var e=["opacity",b,~~(100*a),c,d].join("-"),f=.01+c/d*100,g=Math.max(1-(1-a)/b*(100-f),a),h=j.substring(0,j.indexOf("Animation")).toLowerCase(),i=h&&"-"+h+"-"||"";return l[e]||(m.insertRule("@"+i+"keyframes "+e+"{0%{opacity:"+g+"}"+f+"%{opacity:"+a+"}"+(f+.01)+"%{opacity:1}"+(f+b)%100+"%{opacity:"+a+"}100%{opacity:"+g+"}}",m.cssRules.length),l[e]=1),e}function d(a,b){var c,d,e=a.style;for(b=b.charAt(0).toUpperCase()+b.slice(1),d=0;d',c)}m.addRule(".spin-vml","behavior:url(#default#VML)"),h.prototype.lines=function(a,d){function f(){return e(c("group",{coordsize:k+" "+k,coordorigin:-j+" "+-j}),{width:k,height:k})}function h(a,h,i){b(m,b(e(f(),{rotation:360/d.lines*a+"deg",left:~~h}),b(e(c("roundrect",{arcsize:d.corners}),{width:j,height:d.width,left:d.radius,top:-d.width>>1,filter:i}),c("fill",{color:g(d.color,a),opacity:d.opacity}),c("stroke",{opacity:0}))))}var i,j=d.length+d.width,k=2*j,l=2*-(d.width+d.length)+"px",m=e(f(),{position:"absolute",top:l,left:l});if(d.shadow)for(i=1;i<=d.lines;i++)h(i,-2,"progid:DXImageTransform.Microsoft.Blur(pixelradius=2,makeshadow=1,shadowopacity=.3)");for(i=1;i<=d.lines;i++)h(i);return b(a,m)},h.prototype.opacity=function(a,b,c,d){var e=a.firstChild;d=d.shadow&&d.lines||0,e&&b+d>1)+"px"})}for(var i,k=0,l=(f.lines-1)*(1-f.direction)/2;k 2 | 3 | 4 | 5 | 6 | 7 | 8 | KLE-render 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 |
24 | 25 |

26 | 27 | KLE-Render 28 |

29 |

30 | Get prettier images of Keyboard Layout Editor designs 31 |

32 | 33 | 34 | {% if get_flashed_messages() %} 35 | {% for message in get_flashed_messages() %} 36 | 42 | {% endfor %} 43 | {% else %} 44 | 50 | {% endif %} 51 | 52 | 53 |
54 |
55 |
56 | {{ form.url.label }} 57 | 58 | 59 | 60 | 61 | {{ form.url(class="form-control url", placeholder="keyboard-layout-editor.com/#/gists/...") }} 62 |
63 |
64 | {{ form.json.label }} 65 | 66 | 67 | 68 | 69 |
70 | {{ form.json(class="custom-file-input") }} 71 | 72 |
73 |
74 | 77 |
78 |
79 | 80 | 81 | 103 |
104 |
105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 125 | 126 | 127 | -------------------------------------------------------------------------------- /test.py: -------------------------------------------------------------------------------- 1 | from timeit import default_timer as timer 2 | from keyboard import Keyboard, deserialise 3 | import json, github, dotenv, os 4 | 5 | dotenv_path = os.path.join(os.path.dirname(__file__), '.env') 6 | dotenv.load_dotenv(dotenv_path) 7 | gist_id = '4de8adb88cb4c45c2f43' 8 | 9 | g = github.Github(os.environ.get('API_TOKEN')) 10 | files = [v for k, v in g.get_gist(gist_id).files.items() if k.endswith('.kbd.json')] 11 | content = json.loads(files[0].content) 12 | 13 | start = timer() 14 | img = Keyboard(content).render() 15 | end = timer() 16 | print("--- rendered at %s seconds ---" % (end - start)) 17 | 18 | img.save("render_output.png", 'PNG') 19 | --------------------------------------------------------------------------------