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 9import os 10import struct 11 12from grit import exception 13from grit import lazy_re 14from grit import util 15from grit.gather import interface 16 17 18_PNG_SCALE_CHUNK = '\0\0\0\0csCl\xc1\x30\x60\x4d' 19 20 21def _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 37remains 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 46def _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 65def _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 76def _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 85class 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 if 'default' not in layouts: 104 layouts.append('default') 105 scales = [req_scale] 106 try_low_res = self.grd_node.FindBooleanAttribute( 107 'fallback_to_low_resolution', default=False, skip_self=False) 108 if try_low_res and 100 not in scales: 109 scales.append(100) 110 for layout in layouts: 111 for scale in scales: 112 dir = '%s_%s_percent' % (layout, scale) 113 path = os.path.join(dir, self.rc_file) 114 if os.path.exists(self.grd_node.ToRealPath(path)): 115 return path, scale, req_scale 116 # If we get here then the file is missing, so fail. 117 dir = "%s_%s_percent" % (_MakeBraceGlob(layouts), 118 _MakeBraceGlob(map(str, scales))) 119 raise exception.FileNotFound( 120 'Tried ' + self.grd_node.ToRealPath(os.path.join(dir, self.rc_file))) 121 122 def GetInputPath(self): 123 path, scale, req_scale = self._FindInputFile() 124 return path 125 126 def Parse(self): 127 pass 128 129 def GetTextualIds(self): 130 return [self.extkey] 131 132 def GetData(self, *args): 133 path, scale, req_scale = self._FindInputFile() 134 data = util.ReadFile(self.grd_node.ToRealPath(path), util.BINARY) 135 data = _RescaleImage(data, scale, req_scale) 136 data = _MoveSpecialChunksToFront(data) 137 return data 138 139 def Translate(self, *args, **kwargs): 140 return self.GetData() 141