Class: Rbimg::PNG

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

Defined Under Namespace

Classes: Chunk

Constant Summary collapse

REQUIRED_CHUNKS =
[
    :IHDR,
    :IDAT,
    :IEND
]
COLOR_TYPES =
{
    greyscale: 0,
    rgb: 2,
    pallet: 3,
    greyscale_alpha: 4,
    rgba: 6
}

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(pixels: nil, type: nil, width: nil, height: nil, bit_depth: 8, compression_method: 0, filter_method: 0, interlace_method: 0, palette: nil) ⇒ PNG

Returns a new instance of PNG.

Raises:

  • (ArgumentError)


267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
# File 'lib/image_types/png.rb', line 267

def initialize(pixels: nil, type: nil, width: nil, height: nil, bit_depth: 8, compression_method: 0, filter_method: 0, interlace_method: 0, palette: nil)
    @pixels, @width, @height, @compression_method, @filter_method, @interlace_method = pixels, width, height, compression_method, filter_method, interlace_method
    @bit_depth = bit_depth
    type = :greyscale if type.nil?

    @type = type.is_a?(Integer) ? type : COLOR_TYPES[type]
    raise ArgumentError.new("#{type} is not a valid color type. Please use one of: #{COLOR_TYPES.keys}") if type.nil?
    raise ArgumentError.new("Palettes are not compatible with color types 0 and 4") if palette && (@type == 0 || @type == 4)
    raise ArgumentError.new("palette must be an array") if palette && !palette.is_a?(Array)
    @signature = [137, 80, 78, 71, 13, 10, 26, 10]
    @chunks = [
        Chunk.IHDR(
            width: @width, 
            height: @height, 
            bit_depth: @bit_depth, 
            color_type: @type, 
            compression_method: @compression_method, 
            filter_method: @filter_method, 
            interlace_method: interlace_method
        ),
        *Chunk.IDATs(
            pixels, 
            color_type: @type, 
            bit_depth: @bit_depth, 
            width: @width, 
            height: @height
        ),
        Chunk.IEND
    ]
    @chunks.insert(1, Chunk.PLTE(palette)) if !palette.nil?

    @pixel_size = Rbimg::PNG.pixel_size_for(color_type: @type)

end

Instance Attribute Details

#bit_depthObject (readonly)

Returns the value of attribute bit_depth.



265
266
267
# File 'lib/image_types/png.rb', line 265

def bit_depth
  @bit_depth
end

#compression_methodObject (readonly)

Returns the value of attribute compression_method.



265
266
267
# File 'lib/image_types/png.rb', line 265

def compression_method
  @compression_method
end

#filter_methodObject (readonly)

Returns the value of attribute filter_method.



265
266
267
# File 'lib/image_types/png.rb', line 265

def filter_method
  @filter_method
end

#heightObject (readonly)

Returns the value of attribute height.



265
266
267
# File 'lib/image_types/png.rb', line 265

def height
  @height
end

#interlace_methodObject (readonly)

Returns the value of attribute interlace_method.



265
266
267
# File 'lib/image_types/png.rb', line 265

def interlace_method
  @interlace_method
end

#pixel_sizeObject (readonly)

Returns the value of attribute pixel_size.



266
267
268
# File 'lib/image_types/png.rb', line 266

def pixel_size
  @pixel_size
end

#pixelsObject (readonly)

Returns the value of attribute pixels.



265
266
267
# File 'lib/image_types/png.rb', line 265

def pixels
  @pixels
end

#widthObject (readonly)

Returns the value of attribute width.



265
266
267
# File 'lib/image_types/png.rb', line 265

def width
  @width
end

Class Method Details

.combine(*images, divider: nil, as: :row) ⇒ Object

Raises:

  • (ArgumentError)


18
19
20
21
22
23
24
25
26
27
28
29
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
# File 'lib/image_types/png.rb', line 18

