Class: Prawn::Images::PNG

Inherits:
Object
  • Object
show all
Defined in:
lib/prawn/images/png.rb

Overview

A convenience class that wraps the logic for extracting the parts of a PNG image that we need to embed them in a PDF

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(data) ⇒ PNG

Process a new PNG image

data

A binary string of PNG data



30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
# File 'lib/prawn/images/png.rb', line 30

def initialize(data)
  data = StringIO.new(data.dup)

  data.read(8)  # Skip the default header

  @palette  = ""
  @img_data = ""
  @transparency = {}

  loop do
    chunk_size  = data.read(4).unpack("N")[0]
    section     = data.read(4)
    case section
    when 'IHDR'
      # we can grab other interesting values from here (like width,
      # height, etc)
      values = data.read(chunk_size).unpack("NNCCCCC")

      @width              = values[0]
      @height             = values[1]
      @bits               = values[2]
      @color_type         = values[3]
      @compression_method = values[4]
      @filter_method      = values[5]
      @interlace_method   = values[6]
    when 'PLTE'
      @palette << data.read(chunk_size)
    when 'IDAT'
      @img_data << data.read(chunk_size)
    when 'tRNS'
      # This chunk can only occur once and it must occur after the
      # PLTE chunk and before the IDAT chunk
      @transparency = {}
      case @color_type
      when 3
        # Indexed colour, RGB. Each byte in this chunk is an alpha for
        # the palette index in the PLTE ("palette") chunk up until the
        # last non-opaque entry. Set up an array, stretching over all
        # palette entries which will be 0 (opaque) or 1 (transparent).
        @transparency[:indexed]  = data.read(chunk_size).unpack("C*")
        short = 255 - @transparency[:indexed].size
        @transparency[:indexed] += ([255] * short) if short > 0
      when 0
        # Greyscale. Corresponding to entries in the PLTE chunk.
        # Grey is two bytes, range 0 .. (2 ^ bit-depth) - 1
        grayval = data.read(chunk_size).unpack("n").first
        @transparency[:grayscale] = grayval
      when 2
        # True colour with proper alpha channel.
        @transparency[:rgb] = data.read(chunk_size).unpack("nnn")
      end
    when 'IEND'
      # we've got everything we need, exit the loop
      break
    else
      # unknown (or un-important) section, skip over it
      data.seek(data.pos + chunk_size)
    end

    data.read(4)  # Skip the CRC
  end
end

Instance Attribute Details

#alpha_channelObject (readonly)

Returns the value of attribute alpha_channel.



23
24
25
# File 'lib/prawn/images/png.rb', line 23

def alpha_channel
  @alpha_channel
end

#bitsObject (readonly)

Returns the value of attribute bits.



21
22
23
# File 'lib/prawn/images/png.rb', line 21

def bits
  @bits
end

#color_typeObject (readonly)

Returns the value of attribute color_type.



22
23
24
# File 'lib/prawn/images/png.rb', line 22

def color_type
  @color_type
end

#compression_methodObject (readonly)

Returns the value of attribute compression_method.



22
23
24
# File 'lib/prawn/images/png.rb', line 22

def compression_method
  @compression_method
end

#filter_methodObject (readonly)

Returns the value of attribute filter_method.



22
23
24
# File 'lib/prawn/images/png.rb', line 22

def filter_method
  @filter_method
end

#heightObject (readonly)

Returns the value of attribute height.



21
22
23
# File 'lib/prawn/images/png.rb', line 21

def height
  @height
end

#img_dataObject (readonly)

Returns the value of attribute img_data.



20
21
22
# File 'lib/prawn/images/png.rb', line 20

def img_data
  @img_data
end

#interlace_methodObject (readonly)

Returns the value of attribute interlace_method.



23
24
25
# File 'lib/prawn/images/png.rb', line 23

def interlace_method
  @interlace_method
end

#paletteObject (readonly)

Returns the value of attribute palette.



20
21
22
# File 'lib/prawn/images/png.rb', line 20

def palette
  @palette
end

#scaled_heightObject

Returns the value of attribute scaled_height.



24
25
26
# File 'lib/prawn/images/png.rb', line 24

def scaled_height
  @scaled_height
end

#scaled_widthObject

Returns the value of attribute scaled_width.



24
25
26
# File 'lib/prawn/images/png.rb', line 24

def scaled_width
  @scaled_width
end

#transparencyObject (readonly)

Returns the value of attribute transparency.



20
21
22
# File 'lib/prawn/images/png.rb', line 20

def transparency
  @transparency
end

#widthObject (readonly)

Returns the value of attribute width.



21
22
23
# File 'lib/prawn/images/png.rb', line 21

def width
  @width
end

Instance Method Details

#alpha_channel?Boolean

Returns:

  • (Boolean)


121
122
123
# File 'lib/prawn/images/png.rb', line 121

def alpha_channel?
  @color_type == 4 || @color_type == 6
end

#alpha_channel_bitsObject

