diff --git a/.gitignore b/.gitignore index 72364f9..e205c5f 100644 --- a/.gitignore +++ b/.gitignore @@ -23,6 +23,7 @@ var/ *.egg-info/ .installed.cfg *.egg +test.py # PyInstaller # Usually these files are written by a python script from a template diff --git a/README.md b/README.md index 2acb0bb..9fb7b62 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,9 @@ PIL - Python Imaging Library (or Pillow) ```python data = 'Aztec Code 2D :)' aztec_code = AztecCode(data) -aztec_code.save('aztec_code.png', module_size=4) +Add text: +aztec_code = AztecCode(data, fontfile = 'path/to/your/font.ttf') +aztec_code.save('aztec_code.png', module_size = 4) ``` This code will generate an image file "aztec_code.png" with the Aztec Code that contains "Aztec Code 2D :)" text. diff --git a/aztec_code_generator.py b/aztec_code_generator.py index 844b8b5..befbc33 100644 --- a/aztec_code_generator.py +++ b/aztec_code_generator.py @@ -15,7 +15,7 @@ import sys try: - from PIL import Image, ImageDraw + from PIL import Image, ImageDraw, ImageFont except ImportError: Image = ImageDraw = None missing_pil = sys.exc_info() @@ -179,7 +179,7 @@ def reed_solomon(wd, nd, nc, gf, pp): :param int nc: Number of error correction codewords. :param int gf: Galois Field order. :param int pp: Prime modulus polynomial value. - + :return: None. """ # generate log and anti log tables @@ -242,19 +242,23 @@ def find_optimal_sequence(data): # if changing from punct or digit to binary mode use U/L as intermediate mode # TODO: update for digit back_to[y] = 'upper' - cur_seq[y] = cur_seq[x] + ['U/L', '%s/S' % y.upper()[0], 'size'] + cur_seq[y] = cur_seq[x] + \ + ['U/L', '%s/S' % y.upper()[0], 'size'] else: back_to[y] = x - cur_seq[y] = cur_seq[x] + ['%s/S' % y.upper()[0], 'size'] + cur_seq[y] = cur_seq[x] + \ + ['%s/S' % y.upper()[0], 'size'] else: if cur_seq[x]: # if changing from punct or digit mode - use U/L as intermediate mode # TODO: update for digit if x in ['punct', 'digit'] and y != 'upper': - cur_seq[y] = cur_seq[x] + ['resume', 'U/L', '%s/L' % y.upper()[0]] + cur_seq[y] = cur_seq[x] + \ + ['resume', 'U/L', '%s/L' % y.upper()[0]] back_to[y] = y elif x in ['upper', 'lower'] and y == 'punct': - cur_seq[y] = cur_seq[x] + ['M/L', '%s/L' % y.upper()[0]] + cur_seq[y] = cur_seq[x] + \ + ['M/L', '%s/L' % y.upper()[0]] back_to[y] = y elif x == 'mixed' and y != 'upper': if y == 'punct': @@ -273,42 +277,54 @@ def find_optimal_sequence(data): cur_seq[y] = cur_seq[x] + ['resume'] elif y == 'upper': if back_to[x] == 'lower': - cur_seq[y] = cur_seq[x] + ['resume', 'M/L', 'U/L'] + cur_seq[y] = cur_seq[x] + \ + ['resume', 'M/L', 'U/L'] if back_to[x] == 'mixed': - cur_seq[y] = cur_seq[x] + ['resume', 'U/L'] + cur_seq[y] = cur_seq[x] + \ + ['resume', 'U/L'] back_to[y] = 'upper' elif y == 'lower': - cur_seq[y] = cur_seq[x] + ['resume', 'L/L'] + cur_seq[y] = cur_seq[x] + \ + ['resume', 'L/L'] back_to[y] = 'lower' elif y == 'mixed': - cur_seq[y] = cur_seq[x] + ['resume', 'M/L'] + cur_seq[y] = cur_seq[x] + \ + ['resume', 'M/L'] back_to[y] = 'mixed' elif y == 'punct': if back_to[x] == 'mixed': - cur_seq[y] = cur_seq[x] + ['resume', 'P/L'] + cur_seq[y] = cur_seq[x] + \ + ['resume', 'P/L'] else: - cur_seq[y] = cur_seq[x] + ['resume', 'M/L', 'P/L'] + cur_seq[y] = cur_seq[x] + \ + ['resume', 'M/L', 'P/L'] back_to[y] = 'punct' elif y == 'digit': if back_to[x] == 'mixed': - cur_seq[y] = cur_seq[x] + ['resume', 'U/L', 'D/L'] + cur_seq[y] = cur_seq[x] + \ + ['resume', 'U/L', 'D/L'] else: - cur_seq[y] = cur_seq[x] + ['resume', 'D/L'] + cur_seq[y] = cur_seq[x] + \ + ['resume', 'D/L'] back_to[y] = 'digit' else: - cur_seq[y] = cur_seq[x] + ['resume', '%s/L' % y.upper()[0]] + cur_seq[y] = cur_seq[x] + \ + ['resume', '%s/L' % y.upper()[0]] back_to[y] = y else: # if changing from punct or digit mode - use U/L as intermediate mode # TODO: update for digit if x in ['punct', 'digit']: - cur_seq[y] = cur_seq[x] + ['U/L', '%s/L' % y.upper()[0]] + cur_seq[y] = cur_seq[x] + \ + ['U/L', '%s/L' % y.upper()[0]] back_to[y] = y elif x in ['binary', 'upper', 'lower'] and y == 'punct': - cur_seq[y] = cur_seq[x] + ['M/L', '%s/L' % y.upper()[0]] + cur_seq[y] = cur_seq[x] + \ + ['M/L', '%s/L' % y.upper()[0]] back_to[y] = y else: - cur_seq[y] = cur_seq[x] + ['%s/L' % y.upper()[0]] + cur_seq[y] = cur_seq[x] + \ + ['%s/L' % y.upper()[0]] back_to[y] = y next_len = { 'upper': E, 'lower': E, 'mixed': E, 'punct': E, 'digit': E, 'binary': E @@ -350,14 +366,16 @@ def find_optimal_sequence(data): last_mode = '' for char in cur_seq[x][::-1]: if char.replace('/S', '').replace('/L', '') in abbr_modes: - last_mode = abbr_modes.get(char.replace('/S', '').replace('/L', '')) + last_mode = abbr_modes.get( + char.replace('/S', '').replace('/L', '')) break if last_mode == 'punct': # do not use mixed mode for '\r\n' as in mixed mode '\r' and '\n' are separate if cur_seq[x][-1] + c in punct_2_chars and x != 'mixed': if cur_len[x] < next_len[x]: next_len[x] = cur_len[x] - next_seq[x] = cur_seq[x][:-1] + [cur_seq[x][-1] + c] + next_seq[x] = cur_seq[x][:-1] + \ + [cur_seq[x][-1] + c] if len(next_seq['binary']) - 2 == 32: next_len['binary'] += 11 for i in modes: @@ -475,7 +493,7 @@ def get_data_codewords(bits, codeword_size): :param str bits: Input data bits. :param int codeword_size: Codeword size in bits. - + :return: Data codewords. """ codewords = [] @@ -533,7 +551,7 @@ def find_suitable_matrix_size(data): ec_percent = 23 # recommended: 23% of symbol capacity plus 3 codewords # calculate minimum required number of bits required_bits_count = int(math.ceil(len(out_bits) * 100.0 / ( - 100 - ec_percent) + 3 * 100.0 / (100 - ec_percent))) + 100 - ec_percent) + 3 * 100.0 / (100 - ec_percent))) if required_bits_count < bits: return size, compact raise Exception('Data too big to fit in one Aztec code!') @@ -544,15 +562,18 @@ class AztecCode(object): Aztec code generator. """ - def __init__(self, data, size=None, compact=None): + def __init__(self, data, fontfile=None, size=None, compact=None): """Create Aztec code with given data. If size and compact parameters are None (by default), an optimal size and compactness calculated based on the data. + If you supply a fontfile the Aztec code will have text added to it :param data: Data to encode. + :param fontfile: path to font file you wish to use /your/path/to/font.ttf :param int|None size: Size of matrix. :param bool|None compact: Compactness flag. """ + self.my_font = fontfile self.data = data if size is not None and compact is not None: if (size, compact) in table: @@ -574,7 +595,7 @@ def __create_matrix(self): line.append(' ') self.matrix.append(line) - def save(self, filename, module_size=1): + def save(self, filename, module_size=4): """Save matrix to image file. :param str filename: Output image filename. @@ -586,7 +607,8 @@ def save(self, filename, module_size=1): exc = missing_pil[0](missing_pil[1]) exc.__traceback__ = missing_pil[2] raise exc - image = Image.new('RGB', (self.size * module_size, self.size * module_size), 'white') + image = Image.new('RGB', (self.size * module_size, + self.size * module_size), 'white') image_draw = ImageDraw.Draw(image) for y in range(self.size): for x in range(self.size): @@ -594,6 +616,13 @@ def save(self, filename, module_size=1): (x * module_size, y * module_size, x * module_size + module_size, y * module_size + module_size), fill=(0, 0, 0) if self.matrix[y][x] == '#' else (255, 255, 255)) + if self.my_font is not None: + try: + image = self.add_text(image, self.data, module_size) + except OSError as e: + print(e) + exit() + image.save(filename) def print_out(self): @@ -651,21 +680,27 @@ def __get_mode_message(self, layers_count, data_cw_count): """ if self.compact: # for compact mode - 2 bits with layers count and 6 bits with data codewords count - mode_word = '{0:02b}{1:06b}'.format(layers_count - 1, data_cw_count - 1) + mode_word = '{0:02b}{1:06b}'.format( + layers_count - 1, data_cw_count - 1) # two 4 bits initial codewords with 5 Reed-Solomon check codewords - init_codewords = [int(mode_word[i:i + 4], 2) for i in range(0, 8, 4)] + init_codewords = [int(mode_word[i:i + 4], 2) + for i in range(0, 8, 4)] total_cw_count = 7 else: # for full mode - 5 bits with layers count and 11 bits with data codewords count - mode_word = '{0:05b}{1:011b}'.format(layers_count - 1, data_cw_count - 1) + mode_word = '{0:05b}{1:011b}'.format( + layers_count - 1, data_cw_count - 1) # four 4 bits initial codewords with 6 Reed-Solomon check codewords - init_codewords = [int(mode_word[i:i + 4], 2) for i in range(0, 16, 4)] + init_codewords = [int(mode_word[i:i + 4], 2) + for i in range(0, 16, 4)] total_cw_count = 10 # fill Reed-Solomon check codewords with zeros init_cw_count = len(init_codewords) - codewords = (init_codewords + [0] * (total_cw_count - init_cw_count))[:total_cw_count] + codewords = (init_codewords + + [0] * (total_cw_count - init_cw_count))[:total_cw_count] # update Reed-Solomon check codewords using GF(16) - reed_solomon(codewords, init_cw_count, total_cw_count - init_cw_count, 16, polynomials[4]) + reed_solomon(codewords, init_cw_count, total_cw_count - + init_cw_count, 16, polynomials[4]) return codewords def __add_mode_info(self, data_cw_count): @@ -736,15 +771,18 @@ def __add_data(self, data): ec_percent = 23 # recommended # calculate minimum required number of bits required_bits_count = int(math.ceil(len(out_bits) * 100.0 / ( - 100 - ec_percent) + 3 * 100.0 / (100 - ec_percent))) + 100 - ec_percent) + 3 * 100.0 / (100 - ec_percent))) data_codewords = get_data_codewords(out_bits, cw_bits) if required_bits_count > bits: - raise Exception('Data too big to fit in Aztec code with current size!') + raise Exception( + 'Data too big to fit in Aztec code with current size!') # add Reed-Solomon codewords to init data codewords data_cw_count = len(data_codewords) - codewords = (data_codewords + [0] * (cw_count - data_cw_count))[:cw_count] - reed_solomon(codewords, data_cw_count, cw_count - data_cw_count, 2 ** cw_bits, polynomials[cw_bits]) + codewords = (data_codewords + [0] * + (cw_count - data_cw_count))[:cw_count] + reed_solomon(codewords, data_cw_count, cw_count - + data_cw_count, 2 ** cw_bits, polynomials[cw_bits]) center = self.size // 2 ring_radius = 5 if self.compact else 7 @@ -754,17 +792,23 @@ def __add_data(self, data): layer_index = 0 pos_x = center - ring_radius pos_y = center - ring_radius - 1 - full_bits = ''.join(bin(cw)[2:].zfill(cw_bits) for cw in codewords)[::-1] + full_bits = ''.join(bin(cw)[2:].zfill(cw_bits) + for cw in codewords)[::-1] for i in range(0, len(full_bits), 2): num += 1 - max_num = ring_radius * 2 + layer_index * 4 + (4 if self.compact else 3) - bits_pair = ['#' if bit == '1' else ' ' for bit in full_bits[i:i + 2]] + max_num = ring_radius * 2 + layer_index * \ + 4 + (4 if self.compact else 3) + bits_pair = ['#' if bit == + '1' else ' ' for bit in full_bits[i:i + 2]] if layer_index >= layers_count: - raise Exception('Maximum layer count for current size is exceeded!') + raise Exception( + 'Maximum layer count for current size is exceeded!') if side == 'top': # move right - dy0 = 1 if not self.compact and (center - pos_y) % 16 == 0 else 0 - dy1 = 2 if not self.compact and (center - pos_y + 1) % 16 == 0 else 1 + dy0 = 1 if not self.compact and ( + center - pos_y) % 16 == 0 else 0 + dy1 = 2 if not self.compact and ( + center - pos_y + 1) % 16 == 0 else 1 self.matrix[pos_y - dy0][pos_x] = bits_pair[0] self.matrix[pos_y - dy1][pos_x] = bits_pair[1] pos_x += 1 @@ -780,8 +824,10 @@ def __add_data(self, data): pos_y += 1 elif side == 'right': # move down - dx0 = 1 if not self.compact and (center - pos_x) % 16 == 0 else 0 - dx1 = 2 if not self.compact and (center - pos_x + 1) % 16 == 0 else 1 + dx0 = 1 if not self.compact and ( + center - pos_x) % 16 == 0 else 0 + dx1 = 2 if not self.compact and ( + center - pos_x + 1) % 16 == 0 else 1 self.matrix[pos_y][pos_x - dx0] = bits_pair[1] self.matrix[pos_y][pos_x - dx1] = bits_pair[0] pos_y += 1 @@ -799,8 +845,10 @@ def __add_data(self, data): pos_x -= 1 elif side == 'bottom': # move left - dy0 = 1 if not self.compact and (center - pos_y) % 16 == 0 else 0 - dy1 = 2 if not self.compact and (center - pos_y + 1) % 16 == 0 else 1 + dy0 = 1 if not self.compact and ( + center - pos_y) % 16 == 0 else 0 + dy1 = 2 if not self.compact and ( + center - pos_y + 1) % 16 == 0 else 1 self.matrix[pos_y - dy0][pos_x] = bits_pair[1] self.matrix[pos_y - dy1][pos_x] = bits_pair[0] pos_x -= 1 @@ -818,8 +866,10 @@ def __add_data(self, data): pos_y -= 1 elif side == 'left': # move up - dx0 = 1 if not self.compact and (center - pos_x) % 16 == 0 else 0 - dx1 = 2 if not self.compact and (center - pos_x - 1) % 16 == 0 else 1 + dx0 = 1 if not self.compact and ( + center - pos_x) % 16 == 0 else 0 + dx1 = 2 if not self.compact and ( + center - pos_x - 1) % 16 == 0 else 1 self.matrix[pos_y][pos_x + dx1] = bits_pair[0] self.matrix[pos_y][pos_x + dx0] = bits_pair[1] pos_y -= 1 @@ -840,16 +890,80 @@ def __encode_data(self): data_cw_count = self.__add_data(self.data) self.__add_mode_info(data_cw_count) + def add_text(self, img_obj, text, module_size=4): + """ + this method will add a single line of text to the top + of the aztec code. it is scaled to 90% of the area, + smaller images such as module size 4 do not scale properly.(on the list to fix) + However it is still functional. + + :param img_obj data : the aztezcode image object after being populated with data + :param text : the txt to be added at the top of the generated image + :param module_size : this will determine the size of the aztec code default is 4 + """ + fontsize = 1 + img_fraction = .90 + + aztec_size = img_obj.size + # temporary image to get base dimensions for new image + base = Image.new( + 'RGB', (aztec_size[0]+10, (aztec_size[1]+5)+fontsize+10), 'black') + new_image_size = base.size + myfont = ImageFont.truetype(self.my_font, fontsize) + # loop over font size and increase by 1 untill we reach 90% approximately + while myfont.getsize(text)[0] < img_fraction*base.size[0]: + fontsize += 1 + myfont = ImageFont.truetype(self.my_font, fontsize) + # reduce the font size by 1 if the module size == 4 + # reality is anything smaller than 4 will not turn out as expected. + # if module_size <= 4: + # fontsize -= 1 + # myfont = ImageFont.truetype(self.my_font,fontsize) + + # TODO redo the math on text placement and scaling + # PIL likes to operate from the top left so I ran with it for now + # create our new image and scale the height based on our new font size + image = Image.new( + 'RGB', (aztec_size[0]+10, (aztec_size[1]+5)+fontsize+10), 'white') + new_image_size = image.size + draw = ImageDraw.Draw(image) + # draw our text on our new image this math can be redone but its functional + draw.text((((aztec_size[0]/2+5)), (new_image_size[1] - aztec_size[1] - + (fontsize/2)-15)), text, font=myfont, fill='black', anchor='mm') + # copy the aztec code and store it + temp = img_obj.copy() + # simple math to place the aztec code at the bottom of our new image + # with a 5px border on the sides and across the bottom + w1 = (new_image_size[0] - aztec_size[0]-5) + h1 = ((new_image_size[1]) - aztec_size[1]-5) + # paste the aztec code + image.paste(temp, (w1, h1)) + # return our new image object + return image + def main(): + """ + Font was downloaded from: https://fonts.google.com/specimen/Roboto+Flex?query=Roboto + module size 4 results in a 1.056 sq. in. or 76x76 pixel aztec code with out text + with text it results in a W 1.194 x H 1.375 or 86x99 pixels aztec code. + When using a font file if you see : + + `cannot open resource` + + check that the path to your font file is correct + """ data = 'Aztec Code 2D :)' - aztec_code = AztecCode(data) + aztec_code = AztecCode(data, fontfile='font/RobotoFlex-VariableFont.ttf') + # aztec_code = AztecCode(data) aztec_code.print_out() if ImageDraw is None: print('PIL is not installed, cannot generate PNG') else: aztec_code.save('aztec_code.png', 4) - print('Aztec Code info: {0}x{0} {1}'.format(aztec_code.size, '(compact)' if aztec_code.compact else '')) + print('Aztec Code info: {0}x{0} {1}'.format( + aztec_code.size, '(compact)' if aztec_code.compact else '')) + print("To add text to the top of the aztec code simply add a font file:\naztec_code = AztecCode(data, fontfile = 'path/to/your/font.ttf')") if __name__ == '__main__': diff --git a/aztec_code_no_text.png b/aztec_code_no_text.png new file mode 100644 index 0000000..a0c0221 Binary files /dev/null and b/aztec_code_no_text.png differ diff --git a/aztec_code_with_text.png b/aztec_code_with_text.png new file mode 100644 index 0000000..7026571 Binary files /dev/null and b/aztec_code_with_text.png differ diff --git a/font/OFL.txt b/font/OFL.txt new file mode 100644 index 0000000..f6ebd5a --- /dev/null +++ b/font/OFL.txt @@ -0,0 +1,93 @@ +Copyright 2017 The Roboto Flex Project Authors (https://github.com/TypeNetwork/Roboto-Flex) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/font/README.txt b/font/README.txt new file mode 100644 index 0000000..b6dcc3a --- /dev/null +++ b/font/README.txt @@ -0,0 +1,74 @@ +Roboto Flex Variable Font +========================= + +This download contains Roboto Flex as both a variable font and a static font. + +Roboto Flex is a variable font with these axes: + GRAD + XTRA + YOPQ + YTAS + YTDE + YTFI + YTLC + YTUC + opsz + slnt + wdth + wght + +This means all the styles are contained in a single file: + RobotoFlex-VariableFont_GRAD,XTRA,YOPQ,YTAS,YTDE,YTFI,YTLC,YTUC,opsz,slnt,wdth,wght.ttf + +If your app fully supports variable fonts, you can now pick intermediate styles +that aren’t available as static fonts. Not all apps support variable fonts, and +in those cases you can use the static font file for Roboto Flex: + static/RobotoFlex/RobotoFlex-Regular.ttf + +Get started +----------- + +1. Install the font files you want to use + +2. Use your app's font picker to view the font family and all the +available styles + +Learn more about variable fonts +------------------------------- + + https://developers.google.com/web/fundamentals/design-and-ux/typography/variable-fonts + https://variablefonts.typenetwork.com + https://medium.com/variable-fonts + +In desktop apps + + https://theblog.adobe.com/can-variable-fonts-illustrator-cc + https://helpx.adobe.com/nz/photoshop/using/fonts.html#variable_fonts + +Online + + https://developers.google.com/fonts/docs/getting_started + https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Fonts/Variable_Fonts_Guide + https://developer.microsoft.com/en-us/microsoft-edge/testdrive/demos/variable-fonts + +Installing fonts + + MacOS: https://support.apple.com/en-us/HT201749 + Linux: https://www.google.com/search?q=how+to+install+a+font+on+gnu%2Blinux + Windows: https://support.microsoft.com/en-us/help/314960/how-to-install-or-remove-a-font-in-windows + +Android Apps + + https://developers.google.com/fonts/docs/android + https://developer.android.com/guide/topics/ui/look-and-feel/downloadable-fonts + +License +------- +Please read the full license text (OFL.txt) to understand the permissions, +restrictions and requirements for usage, redistribution, and modification. + +You can use them in your products & projects – print or digital, +commercial or otherwise. + +This isn't legal advice, please consider consulting a lawyer and see the full +license for all details. diff --git a/font/RobotoFlex-VariableFont.ttf b/font/RobotoFlex-VariableFont.ttf new file mode 100644 index 0000000..c39aafb Binary files /dev/null and b/font/RobotoFlex-VariableFont.ttf differ diff --git a/font/static/RobotoFlex/RobotoFlex-Regular.ttf b/font/static/RobotoFlex/RobotoFlex-Regular.ttf new file mode 100644 index 0000000..8f17876 Binary files /dev/null and b/font/static/RobotoFlex/RobotoFlex-Regular.ttf differ