def self.combine(*images, divider: nil, as: :row) 
    raise ArgumentError.new("as: must be :row or :col") if as != :row && as != :col 
    raise ArgumentError.new("Images and divider must all be an Rbimg::PNG") if !images.all?{|i| i.is_a?(Rbimg::PNG)} 
    type = images.first.type
    height = images.first.height
    width = images.first.width
    bit_depth = images.first.bit_depth

    color_type = COLOR_TYPES[type]

    logical_pixel_width = width * Rbimg::PNG.pixel_size_for(color_type: color_type)

    if divider
        width_multiplier = logical_pixel_width / width
        divider_width = divider.width * width_multiplier
        if as == :row
            raise ArgumentError.new("divider must have the same height as images if aligning as a row") if divider.height != height
        elsif as == :col
            raise ArgumentError.new("divider must have the same width as images if aligning as a column") if divider.width != width
        end
        raise ArgumentError.new("divider must have the same type and bit_depth as images") if divider.type != type || divider.bit_depth != bit_depth
    end

    images.each do |i|
        if i.type != type || i.height != height || i.width != width || i.bit_depth != bit_depth
            raise ArgumentError.new("Currently all images must have the same type, height, width, and bit_depth to be combined")
        end
    end


    if as == :row
        new_width = images.length * width 
        new_height = height
        
        if divider
            new_width += (divider.width * (images.length - 1))
        end

        new_pixels = height.times.map do |row|
            row_start = row * logical_pixel_width
            divider_row_start = row * divider_width if divider
            images.map do |img|
                row_pixels = img.pixels[row_start...(row_start + logical_pixel_width)] 
                ((img == images.last) || divider.nil?) ? row_pixels : row_pixels + divider.pixels[divider_row_start...(divider_row_start + divider_width)]
            end
        end.flatten
    
    else
        new_width = width
        new_height = images.length * height
        if divider
            new_height += (divider.height * (images.length - 1))
        end

        new_pixels = images.map do |img|
            img_pixels = img.pixels
            if divider && img != images.last
                img_pixels + divider.pixels
            else
                img_pixels
            end
        end.flatten
    end

    begin
    new_img = Rbimg::PNG.new(pixels: new_pixels, type: type, width: new_width, height: new_height, bit_depth: bit_depth)
    rescue
        binding.pry
    end

end

.pixel_size_for(color_type:) ⇒ Object



90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
# File 'lib/image_types/png.rb', line 90

def self.pixel_size_for(color_type:)
    case color_type
    when 0
        1
    when 2
        3
    when 3
        1
    when 4
        2
    when 6
        4
    else
        raise ArgumentError.new("#{color_type} is not a valid color type. Must be 0,2,3,4, or 6")
    end
end

.read(path: nil, data: nil) ⇒ Object

Raises:

  • (ArgumentError)


107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
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
248
249
250
251
252
253
254
255
256
257
258
259
260
261
# File 'lib/image_types/png.rb', line 107

