├── requirements.txt
├── .docs
└── hello.png
├── .gitignore
├── LICENSE
├── README.md
├── OFL.md
└── main.py
/requirements.txt:
--------------------------------------------------------------------------------
1 | svgwrite
2 | numpy
3 | cairosvg
4 | potracer
--------------------------------------------------------------------------------
/.docs/hello.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dupontgu/scopin-sans-typeface/HEAD/.docs/hello.png
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .Python
2 | [Bb]in
3 | [Ii]nclude
4 | [Ll]ib
5 | [Ll]ib64
6 | [Ll]ocal
7 | [Ss]cripts
8 | pyvenv.cfg
9 | .venv
10 | pip-selfcheck.json
11 | outputs/
12 | **/.DS_Store
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Guy Dupont
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Scopin' Sans
2 | An open source typeface for hardware people!
3 | It renders text as if it were being viewed as serial data on an oscilloscope.
4 | There are currently 3 variations:
5 | 1. `Normal`
6 | 2. `FastBaud` - compressed horizontally
7 | 3. `NoNoise` - no baked-in noise, just square waves.
8 |
9 |
10 |
11 | ## Generating the typeface
12 | 1. Install [`fontforge`](https://fontforge.org/) on your computer. You will not need to use the GUI, but the installation of the app will provide the Python hooks used by this script.
13 | 2. Download/clone this repo, and navigate to it's root directory using a terminal.
14 | 3. Create a Python virtual environment: `python3 -m venv . `
15 | 4. Edit your new `pyvenv.cfg` file to allow the use of system-site packages (this will allow the script to access `fontforge`). Set `include-system-site-packages = true` and save the file.
16 | 5. Install dependencies: `pip install -r requirements.txt`
17 | 6. Run the script: `python main.py`
18 | 7. Two sets of `ttf` files will be generated, choose one set to install from:
19 | 1. The set in `outputs/ScopinSans` are generated such that they are all a part of the same family. Each variation will be installed as a different _weight_. This keeps your system font list a little tidier, but certain applications may not let you choose font weights!
20 | 2. The set in `outputs/ScopinSans-Individuals` are generated such that each variant is a unique font. They will all show up as individual fonts on your system.
21 |
22 | ## Using the typeface
23 | Once the .ttf files are installed on your machine, you should be able to use Scopin' Sans like any other typeface. *Note that only ascii values are generated currently!* There are 1-bit utility characters that can be used to generate arbitrary waveforms:
24 | - `¥` - falling edge (0b10)
25 | - `¦` - rising edge (0b01)
26 | - [`§, ¨, ©, ª, «, ¬, ®, ¯`] - signal high (0b1) (there are multiple so you can randomize the noise)
27 | - [`°, ±, ², ³, ´, ¶, ·, ¸`] - signal low (0b0) (there are multiple so you can randomize the noise)
--------------------------------------------------------------------------------
/OFL.md:
--------------------------------------------------------------------------------
1 | Copyright (c) `2023`, `Guy Dupont` (`gvy.dvupont@gmail.com`)
2 |
3 | This Font Software is licensed under the SIL Open Font License, Version 1.1.
4 | This license is copied below, and is also available with a FAQ at:
5 |
6 |
7 | ---
8 |
9 | # SIL OPEN FONT LICENSE
10 |
11 |
12 |
13 | ##### *Version 1.1 - 26 February 2007*
14 |
15 |
16 | PREAMBLE
17 | ----------
18 |
19 | The goals of the Open Font License (OFL) are to stimulate worldwide
20 | development of collaborative font projects, to support the font creation
21 | efforts of academic and linguistic communities, and to provide a free and
22 | open framework in which fonts may be shared and improved in partnership
23 | with others.
24 |
25 | The OFL allows the licensed fonts to be used, studied, modified and
26 | redistributed freely as long as they are not sold by themselves. The
27 | fonts, including any derivative works, can be bundled, embedded,
28 | redistributed and/or sold with any software provided that any reserved
29 | names are not used by derivative works. The fonts and derivatives,
30 | however, cannot be released under any other type of license. The
31 | requirement for fonts to remain under this license does not apply
32 | to any document created using the fonts or their derivatives.
33 |
34 | DEFINITIONS
35 | -------------
36 |
37 | ***Font Software*** refers to the set of files released by the Copyright
38 | Holder(s) under this license and clearly marked as such. This may
39 | include source files, build scripts and documentation.
40 |
41 | ***Reserved Font Name*** refers to any names specified as such after the
42 | copyright statement(s).
43 |
44 | ***Original Version*** refers to the collection of Font Software components as
45 | distributed by the Copyright Holder(s).
46 |
47 | ***Modified Version*** refers to any derivative made by adding to, deleting,
48 | or substituting -- in part or in whole -- any of the components of the
49 | Original Version, by changing formats or by porting the Font Software to a
50 | new environment.
51 |
52 | ***Author*** refers to any designer, engineer, programmer, technical
53 | writer or other person who contributed to the Font Software.
54 |
55 | PERMISSION & CONDITIONS
56 | ------------------------
57 |
58 | Permission is hereby granted, free of charge, to any person obtaining
59 | a copy of the ***Font Software***, to use, study, copy, merge, embed, modify,
60 | redistribute, and sell modified and unmodified copies of the ***Font
61 | Software***, subject to the following conditions:
62 |
63 | 1) Neither the ***Font Software*** nor any of its individual components,
64 | in ***Original*** or ***Modified Versions***, may be sold by itself.
65 |
66 | 2) ***Original*** or ***Modified Versions*** of the ***Font Software*** may be bundled,
67 | redistributed and/or sold with any software, provided that each copy
68 | contains the above copyright notice and this license. These can be
69 | included either as stand-alone text files, human-readable headers or
70 | in the appropriate machine-readable metadata fields within text or
71 | binary files as long as those fields can be easily viewed by the user.
72 |
73 | 3) No ***Modified Version*** of the ***Font Software*** may use the ***Reserved Font
74 | Name(s)*** unless explicit written permission is granted by the corresponding
75 | Copyright Holder. This restriction only applies to the primary font name as
76 | presented to the users.
77 |
78 | 4) The name(s) of the Copyright Holder(s) or the ***Author(s)*** of the ***Font
79 | Software*** shall not be used to promote, endorse or advertise any
80 | ***Modified Version***, except to acknowledge the contribution(s) of the
81 | Copyright Holder(s) and the ***Author(s)*** or with their explicit written
82 | permission.
83 |
84 | 5) The ***Font Software***, modified or unmodified, in part or in whole,
85 | must be distributed entirely under this license, and must not be
86 | distributed under any other license. The requirement for fonts to
87 | remain under this license does not apply to any document created
88 | using the ***Font Software***.
89 |
90 | TERMINATION
91 | -----------
92 |
93 | This license becomes null and void if any of the above conditions are
94 | not met.
95 |
96 |
97 | DISCLAIMER
98 | -----------
99 |
100 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
101 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
102 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
103 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
104 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
105 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
106 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
107 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
108 | OTHER DEALINGS IN THE FONT SOFTWARE.
109 |
--------------------------------------------------------------------------------
/main.py:
--------------------------------------------------------------------------------
1 | import fontforge
2 | import svgwrite
3 | import math
4 | import numpy as np
5 | import cairosvg
6 | from PIL import Image
7 | import potrace
8 | from pathlib import Path
9 |
10 | root_output_dir = "outputs"
11 | svg_working_file = f"{root_output_dir}/temp.svg"
12 | png_working_file = f"{root_output_dir}/temp.png"
13 | regen_svgs = True
14 | version = "0.4"
15 |
16 | # should probably parameterize these
17 | height = 200
18 | stroke_width = 4.5
19 |
20 |
21 | def createCharFromSvg(font, char, filename, translation_factor, force_scale=None):
22 | fontChar = font.createChar(ord(char))
23 | fontChar.importOutlines(filename)
24 | box = fontChar.boundingBox()
25 | original_height = box[3] - box[1]
26 | original_width = box[2] - box[0]
27 | scale = (original_width / original_height) if force_scale is None else force_scale
28 | width = font.em * scale
29 | transformation_matrix = (1, 0, 0, 1, -4, 0)
30 | if force_scale is not None:
31 | width = min(original_width, width)
32 | fontChar.transform(transformation_matrix)
33 | elif scale > 1:
34 | transformation_matrix = (scale, 0, 0, scale, -4, translation_factor)
35 | fontChar.transform(transformation_matrix)
36 | # - 4 to ensure a little overlap with previous
37 | fontChar.width = math.ceil(width) - 6
38 | return scale
39 |
40 |
41 | # adapted from: https://github.com/tatarize/potrace/pull/8/files
42 | def path_to_svg(width, height, path, output_file):
43 | with open(output_file, "w") as fp:
44 | fp.write(
45 | f'")
71 |
72 |
73 | def svg_for_code(
74 | number: int,
75 | output_dir: str,
76 | samples_per_bit: int,
77 | noise_amount: float,
78 | start_bit: bool,
79 | end_bit: bool,
80 | digits: int = 8,
81 | start_high: bool = True,
82 | ):
83 | output_path = f"{output_dir}/{str(number)}.svg"
84 | if not regen_svgs:
85 | return output_path
86 | binary_string = format(number, f"0{digits}b")
87 | # LSB first
88 | binary_string = binary_string[::-1]
89 | if start_bit:
90 | binary_string = "0" + binary_string
91 | if end_bit:
92 | binary_string = binary_string + "1"
93 | # invert signal
94 | binary_signal = np.array([-1 if bool(int(bit)) else 1 for bit in binary_string])
95 | signal = np.repeat(binary_signal, samples_per_bit)
96 | expanded_time = np.linspace(0, len(binary_string), len(signal))
97 | noise = np.random.normal(0, noise_amount, expanded_time.shape)
98 | signal = signal + noise
99 |
100 | scale = height * (0.99 - (noise_amount * 2))
101 | pos = (3, -scale if start_high else scale)
102 | samples = len(signal)
103 | # add some padding at the end to make room for the falling edge
104 | if end_bit:
105 | samples += 4
106 |
107 | dwg = svgwrite.Drawing(
108 | svg_working_file,
109 | profile="tiny",
110 | size=(f"{samples}mm", f"{2*height}mm"),
111 | viewBox=(f"0 {-height} {samples} {2*height}"),
112 | )
113 |
114 | for p in signal:
115 | (prev_x, prev_y) = pos
116 | y_delt = prev_y - (p * scale)
117 | x = prev_x + 1
118 | dwg.add(
119 | dwg.line(
120 | pos,
121 | (x, prev_y - y_delt),
122 | stroke=svgwrite.rgb(0, 0, 0),
123 | stroke_width=stroke_width,
124 | )
125 | )
126 | pos = (x, prev_y - y_delt)
127 | if end_bit:
128 | dwg.add(
129 | dwg.line(
130 | pos,
131 | (prev_x + 2, prev_y),
132 | stroke=svgwrite.rgb(0, 0, 0),
133 | stroke_width=stroke_width,
134 | )
135 | )
136 |
137 | # You're gonna read this and think: "is he really going from svg -> png -> back to svg?"
138 | # Yes, I am! The potrace result looks more "organic", simplifies the final svg,
139 | # and allows me to write more simple svg generation code up front.
140 | dwg.save(pretty=True)
141 | cairosvg.svg2png(
142 | url=svg_working_file,
143 | write_to=png_working_file,
144 | scale=0.3,
145 | background_color="#FFF",
146 | )
147 | image = Image.open(png_working_file).convert("L")
148 | bitmap = potrace.Bitmap(np.array(image))
149 | path = bitmap.trace(opttolerance=0.8)
150 | path_to_svg(image.width, image.height, path, output_path)
151 | return output_path
152 |
153 |
154 | def create_font(weight, samples_per_bit, noise_amount):
155 | print(f"CREATE VARIANT: {weight}")
156 | svg_dir = f"{root_output_dir}/{weight}"
157 | Path(svg_dir).mkdir(parents=True, exist_ok=True)
158 |
159 | font = fontforge.font()
160 | font.encoding = "UnicodeFull"
161 | font.version = version
162 | font.copyright = (
163 | "Copyright (c) 2023 Guy Dupont (gvy.dvupont@gmail.com), "
164 | + "soure code: https://github.com/dupontgu/scopin-sans-typeface, "
165 | + "OFL License: https://openfontlicense.org/documents/OFL.txt"
166 | )
167 | font.em = 512
168 | max_scale = 0
169 | # This is not real math, these values just made it work for now.
170 | y_adjustment = -((10 * samples_per_bit) - (387 - (noise_amount * height)))
171 |
172 | for code in range(0, 128):
173 | print(code)
174 | svg_path = svg_for_code(
175 | number=code,
176 | output_dir=svg_dir,
177 | samples_per_bit=samples_per_bit,
178 | noise_amount=noise_amount,
179 | start_bit=True,
180 | end_bit=True,
181 | )
182 |
183 | current_scale = createCharFromSvg(font, chr(code), svg_path, y_adjustment)
184 | if current_scale > max_scale:
185 | max_scale = current_scale
186 |
187 | # UTILITY CHARACTERS
188 | # for rising edge
189 | for c in ["¥"]:
190 | svg_path = svg_for_code(
191 | number=0b01,
192 | output_dir=svg_dir,
193 | samples_per_bit=samples_per_bit,
194 | noise_amount=noise_amount,
195 | start_bit=False,
196 | end_bit=False,
197 | digits=2,
198 | )
199 | createCharFromSvg(font, c, svg_path, y_adjustment, force_scale=max_scale)
200 |
201 | # for falling edge
202 | for c in ["¦"]:
203 | svg_path = svg_for_code(
204 | number=0b10,
205 | output_dir=svg_dir,
206 | samples_per_bit=samples_per_bit,
207 | noise_amount=noise_amount,
208 | start_bit=False,
209 | end_bit=False,
210 | digits=2,
211 | start_high=False,
212 | )
213 | createCharFromSvg(font, c, svg_path, y_adjustment, force_scale=max_scale)
214 |
215 | # for high states
216 | for c in ["§", "¨", "©", "ª", "«", "¬", "®", "¯"]:
217 | svg_path = svg_for_code(
218 | number=0b1,
219 | output_dir=svg_dir,
220 | samples_per_bit=samples_per_bit,
221 | noise_amount=noise_amount,
222 | start_bit=False,
223 | end_bit=False,
224 | digits=1,
225 | )
226 | createCharFromSvg(font, c, svg_path, y_adjustment, force_scale=max_scale)
227 |
228 | # for low states
229 | for c in ["°", "±", "²", "³", "´", "¶", "·", "¸"]:
230 | svg_path = svg_for_code(
231 | number=0b0,
232 | output_dir=svg_dir,
233 | samples_per_bit=samples_per_bit,
234 | noise_amount=noise_amount,
235 | start_bit=False,
236 | end_bit=False,
237 | digits=1,
238 | start_high=False,
239 | )
240 | createCharFromSvg(font, c, svg_path, 0, force_scale=max_scale)
241 |
242 | # generate font under the global ScopinSans family, where each variant is a "weight"
243 | # this makes for a cleaner install on some machines, but weights are unavailabe in some apps
244 | font_dir = f"{root_output_dir}/ScopinSans-{version}"
245 | Path(font_dir).mkdir(parents=True, exist_ok=True)
246 | font.weight = weight
247 | filename = f"ScopinSans-{weight}"
248 | font.fontname = filename
249 | font.familyname = "ScopinSans"
250 | font.fullname = filename
251 | font.generate(f"{font_dir}/{filename}.ttf")
252 | font.generate(f"{font_dir}/{filename}.woff2")
253 |
254 | # alternatively create a unique family for each variant, so each is installed independently
255 | individual_dir = f"{root_output_dir}/ScopinSans-Individuals-{version}"
256 | Path(individual_dir).mkdir(parents=True, exist_ok=True)
257 | filename = f"ScopinSans {weight}"
258 | font.familyname = filename
259 | font.fullname = filename
260 | font.weight = "Regular"
261 | font.generate(f"{individual_dir}/{filename}.ttf")
262 | font.close()
263 |
264 |
265 | create_font("NoNoise", samples_per_bit=80, noise_amount=0.0)
266 | create_font("Regular", samples_per_bit=100, noise_amount=0.04)
267 | create_font("FastBaud", samples_per_bit=30, noise_amount=0.04)
268 |
--------------------------------------------------------------------------------