| 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 |