-
Notifications
You must be signed in to change notification settings - Fork 385
/
surface.py
328 lines (261 loc) · 11.1 KB
/
surface.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
# This file is part of MyPaint.
# -*- coding: utf-8 -*-
# Copyright (C) 2015-2018 by the MyPaint Development Team#
#
# 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 2 of the License, or
# (at your option) any later version.
"""Common interfaces & routines for surface and surface-like objects"""
from __future__ import division, print_function
import abc
import os
import logging
import numpy as np
from . import mypaintlib
import lib.helpers
from lib.errors import FileHandlingError
from lib.gettext import C_
import lib.feedback
from lib.pycompat import xrange
logger = logging.getLogger(__name__)
N = mypaintlib.TILE_SIZE
# throttle excesssive calls to the save/render progress monitor objects
TILES_PER_CALLBACK = 256
class Bounded (object):
"""Interface for objects with an inherent size"""
__metaclass__ = abc.ABCMeta
@abc.abstractmethod
def get_bbox(self):
"""Returns the bounding box of the object, in model coords
:returns: the data bounding box
:rtype: lib.helpers.Rect
"""
class TileAccessible (Bounded):
"""Interface for objects whose memory is accessible by tile"""
__metaclass__ = abc.ABCMeta
@abc.abstractmethod
def tile_request(self, tx, ty, readonly):
"""Access by tile, read-only or read/write
:param int tx: Tile X coord (multiply by TILE_SIZE for pixels)
:param int ty: Tile Y coord (multiply by TILE_SIZE for pixels)
:param bool readonly: get a read-only tile
Implementations must be `@contextlib.contextmanager`s which
yield one tile array (NxNx16, fix15 data). If called in
read/write mode, implementations must either put back changed
data, or alternatively they must allow the underlying data to be
manipulated directly via the yielded object.
See lib.tiledsurface.MyPaintSurface.tile_request() for a fuller
explanation of this interface and its expectations.
"""
class TileBlittable (Bounded):
"""Interface for unconditional copying by tile"""
__metaclass__ = abc.ABCMeta
@abc.abstractmethod
def blit_tile_into(self, dst, dst_has_alpha, tx, ty, *args, **kwargs):
"""Copies one tile from this object into a NumPy array
:param numpy.ndarray dst: destination array
:param bool dst_has_alpha: destination has an alpha channel
:param int tx: Tile X coord (multiply by TILE_SIZE for pixels)
:param int ty: Tile Y coord (multiply by TILE_SIZE for pixels)
:param \*args: Implementation may extend this interface
:param \*\*kwargs: Implementation may extend this interface
The destination is typically of dimensions NxNx4, and is
typically of type uint16 or uint8. Implementations are expected
to check the details, and should raise ValueError if dst doesn't
have a sensible shape or type.
This is an unconditional copy of this object's raw visible data,
ignoring any flags or opacities on the object itself which would
otherwise control what you see.
If the source object really consists of multiple compositables
with special rendering flags, they should be composited normally
into an empty tile, and that resultant tile blitted.
"""
class TileCompositable (Bounded):
"""Interface for compositing by tile, with modes/opacities/flags"""
__metaclass__ = abc.ABCMeta
@abc.abstractmethod
def composite_tile(self, dst, dst_has_alpha, tx, ty, mipmap_level=0,
*args, **kwargs):
"""Composites one tile from this object over a NumPy array.
:param dst: target tile array (uint16, NxNx4, 15-bit scaled int)
:param dst_has_alpha: alpha channel in dst should be preserved
:param int tx: Tile X coord (multiply by TILE_SIZE for pixels)
:param int ty: Tile Y coord (multiply by TILE_SIZE for pixels)
:param int mode: mode to use when compositing
:param \*args: Implementation may extend this interface
:param \*\*kwargs: Implementation may extend this interface
Composite one tile of this surface over the array dst, modifying
only dst. Unlike `blit_tile_into()`, this method must respect
any special rendering settings on the object itself.
"""
def get_tiles_bbox(tile_coords):
"""Convert tile coords to a data bounding box
:param tile_coords: iterable of (tx, ty) coordinate pairs
>>> coords = [(0, 0), (-10, 4), (5, -2), (-3, 7)]
>>> get_tiles_bbox(coords[0:1])
Rect(0, 0, 64, 64)
>>> get_tiles_bbox(coords)
Rect(-640, -128, 1024, 640)
>>> get_tiles_bbox(coords[1:])
Rect(-640, -128, 1024, 640)
>>> get_tiles_bbox(coords[1:-1])
Rect(-640, -128, 1024, 448)
"""
bounds = lib.helpers.coordinate_bounds(tile_coords)
if bounds is None:
return lib.helpers.Rect()
else:
x0, y0, x1, y1 = bounds
return lib.helpers.Rect(
N * x0, N * y0, N * (x1 - x0 + 1), N * (y1 - y0 + 1)
)
def scanline_strips_iter(surface, rect, alpha=False,
single_tile_pattern=False, **kwargs):
"""Generate (render) scanline strips from a tile-blittable object
:param TileBlittable surface: Surface to iterate over
:param bool alpha: If true, write a PNG with alpha
:param bool single_tile_pattern: True if surface is a one tile only.
:param tuple \*\*kwargs: Passed to blit_tile_into.
The `alpha` parameter is passed to the surface's `blit_tile_into()`.
Rendering is skipped for all but the first line of single-tile patterns.
The scanline strips yielded by this generator are suitable for
feeding to a mypaintlib.ProgressivePNGWriter.
"""
# Sizes
x, y, w, h = rect
assert w > 0
assert h > 0
# calculate bounding box in full tiles
render_tx = x // N
render_ty = y // N
render_tw = (x + w - 1) // N - render_tx + 1
render_th = (y + h - 1) // N - render_ty + 1
# buffer for rendering one tile row at a time
arr = np.empty((N, render_tw * N, 4), 'uint8') # rgba or rgbu
# view into arr without the horizontal padding
arr_xcrop = arr[:, x-render_tx*N:x-render_tx*N+w, :]
first_row = render_ty
last_row = render_ty+render_th-1
for ty in range(render_ty, render_ty+render_th):
skip_rendering = False
if single_tile_pattern:
# optimization for simple background patterns
# e.g. solid color
if ty != first_row:
skip_rendering = True
for tx_rel in xrange(render_tw):
# render one tile
dst = arr[:, tx_rel*N:(tx_rel+1)*N, :]
if not skip_rendering:
tx = render_tx + tx_rel
try:
surface.blit_tile_into(dst, alpha, tx, ty, **kwargs)
except Exception:
logger.exception("Failed to blit tile %r of %r",
(tx, ty), surface)
mypaintlib.tile_clear_rgba8(dst)
# yield a numpy array of the scanline without padding
res = arr_xcrop
if ty == last_row:
res = res[:y+h-ty*N, :, :]
if ty == first_row:
res = res[y-render_ty*N:, :, :]
yield res
def save_as_png(surface, filename, *rect, **kwargs):
"""Saves a tile-blittable surface to a file in PNG format
:param TileBlittable surface: Surface to save
:param unicode filename: The file to write
:param tuple \*rect: Rectangle (x, y, w, h) to save
:param bool alpha: If true, write a PNG with alpha
:param progress: Updates a UI every scanline strip.
:type progress: lib.feedback.Progress or None
:param bool single_tile_pattern: True if surface is one tile only.
:param bool save_srgb_chunks: Set to False to not save sRGB flags.
:param tuple \*\*kwargs: Passed to blit_tile_into (minus the above)
The `alpha` parameter is passed to the surface's `blit_tile_into()`
method, as well as to the PNG writer. Rendering is
skipped for all but the first line for single-tile patterns.
If `*rect` is left unspecified, the surface's own bounding box will
be used.
If `save_srgb_chunks` is set to False, sRGB (and associated fallback
cHRM and gAMA) will not be saved. MyPaint's default behaviour is
currently to save these chunks.
Raises `lib.errors.FileHandlingError` with a descriptive string if
something went wrong.
"""
# Horrible, dirty argument handling
alpha = kwargs.pop('alpha', False)
progress = kwargs.pop('progress', None)
single_tile_pattern = kwargs.pop("single_tile_pattern", False)
save_srgb_chunks = kwargs.pop("save_srgb_chunks", True)
# Sizes. Save at least one tile to allow empty docs to be written
if not rect:
rect = surface.get_bbox()
x, y, w, h = rect
if w == 0 or h == 0:
x, y, w, h = (0, 0, 1, 1)
rect = (x, y, w, h)
if not progress:
progress = lib.feedback.Progress()
num_strips = int((1 + ((y + h) // N)) - (y // N))
progress.items = num_strips
try:
logger.debug(
"Writing %r (%dx%d) alpha=%r srgb=%r",
filename,
w, h,
alpha,
save_srgb_chunks,
)
with open(filename, "wb") as writer_fp:
pngsave = mypaintlib.ProgressivePNGWriter(
writer_fp,
w, h,
alpha,
save_srgb_chunks,
)
scanline_strips = scanline_strips_iter(
surface, rect,
alpha=alpha,
single_tile_pattern=single_tile_pattern,
**kwargs
)
for scanline_strip in scanline_strips:
pngsave.write(scanline_strip)
if not progress:
continue
try:
progress += 1
except Exception:
logger.exception(
"Failed to update lib.feedback.Progress: "
"dropping it"
)
progress = None
pngsave.close()
logger.debug("Finished writing %r", filename)
if progress:
progress.close()
except (IOError, OSError, RuntimeError) as err:
logger.exception(
"Caught %r from C++ png-writer code, re-raising as a "
"FileHandlingError",
err,
)
raise FileHandlingError(C_(
"low-level PNG writer failure report (dialog)",
u"Failed to write “{basename}”.\n\n"
u"Reason: {err}\n"
u"Target folder: “{dirname}”."
).format(
err = err,
basename = os.path.basename(filename),
dirname = os.path.dirname(filename),
))
# Other possible exceptions include TypeError, ValueError, but
# those indicate incorrect coding usually; just raise them
# normally.
if __name__ == "__main__":
import doctest
doctest.testmod()