OLD | NEW |
| (Empty) |
1 #!/usr/bin/env python | |
2 # Copyright (c) 2012 The Chromium Authors. All rights reserved. | |
3 # Use of this source code is governed by a BSD-style license that can be | |
4 # found in the LICENSE file. | |
5 | |
6 '''Gatherer for <structure type="chrome_scaled_image">. | |
7 ''' | |
8 | |
9 import os | |
10 import struct | |
11 | |
12 from grit import exception | |
13 from grit import lazy_re | |
14 from grit import util | |
15 from grit.gather import interface | |
16 | |
17 | |
18 _PNG_SCALE_CHUNK = '\0\0\0\0csCl\xc1\x30\x60\x4d' | |
19 | |
20 | |
21 def _RescaleImage(data, from_scale, to_scale): | |
22 if from_scale != to_scale: | |
23 assert from_scale == 100 | |
24 # Rather than rescaling the image we add a custom chunk directing Chrome to | |
25 # rescale it on load. Just append it to the PNG data since | |
26 # _MoveSpecialChunksToFront will move it later anyway. | |
27 data += _PNG_SCALE_CHUNK | |
28 return data | |
29 | |
30 | |
31 _PNG_MAGIC = '\x89PNG\r\n\x1a\n' | |
32 | |
33 '''Mandatory first chunk in order for the png to be valid.''' | |
34 _FIRST_CHUNK = 'IHDR' | |
35 | |
36 '''Special chunks to move immediately after the IHDR chunk. (so that the PNG | |
37 remains valid.) | |
38 ''' | |
39 _SPECIAL_CHUNKS = frozenset('csCl npTc'.split()) | |
40 | |
41 '''Any ancillary chunk not in this list is deleted from the PNG.''' | |
42 _ANCILLARY_CHUNKS_TO_LEAVE = frozenset( | |
43 'bKGD cHRM gAMA iCCP pHYs sBIT sRGB tRNS'.split()) | |
44 | |
45 | |
46 def _MoveSpecialChunksToFront(data): | |
47 '''Move special chunks immediately after the IHDR chunk (so that the PNG | |
48 remains valid). Also delete ancillary chunks that are not on our whitelist. | |
49 ''' | |
50 first = [_PNG_MAGIC] | |
51 special_chunks = [] | |
52 rest = [] | |
53 for chunk in _ChunkifyPNG(data): | |
54 type = chunk[4:8] | |
55 critical = type < 'a' | |
56 if type == _FIRST_CHUNK: | |
57 first.append(chunk) | |
58 elif type in _SPECIAL_CHUNKS: | |
59 special_chunks.append(chunk) | |
60 elif critical or type in _ANCILLARY_CHUNKS_TO_LEAVE: | |
61 rest.append(chunk) | |
62 return ''.join(first + special_chunks + rest) | |
63 | |
64 | |
65 def _ChunkifyPNG(data): | |
66 '''Given a PNG image, yield its chunks in order.''' | |
67 assert data.startswith(_PNG_MAGIC) | |
68 pos = 8 | |
69 while pos != len(data): | |
70 length = 12 + struct.unpack_from('>I', data, pos)[0] | |
71 assert 12 <= length <= len(data) - pos | |
72 yield data[pos:pos+length] | |
73 pos += length | |
74 | |
75 | |
76 def _MakeBraceGlob(strings): | |
77 '''Given ['foo', 'bar'], return '{foo,bar}', for error reporting. | |
78 ''' | |
79 if len(strings) == 1: | |
80 return strings[0] | |
81 else: | |
82 return '{' + ','.join(strings) + '}' | |
83 | |
84 | |
85 class ChromeScaledImage(interface.GathererBase): | |
86 '''Represents an image that exists in multiple layout variants | |
87 (e.g. "default", "touch") and multiple scale variants | |
88 (e.g. "100_percent", "200_percent"). | |
89 ''' | |
90 | |
91 split_context_re_ = lazy_re.compile(r'(.+)_(\d+)_percent\Z') | |
92 | |
93 def _FindInputFile(self): | |
94 output_context = self.grd_node.GetRoot().output_context | |
95 match = self.split_context_re_.match(output_context) | |
96 if not match: | |
97 raise exception.MissingMandatoryAttribute( | |
98 'All <output> nodes must have an appropriate context attribute' | |
99 ' (e.g. context="touch_200_percent")') | |
100 req_layout, req_scale = match.group(1), int(match.group(2)) | |
101 | |
102 layouts = [req_layout] | |
103 try_default_layout = self.grd_node.GetRoot().fallback_to_default_layout | |
104 if try_default_layout and 'default' not in layouts: | |
105 layouts.append('default') | |
106 | |
107 # TODO(tdanderson): Search in descending order of all image scales | |
108 # instead of immediately falling back to 100. | |
109 # See crbug.com/503643. | |
110 scales = [req_scale] | |
111 try_low_res = self.grd_node.FindBooleanAttribute( | |
112 'fallback_to_low_resolution', default=False, skip_self=False) | |
113 if try_low_res and 100 not in scales: | |
114 scales.append(100) | |
115 | |
116 for layout in layouts: | |
117 for scale in scales: | |
118 dir = '%s_%s_percent' % (layout, scale) | |
119 path = os.path.join(dir, self.rc_file) | |
120 if os.path.exists(self.grd_node.ToRealPath(path)): | |
121 return path, scale, req_scale | |
122 | |
123 if not try_default_layout: | |
124 # The file was not found in the specified output context and it was | |
125 # explicitly indicated that the default context should not be searched | |
126 # as a fallback, so return an empty path. | |
127 return None, 100, req_scale | |
128 | |
129 # The file was found in neither the specified context nor the default | |
130 # context, so raise an exception. | |
131 dir = "%s_%s_percent" % (_MakeBraceGlob(layouts), | |
132 _MakeBraceGlob(map(str, scales))) | |
133 raise exception.FileNotFound( | |
134 'Tried ' + self.grd_node.ToRealPath(os.path.join(dir, self.rc_file))) | |
135 | |
136 def GetInputPath(self): | |
137 path, scale, req_scale = self._FindInputFile() | |
138 return path | |
139 | |
140 def Parse(self): | |
141 pass | |
142 | |
143 def GetTextualIds(self): | |
144 return [self.extkey] | |
145 | |
146 def GetData(self, *args): | |
147 path, scale, req_scale = self._FindInputFile() | |
148 if path is None: | |
149 return None | |
150 | |
151 data = util.ReadFile(self.grd_node.ToRealPath(path), util.BINARY) | |
152 data = _RescaleImage(data, scale, req_scale) | |
153 data = _MoveSpecialChunksToFront(data) | |
154 return data | |
155 | |
156 def Translate(self, *args, **kwargs): | |
157 return self.GetData() | |
OLD | NEW |