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