Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f40b130ad5 |
No files matched your search
@@ -1,6 +1,6 @@
|
||||
build/
|
||||
dist/
|
||||
runekit/image/format.c
|
||||
runekit/image/format.*.*
|
||||
runekit/image/*.c
|
||||
runekit/image/*.*.*
|
||||
setup.py
|
||||
*.egg-info
|
||||
@@ -2,7 +2,9 @@ from Cython.Build import cythonize
|
||||
|
||||
# use cythonize to build the extensions
|
||||
modules = [
|
||||
"runekit/image/format.pyx",
|
||||
"runekit/image/_utils.pyx",
|
||||
"runekit/image/_chat.pyx",
|
||||
"runekit/image/_font.pyx",
|
||||
]
|
||||
|
||||
extensions = cythonize(modules)
|
||||
|
||||
@@ -25,3 +25,19 @@ class RequestHandler(typing.TypedDict):
|
||||
handlerName: str
|
||||
handlerUrl: str
|
||||
handlerScript: str
|
||||
|
||||
|
||||
UnblendModes = typing.Literal["raw", "removebg", "blackbg"]
|
||||
|
||||
|
||||
class FontMeta(typing.TypedDict):
|
||||
basey: int
|
||||
spacewidth: int
|
||||
treshold: float
|
||||
color: typing.Tuple[int, int, int]
|
||||
unblendmode: UnblendModes
|
||||
shadow: bool
|
||||
chars: str
|
||||
seconds: str
|
||||
img: typing.Optional[str]
|
||||
bonus: typing.Optional[typing.Dict[str, int]]
|
||||
@@ -100,7 +100,7 @@
|
||||
},
|
||||
|
||||
get captureInterval() {
|
||||
if(!syncChan) return 1000;
|
||||
if(!syncChan) return 300;
|
||||
return api.captureInterval;
|
||||
},
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ if TYPE_CHECKING:
|
||||
|
||||
|
||||
class GameInstance(QObject):
|
||||
refresh_rate = 1000
|
||||
refresh_rate = 300
|
||||
manager: "GameManager"
|
||||
_last_game_activity: float = 0
|
||||
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
import logging
|
||||
import struct
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from PIL import Image
|
||||
|
||||
try:
|
||||
from .format import image_to_bgra
|
||||
except ImportError:
|
||||
logging.warning("Cannot import native image code, using pure python")
|
||||
|
||||
image_8bpp = struct.Struct("BBBB")
|
||||
|
||||
def image_to_bgra(image: "Image", x, y, w, h) -> bytes:
|
||||
buf = bytearray(w * h * image_8bpp.size)
|
||||
index = 0
|
||||
for p in image.crop((x, y, x + w, y + h)).getdata():
|
||||
r = p[0]
|
||||
g = p[1]
|
||||
b = p[2]
|
||||
a = p[3] if len(p) == 4 else 255
|
||||
|
||||
image_8bpp.pack_into(buf, index, b, g, r, a)
|
||||
index += image_8bpp.size
|
||||
|
||||
return buf
|
||||
@@ -0,0 +1,55 @@
|
||||
from ._utils cimport image_to_ptr, unblend, ImageRGBA, RGB
|
||||
|
||||
cdef struct Rectangle:
|
||||
int x
|
||||
int y
|
||||
int width
|
||||
int height
|
||||
|
||||
def get_chat_color(image, tuple box_, list colors):
|
||||
cdef Rectangle box = Rectangle(box_[0], box_[1], box_[2], box_[3])
|
||||
|
||||
cdef float best_score = -1.0
|
||||
cdef RGB best = RGB(0, 0, 0)
|
||||
cdef ImageRGBA **data = image_to_ptr(image)
|
||||
|
||||
cdef double score
|
||||
cdef ImageRGBA i1, i2
|
||||
cdef (unsigned char, unsigned char) pixel1, pixel2
|
||||
cdef int x, y
|
||||
cdef RGB color
|
||||
|
||||
for color in colors:
|
||||
score = 0.0
|
||||
for y in range(box.y, box.y + box.height):
|
||||
for x in range(box.x, box.x + box.width):
|
||||
if x < 0 or x + 1 >= box.height:
|
||||
continue
|
||||
if y < 0 or y + 1 >= box.width:
|
||||
continue
|
||||
i1 = data[y][x]
|
||||
i2 = data[y + 1][x + 1]
|
||||
pixel1 = unblend(i1.r, i1.g, i1.b, color.r, color.g, color.b)
|
||||
|
||||
pixel2 = unblend(i2.r, i2.g, i2.b, color.r, color.g, color.b)
|
||||
score += (pixel1[0] / 255 * pixel1[1] / 255) * (pixel2[0] / 255 * (255.0 - pixel2[1]) / 255)
|
||||
|
||||
if score > best_score:
|
||||
best_score = score
|
||||
best = color
|
||||
|
||||
return (best.r, best.g, best.b)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
cdef struct CharInfo:
|
||||
int width
|
||||
int height
|
||||
Py_UNICODE chr
|
||||
int bonus
|
||||
bint secondary
|
||||
|
||||
unsigned char *pixels
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
from cpython.mem cimport PyMem_Malloc, PyMem_Free
|
||||
from ._font cimport CharInfo
|
||||
from ._utils cimport image_to_ptr, ImageRGBA
|
||||
|
||||
cdef class FontDefinition:
|
||||
cdef CharInfo *chars
|
||||
cdef size_t chars_len
|
||||
cdef readonly int width
|
||||
cdef readonly int height
|
||||
cdef readonly int space_width
|
||||
cdef readonly bint shadow
|
||||
cdef readonly int base_y
|
||||
|
||||
cdef readonly int min_rating
|
||||
cdef readonly int max_spaces
|
||||
|
||||
def __dealloc__(self):
|
||||
for i in range(self.chars_len):
|
||||
PyMem_Free(self.chars[i].pixels)
|
||||
|
||||
PyMem_Free(self.chars)
|
||||
|
||||
cpdef FontDefinition generate_font(image, str chars, str seconds, bonuses, int base_y, int space_width, int threshold, bint shadow):
|
||||
cdef int chars_len = len(chars)
|
||||
cdef FontDefinition out = FontDefinition()
|
||||
cdef CharInfo *cinfo = <CharInfo*> PyMem_Malloc(sizeof(CharInfo) * chars_len)
|
||||
cdef ImageRGBA **raw = image_to_ptr(image)
|
||||
|
||||
cdef int width = image.width
|
||||
cdef int height = image.height
|
||||
cdef int min_y = image.height - 1
|
||||
cdef int max_y = 0
|
||||
cdef int ds = -1
|
||||
cdef int i, dx, cur_char, letter_width, x, y
|
||||
cdef Py_UNICODE letter
|
||||
cdef CharInfo *chr
|
||||
|
||||
threshold *= 255
|
||||
|
||||
for dx in range(width):
|
||||
if ds == -1:
|
||||
i = height - 1
|
||||
if raw[i][dx].r == 255 and raw[i][dx].a == 255:
|
||||
ds = dx
|
||||
|
||||
continue
|
||||
|
||||
letter = chars[cur_char]
|
||||
letter_width = ds - dx
|
||||
cinfo[cur_char] = CharInfo(
|
||||
width=letter_width,
|
||||
chr=letter,
|
||||
bonus=0,
|
||||
secondary=letter in seconds,
|
||||
)
|
||||
if bonuses and letter in bonuses:
|
||||
cinfo[cur_char].bonus = bonuses[letter]
|
||||
|
||||
cur_char += 1
|
||||
out.width = max(out.width, letter_width)
|
||||
|
||||
for y in range(height - 1):
|
||||
for x in range(letter_width):
|
||||
if raw[y][x + ds].r >= threshold:
|
||||
min_y = min(min_y, y)
|
||||
max_y = max(max_y, y)
|
||||
|
||||
ds = -1
|
||||
|
||||
out.height = max_y + 1 - min_y
|
||||
out.base_y = base_y - min_y
|
||||
|
||||
for a in range(chars_len):
|
||||
chr = &cinfo[a]
|
||||
# TODO
|
||||
|
||||
return out
|
||||
@@ -0,0 +1,16 @@
|
||||
cdef struct ImageRGBA:
|
||||
unsigned char r
|
||||
unsigned char g
|
||||
unsigned char b
|
||||
unsigned char a
|
||||
|
||||
cdef struct RGB:
|
||||
unsigned char r
|
||||
unsigned char g
|
||||
unsigned char b
|
||||
|
||||
cpdef (unsigned char, unsigned char) unblend(int r, int g, int b, int R, int G, int B) nogil
|
||||
cdef ImageRGBA** image_to_ptr(image)
|
||||
cpdef int can_blend(int rm, int gm, int bm, int r1, int g1, int b1, int p) nogil
|
||||
# cdef (double, double, double) decompose2col(unsigned char rp, unsigned char gp, unsigned char bp, unsigned char r1, unsigned char g1, unsigned char b1, unsigned char r2, unsigned char g2, unsigned char b2) nogil
|
||||
# cdef (double, double, double) decompose3col(unsigned char rp, unsigned char gp, unsigned char bp, unsigned char r1, unsigned char g1, unsigned char b1, unsigned char r2, unsigned char g2, unsigned char b2, double r3, double g3, double b3) nogil
|
||||
@@ -0,0 +1,116 @@
|
||||
from PIL.Image import new as image_new
|
||||
from libc.math cimport sqrt, fabs
|
||||
from ._utils cimport ImageRGBA
|
||||
|
||||
cpdef (unsigned char, unsigned char) unblend(int r, int g, int b, int R, int G, int B) nogil:
|
||||
cdef double m = sqrt(r * r + g * g + b * b)
|
||||
cdef double n = sqrt(R * R + G * G + B * B)
|
||||
|
||||
cdef double x = (r * R + g * G + b * B) / n
|
||||
cdef double y = sqrt(max(0.0, m * m - x * x))
|
||||
|
||||
cdef int r1 = int(max(0.0, (63.75 - y) * 4))
|
||||
cdef int r2 = int(x / n * 255)
|
||||
|
||||
if r2 > 255:
|
||||
r1 = max(0, r1 - r2 + 255)
|
||||
r2 = 255
|
||||
|
||||
return r1, r2
|
||||
|
||||
cdef ImageRGBA** image_to_ptr(image):
|
||||
ptr = dict(image.im.unsafe_ptrs)['image32']
|
||||
cdef size_t ptr_int = <size_t> ptr
|
||||
return <ImageRGBA**> ptr_int
|
||||
|
||||
def rgb(unsigned char r, unsigned char g, unsigned char b):
|
||||
return RGB(r, g,b)
|
||||
|
||||
cpdef int can_blend(int rm, int gm, int bm, int r1, int g1, int b1, int p) nogil:
|
||||
"""
|
||||
Determine whether color [rgb]m can be a result of a blend with color [rgb]1 that is p (0-1) of the mix
|
||||
:param rm: resulting color
|
||||
:param r1: first color of the mix (the other color is unknown)
|
||||
:param p: the portion of the [rgb]1 in the mix (0-1)
|
||||
:return: the number that the second color has to lie outside of the possible color ranges
|
||||
"""
|
||||
cdef int m = min(50, p/(1-p))
|
||||
cdef int r = rm + (rm - r1) * m
|
||||
cdef int g = gm + (gm - g1) * m
|
||||
cdef int b = bm + (bm - b1) * m
|
||||
return max(-r, -g, -b, r - 255, g - 255, b - 255)
|
||||
|
||||
def unblend_known_background(img, bg_img, bint shadow, unsigned char r, unsigned char g, unsigned char b):
|
||||
if bg_img:
|
||||
assert img.width == bg_img.width and img.height == bg_img.height
|
||||
|
||||
out_img = image_new('RGBA', (img.width, img.height), None)
|
||||
|
||||
cdef ImageRGBA **outptr = image_to_ptr(out_img)
|
||||
cdef ImageRGBA **imgptr = image_to_ptr(img)
|
||||
cdef ImageRGBA **bgptr = image_to_ptr(bg_img)
|
||||
cdef int width = img.width
|
||||
cdef int height = img.height
|
||||
|
||||
cdef int x, y
|
||||
cdef (double, double, double) col
|
||||
cdef double m
|
||||
cdef unsigned char red
|
||||
|
||||
with nogil:
|
||||
for y in range(height):
|
||||
for x in range(width):
|
||||
pixel = imgptr[y][x]
|
||||
bg_pixel = bgptr[y][x]
|
||||
|
||||
col = decompose2col(pixel.r, pixel.g, pixel.b, r, g, b, bg_pixel.r, bg_pixel.g, bg_pixel.b)
|
||||
|
||||
if shadow:
|
||||
m = 1 - col[1] - fabs(col[2])
|
||||
red = <unsigned char> (m * 255)
|
||||
outptr[y][x].r = red
|
||||
outptr[y][x].g = <unsigned char> (col[0] / m * 255.0)
|
||||
outptr[y][x].b = red
|
||||
else:
|
||||
red = <unsigned char> (col[0] * 255)
|
||||
outptr[y][x].r = red
|
||||
outptr[y][x].g = red
|
||||
outptr[y][x].b = red
|
||||
|
||||
outptr[y][x].a = 255
|
||||
|
||||
return out_img
|
||||
|
||||
cdef inline (double, double, double) decompose2col(double rp, double gp, double bp, double r1, double g1, double b1, double r2, double g2, double b2) nogil:
|
||||
cdef double r3 = g1 * b2 - g2 * b1
|
||||
cdef double g3 = b1 * r2 - b2 * r1
|
||||
cdef double b3 = r1 * g2 - r2 * g1
|
||||
|
||||
cdef double norm = 255.0 / sqrt(r3 * r3 + g3 * g3 + b3 * b3)
|
||||
r3 *= norm
|
||||
g3 *= norm
|
||||
b3 *= norm
|
||||
|
||||
return decompose3col(rp, gp, bp, r1, g1, b1, r2, g2, b2, r3, g3, b3)
|
||||
|
||||
cdef inline (double, double, double) decompose3col(double rp, double gp, double bp, double r1, double g1, double b1, double r2, double g2, double b2, double r3, double g3, double b3) nogil:
|
||||
cdef double A = g2 * b3 - b2 * g3
|
||||
cdef double B = g3 * b1 - b3 * g1
|
||||
cdef double C = g1 * b2 - b1 * g2
|
||||
|
||||
cdef double D = b2 * r3 - r2 * b3
|
||||
cdef double E = b3 * r1 - r3 * b1
|
||||
cdef double F = b1 * r2 - r1 * b2
|
||||
|
||||
cdef double G = r2 * g3 - g2 * r3
|
||||
cdef double H = r3 * g1 - g3 * r1
|
||||
cdef double I = r1 * g2 - g1 * r2
|
||||
|
||||
cdef double det = r1 * A + g1 * D + b1 * G
|
||||
|
||||
cdef double x = (A * rp + D * gp + G * bp) / det
|
||||
cdef double y = (B * rp + E * gp + H * bp) / det
|
||||
cdef double z = (C * rp + F * gp + I * bp) / det
|
||||
|
||||
return x, y, z
|
||||
|
||||
@@ -0,0 +1,291 @@
|
||||
from typing import List, Tuple, Optional, TypedDict, NamedTuple
|
||||
|
||||
from PIL import Image
|
||||
|
||||
from runekit.alt1.schema import FontMeta
|
||||
from ._chat import get_chat_color
|
||||
from ._utils import rgb, can_blend
|
||||
|
||||
Color = Tuple[int, int, int]
|
||||
|
||||
|
||||
class Rectangle(NamedTuple):
|
||||
x: int
|
||||
y: int
|
||||
width: int
|
||||
height: int
|
||||
|
||||
|
||||
class OCRLine(NamedTuple):
|
||||
text: str
|
||||
area: Rectangle
|
||||
|
||||
|
||||
class ChatboxLineFragment(TypedDict):
|
||||
color: int
|
||||
text: str
|
||||
index: int
|
||||
|
||||
|
||||
class ReadCharInfo(NamedTuple):
|
||||
chr: str
|
||||
base_char: CharInfo
|
||||
x: int
|
||||
y: int
|
||||
score: float
|
||||
size_score: float
|
||||
|
||||
|
||||
def read_chatbox_line(
|
||||
image: Image,
|
||||
x: int,
|
||||
y: int,
|
||||
colors: List[Color],
|
||||
font: FontDefinition,
|
||||
backwards=False,
|
||||
allow_gap=False,
|
||||
budget=15,
|
||||
allow_multi_col=True,
|
||||
):
|
||||
colors = [rgb(*x) for x in colors]
|
||||
|
||||
fragment_count = 0
|
||||
max_fragment_len = 0
|
||||
scan_x = x
|
||||
text = ""
|
||||
fragments: List[ChatboxLineFragment] = []
|
||||
|
||||
total_space_size = 0
|
||||
while True:
|
||||
chat_color = get_chat_color(image, (scan_x, y - 8, 6, 8), colors)
|
||||
ocr_line = read_chat_line(
|
||||
image,
|
||||
chat_color,
|
||||
font,
|
||||
scan_x,
|
||||
y,
|
||||
backward=backwards and fragment_count == 0,
|
||||
forward=True,
|
||||
scan_mode=True,
|
||||
)
|
||||
|
||||
if not ocr_line or not ocr_line.text or not ocr_line.area[4]:
|
||||
if not allow_multi_col and fragment_count != 0:
|
||||
break
|
||||
if total_space_size >= font["width"] and (
|
||||
not allow_gap
|
||||
or total_space_size >= 20
|
||||
or scan_x - x >= 100
|
||||
or budget < 0
|
||||
):
|
||||
break
|
||||
|
||||
space_size = font["space_width"]
|
||||
if total_space_size != 0 or fragment_count == 0:
|
||||
space_size = 1
|
||||
|
||||
scan_x += space_size
|
||||
total_space_size += space_size
|
||||
budget -= 1
|
||||
continue
|
||||
|
||||
if total_space_size >= 2 and ocr_line.text:
|
||||
text += " "
|
||||
if len(fragments) > 0:
|
||||
fragments[-1]["text"] += " "
|
||||
|
||||
coded_color = chat_color[0] * 256 * 256 + chat_color[1] * 256 + chat_color[2]
|
||||
fragments.append(
|
||||
{
|
||||
"color": coded_color,
|
||||
"text": ocr_line.text,
|
||||
"index": len(text),
|
||||
}
|
||||
)
|
||||
fragment_count += 1
|
||||
text += ocr_line.text
|
||||
max_fragment_len = max(max_fragment_len, len(ocr_line.text))
|
||||
scan_x = max(
|
||||
scan_x + font["space_width"], ocr_line.area.x + ocr_line.area.width
|
||||
)
|
||||
total_space_size = 0
|
||||
|
||||
if len(fragments) == 0 or max_fragment_len <= 2:
|
||||
return None
|
||||
|
||||
return {
|
||||
"fragments": fragments,
|
||||
"text": text,
|
||||
}
|
||||
|
||||
|
||||
def read_chat_line(
|
||||
image: Image,
|
||||
ref_color: Color,
|
||||
font: FontDefinition,
|
||||
x: int,
|
||||
y: int,
|
||||
backward=False,
|
||||
forward=True,
|
||||
scan_mode=False,
|
||||
) -> Optional[OCRLine]:
|
||||
text = ""
|
||||
w_fw = x
|
||||
w_bw = x
|
||||
|
||||
if forward:
|
||||
shift_x = 0
|
||||
last_not_found = False
|
||||
cur_text = ""
|
||||
while True:
|
||||
flag2 = scan_mode and not text # FIXME: What's my name?
|
||||
char = read_chat_char(
|
||||
image,
|
||||
ref_color,
|
||||
font,
|
||||
x + shift_x,
|
||||
y,
|
||||
backward=False,
|
||||
allow_secondary=not flag2,
|
||||
)
|
||||
if not char:
|
||||
if flag2:
|
||||
backward = False
|
||||
break
|
||||
if last_not_found:
|
||||
break
|
||||
|
||||
cur_text += " "
|
||||
shift_x += font["space_width"]
|
||||
last_not_found = True
|
||||
else:
|
||||
last_not_found = False
|
||||
shift_x += char.base_char["width"]
|
||||
|
||||
if char.base_char.secondary:
|
||||
cur_text += char.chr
|
||||
continue
|
||||
|
||||
text = text + cur_text + char.chr
|
||||
cur_text = ""
|
||||
w_fw = x + shift_x
|
||||
|
||||
if backward:
|
||||
shift_x = 0
|
||||
last_not_found = False
|
||||
cur_text = ""
|
||||
while True:
|
||||
char = read_chat_char(image, ref_color, font, x + shift_x, y, backward=True)
|
||||
|
||||
if not char:
|
||||
if last_not_found:
|
||||
break
|
||||
cur_text = " " + cur_text
|
||||
shift_x -= font["space_width"]
|
||||
last_not_found = True
|
||||
continue
|
||||
|
||||
last_not_found = False
|
||||
shift_x -= char.base_char.width
|
||||
if char.base_char.secondary:
|
||||
cur_text = char.chr + cur_text
|
||||
continue
|
||||
text = char.chr + cur_text + text
|
||||
cur_text = ""
|
||||
w_bw = x + shift_x
|
||||
|
||||
return OCRLine(text=text, area=Rectangle(w_bw, y - 9, w_fw - w_bw, 10))
|
||||
|
||||
|
||||
class _chatScore(NamedTuple):
|
||||
score: int
|
||||
size_score: int
|
||||
chr: CharInfo
|
||||
|
||||
|
||||
def read_chat_char(
|
||||
image: Image,
|
||||
ref_color: Color,
|
||||
font: FontDefinition,
|
||||
x: int,
|
||||
y: int,
|
||||
backward=False,
|
||||
allow_secondary=True,
|
||||
) -> Optional[ReadCharInfo]:
|
||||
y -= font["base_y"]
|
||||
shadow = font["shadow"]
|
||||
|
||||
# Boundary check
|
||||
if y < 0 or y + font["height"] >= image.height:
|
||||
return None
|
||||
if not backward:
|
||||
if x < 0 or x + font["width"] > image.width:
|
||||
return None
|
||||
else:
|
||||
if x - font["width"] < 0 or x > image.width:
|
||||
return None
|
||||
|
||||
scores: List[_chatScore] = []
|
||||
for chr in font["chars"]:
|
||||
if chr.secondary and not allow_secondary:
|
||||
continue
|
||||
if backward:
|
||||
chrx = x - chr.width
|
||||
else:
|
||||
chrx = x
|
||||
|
||||
a = 0
|
||||
score = 0
|
||||
while a < len(chr.pixels):
|
||||
ix = chrx + chr.pixels[a]
|
||||
iy = y + chr.pixels[a + 1]
|
||||
pixel = image.getpixel((ix, iy))
|
||||
if not shadow:
|
||||
penalty = can_blend(
|
||||
pixel[0],
|
||||
pixel[1],
|
||||
pixel[2],
|
||||
ref_color[0],
|
||||
ref_color[1],
|
||||
ref_color[2],
|
||||
chr.pixels[a + 2] / 255,
|
||||
)
|
||||
a += 3
|
||||
else:
|
||||
lum = chr.pixels[a + 3] / 255
|
||||
penalty = can_blend(
|
||||
pixel[0],
|
||||
pixel[1],
|
||||
pixel[2],
|
||||
ref_color[0] * lum,
|
||||
ref_color[1] * lum,
|
||||
ref_color[2] * lum,
|
||||
chr.pixels[a + 2] / 255,
|
||||
)
|
||||
a += 4
|
||||
|
||||
score += max(0, penalty)
|
||||
|
||||
size_score = score - chr.bonus
|
||||
scores.append(_chatScore(score=score, size_score=size_score, chr=chr))
|
||||
|
||||
scores.sort(key=lambda item: item.size_score)
|
||||
|
||||
if not scores:
|
||||
return None
|
||||
|
||||
winner = scores[0]
|
||||
if winner.score > 400:
|
||||
return None
|
||||
|
||||
shift_x = 0
|
||||
shift_y = font["base_y"]
|
||||
|
||||
return ReadCharInfo(
|
||||
chr=winner.chr.chr,
|
||||
base_char=winner.chr,
|
||||
x=x + shift_x,
|
||||
y=y + shift_y,
|
||||
score=winner.score,
|
||||
size_score=winner.size_score,
|
||||
)
|
||||
@@ -0,0 +1,65 @@
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import TypedDict, NamedTuple, List, Optional
|
||||
|
||||
from PIL import Image
|
||||
|
||||
from runekit.alt1.schema import FontMeta
|
||||
from ._utils import unblend_known_background
|
||||
|
||||
FONT_BASE = Path(__file__).parent / "fonts"
|
||||
|
||||
|
||||
class CharInfo(NamedTuple):
|
||||
width: int
|
||||
chr: str
|
||||
bonus: int
|
||||
secondary: bool
|
||||
pixels: List[int]
|
||||
|
||||
|
||||
class FontDefinition(TypedDict):
|
||||
chars: List[CharInfo]
|
||||
width: int
|
||||
height: int
|
||||
space_width: int
|
||||
shadow: bool
|
||||
base_y: int
|
||||
min_rating: Optional[int]
|
||||
max_spaces: Optional[int]
|
||||
|
||||
|
||||
def load_font(name: str) -> FontDefinition:
|
||||
meta_file = FONT_BASE / (name + ".fontmeta.json")
|
||||
with meta_file.open() as fp:
|
||||
meta: FontMeta = json.load(fp)
|
||||
|
||||
return load_font_file(FONT_BASE / (name + ".data.png"), meta)
|
||||
|
||||
|
||||
def load_font_file(image: Path, meta: FontMeta) -> FontDefinition:
|
||||
img = Image.open(image)
|
||||
return load_font_image(img, meta)
|
||||
|
||||
|
||||
def load_font_image(image: Image, meta: FontMeta) -> FontDefinition:
|
||||
if meta["unblendmode"] == "removebg":
|
||||
bg_height = int((image.height - 1) / 2)
|
||||
background = image.crop((0, bg_height + 1, image.width, image.height))
|
||||
font_img = unblend_known_background(
|
||||
image.crop((0, 0, image.width, bg_height)),
|
||||
background,
|
||||
meta["shadow"],
|
||||
meta["color"][0],
|
||||
meta["color"][1],
|
||||
meta["color"][2],
|
||||
)
|
||||
font_img.show()
|
||||
elif meta["unblendmode"] == "raw":
|
||||
raise NotImplementedError
|
||||
elif meta["unblendmode"] == "blackbg":
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
load_font("chat_8px")
|
||||
|
After Width: | Height: | Size: 6.4 KiB |
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"basey": 11,
|
||||
"spacewidth": 3,
|
||||
"treshold": 0.6,
|
||||
"color": [ 255, 255, 255 ],
|
||||
"unblendmode": "raw",
|
||||
"shadow": false,
|
||||
"chars": "!\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_abcdefghijklmnopqrstuvwxyz{|}~",
|
||||
"seconds": ",.-:;\"'",
|
||||
"spriteid": 17814
|
||||
}
|
||||
|
After Width: | Height: | Size: 8.4 KiB |
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"basey": 17,
|
||||
"spacewidth": 4,
|
||||
"treshold": 0.6,
|
||||
"color": [ 255, 255, 255 ],
|
||||
"unblendmode": "raw",
|
||||
"shadow": false,
|
||||
"chars": "!\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_abcdefghijklmnopqrstuvwxyz{|}~",
|
||||
"seconds": ",.-:;\"'"
|
||||
}
|
||||
|
After Width: | Height: | Size: 5.1 KiB |
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"basey": 8,
|
||||
"spacewidth": 3,
|
||||
"treshold": 0.6,
|
||||
"color": [ 255, 255, 255 ],
|
||||
"unblendmode": "raw",
|
||||
"shadow": false,
|
||||
"chars": "!\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_abcdefghijklmnopqrstuvwxyz{|}~",
|
||||
"seconds": ",.-:;\"'"
|
||||
}
|
||||
|
After Width: | Height: | Size: 6.2 KiB |
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"basey": 10,
|
||||
"spacewidth": 3,
|
||||
"treshold": 0.6,
|
||||
"color": [ 255, 255, 255 ],
|
||||
"unblendmode": "raw",
|
||||
"shadow": false,
|
||||
"chars": "!\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_abcdefghijklmnopqrstuvwxyz{|}~",
|
||||
"seconds": ",.-:;\"'|"
|
||||
}
|
||||
|
After Width: | Height: | Size: 3.5 KiB |
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"basey": 10,
|
||||
"spacewidth": 3,
|
||||
"treshold": 0.6,
|
||||
"color": [ 127, 169, 255 ],
|
||||
"unblendmode": "blackbg",
|
||||
"shadow": false,
|
||||
"chars": "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789%/+?!@#$^~&*()_-=[]{}:;\"'<>\\.,|",
|
||||
"seconds": ",.-:;\"'"
|
||||
}
|
||||
|
After Width: | Height: | Size: 7.9 KiB |
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"basey": 8,
|
||||
"spacewidth": 3,
|
||||
"treshold": 0.6,
|
||||
"color": [ 255, 255, 255 ],
|
||||
"unblendmode": "raw",
|
||||
"shadow": true,
|
||||
"chars": "!\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_abcdefghijklmnopqrstuvwxyz{|}~",
|
||||
"seconds": ",.-:;\"'"
|
||||
}
|
||||
|
After Width: | Height: | Size: 4.9 KiB |
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"basey": 8,
|
||||
"spacewidth": 3,
|
||||
"treshold": 0.6,
|
||||
"color": [ 255, 255, 255 ],
|
||||
"unblendmode": "raw",
|
||||
"shadow": false,
|
||||
"chars": "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz:()0123456789%,.'?",
|
||||
"seconds": ",.-:;\"'"
|
||||
}
|
||||
|
After Width: | Height: | Size: 7.0 KiB |
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"basey": 10,
|
||||
"spacewidth": 3,
|
||||
"treshold": 0.6,
|
||||
"color": [ 127, 169, 255 ],
|
||||
"unblendmode": "removebg",
|
||||
"shadow": true,
|
||||
"chars": "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789%/+?!@#$^~&*()_-=[]{}:;\"'<>\\.,|",
|
||||
"seconds": ",.-:;\"'"
|
||||
}
|
||||
|
After Width: | Height: | Size: 5.5 KiB |
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"basey": 13,
|
||||
"spacewidth": 5,
|
||||
"treshold": 0.6,
|
||||
"color": [ 255, 255, 255 ],
|
||||
"unblendmode": "raw",
|
||||
"shadow": false,
|
||||
"chars": "!\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_abcdefghijklmnopqrstuvwxyz{|}~",
|
||||
"seconds": ",.-:;\"'|*"
|
||||
}
|
||||
|
After Width: | Height: | Size: 5.8 KiB |
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"basey": 12,
|
||||
"spacewidth": 3,
|
||||
"treshold": 0.6,
|
||||
"color": [ 127, 169, 255 ],
|
||||
"unblendmode": "removebg",
|
||||
"shadow": true,
|
||||
"chars": "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789%/+?!@#$^~&*()_-=[]{}:;\"'<>\\.,|",
|
||||
"seconds": ",.-:;\"'"
|
||||
}
|
||||
|
After Width: | Height: | Size: 6.1 KiB |
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"basey": 13,
|
||||
"spacewidth": 4,
|
||||
"treshold": 0.6,
|
||||
"color": [ 127, 169, 255 ],
|
||||
"unblendmode": "removebg",
|
||||
"shadow": true,
|
||||
"chars": "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789%/+?!@#$^~&*()_-=[]{}:;\"'<>\\.,|",
|
||||
"seconds": ",.-:;\"'"
|
||||
}
|
||||
|
After Width: | Height: | Size: 8.3 KiB |
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"basey": 15,
|
||||
"spacewidth": 6,
|
||||
"treshold": 0.6,
|
||||
"color": [ 127, 169, 255 ],
|
||||
"unblendmode": "removebg",
|
||||
"shadow": true,
|
||||
"chars": "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789%/+?!@#$^~&*()_-=[]{}:;\"'<>\\.,|",
|
||||
"seconds": ",.-:;\"'",
|
||||
"//":"Seems to have a 2px letter spacing and 4px spacewidth, the letter spacing is included in the font definition after the characters"
|
||||
}
|
||||
|
After Width: | Height: | Size: 7.0 KiB |
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"basey": 10,
|
||||
"spacewidth": 3,
|
||||
"treshold": 0.6,
|
||||
"color": [ 127, 169, 255 ],
|
||||
"unblendmode": "removebg",
|
||||
"shadow": true,
|
||||
"chars": "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789%/+?!@#$^~&*()_-=[]{}:;\"'<>\\.,|",
|
||||
"seconds": ",.-:;\"'"
|
||||
}
|
||||
|
After Width: | Height: | Size: 5.7 KiB |
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"basey": 8,
|
||||
"spacewidth": 4,
|
||||
"treshold": 0.9,
|
||||
"color": [ 255, 255, 255 ],
|
||||
"unblendmode": "raw",
|
||||
"shadow": false,
|
||||
"chars": "!\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_abcdefghijklmnopqrstuvwxyz{|}~",
|
||||
"seconds": ",.-:;\"'"
|
||||
}
|
||||
|
After Width: | Height: | Size: 530 B |
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"basey": 7,
|
||||
"spacewidth": 4,
|
||||
"treshold": 0.9,
|
||||
"color": [ 255, 255, 255 ],
|
||||
"unblendmode": "raw",
|
||||
"shadow": true,
|
||||
"chars": "0123456789m()",
|
||||
"seconds": ""
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
from cpython.mem cimport PyMem_Malloc, PyMem_Free
|
||||
|
||||
cdef struct ImageRGBA:
|
||||
unsigned char r
|
||||
unsigned char g
|
||||
unsigned char b
|
||||
unsigned char a
|
||||
|
||||
def image_to_bgra(image, long ix, long iy, unsigned long w, unsigned long h):
|
||||
ptr = dict(image.im.unsafe_ptrs)['image32']
|
||||
cdef size_t ptr_int = <size_t> ptr
|
||||
cdef ImageRGBA **image32 = <ImageRGBA**> ptr_int
|
||||
|
||||
if ix < 0:
|
||||
ix = 0
|
||||
if iy < 0:
|
||||
iy = 0
|
||||
if ix + w > image.width:
|
||||
w = image.width - ix
|
||||
if iy + h > image.height:
|
||||
h = image.height - iy
|
||||
|
||||
cdef size_t size = w * h * 4
|
||||
cdef unsigned char *buf = <unsigned char*> PyMem_Malloc(size)
|
||||
|
||||
cdef size_t index, x, y
|
||||
cdef unsigned char r, g, b, a
|
||||
|
||||
with nogil:
|
||||
for y in range(iy, iy+h):
|
||||
for x in range(ix, ix+w):
|
||||
index = (((y-iy) * w) + (x-ix)) * 4
|
||||
r = image32[y][x].r
|
||||
g = image32[y][x].g
|
||||
b = image32[y][x].b
|
||||
a = image32[y][x].a
|
||||
|
||||
buf[index] = b
|
||||
buf[index+1] = g
|
||||
buf[index+2] = r
|
||||
buf[index+3] = a
|
||||
|
||||
try:
|
||||
return buf[:size]
|
||||
finally:
|
||||
PyMem_Free(buf)
|
||||