~corey.bryant/ubuntu/wily/python-pyscss/thedac

« back to all changes in this revision

Viewing changes to scss/functions/compass/images.py

  • Committer: Package Import Robot
  • Author(s): Thomas Goirand
  • Date: 2014-06-26 12:10:36 UTC
  • mfrom: (1.1.1)
  • Revision ID: package-import@ubuntu.com-20140626121036-3dv13zn5zptk9fpx
Tags: 1.2.0.post3-1
* Team upload.
* New upstream release (Closes: #738776).
* Added a debian/gbp.conf.
* Added missing ${python:Depends}
* Added Python 3 support.
* Removed duplicate debhelper build-depends.
* Cannonical VCS URLs.
* Standards-Version: is now 3.9.5.
* Added a watch file.
* override dh helpers which the package doesn't use.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
"""Image utilities ported from Compass."""
 
2
 
 
3
from __future__ import absolute_import
 
4
from __future__ import print_function
 
5
 
 
6
import base64
 
7
import hashlib
 
8
import logging
 
9
import mimetypes
 
10
import os.path
 
11
import time
 
12
 
 
13
import six
 
14
from six.moves import xrange
 
15
 
 
16
from scss import config
 
17
from scss.functions.compass import _image_size_cache
 
18
from scss.functions.compass.helpers import add_cache_buster
 
19
from scss.functions.library import FunctionLibrary
 
20
from scss.types import Color, List, Number, String
 
21
from scss.util import escape
 
22
 
 
23
try:
 
24
    from PIL import Image
 
25
except ImportError:
 
26
    try:
 
27
        import Image
 
28
    except:
 
29
        Image = None
 
30
 
 
31
log = logging.getLogger(__name__)
 
32
 
 
33
COMPASS_IMAGES_LIBRARY = FunctionLibrary()
 
34
register = COMPASS_IMAGES_LIBRARY.register
 
35
 
 
36
 
 
37
# ------------------------------------------------------------------------------
 
38
 
 
39
def _images_root():
 
40
    return config.STATIC_ROOT if config.IMAGES_ROOT is None else config.IMAGES_ROOT
 
41
 
 
42
 
 
43
def _image_url(path, only_path=False, cache_buster=True, dst_color=None, src_color=None, inline=False, mime_type=None, spacing=None, collapse_x=None, collapse_y=None):
 
44
    """
 
45
    src_color - a list of or a single color to be replaced by each corresponding dst_color colors
 
46
    spacing - spaces to be added to the image
 
47
    collapse_x, collapse_y - collapsable (layered) image of the given size (x, y)
 
48
    """
 
49
    if inline or dst_color or spacing:
 
50
        if not Image:
 
51
            raise Exception("Images manipulation require PIL")
 
52
    filepath = String.unquoted(path).value
 
53
    fileext = os.path.splitext(filepath)[1].lstrip('.').lower()
 
54
    if mime_type:
 
55
        mime_type = String.unquoted(mime_type).value
 
56
    if not mime_type:
 
57
        mime_type = mimetypes.guess_type(filepath)[0]
 
58
    if not mime_type:
 
59
        mime_type = 'image/%s' % fileext
 
60
    path = None
 
61
    IMAGES_ROOT = _images_root()
 
62
    if callable(IMAGES_ROOT):
 
63
        try:
 
64
            _file, _storage = list(IMAGES_ROOT(filepath))[0]
 
65
            d_obj = _storage.modified_time(_file)
 
66
            filetime = int(time.mktime(d_obj.timetuple()))
 
67
            if inline or dst_color or spacing:
 
68
                path = _storage.open(_file)
 
69
        except:
 
70
            filetime = 'NA'
 
71
    else:
 
72
        _path = os.path.join(IMAGES_ROOT.rstrip('/'), filepath.strip('/'))
 
73
        if os.path.exists(_path):
 
74
            filetime = int(os.path.getmtime(_path))
 
75
            if inline or dst_color or spacing:
 
76
                path = open(_path, 'rb')
 
77
        else:
 
78
            filetime = 'NA'
 
79
 
 
80
    BASE_URL = config.IMAGES_URL or config.STATIC_URL
 
81
    if path:
 
82
        dst_colors = [list(Color(v).value[:3]) for v in List.from_maybe(dst_color) if v]
 
83
 
 
84
        src_color = Color.from_name('black') if src_color is None else src_color
 
85
        src_colors = [tuple(Color(v).value[:3]) for v in List.from_maybe(src_color)]
 
86
 
 
87
        len_colors = max(len(dst_colors), len(src_colors))
 
88
        dst_colors = (dst_colors * len_colors)[:len_colors]
 
89
        src_colors = (src_colors * len_colors)[:len_colors]
 
90
 
 
91
        spacing = Number(0) if spacing is None else spacing
 
92
        spacing = [int(Number(v).value) for v in List.from_maybe(spacing)]
 
93
        spacing = (spacing * 4)[:4]
 
94
 
 
95
        file_name, file_ext = os.path.splitext(os.path.normpath(filepath).replace('\\', '_').replace('/', '_'))
 
96
        key = (filetime, src_color, dst_color, spacing)
 
97
        key = file_name + '-' + base64.urlsafe_b64encode(hashlib.md5(repr(key)).digest()).rstrip('=').replace('-', '_')
 
98
        asset_file = key + file_ext
 
99
        ASSETS_ROOT = config.ASSETS_ROOT or os.path.join(config.STATIC_ROOT, 'assets')
 
100
        asset_path = os.path.join(ASSETS_ROOT, asset_file)
 
101
 
 
102
        if os.path.exists(asset_path):
 
103
            filepath = asset_file
 
104
            BASE_URL = config.ASSETS_URL
 
105
            if inline:
 
106
                path = open(asset_path, 'rb')
 
107
                url = 'data:' + mime_type + ';base64,' + base64.b64encode(path.read())
 
108
            else:
 
109
                url = '%s%s' % (BASE_URL, filepath)
 
110
                if cache_buster:
 
111
                    filetime = int(os.path.getmtime(asset_path))
 
112
                    url = add_cache_buster(url, filetime)
 
113
        else:
 
114
            simply_process = False
 
115
            image = None
 
116
 
 
117
            if fileext in ('cur',):
 
118
                simply_process = True
 
119
            else:
 
120
                try:
 
121
                    image = Image.open(path)
 
122
                except IOError:
 
123
                    if not collapse_x and not collapse_y and not dst_colors:
 
124
                        simply_process = True
 
125
 
 
126
            if simply_process:
 
127
                if inline:
 
128
                    url = 'data:' + mime_type + ';base64,' + base64.b64encode(path.read())
 
129
                else:
 
130
                    url = '%s%s' % (BASE_URL, filepath)
 
131
                    if cache_buster:
 
132
                        filetime = int(os.path.getmtime(asset_path))
 
133
                        url = add_cache_buster(url, filetime)
 
134
            else:
 
135
                width, height = collapse_x or image.size[0], collapse_y or image.size[1]
 
136
                new_image = Image.new(
 
137
                    mode='RGBA',
 
138
                    size=(width + spacing[1] + spacing[3], height + spacing[0] + spacing[2]),
 
139
                    color=(0, 0, 0, 0)
 
140
                )
 
141
                for i, dst_color in enumerate(dst_colors):
 
142
                    src_color = src_colors[i]
 
143
                    pixdata = image.load()
 
144
                    for _y in xrange(image.size[1]):
 
145
                        for _x in xrange(image.size[0]):
 
146
                            pixel = pixdata[_x, _y]
 
147
                            if pixel[:3] == src_color:
 
148
                                pixdata[_x, _y] = tuple([int(c) for c in dst_color] + [pixel[3] if len(pixel) == 4 else 255])
 
149
                iwidth, iheight = image.size
 
150
                if iwidth != width or iheight != height:
 
151
                    cy = 0
 
152
                    while cy < iheight:
 
153
                        cx = 0
 
154
                        while cx < iwidth:
 
155
                            cropped_image = image.crop((cx, cy, cx + width, cy + height))
 
156
                            new_image.paste(cropped_image, (int(spacing[3]), int(spacing[0])), cropped_image)
 
157
                            cx += width
 
158
                        cy += height
 
159
                else:
 
160
                    new_image.paste(image, (int(spacing[3]), int(spacing[0])))
 
161
 
 
162
                if not inline:
 
163
                    try:
 
164
                        new_image.save(asset_path)
 
165
                        filepath = asset_file
 
166
                        BASE_URL = config.ASSETS_URL
 
167
                        if cache_buster:
 
168
                            filetime = int(os.path.getmtime(asset_path))
 
169
                    except IOError:
 
170
                        log.exception("Error while saving image")
 
171
                        inline = True  # Retry inline version
 
172
                    url = os.path.join(config.ASSETS_URL.rstrip('/'), asset_file.lstrip('/'))
 
173
                    if cache_buster:
 
174
                        url = add_cache_buster(url, filetime)
 
175
                if inline:
 
176
                    output = six.BytesIO()
 
177
                    new_image.save(output, format='PNG')
 
178
                    contents = output.getvalue()
 
179
                    output.close()
 
180
                    url = 'data:' + mime_type + ';base64,' + base64.b64encode(contents)
 
181
    else:
 
182
        url = os.path.join(BASE_URL.rstrip('/'), filepath.lstrip('/'))
 
183
        if cache_buster and filetime != 'NA':
 
184
            url = add_cache_buster(url, filetime)
 
185
 
 
186
    if not only_path:
 
187
        url = 'url(%s)' % escape(url)
 
188
    return String.unquoted(url)
 
189
 
 
190
 
 
191
@register('inline-image', 1)
 
192
@register('inline-image', 2)
 
193
@register('inline-image', 3)
 
194
@register('inline-image', 4)
 
195
@register('inline-image', 5)
 
196
def inline_image(image, mime_type=None, dst_color=None, src_color=None, spacing=None, collapse_x=None, collapse_y=None):
 
197
    """
 
198
    Embeds the contents of a file directly inside your stylesheet, eliminating
 
199
    the need for another HTTP request. For small files such images or fonts,
 
200
    this can be a performance benefit at the cost of a larger generated CSS
 
201
    file.
 
202
    """
 
203
    return _image_url(image, False, False, dst_color, src_color, True, mime_type, spacing, collapse_x, collapse_y)
 
204
 
 
205
 
 
206
@register('image-url', 1)
 
207
@register('image-url', 2)
 
208
@register('image-url', 3)
 
209
@register('image-url', 4)
 
210
@register('image-url', 5)
 
211
@register('image-url', 6)
 
212
def image_url(path, only_path=False, cache_buster=True, dst_color=None, src_color=None, spacing=None, collapse_x=None, collapse_y=None):
 
213
    """
 
214
    Generates a path to an asset found relative to the project's images
 
215
    directory.
 
216
    Passing a true value as the second argument will cause the only the path to
 
217
    be returned instead of a `url()` function
 
218
    """
 
219
    return _image_url(path, only_path, cache_buster, dst_color, src_color, False, None, spacing, collapse_x, collapse_y)
 
220
 
 
221
 
 
222
@register('image-width', 1)
 
223
def image_width(image):
 
224
    """
 
225
    Returns the width of the image found at the path supplied by `image`
 
226
    relative to your project's images directory.
 
227
    """
 
228
    if not Image:
 
229
        raise Exception("Images manipulation require PIL")
 
230
    filepath = String.unquoted(image).value
 
231
    path = None
 
232
    try:
 
233
        width = _image_size_cache[filepath][0]
 
234
    except KeyError:
 
235
        width = 0
 
236
        IMAGES_ROOT = _images_root()
 
237
        if callable(IMAGES_ROOT):
 
238
            try:
 
239
                _file, _storage = list(IMAGES_ROOT(filepath))[0]
 
240
                path = _storage.open(_file)
 
241
            except:
 
242
                pass
 
243
        else:
 
244
            _path = os.path.join(IMAGES_ROOT, filepath.strip('/'))
 
245
            if os.path.exists(_path):
 
246
                path = open(_path, 'rb')
 
247
        if path:
 
248
            image = Image.open(path)
 
249
            size = image.size
 
250
            width = size[0]
 
251
            _image_size_cache[filepath] = size
 
252
    return Number(width, 'px')
 
253
 
 
254
 
 
255
@register('image-height', 1)
 
256
def image_height(image):
 
257
    """
 
258
    Returns the height of the image found at the path supplied by `image`
 
259
    relative to your project's images directory.
 
260
    """
 
261
    if not Image:
 
262
        raise Exception("Images manipulation require PIL")
 
263
    filepath = String.unquoted(image).value
 
264
    path = None
 
265
    try:
 
266
        height = _image_size_cache[filepath][1]
 
267
    except KeyError:
 
268
        height = 0
 
269
        IMAGES_ROOT = _images_root()
 
270
        if callable(IMAGES_ROOT):
 
271
            try:
 
272
                _file, _storage = list(IMAGES_ROOT(filepath))[0]
 
273
                path = _storage.open(_file)
 
274
            except:
 
275
                pass
 
276
        else:
 
277
            _path = os.path.join(IMAGES_ROOT, filepath.strip('/'))
 
278
            if os.path.exists(_path):
 
279
                path = open(_path, 'rb')
 
280
        if path:
 
281
            image = Image.open(path)
 
282
            size = image.size
 
283
            height = size[1]
 
284
            _image_size_cache[filepath] = size
 
285
    return Number(height, 'px')