def self.read(path: nil, data: nil)
    
    raise ArgumentError.new(".read must be initialized with a path or a datastream") if (path.nil? && data.nil?) || (!path.nil? && !data.nil?)
    raise ArgumentError.new("data must be an array of byte integers or a byte string") if data && !data.is_a?(Array) && !data.is_a?(String)
    raise ArgumentError.new("data must be an array of byte integers or a byte string") if data && data.is_a?(Array) && !data.first.is_a?(Integer) 
    path += ".png" if path && !path.end_with?('.png')
    begin

        if path
            data = File.read(path).bytes
        else
            data = data.bytes if data.is_a?(String)
        end
        
        chunk_start = 8
        chunks = []
        loop do 
            len_end = chunk_start + 4
            type_end = len_end + 4
            len = Byteman.buf2int(data[chunk_start...len_end])
            type = data[len_end...type_end]
            chunk_end = type_end + len + 4
            case type.pack("C*")
            when "IHDR"
                chunks << Chunk.readIHDR(data[chunk_start...chunk_end])
            when "IDAT"
                chunks << Chunk.readIDAT(data[chunk_start...chunk_end])
            when "PLTE"
                chunks << Chunk.readPLTE(data[chunk_start...chunk_end])
            else
                chunks << data[chunk_start...chunk_end]
            end

            chunk_start = chunk_end
            #TODO: Make sure last chunk is IEND
            break if chunk_end >= data.length - 1

        end

        width = chunks.first[:width]
        height = chunks.first[:height]
        bit_depth = chunks.first[:bit_depth]
        color_type = chunks.first[:color_type]
        compression_method = chunks.first[:compression_method]
        filter_method = chunks.first[:filter_method]
        interlace_method = chunks.first[:interlace_method]

        all_idats = chunks.filter{ |c| c.is_a?(Hash) && c[:type] == "IDAT" }
        compressed_pixels = all_idats.reduce([]) { |mem, idat| mem + idat[:compressed_pixels] }
        pixels_and_filter = Zlib::Inflate.inflate(compressed_pixels.pack("C*")).unpack("C*")
        

        logical_pixel_width = Rbimg::PNG.pixel_size_for(color_type: color_type) * width

        pixel_width = (logical_pixel_width * (bit_depth / 8.0)).ceil


        scanline_filters = Array.new(height, nil)
        pixels = Array.new(pixels_and_filter.length - height, nil)
        
        pixels_and_filter.each.with_index do |pixel,i| 
            scanline = i / (pixel_width + 1)
            pixel_number = (i % (pixel_width + 1)) - 1
            pixel_loc = scanline * pixel_width + pixel_number
            if (pixel_number == -1)
                scanline_filters[scanline] = pixel
            else
                case scanline_filters[scanline]
                when 0
                    pixels[pixel_loc] = pixel
                when 1
                    x = pixel_number
                    bpp = (pixel_width / width) * (bit_depth / 8.0).ceil
                    prev_raw = x - bpp < 0 ? 0 : pixels[pixel_loc - bpp]
                    new_pixel = (pixel + prev_raw) % 256
                    pixels[pixel_loc] = new_pixel
                when 2
                    x = pixel_number
                    prev_raw = scanline == 0 ? 0 : pixels[pixel_loc - pixel_width]
                    new_pixel = (pixel + prev_raw) % 256
                    pixels[pixel_loc] = new_pixel
                when 3
                    x = pixel_number
                    bpp = (pixel_width / width) * (bit_depth / 8.0).ceil
                    left_pix = x - bpp < 0 ? 0 : pixels[pixel_loc - bpp]
                    above_pix = scanline == 0 ? 0 : pixels[pixel_loc - pixel_width]
                    new_pixel = (pixel + ((left_pix + above_pix) / 2).floor) % 256
                    pixels[pixel_loc] = new_pixel
                when 4
                    x = pixel_number
                    bpp = (pixel_width / width) * (bit_depth / 8.0).ceil

                    paeth_predictor = Proc.new do |left, above, upper_left|
                        p = left + above - upper_left
                        pa = (p - left).abs
                        pb = (p - above).abs
                        pc = (p - upper_left).abs
                        if pa <= pb && pa <= pc
                            left
                        elsif pb <= pc
                            above
                        else
                            upper_left
                        end
                    end

                    left_pix = x - bpp < 0 ? 0 : pixels[pixel_loc - bpp]
                    above_pix = scanline == 0 ? 0 : pixels[pixel_loc - pixel_width]
                    upper_left_pix = scanline == 0 ? 0 : x - bpp < 0 ? 0 : pixels[pixel_loc - pixel_width - bpp]
                    pp_out = paeth_predictor[left_pix, above_pix, upper_left_pix]
                    new_pixel = (pixel + pp_out) % 256
                    pixels[pixel_loc] = new_pixel
                else
                    raise Rbimg::FormatError.new("Incorrect Filtering type used on this PNG file")
                end
            end
        end


        

        if bit_depth != 8
            corrected_pixels = Array.new(logical_pixel_width * height, nil)
            height.times do |row_num| 
                row_start = row_num * pixel_width
                row_end = row_start + pixel_width
                row_data = pixels[row_start...row_end]
                binary_data = Byteman.buf2int(row_data).to_s(2)
                pad_size = ((logical_pixel_width * bit_depth) / 8.0).ceil * 8
                binary_data = Byteman.pad(num: binary_data, len: pad_size, type: :bits)
                corrected_row_start = row_num * logical_pixel_width
                logical_pixel_width.times do |pixel_num_in_row|
                    binary_segment_start = pixel_num_in_row * bit_depth
                    binary_segment_end = binary_segment_start + bit_depth
                    binary_segment = binary_data[binary_segment_start...binary_segment_end]
                    logical_pixel_value = binary_segment.to_i(2)
                    corrected_pixel_location = corrected_row_start + pixel_num_in_row
                    corrected_pixels[corrected_pixel_location] = logical_pixel_value
                end
            end
        else
            corrected_pixels = pixels
        end

        args = {pixels: corrected_pixels, type: color_type, width: width, height: height, bit_depth: bit_depth}
        plte = chunks.find{|c| c[:type] == "PLTE" unless c.is_a?(Array)}
        args[:palette] = plte[:chunk_data] if plte
        new(**args)
    rescue Errno::ENOENT => e
        raise ArgumentError.new("Invalid path #{path}")
    rescue => e
        raise e if e.is_a?(Rbimg::FormatError)
        raise Rbimg::FormatError.new("This PNG file is not in the correct format or has been corrupted :)")
    end
end

Instance Method Details

#bytesObject



323
324
325
# File 'lib/image_types/png.rb', line 323

def bytes
    all_data.pack("C*")
end

#pixel(num) ⇒ Object



302
303
304
305
306
# File 'lib/image_types/png.rb', line 302

def pixel(num)
    start = num * @pixel_size
    pend = start + @pixel_size
    pixels[start...pend]
end

#row(rownum) ⇒ Object



308
309
310
311
312
313
314
# File 'lib/image_types/png.rb', line 308

def row(rownum)
    return nil if rownum > self.height
    pix_width = pixel_size * width
    start = rownum * pix_width
    pend = start + pix_width
    pixels[start...pend]
end

#typeObject



317
318
319
320
321
# File 'lib/image_types/png.rb', line 317

def type
    COLOR_TYPES.each do |k,v|
        return k if v == @type
    end
end

#write(path: Dir.pwd + "/output.png") ⇒ Object



327
328
329
330
# File 'lib/image_types/png.rb', line 327

def write(path: Dir.pwd + "/output.png")
    postscript = path.split(".").last == "png" ? "" : ".png"
    File.write(path + postscript, bytes)
end