23 Commits
Author SHA1 Message Date
Idrees ebd345245f Merge pull request #113 from IdreesInc/106-cyrillic-ц-renders-higher-than-it-should
Add descent to ц and bump version
2023-02-20 10:36:18 -05:00
Idrees Hassan cec34615f2 Add descent to ц and bump version 2023-02-20 10:36:06 -05:00
Idrees c3be0076a6 Merge pull request #112 from Dheatly23/test-join-polygon
Try to Join Polygons
2023-02-20 10:12:48 -05:00
Idrees 4fb297a1da Merge pull request #108 from Dheatly23/fix-ligature-windows
Fix Broken Ligature on Windows
2023-02-20 10:00:38 -05:00
Dheatly23 cb1ac86f1e Fix Skipping Holes 2023-02-16 13:40:28 +07:00
Dheatly23 d6ef5a8aa4 Try to Join Polygons 2023-02-16 13:32:11 +07:00
Idrees f2b5d120f4 Merge pull request #109 from Dheatly23/non-integer-margin
Take Into Account Non-integer Offsets
2023-02-12 16:23:10 -05:00
Dheatly23 47696c0c27 Use Keyword Argument
Ensure future-proofing for extra data with kwarg.
2023-02-10 11:35:16 +07:00
Dheatly23 6f343ad936 Take Into Account Non-integer Offsets 2023-02-09 11:24:37 +07:00
Dheatly23 98395d8764 Fix Broken Ligature 2023-02-09 09:14:31 +07:00
Idrees 6895837d66 Merge pull request #103 from IdreesInc/88-a-small-break-between-pixels
88 Merge adjacent pixels into combined shapes
2023-02-05 13:46:18 -05:00
Idrees Hassan 536227370c Update version and copyright year 2023-02-05 13:45:23 -05:00
Idrees 78d0a7197a Merge pull request #98 from Dheatly23/main
Combine Pixels Into Polygons
2023-02-05 13:38:58 -05:00
Dheatly23 19e0f59987 Adds License 2023-01-13 23:25:24 +07:00
Dheatly23 cbcb5331c2 Fix some bug with mutating iterator variable 2023-01-13 22:38:02 +07:00
Dheatly23 d5fbed6802 Discard polygon splicing
As it turns out, to add holes, just emit the inner polygon
(RIP colinear remover).
2023-01-13 11:37:45 +07:00
Dheatly23 8e1331f85b Rename some more functions 2023-01-13 11:31:33 +07:00
Dheatly23 09ae18caea Removes adjacent colinear points 2023-01-13 11:26:03 +07:00
Dheatly23 42b4b79cf9 Update generator using polygonizer 2023-01-12 17:05:08 +07:00
Dheatly23 eabb3edc2a Rename identifiers 2023-01-12 16:25:16 +07:00
Dheatly23 bd42295cab First prototype of polygonizer 2023-01-12 15:16:42 +07:00
Idrees e1c44cf227 Merge pull request #94 from IdreesInc/93-improve-spacing-around-thin-letters-like-t 2022-12-31 21:37:08 -05:00
Idrees Hassan 44034119d7 Significantly improve spacing for thin characters 2022-12-31 20:07:10 -05:00
5 changed files with 765 additions and 54 deletions

No files matched your search

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 606 KiB

After

Width:  |  Height:  |  Size: 615 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 185 KiB

After

Width:  |  Height:  |  Size: 158 KiB