Adobe Reader can’t handle 16-bit png channels – chop off the second byte (least significant)



128
129
130
# File 'lib/prawn/images/png.rb', line 128

def alpha_channel_bits
  8
end

#build_pdf_object(document) ⇒ Object

Build a PDF object representing this image in document, and return a Reference to it.



135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
# File 'lib/prawn/images/png.rb', line 135

def build_pdf_object(document)
  if compression_method != 0
    raise Errors::UnsupportedImageType,
      'PNG uses an unsupported compression method'
  end

  if filter_method != 0
    raise Errors::UnsupportedImageType,
      'PNG uses an unsupported filter method'
  end

  if interlace_method != 0
    raise Errors::UnsupportedImageType,
      'PNG uses unsupported interlace method'
  end

  # some PNG types store the colour and alpha channel data together,
  # which the PDF spec doesn't like, so split it out.
  split_alpha_channel!

  case colors
  when 1
    color = :DeviceGray
  when 3
    color = :DeviceRGB
  else
    raise Errors::UnsupportedImageType,
      "PNG uses an unsupported number of colors (#{png.colors})"
  end

  # build the image dict
  obj = document.ref!(
    :Type             => :XObject,
    :Subtype          => :Image,
    :Height           => height,
    :Width            => width,
    :BitsPerComponent => bits,
    :Length           => img_data.size,
    :Filter           => :FlateDecode
  )

  unless alpha_channel
    obj.data[:DecodeParms] = {:Predictor => 15,
                              :Colors    => colors,
                              :BitsPerComponent => bits,
                              :Columns   => width}
  end

  # append the actual image data to the object as a stream
  obj << img_data
  
  # sort out the colours of the image
  if palette.empty?
    obj.data[:ColorSpace] = color
  else
    # embed the colour palette in the PDF as a object stream
    palette_obj = document.ref!(:Length => palette.size)
    palette_obj << palette

    # build the color space array for the image
    obj.data[:ColorSpace] = [:Indexed, 
                             :DeviceRGB,
                             (palette.size / 3) -1,
                             palette_obj]
  end

  # *************************************
  # add transparency data if necessary
  # *************************************

  # For PNG color types 0, 2 and 3, the transparency data is stored in
  # a dedicated PNG chunk, and is exposed via the transparency attribute
  # of the PNG class.
  if transparency[:grayscale]
    # Use Color Key Masking (spec section 4.8.5)
    # - An array with N elements, where N is two times the number of color
    #   components.
    val = transparency[:grayscale]
    obj.data[:Mask] = [val, val]
  elsif transparency[:rgb]
    # Use Color Key Masking (spec section 4.8.5)
    # - An array with N elements, where N is two times the number of color
    #   components.
    rgb = transparency[:rgb]
    obj.data[:Mask] = rgb.collect { |x| [x,x] }.flatten
  elsif transparency[:indexed]
    # TODO: broken. I was attempting to us Color Key Masking, but I think
    #       we need to construct an SMask i think. Maybe do it inside
    #       the PNG class, and store it in alpha_channel
    #obj.data[:Mask] = transparency[:indexed]
  end

  # For PNG color types 4 and 6, the transparency data is stored as a alpha
  # channel mixed in with the main image data. The PNG class seperates
  # it out for us and makes it available via the alpha_channel attribute
  if alpha_channel?
    smask_obj = document.ref!(
      :Type             => :XObject,
      :Subtype          => :Image,
      :Height           => height,
      :Width            => width,
      :BitsPerComponent => alpha_channel_bits,
      :Length           => alpha_channel.size,
      :Filter           => :FlateDecode,
      :ColorSpace       => :DeviceGray,
      :Decode           => [0, 1]
    )
    smask_obj << alpha_channel
    obj.data[:SMask] = smask_obj
  end

  obj
end

#colorsObject

number of color components to each pixel



95
96
97
98
99
100
101
102
# File 'lib/prawn/images/png.rb', line 95

def colors
  case self.color_type
  when 0, 3, 4
    return 1
  when 2, 6
    return 3
  end
end

#min_pdf_versionObject

Returns the minimum PDF version required to support this image.



250
251
252
253
254
255
256
257
258
259
260
# File 'lib/prawn/images/png.rb', line 250

def min_pdf_version
  if bits > 8
    # 16-bit color only supported in 1.5+ (ISO 32000-1:2008 8.9.5.1)
    1.5
  elsif alpha_channel?
    # Need transparency for SMask
    1.4
  else
    1.0
  end
end

#pixel_bitlengthObject

number of bits used per pixel



106
107
108
109
110
111
112
# File 'lib/prawn/images/png.rb', line 106

def pixel_bitlength
  if alpha_channel?
    self.bits * (self.colors + 1)
  else
    self.bits * self.colors
  end
end

#split_alpha_channel!Object

split the alpha channel data from the raw image data in images where it’s required.



117
118
119
# File 'lib/prawn/images/png.rb', line 117

def split_alpha_channel!
  unfilter_image_data if alpha_channel?
end