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