File: surface.py

package info (click to toggle)
mypaint 2.0.1-14
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 27,884 kB
  • sloc: python: 43,893; cpp: 6,931; xml: 2,475; sh: 473; makefile: 25
file content (328 lines) | stat: -rw-r--r-- 11,344 bytes parent folder | download | duplicates (4)
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()