Skip to content

Commit 230ad4f

Browse files
authored
Add files via upload
1 parent 04c7dfc commit 230ad4f

2 files changed

Lines changed: 393 additions & 10 deletions

File tree

upcean/predraw/predrawlib.py

Lines changed: 393 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,393 @@
1+
# -*- coding: utf-8 -*-
2+
'''
3+
This program is free software; you can redistribute it and/or modify
4+
it under the terms of the Revised BSD License.
5+
6+
This program is distributed in the hope that it will be useful,
7+
but WITHOUT ANY WARRANTY; without even the implied warranty of
8+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9+
Revised BSD License for more details.
10+
11+
Copyright 2011-2025 Game Maker 2k - https://github.com/GameMaker2k
12+
Copyright 2011-2025 Kazuki Przyborowski - https://github.com/KazukiPrzyborowski
13+
14+
$FileInfo: predrawlib.py - Last Update: 7/2/2025 Ver. 2.20.2 RC 1 - Author: cooldude2k $
15+
'''
16+
17+
from __future__ import absolute_import, division, print_function, unicode_literals, generators, with_statement, nested_scopes
18+
19+
import os
20+
import re
21+
import math
22+
23+
from drawlib.apis import (
24+
clear, config, rectangle, line, text, save,
25+
ShapeStyle, LineStyle, TextStyle, Colors, FontFile
26+
)
27+
from upcean.downloader import upload_file_to_internet_file
28+
import upcean.fonts
29+
30+
# -------------------------
31+
# Py2 / Py3 compatibility
32+
# -------------------------
33+
try:
34+
basestring # Py2
35+
except NameError: # Py3
36+
basestring = str
37+
38+
try:
39+
from io import BytesIO, IOBase
40+
except ImportError: # Py2
41+
from StringIO import StringIO as BytesIO # bytes-ish in many Py2 setups
42+
try:
43+
from StringIO import StringIO as IOBase
44+
except Exception:
45+
IOBase = object
46+
47+
try:
48+
import gzip
49+
except ImportError:
50+
gzip = None
51+
52+
# -------------------------
53+
# Fonts
54+
# -------------------------
55+
fontpathocra = upcean.fonts.fontpathocra
56+
fontpathocraalt = upcean.fonts.fontpathocraalt
57+
fontpathocrb = upcean.fonts.fontpathocrb
58+
fontpathocrbalt = upcean.fonts.fontpathocrbalt
59+
60+
# -------------------------
61+
# Regex / constants
62+
# -------------------------
63+
_RE_URL = re.compile(r"^(ftp|ftps|sftp)://", re.IGNORECASE)
64+
_RE_NAME_EXT = re.compile(r"^(?P<name>.+):(?P<ext>[A-Za-z0-9]+)$")
65+
66+
_DEFAULT_EXT = "png"
67+
68+
# Cache FontFile instances (FontFile(ttf) can be non-trivial)
69+
_FONTFILE_CACHE = {}
70+
71+
72+
def _get_fontfile(ftype):
73+
f = (ftype or "ocrb").lower()
74+
if f == "ocra":
75+
ttf = fontpathocra if os.path.exists(fontpathocra) else fontpathocraalt
76+
else:
77+
ttf = fontpathocrb if os.path.exists(fontpathocrb) else fontpathocrbalt
78+
79+
key = ttf
80+
ff = _FONTFILE_CACHE.get(key)
81+
if ff is None:
82+
ff = FontFile(ttf)
83+
_FONTFILE_CACHE[key] = ff
84+
return ff
85+
86+
87+
def _is_file_like(x):
88+
return hasattr(x, "write") or isinstance(x, IOBase)
89+
90+
91+
def _to_bytes(data):
92+
# Py2/3 safe: ensure bytes
93+
if isinstance(data, bytes):
94+
return data
95+
try:
96+
return data.encode("utf-8")
97+
except Exception:
98+
return bytes(data)
99+
100+
101+
def _gzip_bytes(data_bytes, compresslevel=9):
102+
if gzip is None:
103+
raise ImportError("gzip module not available")
104+
out = BytesIO()
105+
gz = gzip.GzipFile(filename="", mode="wb", fileobj=out, compresslevel=compresslevel)
106+
try:
107+
gz.write(data_bytes)
108+
finally:
109+
gz.close()
110+
return out.getvalue()
111+
112+
113+
def snapCoords(ctx, x, y):
114+
# keeps signature compatibility with other backends
115+
return (round(x) + 0.5, round(y) + 0.5)
116+
117+
118+
# -------------------------
119+
# Coordinate conversion
120+
# -------------------------
121+
def _img_to_draw_y(H, y_img):
122+
"""
123+
Convert image coords (origin top-left, y down)
124+
to drawlib coords (origin bottom-left, y up).
125+
"""
126+
return float(H) - float(y_img)
127+
128+
129+
def _rect_img_to_draw(H, x1, y1, x2, y2):
130+
"""
131+
Input is image-style half-open rectangle:
132+
[x1,x2) and [y1,y2)
133+
Return drawlib-style rectangle (x, y, w, h)
134+
where (x,y) is *top-left* in image coords, but drawlib uses bottom-left.
135+
So we flip by using y = H - y2.
136+
"""
137+
x1 = float(x1); y1 = float(y1); x2 = float(x2); y2 = float(y2)
138+
w = x2 - x1
139+
h = y2 - y1
140+
# y2 is "lower" in image coords; in draw coords it becomes the bottom distance
141+
y_draw = float(H) - y2
142+
return x1, y_draw, w, h
143+
144+
145+
# -------------------------
146+
# Drawlib context shim
147+
# -------------------------
148+
class DrawlibContext(object):
149+
"""
150+
Shim context so existing code can call ctx.rectangle/line/text like Pillow/Cairo backends.
151+
152+
IMPORTANT: drawlib's coordinate system is (0,0) bottom-left. Upcean renderers are top-left.
153+
We convert incoming coords from image-space to drawlib-space here.
154+
"""
155+
def __init__(self, width, height, dpi=100):
156+
self.width = float(width)
157+
self.height = float(height)
158+
self.dpi = int(dpi)
159+
160+
def user_to_device(self, x, y):
161+
# keep signature compatibility; return drawlib coords
162+
return float(x), _img_to_draw_y(self.height, y)
163+
164+
def rectangle(self, coords, fill=None, outline=None):
165+
(x1, y1), (x2, y2) = coords
166+
x, y, w, h = _rect_img_to_draw(self.height, x1, y1, x2, y2)
167+
168+
if fill is not None:
169+
style = ShapeStyle(halign="left", valign="bottom", fcolor=fill, lwidth=0)
170+
else:
171+
style = ShapeStyle(
172+
halign="left", valign="bottom",
173+
fcolor=Colors.Transparent, lcolor=outline, lwidth=1
174+
)
175+
176+
rectangle(xy=(x, y), width=w, height=h, style=style)
177+
178+
def line(self, pts, fill, width):
179+
(x1, y1), (x2, y2) = pts
180+
x1 = float(x1); x2 = float(x2)
181+
y1d = _img_to_draw_y(self.height, y1)
182+
y2d = _img_to_draw_y(self.height, y2)
183+
184+
style = LineStyle(width=max(1, int(width)), color=fill)
185+
line((x1, y1d), (x2, y2d), style=style)
186+
187+
def text(self, pos, txt, font=None, fill=None, size=None):
188+
x, y = pos
189+
s = int(size) if size is not None else None
190+
191+
# Upcean y is "top aligned" for text in several backends.
192+
# Convert to drawlib coords and subtract size to keep it top-aligned.
193+
y_draw = _img_to_draw_y(self.height, y)
194+
if s:
195+
y_draw -= float(s)
196+
197+
style = TextStyle(color=fill, font=font)
198+
# enforce sensible anchor (if supported by drawlib)
199+
try:
200+
style.halign = "left"
201+
style.valign = "bottom"
202+
except Exception:
203+
pass
204+
if s is not None:
205+
style.size = s
206+
207+
text(xy=(float(x), float(y_draw)), text=str(txt), style=style)
208+
209+
210+
# -------------------------
211+
# Drawing functions (public API)
212+
# -------------------------
213+
def drawColorRectangle(ctx, x1, y1, x2, y2, color, imageoutlib=None):
214+
ctx.rectangle([(x1, y1), (x2, y2)], fill=color)
215+
return True
216+
217+
218+
def drawColorRectangleAlt(ctx, x1, y1, x2, y2, color, imageoutlib=None):
219+
ctx.rectangle([(x1, y1), (x2, y2)], outline=color)
220+
return True
221+
222+
223+
def drawColorLine(ctx, x1, y1, x2, y2, width, color, imageoutlib=None):
224+
"""
225+
IMPORTANT: For barcode bars, stroked lines can create end-caps/oddness.
226+
Emulate Cairo backend behavior:
227+
- if perfectly vertical/horizontal -> draw a filled rectangle of thickness `width`
228+
- else fallback to drawlib line()
229+
"""
230+
try:
231+
w = int(width)
232+
except Exception:
233+
w = 1
234+
if w < 1:
235+
w = 1
236+
237+
# Vertical line => rectangle
238+
if int(x1) == int(x2):
239+
cx = float(x1) - (w / 2.0)
240+
y_top = min(float(y1), float(y2))
241+
y_bot = max(float(y1), float(y2))
242+
ctx.rectangle([(cx, y_top), (cx + w, y_bot)], fill=color)
243+
return True
244+
245+
# Horizontal line => rectangle
246+
if int(y1) == int(y2):
247+
cy = float(y1) - (w / 2.0)
248+
x_left = min(float(x1), float(x2))
249+
x_right = max(float(x1), float(x2))
250+
ctx.rectangle([(x_left, cy), (x_right, cy + w)], fill=color)
251+
return True
252+
253+
# Diagonal => stroke
254+
ctx.line([(x1, y1), (x2, y2)], fill=color, width=w)
255+
return True
256+
257+
258+
def drawColorText(ctx, size, x, y, txt, color, ftype="ocrb", imageoutlib=None):
259+
font_file = _get_fontfile(ftype)
260+
261+
# drawlib’s “normal” look is around dpi=100
262+
base_dpi = 100
263+
dpi = getattr(ctx, "dpi", base_dpi) or base_dpi
264+
265+
scaled_size = int(round(float(size) * (float(base_dpi) / float(dpi))))
266+
if scaled_size < 1:
267+
scaled_size = 1
268+
269+
ctx.text((x, y), str(txt), font=font_file, fill=color, size=scaled_size)
270+
return True
271+
272+
273+
# -------------------------
274+
# Save filename parsing
275+
# -------------------------
276+
def get_save_filename(outfile, imageoutlib=None):
277+
"""
278+
Returns:
279+
- outfile unchanged if None/bool
280+
- (outfile, "png") for file-like objects or "-"
281+
- (name_or_path_or_url, ext) for strings/tuples
282+
"""
283+
if outfile is None or isinstance(outfile, bool):
284+
return outfile
285+
286+
if outfile == "-" or _is_file_like(outfile):
287+
return (outfile, _DEFAULT_EXT)
288+
289+
if isinstance(outfile, basestring):
290+
s = outfile.strip()
291+
if s == "" or s == "-":
292+
return (s, _DEFAULT_EXT)
293+
if _RE_URL.match(s):
294+
return (s, _DEFAULT_EXT)
295+
296+
base, ext = os.path.splitext(s)
297+
if ext:
298+
ext = ext.lstrip(".").lower()
299+
name = base
300+
else:
301+
m = _RE_NAME_EXT.match(s)
302+
if m:
303+
name = m.group("name")
304+
ext = m.group("ext").lower()
305+
else:
306+
name = s
307+
ext = _DEFAULT_EXT
308+
309+
if not ext:
310+
ext = _DEFAULT_EXT
311+
return (name, ext)
312+
313+
if isinstance(outfile, (tuple, list)) and len(outfile) == 2:
314+
return (outfile[0], str(outfile[1]).lower())
315+
316+
return False
317+
318+
319+
get_save_file = get_save_filename
320+
321+
322+
# -------------------------
323+
# Surface creation
324+
# -------------------------
325+
def new_image_surface(sizex, sizey, bgcolor, imageoutlib=None):
326+
clear()
327+
328+
dpi = int(math.ceil(float(sizex) / 10.0))
329+
if dpi < 1:
330+
dpi = 1
331+
332+
config(width=sizex, height=sizey, dpi=dpi, background_color=bgcolor)
333+
print(sizex, sizey, "dpi=", dpi)
334+
335+
return [DrawlibContext(sizex, sizey, dpi=dpi), None]
336+
337+
338+
# -------------------------
339+
# Saving
340+
# -------------------------
341+
def _normalize_local_path(path, outfileext):
342+
p = os.path.abspath(os.path.expandvars(os.path.expanduser(path)))
343+
ext = str(outfileext).lower()
344+
if ext and not p.lower().endswith("." + ext):
345+
p = p + "." + ext
346+
return p
347+
348+
349+
def save_to_file(inimage, outfile, outfileext, imgcomment="barcode", imageoutlib=None):
350+
"""
351+
drawlib save() can write to:
352+
- path string
353+
- file-like (e.g., BytesIO)
354+
"""
355+
upload_target = None
356+
return_bytes = False
357+
358+
ext = (outfileext or _DEFAULT_EXT).lower()
359+
360+
if isinstance(outfile, basestring) and _RE_URL.match(outfile):
361+
upload_target = outfile
362+
outfile = BytesIO()
363+
elif outfile == "-":
364+
outfile = BytesIO()
365+
return_bytes = True
366+
367+
if isinstance(outfile, basestring) and not _RE_URL.match(outfile) and outfile not in ("-", ""):
368+
outfile = _normalize_local_path(outfile, ext)
369+
370+
save(file=outfile, format=ext)
371+
372+
if upload_target:
373+
outfile.seek(0)
374+
upload_file_to_internet_file(outfile, upload_target)
375+
outfile.close()
376+
return True
377+
378+
if return_bytes:
379+
outfile.seek(0)
380+
data = outfile.read()
381+
outfile.close()
382+
return data
383+
384+
return True
385+
386+
387+
def save_to_filename(imgout, outfile, imgcomment="barcode", imageoutlib=None):
388+
parsed = get_save_filename(outfile, imageoutlib)
389+
if not parsed or isinstance(parsed, bool):
390+
return False if parsed is False else [None, None, imageoutlib]
391+
392+
name, ext = parsed
393+
return save_to_file(imgout, name, ext, imgcomment, imageoutlib)

0 commit comments

Comments
 (0)