Skip to content

Commit a44f2f0

Browse files
committed
Font selection, some internal changes
1 parent a5b38f2 commit a44f2f0

10 files changed

+428
-220
lines changed

AnsiGraphics.py

+57-26
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import numpy as np
2+
import PIL
23

34
class AnsiGraphics:
45
"""
56
This class manages ansi fonts and colours.
67
"""
78

8-
# VGA Palette
9-
VGA_PAL = [
9+
# CGA Palette
10+
CGA_PAL = [
1011
np.array([0, 0, 0]) / 255.0,
1112
np.array([170, 0, 0]) / 255.0,
1213
np.array([0, 170, 0]) / 255.0,
@@ -25,30 +26,54 @@ class AnsiGraphics:
2526
np.array([255, 255, 255]) / 255.0
2627
];
2728

28-
def __init__(self, font_file):
29+
def __init__(self, font_file, char_size_x = 8, char_size_y = 16):
2930
"""
3031
Reads the font to use and prepares it for usage.
3132
Fonts are files with a sequence of bits specifying 8x16 characters.
3233
"""
33-
# Read the font
34-
f = open(font_file, "rb")
35-
in_bits = []
36-
try:
37-
byte = f.read(1)
38-
while len(byte) != 0:
39-
bin_str = bin(int.from_bytes(byte, 'little'))[2:]
40-
for bit in bin_str.zfill(8):
41-
in_bits.append(bit)
42-
byte = f.read(1)
43-
finally:
44-
f.close()
34+
# Character sizes
35+
self.char_size_x = char_size_x
36+
self.char_size_y = char_size_y
4537

46-
# Split into chars
47-
self.font_chars = []
48-
for i in range(0, 256):
49-
char_bits = np.array(in_bits[i * 16 * 8 : (i + 1) * 16 * 8])
50-
char_bits = char_bits.reshape(16, 8).astype(float)
51-
self.font_chars.append(char_bits)
38+
if font_file.endswith(".fnt"):
39+
# Read the font
40+
f = open(font_file, "rb")
41+
in_bits = []
42+
try:
43+
byte = f.read(1)
44+
while len(byte) != 0:
45+
bin_str = bin(int.from_bytes(byte, 'little'))[2:]
46+
for bit in bin_str.zfill(8):
47+
in_bits.append(bit)
48+
byte = f.read(1)
49+
finally:
50+
f.close()
51+
52+
# Split into chars
53+
self.font_chars = []
54+
for i in range(0, 256):
55+
char_bits = np.array(in_bits[i * self.char_size_y * self.char_size_x : (i + 1) * self.char_size_y * self.char_size_x])
56+
char_bits = char_bits.reshape(self.char_size_y, self.char_size_x).astype(float)
57+
self.font_chars.append(char_bits)
58+
else:
59+
font_pic = PIL.Image.open(font_file).convert('RGB')
60+
font_arr = np.array(font_pic)
61+
font_arr = font_arr[:,:,0]
62+
font_arr[font_arr != 0] = 1
63+
64+
image_rows = font_pic.width // char_size_x
65+
image_cols = font_pic.height // char_size_y
66+
67+
if image_rows * image_cols != 256:
68+
raise ValueError("Font must have 256 characters.")
69+
70+
self.font_chars = []
71+
for row in range(image_rows):
72+
for col in range(image_cols):
73+
self.font_chars.append(font_arr[
74+
row * self.char_size_y : (row + 1) * self.char_size_y,
75+
col * self.char_size_x : (col + 1) * self.char_size_x
76+
])
5277

5378
# It's 2018 and memory is cheap, so lets precompute every possible fore/back/char combination
5479
self.colour_chars = []
@@ -59,17 +84,23 @@ def __init__(self, font_file):
5984
for bg_idx in range(0, 16):
6085
char_base = self.font_chars[char_idx][:]
6186
char_col = np.zeros((char_base.shape[0], char_base.shape[1], 3))
62-
char_col[np.where(char_base == 1)] = AnsiGraphics.VGA_PAL[fg_idx]
63-
char_col[np.where(char_base == 0)] = AnsiGraphics.VGA_PAL[bg_idx]
87+
char_col[np.where(char_base == 1)] = AnsiGraphics.CGA_PAL[fg_idx]
88+
char_col[np.where(char_base == 0)] = AnsiGraphics.CGA_PAL[bg_idx]
6489
bg_chars.append(char_col)
6590
fg_chars.append(bg_chars)
6691
self.colour_chars.append(fg_chars)
67-
68-
def vga_colour(self, pal_idx):
92+
93+
def get_char_size(self):
94+
"""
95+
Return the character size, in pixels
96+
"""
97+
return (self.char_size_x, self.char_size_y)
98+
99+
def cga_colour(self, pal_idx):
69100
"""
70101
Returns the VGA palette colour with the given index as an RGP triplet.
71102
"""
72-
return AnsiGraphics.VGA_PAL[idx]
103+
return AnsiGraphics.CGA_PAL[idx]
73104

74105
def char_bitmap(self, char_idx):
75106
"""

AnsiImage.py

+63-42
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,7 @@ class AnsiImage:
77
"""
88
Manages a rectangular image made of ansi character cells
99
"""
10-
11-
CHAR_SIZE_X = 8
12-
CHAR_SIZE_Y = 16
13-
14-
def __init__(self, min_line_len = None):
10+
def __init__(self, graphics, min_line_len = None):
1511
"""
1612
Optionally allows the specification of a minimum
1713
line length, used when loading
@@ -25,6 +21,10 @@ def __init__(self, min_line_len = None):
2521
self.is_dirty = False
2622
self.write_allowed = [True, True, True]
2723

24+
self.ansi_graphics = graphics
25+
self.char_size_x = self.ansi_graphics.get_char_size()[0]
26+
self.char_size_y = self.ansi_graphics.get_char_size()[1]
27+
2828
# Selection related stuff
2929
self.selection = None
3030
self.selection_preliminary = set()
@@ -35,20 +35,40 @@ def __init__(self, min_line_len = None):
3535
self.cache_params = None
3636

3737
# The cursor
38+
self.rebuild_cursor()
39+
40+
def get_size(self):
41+
"""
42+
Return the current canvas size in characters
43+
"""
44+
return (self.width, self.height)
45+
46+
def rebuild_cursor(self):
3847
self.cursor_shape = []
39-
for y in [0, 1, 14, 15]:
40-
for x in range(0, 8):
48+
for y in [0, 1, self.char_size_y - 2, self.char_size_y - 1]:
49+
for x in range(0, self.char_size_x):
4150
self.cursor_shape.append((y, x))
42-
for y in range(2, 14):
43-
for x in [0, 1, 6, 7]:
51+
for y in range(2, self.char_size_y - 2):
52+
for x in [0, 1, self.char_size_x - 2, self.char_size_x - 1]:
4453
self.cursor_shape.append((y, x))
54+
4555

46-
def get_size(self):
56+
def get_char_size(self):
4757
"""
48-
Return the current canvas size in characters
58+
Return the character size, in pixels
4959
"""
50-
return (self.width, self.height)
60+
return (self.char_size_x, self.char_size_y)
5161

62+
def change_graphics(self, new_graphics):
63+
"""
64+
Changes the graphics in use
65+
"""
66+
self.ansi_graphics = new_graphics
67+
self.char_size_x = self.ansi_graphics.get_char_size()[0]
68+
self.char_size_y = self.ansi_graphics.get_char_size()[1]
69+
self.have_cache = False
70+
self.rebuild_cursor()
71+
5272
def change_size(self, new_width, new_height, new_state = None):
5373
"""
5474
Make the image larger or smaller, retaining contents.
@@ -125,7 +145,7 @@ def set_selection(self, new_selection_initial = None, append = False, remove = F
125145
self.redraw_set.update(self.selection)
126146
self.selection = None
127147

128-
def get_selected(self, selection = None):
148+
def get_selected(self, skip_space = False, selection = None):
129149
"""
130150
Returns the selected characters, offset-augmnented
131151
"""
@@ -140,7 +160,8 @@ def get_selected(self, selection = None):
140160
for x, y in selection:
141161
min_x = min(x, min_x)
142162
min_y = min(y, min_y)
143-
selected.append([x, y, self.ansi_image[y][x]])
163+
if not skip_space or self.ansi_image[y][x][0] != ord(' '):
164+
selected.append([x, y, self.ansi_image[y][x]])
144165

145166
for sel_item in selected:
146167
sel_item[0] -= min_x
@@ -636,15 +657,15 @@ def save_ans(self, out_path):
636657
with open(out_path, "wb") as f:
637658
f.write(self.to_ans())
638659

639-
def to_bitmap(self, ansi_graphics, transparent = False, cursor = False, area = None):
660+
def to_bitmap(self, transparent = False, cursor = False, area = None):
640661
"""
641662
Returns pixel representation of this image as a PIL Image object
642663
643664
Can be passed an area. If so, only character cells overlapping the requested area will be
644665
drawn. In this case the return value is a tuple of (real x start, real y start, bitmap image, actual size w, actual size h)
645666
"""
646667
if self.have_cache == False or self.cache_params != [transparent, cursor]:
647-
self.ansi_bitmap = np.ones((AnsiImage.CHAR_SIZE_Y * self.height, AnsiImage.CHAR_SIZE_X * self.width, 4))
668+
self.ansi_bitmap = np.ones((self.char_size_y * self.height, self.char_size_x * self.width, 4))
648669
for y in range(0, self.height):
649670
for x in range(0, self.width):
650671
self.redraw_set.add((x, y))
@@ -656,33 +677,33 @@ def to_bitmap(self, ansi_graphics, transparent = False, cursor = False, area = N
656677
continue
657678

658679
char_info = self.ansi_image[y][x]
659-
char_col = ansi_graphics.coloured_char(char_info[0], char_info[1], char_info[2])
680+
char_col = self.ansi_graphics.coloured_char(char_info[0], char_info[1], char_info[2])
660681
self.ansi_bitmap[
661-
AnsiImage.CHAR_SIZE_Y * y : AnsiImage.CHAR_SIZE_Y * (y + 1),
662-
AnsiImage.CHAR_SIZE_X * x : AnsiImage.CHAR_SIZE_X * (x + 1),
682+
self.char_size_y * y : self.char_size_y * (y + 1),
683+
self.char_size_x * x : self.char_size_x * (x + 1),
663684
0:3
664685
] = char_col
665686

666687
# Make pixels transparent or no
667688
if transparent == True and char_info[0] == ord(' '):
668689
self.ansi_bitmap[
669-
AnsiImage.CHAR_SIZE_Y * y : AnsiImage.CHAR_SIZE_Y * (y + 1),
670-
AnsiImage.CHAR_SIZE_X * x : AnsiImage.CHAR_SIZE_X * (x + 1),
690+
self.char_size_y * y : self.char_size_y * (y + 1),
691+
self.char_size_x * x : self.char_size_x * (x + 1),
671692
3
672-
] = np.zeros((AnsiImage.CHAR_SIZE_Y, AnsiImage.CHAR_SIZE_X))
693+
] = np.zeros((self.char_size_y, self.char_size_x))
673694
else:
674695
self.ansi_bitmap[
675-
AnsiImage.CHAR_SIZE_Y * y : AnsiImage.CHAR_SIZE_Y * (y + 1),
676-
AnsiImage.CHAR_SIZE_X * x : AnsiImage.CHAR_SIZE_X * (x + 1),
696+
self.char_size_y * y : self.char_size_y * (y + 1),
697+
self.char_size_x * x : self.char_size_x * (x + 1),
677698
3
678-
] = np.ones((AnsiImage.CHAR_SIZE_Y, AnsiImage.CHAR_SIZE_X))
699+
] = np.ones((self.char_size_y, self.char_size_x))
679700

680701
# Draw cursor on top
681702
if cursor == True and x == self.cursor_x and y == self.cursor_y:
682703
for cursor_pix_y, cursor_pix_x in self.cursor_shape:
683-
self.ansi_bitmap[AnsiImage.CHAR_SIZE_Y * y + cursor_pix_y, AnsiImage.CHAR_SIZE_X * x + cursor_pix_x, 0:3] = \
684-
1.0 - self.ansi_bitmap[AnsiImage.CHAR_SIZE_Y * y + cursor_pix_y, AnsiImage.CHAR_SIZE_X * x + cursor_pix_x, 0:3]
685-
self.ansi_bitmap[AnsiImage.CHAR_SIZE_Y * y + cursor_pix_y, AnsiImage.CHAR_SIZE_X * x + cursor_pix_x, 3] = 1.0
704+
self.ansi_bitmap[self.char_size_y * y + cursor_pix_y, self.char_size_x * x + cursor_pix_x, 0:3] = \
705+
1.0 - self.ansi_bitmap[self.char_size_y * y + cursor_pix_y, self.char_size_x * x + cursor_pix_x, 0:3]
706+
self.ansi_bitmap[self.char_size_y * y + cursor_pix_y, self.char_size_x * x + cursor_pix_x, 3] = 1.0
686707

687708
# Invert selection
688709
if cursor == True:
@@ -696,12 +717,12 @@ def to_bitmap(self, ansi_graphics, transparent = False, cursor = False, area = N
696717
if not (x, y) in self.redraw_set or x >= self.width or y >= self.height:
697718
continue
698719
self.ansi_bitmap[
699-
AnsiImage.CHAR_SIZE_Y * y : AnsiImage.CHAR_SIZE_Y * (y + 1),
700-
AnsiImage.CHAR_SIZE_X * x : AnsiImage.CHAR_SIZE_X * (x + 1),
720+
self.char_size_y * y : self.char_size_y * (y + 1),
721+
self.char_size_x * x : self.char_size_x * (x + 1),
701722
0:3
702723
] = 1.0 - self.ansi_bitmap[
703-
AnsiImage.CHAR_SIZE_Y * y : AnsiImage.CHAR_SIZE_Y * (y + 1),
704-
AnsiImage.CHAR_SIZE_X * x : AnsiImage.CHAR_SIZE_X * (x + 1),
724+
self.char_size_y * y : self.char_size_y * (y + 1),
725+
self.char_size_x * x : self.char_size_x * (x + 1),
705726
0:3
706727
]
707728

@@ -718,21 +739,21 @@ def to_bitmap(self, ansi_graphics, transparent = False, cursor = False, area = N
718739
self.redraw_set = set()
719740

720741
if area != None:
721-
start_x = min(area[0] // self.CHAR_SIZE_X, redraw_start_x)
722-
end_x = max((area[2] // self.CHAR_SIZE_X) + 1, redraw_end_y)
742+
start_x = min(area[0] // self.char_size_x, redraw_start_x)
743+
end_x = max((area[2] // self.char_size_x) + 1, redraw_end_y)
723744

724-
start_y = min(area[1] // self.CHAR_SIZE_Y, redraw_start_y)
725-
end_y = max((area[3] // self.CHAR_SIZE_Y) + 1, redraw_end_y)
745+
start_y = min(area[1] // self.char_size_y, redraw_start_y)
746+
end_y = max((area[3] // self.char_size_y) + 1, redraw_end_y)
726747

727748
return (
728-
start_x * AnsiImage.CHAR_SIZE_X,
729-
start_y * AnsiImage.CHAR_SIZE_Y,
749+
start_x * self.char_size_x,
750+
start_y * self.char_size_y,
730751
Image.fromarray((self.ansi_bitmap[
731-
start_y * AnsiImage.CHAR_SIZE_Y : end_y * AnsiImage.CHAR_SIZE_Y,
732-
start_x * AnsiImage.CHAR_SIZE_X : end_x * AnsiImage.CHAR_SIZE_X
752+
start_y * self.char_size_y : end_y * self.char_size_y,
753+
start_x * self.char_size_x : end_x * self.char_size_x
733754
] * 255.0).astype('int8'), mode='RGBA'),
734-
AnsiImage.CHAR_SIZE_X * self.width,
735-
AnsiImage.CHAR_SIZE_Y * self.height,
755+
self.char_size_x * self.width,
756+
self.char_size_y * self.height,
736757
)
737758
else:
738759
return Image.fromarray((self.ansi_bitmap * 255.0).astype('int8'), mode='RGBA')

0 commit comments

Comments
 (0)