+40 -21
View File
@@ -102,8 +102,8 @@
"name": "single_quote",
"codepoint": 39,
"pixels": [
[0, 1, 0, 0, 0],
[0, 1, 0, 0, 0],
[0, 0, 1, 0, 0],
[0, 0, 1, 0, 0],
[0, 0, 0, 0, 0],
[0, 0, 0, 0, 0],
[0, 0, 0, 0, 0],
@@ -997,7 +997,8 @@
[0, 0, 1, 0, 0],
[0, 0, 1, 0, 0],
[0, 0, 1, 0, 0]
]
],
"leftMargin": -0.5
},
{
"character": "g",
@@ -1041,7 +1042,8 @@
[0, 0, 1, 0, 0],
[0, 0, 1, 0, 0],
[0, 0, 0, 1, 1]
]
],
"leftMargin": -0.5
},
{
"character": "j",
@@ -1071,7 +1073,8 @@
[1, 1, 0, 0, 0],
[1, 0, 1, 0, 0],
[1, 0, 0, 1, 0]
]
],
"leftMargin": 0.5
},
{
"character": "l",
@@ -1085,7 +1088,8 @@
[0, 0, 1, 0, 0],
[0, 0, 1, 0, 0],
[0, 0, 0, 1, 1]
]
],
"leftMargin": -0.5
},
{
"character": "m",
@@ -1201,7 +1205,8 @@
[0, 0, 1, 0, 0],
[0, 0, 1, 0, 0],
[0, 0, 0, 1, 1]
]
],
"leftMargin": -0.5
},
{
"character": "u",
@@ -1947,7 +1952,8 @@
[0, 0, 1, 0, 0],
[0, 0, 1, 0, 0],
[0, 0, 0, 1, 1]
]
],
"leftMargin": -0.5
},
{
"character": "í",
@@ -1962,7 +1968,8 @@
[0, 0, 1, 0, 0],
[0, 0, 1, 0, 0],
[0, 0, 0, 1, 1]
]
],
"leftMargin": -0.5
},
{
"character": "î",
@@ -1977,7 +1984,8 @@
[0, 0, 1, 0, 0],
[0, 0, 1, 0, 0],
[0, 0, 0, 1, 1]
]
],
"leftMargin": -0.5
},
{
"character": "ï",
@@ -1991,7 +1999,8 @@
[0, 0, 1, 0, 0],
[0, 0, 1, 0, 0],
[0, 0, 0, 1, 1]
]
],
"leftMargin": -0.5
},
{
"character": "ð",
@@ -2164,7 +2173,8 @@
[0, 0, 1, 0, 0],
[0, 0, 1, 0, 0],
[0, 0, 0, 1, 1]
]
],
"leftMargin": -0.5
},
{
"character": "ī",
@@ -2178,7 +2188,8 @@
[0, 0, 1, 0, 0],
[0, 0, 1, 0, 0],
[0, 0, 0, 1, 1]
]
],
"leftMargin": -0.5
},
{
"character": "ĭ",
@@ -2193,7 +2204,8 @@
[0, 0, 1, 0, 0],
[0, 0, 1, 0, 0],
[0, 0, 0, 1, 1]
]
],
"leftMargin": -0.5
},
{
"character": "ĵ",
@@ -2244,7 +2256,8 @@
[0, 0, 1, 0, 0],
[0, 1, 0, 0, 0]
],
"descent": 2
"descent": 2,
"leftMargin": 0.5
},
{
"character": "Ļ",
@@ -2278,7 +2291,8 @@
[0, 0, 0, 1, 0],
[0, 0, 1, 0, 0]
],
"descent": 2
"descent": 2,
"leftMargin": -0.5
},
{
"character": "Ł",
@@ -2306,7 +2320,8 @@
[0, 1, 1, 0, 0],
[0, 0, 1, 0, 0],
[0, 0, 0, 1, 1]
]
],
"leftMargin": -0.5
},
{
"character": "Ņ",
@@ -2417,7 +2432,8 @@
[0, 0, 1, 0, 0],
[0, 0, 1, 0, 0],
[0, 0, 0, 1, 1]
]
],
"leftMargin": -0.5
},
{
"character": "Ů",
@@ -2440,7 +2456,8 @@
[0, 0, 1, 0, 0],
[0, 0, 1, 0, 0],
[0, 0, 0, 1, 1]
]
],
"leftMargin": -0.5
},
{
"character": "ĵ",
@@ -2525,7 +2542,8 @@
[0, 0, 1, 0, 0],
[0, 1, 0, 0, 0]
],
"descent": 2
"descent": 2,
"leftMargin": -0.5
},
{
"character": "Ѐ",
@@ -3335,7 +3353,8 @@
[1, 0, 0, 1, 0],
[1, 1, 1, 1, 1],
[0, 0, 0, 0, 1]
]
],
"descent": 1
},
{
"character": "ч",
+65 -33
View File
@@ -1,5 +1,5 @@
# Monocraft, a monospaced font for developers who like Minecraft a bit too much.
# Copyright (C) 2022 Idrees Hassan
# Copyright (C) 2022-2023 Idrees Hassan
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
@@ -16,8 +16,10 @@
import os
import fontforge
import json
import math
from generate_diacritics import generateDiacritics
from generate_examples import generateExamples
from polygonizer import PixelImage, generatePolygons
PIXEL_SIZE = 120
@@ -35,13 +37,13 @@ def generateFont():
monocraft.fullname = "Monocraft"
monocraft.copyright = "Idrees Hassan, https://github.com/IdreesInc/Monocraft"
monocraft.encoding = "UnicodeFull"
monocraft.version = "2.2"
monocraft.weight = "Medium"
monocraft.version = "2.5"
monocraft.weight = "Regular"
monocraft.ascent = PIXEL_SIZE * 8
monocraft.descent = PIXEL_SIZE
monocraft.em = PIXEL_SIZE * 9
monocraft.upos = -PIXEL_SIZE # Underline position
monocraft.addLookup("ligatures", "gsub_ligature", (), (("liga",(("dflt",("dflt")),)),))
monocraft.addLookup("ligatures", "gsub_ligature", (), (("liga",(("dflt",("dflt")),("latn",("dflt")))),))
monocraft.addLookupSubtable("ligatures", "ligatures-subtable")
for character in characters:
@@ -49,15 +51,10 @@ def generateFont():
monocraft.createChar(character["codepoint"], character["name"])
pen = monocraft[character["name"]].glyphPen()
top = 0
if "pixels" in character:
top = drawCharacter(character, pen)
elif "reference" in character:
top = drawCharacter(charactersByCodepoint[character["reference"]], pen)
if "diacritic" in character:
diacritic = diacritics[character["diacritic"]]
if "diacriticSpace" in character:
top += PIXEL_SIZE * character["diacriticSpace"]
drawGlyph(diacritic["pixels"], pen, 0, top)
drawn = character
image, kw = generateImage(character)
drawImage(image, pen, **kw)
monocraft[character["name"]].width = PIXEL_SIZE * 6
print(f"Generated {len(characters)} characters")
@@ -70,7 +67,8 @@ def generateFont():
for ligature in ligatures:
lig = monocraft.createChar(-1, ligature["name"])
pen = monocraft[ligature["name"]].glyphPen()
drawCharacter(ligature, pen)
image, kw = generateImage(ligature)
drawImage(image, pen, **kw)
monocraft[ligature["name"]].width = PIXEL_SIZE * len(ligature["sequence"]) * 6
lig.addPosSub("ligatures-subtable", tuple(map(lambda codepoint: charactersByCodepoint[codepoint]["name"], ligature["sequence"])))
print(f"Generated {len(ligatures)} ligatures")
@@ -78,26 +76,60 @@ def generateFont():
monocraft.generate(outputDir + "Monocraft.ttf")
monocraft.generate(outputDir + "Monocraft.otf")
def drawCharacter(character, pen):
if "reference" in character: return drawCharacter(charactersByCodepoint[character["reference"]],pen)
leftMargin = PIXEL_SIZE * character["leftMargin"] if "leftMargin" in character else 0
floor = -PIXEL_SIZE * character["descent"] if "descent" in character else 0
return drawGlyph(character["pixels"], pen, leftMargin, floor)
def generateImage(character):
image = PixelImage()
kw = {}
if "pixels" in character:
arr = character["pixels"]
leftMargin = character["leftMargin"] if "leftMargin" in character else 0
x = math.floor(leftMargin)
kw['dx'] = leftMargin - x
descent = -character["descent"] if "descent" in character else 0
y = math.floor(descent)
kw['dy'] = descent - y
image = image | imageFromArray(arr, x, y)
if "reference" in character:
other = generateImage(charactersByCodepoint[character["reference"]])
kw.update(other[1])
image = image | other[0]
if "diacritic" in character:
diacritic = diacritics[character["diacritic"]]
arr = diacritic["pixels"]
x = image.x
y = findHighestY(image) + 1
if "diacriticSpace" in character:
y += int(character["diacriticSpace"])
image = image | imageFromArray(arr, x, y)
return (image, kw)
def drawGlyph(pixels, pen, startingX, startingY):
top = 0 # The highest point of the character
for rowIndex in range(len(pixels)):
row = pixels[-(rowIndex + 1)]
for columnIndex in range(len(row)):
if row[columnIndex] == 0:
continue
pen.moveTo((columnIndex * PIXEL_SIZE + startingX, rowIndex * PIXEL_SIZE + startingY)) # Bottom left
pen.lineTo((columnIndex * PIXEL_SIZE + startingX, (rowIndex + 1) * PIXEL_SIZE + startingY))
pen.lineTo(((columnIndex + 1) * PIXEL_SIZE + startingX, (rowIndex + 1) * PIXEL_SIZE + startingY))
pen.lineTo(((columnIndex + 1) * PIXEL_SIZE + startingX, rowIndex * PIXEL_SIZE + startingY))
pen.closePath()
top = (rowIndex + 1) * PIXEL_SIZE + startingY
return top
def findHighestY(image):
for y in range(image.y_end - 1, image.y, -1):
for x in range(image.x, image.x_end):
if image[x, y]:
return y
return image.y
def imageFromArray(arr, x=0, y=0):
return PixelImage(
x=x,
y=y,
width=len(arr[0]),
height=len(arr),
data=bytes(x for a in reversed(arr) for x in a),
)
def drawImage(image, pen, *, dx=0, dy=0):
for polygon in generatePolygons(image):
start = True
for x, y in polygon:
x = (x + dx) * PIXEL_SIZE
y = (y + dy) * PIXEL_SIZE
if start:
pen.moveTo(x, y)
start = False
else:
pen.lineTo(x, y)
pen.closePath()
generateFont()
generateExamples(characters, ligatures, charactersByCodepoint)
+660
View File
@@ -0,0 +1,660 @@
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
__all__ = [
'PixelImage',
'generatePolygons',
]
from collections import defaultdict
from enum import IntFlag
import itertools
class PixelImage:
''' Class for managing a pixel image.
Each pixel is a 8-bit integer.
It also store the position and size of the image, allowing easier operation.
'''
__slots__ = ['__data', '_x', '_y', '_w', '_h']
def __init__(self, src=None, *, x=0, y=0, width=0, height=0, data=None):
if src is not None:
self._x, self._y, self._w, self._h, self.__data = src._x, src._y, src._w, src._h, bytearray(
src.__data)
return
if width < 0:
raise ValueError('Width < 0')
if height < 0:
raise ValueError('Height < 0')
self._x, self._y, self._w, self._h = x, y, width, height
if data is not None:
data = bytearray(data)
if len(data) != width * height:
raise ValueError(
f'Data length mismatch (expected {width * height}, got {len(data)})'
)
self.__data = data
else:
self.__data = bytearray(width * height)
@property
def x(self):
return self._x
@property
def y(self):
return self._y
@property
def x_end(self):
return self._x + self._w
@property
def y_end(self):
return self._y + self._h
@property
def width(self):
return self._w
@property
def height(self):
return self._h
@property
def data(self):
return bytes(self.__data)
def __getitem__(self, key):
''' Gets a pixel at (x, y).
Defaults to 0 if out of bounds.
'''
x, y = key
x -= self._x
y -= self._y
if x < 0 or x >= self._w or y < 0 or y >= self._h:
return 0
return self.__data[x + y * self._w]
def __setitem__(self, key, value):
''' Sets a pixel at (x, y).
Do nothing if out of bounds.
'''
x, y = key
x -= self._x
y -= self._y
if x < 0 or x >= self._w or y < 0 or y >= self._h:
return
self.__data[x + y * self._w] = value
def __len__(self):
return self._w * self._h
def __str__(self):
return '\n'.join(' '.join(
str(self[x, y]) for x in range(self.x, self.x_end))
for y in range(self.y, self.y_end))
def __repr__(self):
return f'PixelImage(\n x={self._x},\n' \
f' y={self._y},\n' \
f' width={self._w},\n' \
f' height={self._h},\n' \
f' data=bytes([\n ' + \
',\n '.join(
', '.join(
f'{self[x, y]:#04x}' for x in range(self.x, self.x_end)
) for y in range(self.y, self.y_end)
) + \
'\n ])\n)'
def __hash__(self):
return hash((self._x, self._y, self._w, self._h, self.__data))
def __eq__(self, other):
if not isinstance(other, PixelImage):
return NotImplemented
return self._x == other._x and \
self._y == other._y and \
self._w == other._w and \
self._h == other._h and \
self.__data == other.__data
def __ne__(self, other):
if not isinstance(other, PixelImage):
return NotImplemented
return self._x != other._x or \
self._y != other._y or \
self._w != other._w or \
self._h != other._h or \
self.__data != other.__data
def __or__(self, other):
if not isinstance(other, PixelImage):
return NotImplemented
if self.width == 0 or self.height == 0:
return other
elif other.width == 0 or other.height == 0:
return self
else:
x = min(self._x, other.x)
y = min(self._y, other.y)
x2 = max(self.x_end, other.x_end)
y2 = max(self.y_end, other.y_end)
ret = self.__class__(
x=x,
y=y,
width=x2 - x,
height=y2 - y,
)
dj = self._y - y
i_min = self._x - x
i_max = i_min + self._w
for j in range(self._h):
j_min = j * self._w
_j_min = (j + dj) * ret._w
row = self.__data[j_min:j_min + self._w]
ret.__data[i_min + _j_min:i_max + _j_min] = row
dj = other._y - y
i_min = other.x - x
i_max = i_min + other.width
for j in range(other.height):
j_min = j * other.width
_j_min = (j + dj) * ret._w
for i, i_ in enumerate(range(i_min + _j_min, i_max + _j_min)):
ret.__data[i_] |= other.__data[j_min + i]
return ret
def generatePolygons(image):
for segment, start_pos in segmentize(image):
yield from polygonizeSegment(segment, start_pos)
def segmentize(image):
image = PixelImage(image)
for y in range(image.y, image.y_end):
for x in range(image.x, image.x_end):
if not image[x, y]:
continue
# Move segment into new image
ret = PixelImage(
x=image.x,
y=image.y,
width=image.width,
height=image.height,
)
p = (x, y)
# Flood fill
stack = [(x, y)]
while stack:
x, y = stack.pop()
if not image[x, y]:
continue
ret[x, y] = 1
image[x, y] = 0
if image[x - 1, y - 1]:
stack.append((x - 1, y - 1))
if image[x - 1, y]:
stack.append((x - 1, y))
if image[x - 1, y + 1]:
stack.append((x - 1, y + 1))
if image[x, y - 1]:
stack.append((x, y - 1))
if image[x, y + 1]:
stack.append((x, y + 1))
if image[x + 1, y - 1]:
stack.append((x + 1, y - 1))
if image[x + 1, y]:
stack.append((x + 1, y))
if image[x + 1, y + 1]:
stack.append((x + 1, y + 1))
# Yields segment and staring position
x, y = p
yield (ret, p)
class CellFlag(IntFlag):
ACTIVE = 0b0000_0001
UP = 0b0001_0000
DOWN = 0b0010_0000
LEFT = 0b0100_0000
RIGHT = 0b1000_0000
UP_LEFT = UP | LEFT
UP_RIGHT = UP | RIGHT
DOWN_LEFT = DOWN | LEFT
DOWN_RIGHT = DOWN | RIGHT
def move(self, pos):
''' Move coordinate according to direction '''
x, y = pos
if self & self.__class__.UP:
y -= 1
if self & self.__class__.DOWN:
y += 1
if self & self.__class__.LEFT:
x -= 1
if self & self.__class__.RIGHT:
x += 1
return (x, y)
def corner(self, pos):
''' Get corner of a cell '''
x, y = pos
if self & self.__class__.UP_LEFT == self.__class__.UP_LEFT:
return (x, y)
elif self & self.__class__.UP_RIGHT == self.__class__.UP_RIGHT:
return (x + 1, y)
elif self & self.__class__.DOWN_RIGHT == self.__class__.DOWN_RIGHT:
return (x + 1, y + 1)
elif self & self.__class__.DOWN_LEFT == self.__class__.DOWN_LEFT:
return (x, y + 1)
else:
raise ValueError(f'Invalid corner {self}')
def cw(self):
''' Rotate self clockwise '''
ret = self & self.__class__.ACTIVE
if self & self.__class__.UP:
ret |= self.__class__.RIGHT
if self & self.__class__.DOWN:
ret |= self.__class__.LEFT
if self & self.__class__.LEFT:
ret |= self.__class__.UP
if self & self.__class__.RIGHT:
ret |= self.__class__.DOWN
return ret
def ccw(self):
''' Rotate self counterclockwise '''
ret = self & self.__class__.ACTIVE
if self & self.__class__.UP:
ret |= self.__class__.LEFT
if self & self.__class__.DOWN:
ret |= self.__class__.RIGHT
if self & self.__class__.LEFT:
ret |= self.__class__.DOWN
if self & self.__class__.RIGHT:
ret |= self.__class__.UP
return ret
def reverse(self):
''' Reverse self '''
ret = self & self.__class__.ACTIVE
if self & self.__class__.UP:
ret |= self.__class__.DOWN
if self & self.__class__.DOWN:
ret |= self.__class__.UP
if self & self.__class__.LEFT:
ret |= self.__class__.RIGHT
if self & self.__class__.RIGHT:
ret |= self.__class__.LEFT
return ret
def polygonizeSegment(image, start_pos):
x, y = start_pos
# Make sure position is top left
assert image[x, y]
assert not (image[x - 1, y] or image[x - 1, y - 1] or image[x, y - 1]
or image[x + 1, y - 1])
dir = CellFlag.RIGHT
def doMove():
nonlocal x, y, dir
# Set wall
d_ccw, d_cw, d_rev = dir.ccw(), dir.cw(), dir.reverse()
image[x, y] |= d_ccw
x_, y_ = d_ccw.move((x, y))
assert not image[x_, y_] & CellFlag.ACTIVE
# Build boundary wall from this point
# For all diagrams, it is assumed movement direction is right
# ? . #
# ? X ?
# ? ? ?
x_, y_ = (dir | d_ccw).move((x, y))
if image[x_, y_] & CellFlag.ACTIVE:
# Top-right corner
ret = [(dir | d_ccw).corner((x, y))]
# ? . ^
# ? X ?
# ? ? ?
x, y = x_, y_
dir = d_ccw
return ret
# ? . .
# ? X #
# ? ? ?
x_, y_ = dir.move((x, y))
if image[x_, y_] & CellFlag.ACTIVE:
# ? . .
# ? X >
# ? ? ?
x, y = x_, y_
return [] # No corner
# ? . .
# ? X .
# ? ? #
x_, y_ = (dir | d_cw).move((x, y))
if image[x_, y_] & CellFlag.ACTIVE:
# Two corners: top-right and bottom-right
ret = [
(dir | d_ccw).corner((x, y)),
(dir | d_cw).corner((x, y)),
]
# ? . .
# ? X .
# ? ? >
image[x, y] |= dir
x, y = x_, y_
return ret
# ? . .
# ? X .
# ? # .
x_, y_ = d_cw.move((x, y))
if image[x_, y_] & CellFlag.ACTIVE:
# Top-right corner
ret = [(dir | d_ccw).corner((x, y))]
# ? . .
# ? X .
# ? v .
image[x, y] |= dir
x, y = x_, y_
dir = d_cw
return ret
# ? . .
# ? X .
# # . .
x_, y_ = (d_rev | d_cw).move((x, y))
if image[x_, y_] & CellFlag.ACTIVE:
# Three corners: top-right, bottom-right, and bottom-left
ret = [
(dir | d_ccw).corner((x, y)),
(dir | d_cw).corner((x, y)),
(d_cw | d_rev).corner((x, y)),
]
# ? . .
# ? X .
# v . .
image[x, y] |= dir | d_cw
x, y = x_, y_
dir = d_cw
return ret
# ? . .
# # X .
# . . .
x_, y_ = d_rev.move((x, y))
if image[x_, y_] & CellFlag.ACTIVE:
# Two corners: top-right and bottom-right
ret = [
(dir | d_ccw).corner((x, y)),
(dir | d_cw).corner((x, y)),
]
# ? . .
# < X .
# . . .
image[x, y] |= dir | d_cw
x, y = x_, y_
dir = d_rev
return ret
# # . .
# . X .
# . . .
x_, y_ = (d_rev | d_ccw).move((x, y))
if image[x_, y_] & CellFlag.ACTIVE:
# Four corners: top-right, bottom-right, bottom-left, and top-left
ret = [
(dir | d_ccw).corner((x, y)),
(dir | d_cw).corner((x, y)),
(d_cw | d_rev).corner((x, y)),
(d_ccw | d_rev).corner((x, y)),
]
# < . .
# . X .
# . . .
image[x, y] |= dir | d_cw | d_rev
x, y = x_, y_
dir = d_rev
return ret
# . . .
# . X .
# . . .
image[x, y] |= dir | d_cw | d_rev
# Four corners: top-right, bottom-right, bottom-left, and top-left
return [
(dir | d_ccw).corner((x, y)),
(dir | d_cw).corner((x, y)),
(d_cw | d_rev).corner((x, y)),
(d_ccw | d_rev).corner((x, y)),
]
def find(l, x):
try:
return l.index(x)
except ValueError:
return None
# Generate outer polygon
outer_poly = []
line = doMove()
i = find(line, start_pos)
while i is None:
outer_poly += line
line = doMove()
i = find(line, start_pos)
outer_poly += line[:i + 1]
assert checkPoly(outer_poly)
points_ = dict(
zip(
reversed(outer_poly),
range(len(outer_poly) - 1, -1, -1),
))
points = defaultdict(set)
for p in outer_poly:
points[p].add(0)
poly = [(outer_poly, points_)]
# Calculate bounding box
x_min, y_min, x_max, y_max = x, y, x, y
for x, y in outer_poly:
if x_min > x:
x_min = x
if x_max < x:
x_max = x
if y_min > y:
y_min = y
if y_max < y:
y_max = y
# Generate inner polygon
for y in range(y_min, y_max):
for x in range(x_min, x_max):
v = image[x, y]
if not v & CellFlag.ACTIVE:
continue
if not (image[x, y + 1] & CellFlag.ACTIVE or v & CellFlag.DOWN):
dir = CellFlag.LEFT
else:
continue
# Store iterator value
p = x, y
inner_poly = []
line = doMove()
i = None
while i is None:
inner_poly += line
line = doMove()
i = find(line, inner_poly[0])
inner_poly += line[:i]
assert checkPoly(inner_poly)
j = len(poly)
poly.append((
inner_poly,
dict(
zip(
reversed(inner_poly),
range(len(inner_poly) - 1, -1, -1),
)),
))
for i in inner_poly:
points[i].add(j)
# Restore iterator value
x, y = p
# Try to join polygons
for i in range(len(poly) - 1, -1, -1):
inner_poly, points_ = poly[i]
for v in points.values():
if len(v) > 1 and i in v:
j = min(v)
break
else:
continue
outer_poly, points__ = poly[j]
for i_, p in enumerate(inner_poly):
j_ = points__.get(p)
if j_ is not None:
break
else:
raise RuntimeError(
f'Should not happened {inner_poly} {outer_poly}')
outer_poly[j_:j_] = [*inner_poly[i_:], *inner_poly[:i_]]
points__.update(
zip(
reversed(outer_poly),
range(len(outer_poly) - 1, -1, -1),
))
del poly[i]
for v in points.values():
v.discard(i)
# Emit polygons
for i, _ in poly:
yield i
def checkPoly(poly):
for i in range(-1, len(poly) - 1):
x, y = poly[i]
x_, y_ = poly[i + 1]
if x != x_ and y != y_:
print(f'{poly} Error at {i}')
return False
for i in range(-2, len(poly) - 2):
x, y = poly[i]
x_, y_ = poly[i + 1]
x__, y__ = poly[i + 2]
if x == x_ == x__ or y == y_ == y__:
print(f'{poly} Error at {i}')
return False
return True
# Testing
def runTest():
import json
characters = json.load(open("./characters.json"))
diacritics = json.load(open("./diacritics.json"))
ligatures = json.load(open("./ligatures.json"))
for v in characters:
if 'pixels' not in v:
continue
testChar(v['character'], v['pixels'])
for k, v in diacritics.items():
if 'pixels' not in v:
continue
testChar(k, v['pixels'])
for v in ligatures:
if 'pixels' not in v:
continue
testChar(v['ligature'], v['pixels'])
filter = {}
def testChar(name, pixels):
if filter and name not in filter:
return
image = PixelImage(
width=len(pixels[0]),
height=len(pixels),
data=bytes(x for a in pixels[::-1] for x in a),
)
print(f'Character: {name}\n{image}\n\n')
for poly in generatePolygons(image):
print('Polygon:\n ' + '\n '.join(f'{x}, {y}'
for x, y in poly) + '\n\n')
if __name__ == '__main__':
runTest()