Compare commits
	
		
			5 Commits
		
	
	
		
			adfd9b1ba4
			...
			e90da47cfa
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| e90da47cfa | |||
| 3f60573b25 | |||
| 0f0711ef2b | |||
| 61aef094df | |||
| 9ed1bfcadb | 
							
								
								
									
										109
									
								
								OpenFontFormat/OFF_io_utils.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										109
									
								
								OpenFontFormat/OFF_io_utils.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,109 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					from datetime import datetime, timedelta, timezone
 | 
				
			||||||
 | 
					from types import TracebackType
 | 
				
			||||||
 | 
					from typing import BinaryIO, Callable, List, Optional, Tuple, Type, TypeVar
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from io_utils import Parser, read_fixed_point, read_u16, read_u64
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def read_fixed(f: BinaryIO) -> float: # 16.16
 | 
				
			||||||
 | 
						return read_fixed_point(f, 16, 16)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def read_fixed_version(f: BinaryIO) -> float: # Not the same as parse_fixed
 | 
				
			||||||
 | 
						majorVersion = read_u16(f)
 | 
				
			||||||
 | 
						minorVersion = read_u16(f)
 | 
				
			||||||
 | 
						assert minorVersion in [0x0000, 0x1000, 0x5000], f"Invalid fixed minorVersion: {hex(minorVersion)}"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return majorVersion + minorVersion/0xa000 # will need to change if there are ever any versions with 2 decimal digits
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def read_F2DOT14(f: BinaryIO) -> float: # F2DOT14 (2.14)
 | 
				
			||||||
 | 
						return read_fixed_point(f, 2, 14)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					EPOCH = datetime(1904, 1, 1, tzinfo=timezone.utc)
 | 
				
			||||||
 | 
					def read_long_datetime(f: BinaryIO) -> datetime:
 | 
				
			||||||
 | 
						return EPOCH+timedelta(seconds=read_u64(f))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					T = TypeVar('T')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					The following `parse_at_...` functions all modify the tell of f, so it is recommended that you use `SaveTell` to wrap calling these.
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def parse_at_offset(f: BinaryIO, start_tell: int, offset: int, parser: Parser[T], *, zero_is_null:bool=True) -> T:
 | 
				
			||||||
 | 
						"""
 | 
				
			||||||
 | 
						Parses a `T` using `parser` at an offset of `offset` from `start_tell`. 
 | 
				
			||||||
 | 
						
 | 
				
			||||||
 | 
						If `zero_is_null` is True, then `offset` cannot be 0.
 | 
				
			||||||
 | 
						Only set `zero_is_null` to False when you are sure that 0 is a valid offset
 | 
				
			||||||
 | 
						"""
 | 
				
			||||||
 | 
						if zero_is_null: assert offset != 0, f"Offset was NULL"
 | 
				
			||||||
 | 
						f.seek(start_tell+offset)
 | 
				
			||||||
 | 
						return parser(f)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def parse_at_optional_offset(f: BinaryIO, start_tell: int, offset: int, parser: Parser[T]) -> Optional[T]:
 | 
				
			||||||
 | 
						"""
 | 
				
			||||||
 | 
						Same as `parse_at_offset`, however if the offset is NULL (0), then None is returned.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						Should not be used when 0 is a valid offset to something.
 | 
				
			||||||
 | 
						"""
 | 
				
			||||||
 | 
						if offset == 0: return None
 | 
				
			||||||
 | 
						return parse_at_offset(f, start_tell, offset, parser, zero_is_null=True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def parse_at_offsets(f: BinaryIO, start_tell: int, offsets: List[int], parser: Parser[T], *, zero_is_null:bool=True) -> List[T]:
 | 
				
			||||||
 | 
						return [parse_at_offset(f, start_tell, offset, parser, zero_is_null=zero_is_null) for offset in offsets]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def parse_at_optional_offsets(f: BinaryIO, start_tell: int, offsets: List[int], parser: Parser[T]) -> List[Optional[T]]:
 | 
				
			||||||
 | 
						return [parse_at_optional_offset(f, start_tell, offset, parser) for offset in offsets]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def parse_at_offsets_using_length(f: BinaryIO, start_tell: int, offsets: List[int], parser: Callable[[BinaryIO, int, int], T], *, zero_is_null:bool=True) -> List[Optional[T]]:
 | 
				
			||||||
 | 
						"""
 | 
				
			||||||
 | 
						The length of the returned list will be one less than that of `offsets`, as the last offset is used to calculate the length of the final element.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						`parser` is of the form `(f: BinaryIO, index: int, length: int) -> T`
 | 
				
			||||||
 | 
						"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						elements: List[Optional[T]] = []
 | 
				
			||||||
 | 
						for i, offset in enumerate(offsets[:-1]):
 | 
				
			||||||
 | 
							length = offsets[i+1]-offset
 | 
				
			||||||
 | 
							if length == 0:
 | 
				
			||||||
 | 
								elements.append(None)
 | 
				
			||||||
 | 
								continue
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							elements.append(parse_at_offset(f, start_tell, offset, lambda f: parser(f, i, length), zero_is_null=zero_is_null))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return elements
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def parse_list_at_offset(f: BinaryIO, start_tell: int, offset: int, count: int, parser: Parser[T], *, zero_is_null:bool=True) -> List[T]:
 | 
				
			||||||
 | 
						"""
 | 
				
			||||||
 | 
						Parses a continuous list of `T`s with `count` elements.
 | 
				
			||||||
 | 
						"""
 | 
				
			||||||
 | 
						return parse_at_offset(f, start_tell, offset, lambda f: [parser(f) for _ in range(count)], zero_is_null=zero_is_null)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def parse_list_and_use_offsets_into(f: BinaryIO, start_tell: int, offsets: list[int], count: int, parser: Parser[T]) -> Tuple[List[T], List[T]]:
 | 
				
			||||||
 | 
						elements = [(f.tell(), parser(f)) for _ in range(count)]
 | 
				
			||||||
 | 
						elements_by_offset_into: List[T] = []
 | 
				
			||||||
 | 
						for offset in offsets:
 | 
				
			||||||
 | 
							for (offset_into, element) in elements:
 | 
				
			||||||
 | 
								if offset + start_tell == offset_into:
 | 
				
			||||||
 | 
									elements_by_offset_into.append(element)
 | 
				
			||||||
 | 
									break
 | 
				
			||||||
 | 
							else:
 | 
				
			||||||
 | 
								assert False, (f"No element with offset {offset} into this list of elements", start_tell, offsets, elements)
 | 
				
			||||||
 | 
						return elements_by_offset_into, [element for (_, element) in elements]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class SaveTell:
 | 
				
			||||||
 | 
						"""
 | 
				
			||||||
 | 
						A context manager that allows operations to be done on a BinaryIO without affecting the tell.
 | 
				
			||||||
 | 
						"""
 | 
				
			||||||
 | 
						def __init__(self, f: BinaryIO):
 | 
				
			||||||
 | 
							self.f = f
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						def __enter__(self) -> int:
 | 
				
			||||||
 | 
							self.tell = self.f.tell()
 | 
				
			||||||
 | 
							return self.tell
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						def __exit__(self, exc_type: Optional[Type[BaseException]], exc_value: Optional[BaseException], exc_tb: Optional[TracebackType]):
 | 
				
			||||||
 | 
							self.f.seek(self.tell)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def null_if_zero(n: int) -> Optional[int]: return n if n else None
 | 
				
			||||||
 | 
					def nulls_if_zero(ns: List[int]) -> List[Optional[int]]: return list(map(null_if_zero, ns))
 | 
				
			||||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@ -1,4 +1,6 @@
 | 
				
			|||||||
class ABD:
 | 
					from abc import ABC, abstractmethod
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class ABD(ABC):
 | 
				
			||||||
	"""
 | 
						"""
 | 
				
			||||||
	#### Abstract Base Dataclass
 | 
						#### Abstract Base Dataclass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -9,11 +11,14 @@ class ABD:
 | 
				
			|||||||
			msg = f"Cannot instantiate an Abstract Base Dataclass: {self.__class__.__name__}"
 | 
								msg = f"Cannot instantiate an Abstract Base Dataclass: {self.__class__.__name__}"
 | 
				
			||||||
			raise TypeError(msg)
 | 
								raise TypeError(msg)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# TODO: Make a subclass of EnumMeta to do this
 | 
				
			||||||
class ABE:
 | 
					class ABE:
 | 
				
			||||||
	"""
 | 
						"""
 | 
				
			||||||
	#### Abstract Base Enum
 | 
						#### Abstract Base Enum
 | 
				
			||||||
	
 | 
						
 | 
				
			||||||
	This is for classes that will have an Enum subclass them
 | 
						This is for classes that will have an Enum subclass them.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						Do not implement a __init__ method for the class directly inheriting from ABE
 | 
				
			||||||
	"""
 | 
						"""
 | 
				
			||||||
	def __init__(self, *args, **kwargs):
 | 
						def __init__(self, *args, **kwargs):
 | 
				
			||||||
		if ABE in self.__class__.__bases__ or ABE == self:
 | 
							if ABE in self.__class__.__bases__ or ABE == self:
 | 
				
			||||||
 | 
				
			|||||||
@ -1,9 +1,23 @@
 | 
				
			|||||||
from typing import BinaryIO
 | 
					from io import SEEK_END
 | 
				
			||||||
 | 
					from typing import BinaryIO, Callable, Literal, TypeVar
 | 
				
			||||||
 | 
					
 | 
				
			||||||
ENDIANNESS = 'big'
 | 
					def len_to_end(f: BinaryIO) -> int:
 | 
				
			||||||
 | 
						curr_tell = f.tell()
 | 
				
			||||||
 | 
						f.seek(0, SEEK_END)
 | 
				
			||||||
 | 
						end_tell = f.tell()
 | 
				
			||||||
 | 
						f.seek(curr_tell)
 | 
				
			||||||
 | 
						return end_tell - curr_tell
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def read_int(f: BinaryIO, number: int, signed:bool=False) -> int: return int.from_bytes(f.read(number), ENDIANNESS, signed=signed)
 | 
					def is_at_end(f: BinaryIO) -> bool:
 | 
				
			||||||
def write_int(f: BinaryIO, value: int, number: int, signed:bool=False) -> int: return f.write(value.to_bytes(number, ENDIANNESS, signed=signed))
 | 
						return len_to_end(f) == 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					ENDIANNESS: Literal['little', 'big'] = 'big'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def read_int_from_bytes(s: bytes, *, signed:bool=False) -> int: return int.from_bytes(s, ENDIANNESS, signed=signed)
 | 
				
			||||||
 | 
					def bytes_from_int(value: int, number: int, *, signed:bool=False) -> bytes: return value.to_bytes(number, ENDIANNESS, signed=signed)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def read_int(f: BinaryIO, number: int, *, signed:bool=False) -> int: return read_int_from_bytes(f.read(number), signed=signed)
 | 
				
			||||||
 | 
					def write_int(f: BinaryIO, value: int, number: int, signed:bool=False) -> int: return f.write(bytes_from_int(value, number, signed=signed))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def read_u64(f: BinaryIO) -> int: return read_int(f, 8)
 | 
					def read_u64(f: BinaryIO) -> int: return read_int(f, 8)
 | 
				
			||||||
def read_u32(f: BinaryIO) -> int: return read_int(f, 4)
 | 
					def read_u32(f: BinaryIO) -> int: return read_int(f, 4)
 | 
				
			||||||
@ -11,7 +25,7 @@ def read_u24(f: BinaryIO) -> int: return read_int(f, 3)
 | 
				
			|||||||
def read_u16(f: BinaryIO) -> int: return read_int(f, 2)
 | 
					def read_u16(f: BinaryIO) -> int: return read_int(f, 2)
 | 
				
			||||||
def read_u8(f: BinaryIO) -> int: return read_int(f, 1)
 | 
					def read_u8(f: BinaryIO) -> int: return read_int(f, 1)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def write_u16(f: BinaryIO, value: int) -> int: return f.write(value)
 | 
					def write_u16(f: BinaryIO, value: int) -> int: return write_int(f, value, 2)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def read_i32(f: BinaryIO) -> int: return read_int(f, 4, signed=True)
 | 
					def read_i32(f: BinaryIO) -> int: return read_int(f, 4, signed=True)
 | 
				
			||||||
def read_i16(f: BinaryIO) -> int: return read_int(f, 2, signed=True)
 | 
					def read_i16(f: BinaryIO) -> int: return read_int(f, 2, signed=True)
 | 
				
			||||||
@ -23,9 +37,12 @@ def read_ascii(f: BinaryIO, number: int) -> str: return f.read(number).decode(en
 | 
				
			|||||||
def read_fixed_point(f: BinaryIO, preradix_bits: int, postradix_bits:int, *, signed:bool=True) -> float:
 | 
					def read_fixed_point(f: BinaryIO, preradix_bits: int, postradix_bits:int, *, signed:bool=True) -> float:
 | 
				
			||||||
	assert (preradix_bits+postradix_bits)%8 == 0
 | 
						assert (preradix_bits+postradix_bits)%8 == 0
 | 
				
			||||||
	raw = read_int(f, (preradix_bits+postradix_bits)//8, signed=signed)
 | 
						raw = read_int(f, (preradix_bits+postradix_bits)//8, signed=signed)
 | 
				
			||||||
	return raw/(1<<(postradix_bits))
 | 
						return raw/(1<<postradix_bits)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def read_pascal_string(f: BinaryIO) -> str:
 | 
					def read_pascal_string(f: BinaryIO) -> str:
 | 
				
			||||||
	string_size = read_int(f, 1)
 | 
						string_size = read_u8(f)
 | 
				
			||||||
	pascal_string = read_ascii(f, string_size)
 | 
						pascal_string = read_ascii(f, string_size)
 | 
				
			||||||
	return pascal_string
 | 
						return pascal_string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					T = TypeVar('T')
 | 
				
			||||||
 | 
					Parser = Callable[[BinaryIO], T]
 | 
				
			||||||
							
								
								
									
										136
									
								
								OpenFontFormat/renderer.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										136
									
								
								OpenFontFormat/renderer.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,136 @@
 | 
				
			|||||||
 | 
					import sys
 | 
				
			||||||
 | 
					from typing import Any, Generator, List, Optional, Tuple
 | 
				
			||||||
 | 
					from pyray import GRAY, GREEN, RED, WHITE, YELLOW, Color as Colour, ConfigFlags, KeyboardKey, MouseButton, Rectangle, TextureFilter, Vector2, begin_drawing, check_collision_point_rec, clear_background, close_window, draw_circle, draw_circle_3d, draw_circle_v, draw_line, draw_line_ex, draw_rectangle_lines_ex, draw_rectangle_rec, draw_spline_bezier_quadratic, draw_text, draw_texture_pro, end_drawing, gen_texture_mipmaps, get_char_pressed, get_color as get_colour, get_mouse_position, get_mouse_wheel_move, get_screen_height, get_screen_width, get_time, init_window, is_mouse_button_pressed, is_mouse_button_released, load_image, load_texture_from_image, set_config_flags, set_exit_key, set_target_fps, set_texture_filter, vector2_zero, window_should_close # type: ignore
 | 
				
			||||||
 | 
					from OpenFont import CompoundGlyph, Glyph, SimpleGlyph, SimpleGlyphFlag, TrueTypeOutlines, get_full_name, get_glyph_index, glyph_count, open_font_file
 | 
				
			||||||
 | 
					from slot import Slot
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import builtins
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					_program, file_path = sys.argv
 | 
				
			||||||
 | 
					file = open_font_file(file_path)
 | 
				
			||||||
 | 
					font_x_min, font_y_min, font_x_max, font_y_max = file.font_header.xMin, file.font_header.yMin, file.font_header.xMax, file.font_header.yMax
 | 
				
			||||||
 | 
					SCALE = 0.5
 | 
				
			||||||
 | 
					font_x_range = font_x_max-font_x_min
 | 
				
			||||||
 | 
					font_y_range = font_y_max-font_y_min
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					SCREEN_WIDTH = int(font_x_range*SCALE)
 | 
				
			||||||
 | 
					SCREEN_HEIGHT = int(font_y_range*SCALE)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					COLOUR_BACKGROUND = get_colour(0x181818FF)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def middle_point(point1: Tuple[int, int], point2: Tuple[int, int]) -> Tuple[int, int]:
 | 
				
			||||||
 | 
						(x0, y0), (x1, y1) = point1, point2
 | 
				
			||||||
 | 
						return (x0+x1)//2, (y0+y1)//2
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def triple_iter(coordinates: List[Tuple[int, int]], flags: List[SimpleGlyphFlag]) -> Generator[Tuple[Tuple[int,int], Tuple[int,int], Tuple[int,int]], None, None]:
 | 
				
			||||||
 | 
						assert flags[0].on_curve_point()
 | 
				
			||||||
 | 
						coordinates_and_flags = list(zip(coordinates, flags))
 | 
				
			||||||
 | 
						all_points: List[Tuple[int, int]] = []
 | 
				
			||||||
 | 
						for (point1, flag1), (point2, flag2) in zip(coordinates_and_flags,coordinates_and_flags[1:]+[coordinates_and_flags[0]]):
 | 
				
			||||||
 | 
							all_points.append(point1)
 | 
				
			||||||
 | 
							if flag1.on_curve_point() == flag2.on_curve_point(): all_points.append(middle_point(point1, point2))
 | 
				
			||||||
 | 
						all_points.append(point2)
 | 
				
			||||||
 | 
						assert len(all_points) % 2 != 0
 | 
				
			||||||
 | 
						for i in range(1,len(all_points),2):
 | 
				
			||||||
 | 
							yield all_points[i-1], all_points[i], all_points[i+1]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def draw_bounding_box(slot: Slot, glyph: Glyph, colour: Colour=RED):
 | 
				
			||||||
 | 
						bounding_rect = Rectangle(glyph.xMin, glyph.yMin, glyph.xMax-glyph.xMin, glyph.yMax-glyph.yMin)
 | 
				
			||||||
 | 
						draw_rectangle_lines_ex(slot.r(bounding_rect), 1, colour)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def render_simple_glyph(slot: Slot, glyph: SimpleGlyph):
 | 
				
			||||||
 | 
						"""
 | 
				
			||||||
 | 
						Renders a simple glyph into a slot in coordinate space
 | 
				
			||||||
 | 
						"""
 | 
				
			||||||
 | 
						draw_bounding_box(slot, glyph)
 | 
				
			||||||
 | 
						for prev_end_point, end_point in zip([-1]+glyph.endPtsOfContours, glyph.endPtsOfContours):
 | 
				
			||||||
 | 
							for point1, point2, point3 in triple_iter(glyph.coordinates[prev_end_point+1:end_point+1], glyph.flags[prev_end_point+1:end_point+1]):
 | 
				
			||||||
 | 
								draw_spline_bezier_quadratic([slot.vxy(*point1), slot.vxy(*point2), slot.vxy(*point3)], 3, 2, WHITE)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def render_box_and_get_slot(slot: Slot) -> Tuple[Slot, Slot]:
 | 
				
			||||||
 | 
						"""
 | 
				
			||||||
 | 
						Renders the outer box and axes lines into a slot in UV space and returns the outer box slot and the slot in coordinate space
 | 
				
			||||||
 | 
						"""
 | 
				
			||||||
 | 
						with slot.padding(0.025, 0.025) as slot:
 | 
				
			||||||
 | 
							draw_rectangle_lines_ex(slot.r(), 1, GRAY)
 | 
				
			||||||
 | 
							outer_slot = slot
 | 
				
			||||||
 | 
						with slot.padding(0.05, 0.05).with_ratio(font_x_range/font_y_range).remap(font_x_min, font_y_max, font_x_max, font_y_min) as slot:
 | 
				
			||||||
 | 
							draw_line_ex(slot.vxy(font_x_min, 0), slot.vxy(font_x_max, 0), 1, WHITE)
 | 
				
			||||||
 | 
							draw_line_ex(slot.vxy(0, font_y_min), slot.vxy(0, font_y_max), 1, WHITE)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return outer_slot, slot
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def render_glyph(slot: Slot, glyphID: int):
 | 
				
			||||||
 | 
						# TODO: Keep the current display mode as a 'wireframe' mode and allow user to switch between normal and wireframe modes
 | 
				
			||||||
 | 
						"""
 | 
				
			||||||
 | 
						Renders a glyph into a slot in coordinates space
 | 
				
			||||||
 | 
						"""
 | 
				
			||||||
 | 
						assert isinstance(file.outlines, TrueTypeOutlines)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						glyph = file.outlines.glyph_data.glyphs[glyphID]
 | 
				
			||||||
 | 
						match glyph:
 | 
				
			||||||
 | 
							case SimpleGlyph(): render_simple_glyph(slot, glyph)
 | 
				
			||||||
 | 
							case CompoundGlyph(components=components):
 | 
				
			||||||
 | 
								draw_bounding_box(slot, glyph)
 | 
				
			||||||
 | 
								for component in components:
 | 
				
			||||||
 | 
									# TODO: Scaling
 | 
				
			||||||
 | 
									# arg1 and arg2 will also need to be scaled, depending on the values of scaled_component_offset and unscaled_component_offset
 | 
				
			||||||
 | 
									assert component.scaling is None, f"Unimplemented: {component.scaling}"
 | 
				
			||||||
 | 
									assert component.flag.args_are_xy_values(), "Unimplemented"
 | 
				
			||||||
 | 
									# assert not component.flag.round_xy_to_grid(), ("Unimplemented", component.argument1, component.argument2)
 | 
				
			||||||
 | 
									render_glyph(slot.translate(component.argument1, component.argument2), component.glyphIndex)
 | 
				
			||||||
 | 
							case None: pass # TODO: Render bounding box of empty glyphs
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					set_config_flags(ConfigFlags.FLAG_WINDOW_ALWAYS_RUN | ConfigFlags.FLAG_WINDOW_RESIZABLE)
 | 
				
			||||||
 | 
					init_window(SCREEN_WIDTH, SCREEN_HEIGHT, get_full_name(file))
 | 
				
			||||||
 | 
					set_target_fps(60)
 | 
				
			||||||
 | 
					set_exit_key(KeyboardKey.KEY_NULL)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					COLS = 8
 | 
				
			||||||
 | 
					ROWS = 8
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					scroll_y: float = 0
 | 
				
			||||||
 | 
					MAX_SCROLL_IN_SCREEN_HEIGHTS = (glyph_count(file)+COLS-1)//COLS/ROWS-1
 | 
				
			||||||
 | 
					SCROLL_SPEED = 10
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def rectangle_is_visible(rect: Rectangle) -> bool:
 | 
				
			||||||
 | 
						return -rect.width <= rect.x <= get_screen_width() and -rect.height <= rect.y <= get_screen_height()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def clamp_scroll(scroll_y: float) -> float:
 | 
				
			||||||
 | 
						return max(min(scroll_y,MAX_SCROLL_IN_SCREEN_HEIGHTS),0)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					display_in_detail: Optional[int] = None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					while not window_should_close():
 | 
				
			||||||
 | 
						scroll_y -= get_mouse_wheel_move()*SCROLL_SPEED/get_screen_height()
 | 
				
			||||||
 | 
						scroll_y = clamp_scroll(scroll_y)
 | 
				
			||||||
 | 
						begin_drawing()
 | 
				
			||||||
 | 
						clear_background(COLOUR_BACKGROUND)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						with Slot.get_root_slot() as root:
 | 
				
			||||||
 | 
							curr_display_detail = display_in_detail
 | 
				
			||||||
 | 
							with root.translate(0, -scroll_y) as slot:
 | 
				
			||||||
 | 
								for i, slot in enumerate(slot.extending_grid(ROWS, COLS, -1, total_slots=glyph_count(file))):
 | 
				
			||||||
 | 
									if not rectangle_is_visible(slot.r()): continue
 | 
				
			||||||
 | 
									outer_slot, glyph_slot = render_box_and_get_slot(slot)
 | 
				
			||||||
 | 
									render_glyph(glyph_slot, i)
 | 
				
			||||||
 | 
									if display_in_detail is None and is_mouse_button_released(MouseButton.MOUSE_BUTTON_LEFT):
 | 
				
			||||||
 | 
										if check_collision_point_rec(get_mouse_position(), outer_slot.r()): display_in_detail = i
 | 
				
			||||||
 | 
							if curr_display_detail is not None:
 | 
				
			||||||
 | 
								with root.padding(0.2, 0.2) as slot:
 | 
				
			||||||
 | 
									draw_rectangle_rec(slot.r(), COLOUR_BACKGROUND)
 | 
				
			||||||
 | 
									outer_slot, glyph_slot = render_box_and_get_slot(slot)
 | 
				
			||||||
 | 
									render_glyph(glyph_slot, curr_display_detail)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									if is_mouse_button_released(MouseButton.MOUSE_BUTTON_LEFT): display_in_detail = None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							# TODO: Use glfw_set_char_callback for this instead
 | 
				
			||||||
 | 
							char = get_char_pressed()
 | 
				
			||||||
 | 
							while char > 0:
 | 
				
			||||||
 | 
								display_in_detail = get_glyph_index(file, chr(char))
 | 
				
			||||||
 | 
								char = get_char_pressed()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						# draw_text(f"{scroll_y}", 50, 50, 50, GREEN) # debug for scrolling
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						end_drawing()
 | 
				
			||||||
 | 
					close_window()
 | 
				
			||||||
							
								
								
									
										96
									
								
								OpenFontFormat/slot.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										96
									
								
								OpenFontFormat/slot.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,96 @@
 | 
				
			|||||||
 | 
					from typing import Callable
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from pyray import Rectangle, Vector2, get_screen_height, get_screen_width # type: ignore
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Map = Callable[[float], float]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Slot:
 | 
				
			||||||
 | 
						def __init__(self, x_map: Map, y_map: Map):
 | 
				
			||||||
 | 
							self.x = x_map
 | 
				
			||||||
 | 
							self.y = y_map
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						def v(self, v: Vector2) -> Vector2:
 | 
				
			||||||
 | 
							return Vector2(self.x(v.x), self.y(v.y))
 | 
				
			||||||
 | 
						
 | 
				
			||||||
 | 
						def __enter__(self): return self
 | 
				
			||||||
 | 
						def __exit__(self, exc_type, exc_value, trace): pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						def vxy(self, x: float, y: float) -> Vector2:
 | 
				
			||||||
 | 
							return self.v(Vector2(x, y))
 | 
				
			||||||
 | 
						
 | 
				
			||||||
 | 
						def r(self, r: Rectangle=Rectangle(0,0,1,1)) -> Rectangle:
 | 
				
			||||||
 | 
							x0 = self.x(r.x)
 | 
				
			||||||
 | 
							y0 = self.y(r.y)
 | 
				
			||||||
 | 
							x1 = self.x(r.x+r.width)
 | 
				
			||||||
 | 
							y1 = self.y(r.y+r.height)
 | 
				
			||||||
 | 
							x_min = min(x0, x1)
 | 
				
			||||||
 | 
							y_min = min(y0, y1)
 | 
				
			||||||
 | 
							x_max = max(x0, x1)
 | 
				
			||||||
 | 
							y_max = max(y0, y1)
 | 
				
			||||||
 | 
							return Rectangle(x_min, y_min, x_max-x_min, y_max-y_min)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@staticmethod
 | 
				
			||||||
 | 
						def get_root_slot(*, in_uv:bool=True) -> 'Slot':
 | 
				
			||||||
 | 
							"""
 | 
				
			||||||
 | 
							Returns the root slot for the window
 | 
				
			||||||
 | 
							"""
 | 
				
			||||||
 | 
							screen_width = get_screen_width()
 | 
				
			||||||
 | 
							screen_height = get_screen_height()
 | 
				
			||||||
 | 
							if in_uv:
 | 
				
			||||||
 | 
								return Slot(lambda x: x*screen_width, lambda y: y*screen_height)
 | 
				
			||||||
 | 
							else:
 | 
				
			||||||
 | 
								return Slot(lambda x: x, lambda y: y)
 | 
				
			||||||
 | 
						
 | 
				
			||||||
 | 
						def between(self, x0: float, y0: float, x1: float, y1: float) -> 'Slot':
 | 
				
			||||||
 | 
							return Slot(lambda x: self.x(x*(x1-x0)+x0), lambda y: self.y(y*(y1-y0)+y0))
 | 
				
			||||||
 | 
						
 | 
				
			||||||
 | 
						def remap(self, x0: float, y0: float, x1: float, y1: float) -> 'Slot':
 | 
				
			||||||
 | 
							"""
 | 
				
			||||||
 | 
							Returns the slot with the same region on the window but using a different coordinate space
 | 
				
			||||||
 | 
							"""
 | 
				
			||||||
 | 
							return Slot(lambda x: self.x((x-x0)/(x1-x0)), lambda y: self.y((y-y0)/(y1-y0)))
 | 
				
			||||||
 | 
						
 | 
				
			||||||
 | 
						def grid(self, rows: int, cols: int, *, total_slots:int=-1) -> 'list[Slot]':
 | 
				
			||||||
 | 
							return self.extending_grid(rows, cols, rows, total_slots=total_slots)
 | 
				
			||||||
 | 
						
 | 
				
			||||||
 | 
						def extending_grid(self, rows: int, cols: int, total_rows: int, *, total_slots:int=-1) -> 'list[Slot]':
 | 
				
			||||||
 | 
							slots: list[Slot] = []
 | 
				
			||||||
 | 
							if total_rows == -1:
 | 
				
			||||||
 | 
								assert total_slots != -1
 | 
				
			||||||
 | 
								total_rows = (total_slots+cols-1)//cols
 | 
				
			||||||
 | 
							for j in range(total_rows):
 | 
				
			||||||
 | 
								for i in range(cols):
 | 
				
			||||||
 | 
									slots.append(self.between(i/cols, j/rows, (i+1)/cols, (j+1)/rows))
 | 
				
			||||||
 | 
							if total_slots != -1: return slots[:total_slots]
 | 
				
			||||||
 | 
							return slots
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						
 | 
				
			||||||
 | 
						def translate(self, x0: float, y0: float) -> 'Slot':
 | 
				
			||||||
 | 
							return self.between(x0, y0, x0+1, y0+1)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						
 | 
				
			||||||
 | 
						def square(self) -> 'Slot':
 | 
				
			||||||
 | 
							"""
 | 
				
			||||||
 | 
							Returns the square slot that takes up as much space as possible within parent slot
 | 
				
			||||||
 | 
							"""
 | 
				
			||||||
 | 
							return self.with_ratio(1)
 | 
				
			||||||
 | 
							
 | 
				
			||||||
 | 
						def with_ratio(self, width_to_height_ratio: float) -> 'Slot':
 | 
				
			||||||
 | 
							width = self.x(1) - self.x(0)
 | 
				
			||||||
 | 
							height = self.y(1) - self.y(0)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if width == 0 or height == 0: return Slot(lambda x: self.x(0) + width/2, lambda y: self.y(0) + height/2)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if width/height > width_to_height_ratio:
 | 
				
			||||||
 | 
								ratio = height/width*width_to_height_ratio
 | 
				
			||||||
 | 
								left = (1-ratio)/2
 | 
				
			||||||
 | 
								return self.between(left, 0, 1-left, 1)
 | 
				
			||||||
 | 
							else:
 | 
				
			||||||
 | 
								ratio = width/height/width_to_height_ratio
 | 
				
			||||||
 | 
								up = (1-ratio)/2
 | 
				
			||||||
 | 
								return self.between(0, up, 1, 1-up)
 | 
				
			||||||
 | 
							
 | 
				
			||||||
 | 
						def padding(self, horz_padding: float, vert_padding: float, *, width:float=1, height:float=1) -> 'Slot':
 | 
				
			||||||
 | 
							return self.between(horz_padding, vert_padding, width-horz_padding, height-horz_padding)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -1,9 +1,9 @@
 | 
				
			|||||||
#!/usr/bin/env python3
 | 
					#!/usr/bin/env python3
 | 
				
			||||||
import os
 | 
					import os
 | 
				
			||||||
import sys
 | 
					import sys
 | 
				
			||||||
assert len(sys.argv) == 2, "usage: python3 test.py <test>"
 | 
					assert len(sys.argv) >= 2, "usage: python3 test.py <test> [OPTIONS]"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from OpenFont import FontSpecificNameID, NameID, NameTable_Format_0, OpenFontFile, PredefinedNameID, TrueTypeOutlines, open_font_file, write_font_file
 | 
					from OpenFont import FontSpecificNameID, NameID, NameTable_Format_0, OpenFontFile, PredefinedNameID, TrueTypeOutlines, open_font_file
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def search_names(font: OpenFontFile, nameID: NameID) -> str:
 | 
					def search_names(font: OpenFontFile, nameID: NameID) -> str:
 | 
				
			||||||
	assert isinstance(font.naming_table, NameTable_Format_0)
 | 
						assert isinstance(font.naming_table, NameTable_Format_0)
 | 
				
			||||||
@ -15,7 +15,7 @@ def search_names(font: OpenFontFile, nameID: NameID) -> str:
 | 
				
			|||||||
	assert False, f"Name not found: {nameID}"
 | 
						assert False, f"Name not found: {nameID}"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
_, test = sys.argv
 | 
					_, test, *options = sys.argv
 | 
				
			||||||
 | 
					
 | 
				
			||||||
match test:
 | 
					match test:
 | 
				
			||||||
	case "names":
 | 
						case "names":
 | 
				
			||||||
@ -30,11 +30,6 @@ match test:
 | 
				
			|||||||
				axis_names = [search_names(font, FontSpecificNameID(axis.axisNameID)) for axis in font.font_variations.font_variations.axes]
 | 
									axis_names = [search_names(font, FontSpecificNameID(axis.axisNameID)) for axis in font.font_variations.font_variations.axes]
 | 
				
			||||||
				num_instances = font.font_variations.font_variations.instanceCount
 | 
									num_instances = font.font_variations.font_variations.instanceCount
 | 
				
			||||||
				print(f"\tAxes: [{', '.join(axis_names)}] ({num_instances} instances)")
 | 
									print(f"\tAxes: [{', '.join(axis_names)}] ({num_instances} instances)")
 | 
				
			||||||
	case "rewrite":
 | 
					 | 
				
			||||||
		def test_font(font: OpenFontFile):
 | 
					 | 
				
			||||||
			PATH = "out.ttf"
 | 
					 | 
				
			||||||
			write_font_file(font, PATH)
 | 
					 | 
				
			||||||
			open_font_file(PATH)
 | 
					 | 
				
			||||||
	case _:
 | 
						case _:
 | 
				
			||||||
		assert False, f"Invalid test: '{test}'"
 | 
							assert False, f"Invalid test: '{test}'"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -44,13 +39,17 @@ if not os.path.exists(COMPLETED_PATH):
 | 
				
			|||||||
with open(COMPLETED_PATH, "r") as f: completed = f.read().split('\n')
 | 
					with open(COMPLETED_PATH, "r") as f: completed = f.read().split('\n')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def do_font(file: str):
 | 
					def do_font(file: str):
 | 
				
			||||||
 | 
						file = file.strip()
 | 
				
			||||||
	if file in completed: return
 | 
						if file in completed: return
 | 
				
			||||||
	try:
 | 
						try:
 | 
				
			||||||
		font = open_font_file(file)
 | 
							font = open_font_file(file)
 | 
				
			||||||
		test_font(font)
 | 
							test_font(font)
 | 
				
			||||||
	except AssertionError as err:
 | 
						except AssertionError as err:
 | 
				
			||||||
 | 
							if '--raise' in options:
 | 
				
			||||||
			err.add_note(f"Failed: {file}")
 | 
								err.add_note(f"Failed: {file}")
 | 
				
			||||||
			raise err
 | 
								raise err
 | 
				
			||||||
 | 
							print(f"{file}{':' if '--no-colon' not in options else ''} {err}")
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
	with open(COMPLETED_PATH, 'a') as f: f.write(file+'\n')
 | 
						with open(COMPLETED_PATH, 'a') as f: f.write(file+'\n')
 | 
				
			||||||
	completed.append(file)
 | 
						completed.append(file)
 | 
				
			||||||
	
 | 
						
 | 
				
			||||||
@ -58,5 +57,3 @@ assert not sys.stdin.isatty(), f"Do not run this program directly, instead pipe
 | 
				
			|||||||
for line in sys.stdin:
 | 
					for line in sys.stdin:
 | 
				
			||||||
	file = line.rstrip('\n')
 | 
						file = line.rstrip('\n')
 | 
				
			||||||
	do_font(file)
 | 
						do_font(file)
 | 
				
			||||||
 | 
					 | 
				
			||||||
print("Done!")
 | 
					 | 
				
			||||||
		Loading…
	
		Reference in New Issue
	
	Block a user