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
|
||||
|
||||
@ -9,11 +11,14 @@ class ABD:
|
||||
msg = f"Cannot instantiate an Abstract Base Dataclass: {self.__class__.__name__}"
|
||||
raise TypeError(msg)
|
||||
|
||||
# TODO: Make a subclass of EnumMeta to do this
|
||||
class ABE:
|
||||
"""
|
||||
#### 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):
|
||||
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 write_int(f: BinaryIO, value: int, number: int, signed:bool=False) -> int: return f.write(value.to_bytes(number, ENDIANNESS, signed=signed))
|
||||
def is_at_end(f: BinaryIO) -> bool:
|
||||
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_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_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_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:
|
||||
assert (preradix_bits+postradix_bits)%8 == 0
|
||||
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:
|
||||
string_size = read_int(f, 1)
|
||||
string_size = read_u8(f)
|
||||
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
|
||||
import os
|
||||
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:
|
||||
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}"
|
||||
|
||||
|
||||
_, test = sys.argv
|
||||
_, test, *options = sys.argv
|
||||
|
||||
match test:
|
||||
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]
|
||||
num_instances = font.font_variations.font_variations.instanceCount
|
||||
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 _:
|
||||
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')
|
||||
|
||||
def do_font(file: str):
|
||||
file = file.strip()
|
||||
if file in completed: return
|
||||
try:
|
||||
font = open_font_file(file)
|
||||
test_font(font)
|
||||
except AssertionError as err:
|
||||
err.add_note(f"Failed: {file}")
|
||||
raise err
|
||||
if '--raise' in options:
|
||||
err.add_note(f"Failed: {file}")
|
||||
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')
|
||||
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:
|
||||
file = line.rstrip('\n')
|
||||
do_font(file)
|
||||
|
||||
print("Done!")
|
Loading…
Reference in New Issue
Block a user