pnglitch 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,37 @@
1
+ module PNGlitch
2
+
3
+ class DataSizeError < StandardError
4
+ def initialize filename, over, expected
5
+ over_size = digit_format over
6
+ expected_size = digit_format expected
7
+ message = <<-EOL.gsub(/^\s*/, '')
8
+ The size of your data goes over #{over_size}.
9
+ It should be #{expected_size} actually when it's a normal
10
+ formatted PNG file.
11
+ PNGlitch raised this error to avoid the attack known as
12
+ "zip bomb". If you are sure that the PNG image is safe,
13
+ please set manually your own upper size limit as the variable
14
+ of PNGlitch#open.
15
+ EOL
16
+ message = ["\e[31m", message, "\e[0m"].join if STDOUT.tty? # color red
17
+ message = [
18
+ 'Size of the decompressed data is too large - ',
19
+ filename, "\n\n", message, "\n"
20
+ ].join
21
+ super message
22
+ end
23
+
24
+ def digit_format digit # :nodoc:
25
+ digit.to_s.gsub(/(\d)(?=(\d\d\d)+(?!\d))/, "\\1,") + ' bytes'
26
+ end
27
+ private :digit_format
28
+ end
29
+
30
+ class FormatError < StandardError
31
+ def initialize filename
32
+ m = 'The passed file seems different from a PNG file - ' + filename
33
+ super m
34
+ end
35
+ end
36
+
37
+ end
@@ -0,0 +1,185 @@
1
+ module PNGlitch
2
+
3
+ # Filter represents the filtering functions that is defined in PNG spec.
4
+ #
5
+ class Filter
6
+
7
+ NONE = 0
8
+ SUB = 1
9
+ UP = 2
10
+ AVERAGE = 3
11
+ PAETH = 4
12
+
13
+ #
14
+ # Guesses and retuens the filter type as a number.
15
+ #
16
+ def self.guess filter_type
17
+ type = nil
18
+ if filter_type.is_a?(Numeric) && filter_type.between?(NONE, PAETH)
19
+ type = filter_type.to_i
20
+ elsif filter_type.to_s =~ /[0-4]/
21
+ type = filter_type.to_i
22
+ else
23
+ type = ['n', 's', 'u', 'a', 'p'].index(filter_type.to_s[0])
24
+ end
25
+ type
26
+ end
27
+
28
+ attr_reader :filter_type
29
+ attr_accessor :encoder, :decoder
30
+
31
+ def initialize filter_type, pixel_size
32
+ @filter_type = Filter.guess filter_type || 0
33
+ @filter_type_name = [:none, :sub, :up, :average, :paeth][@filter_type]
34
+ @pixel_size = pixel_size
35
+ @encoder = self.method ('encode_%s' % @filter_type_name.to_s).to_sym
36
+ @decoder = self.method ('decode_%s' % @filter_type_name.to_s).to_sym
37
+ end
38
+
39
+ #
40
+ # Filter with a specified filter type.
41
+ #
42
+ def encode data, prev_data = nil
43
+ @encoder.call data, prev_data
44
+ end
45
+
46
+ #
47
+ # Reconstruct with a specified filter type.
48
+ #
49
+ def decode data, prev_data = nil
50
+ @decoder.call data, prev_data
51
+ end
52
+
53
+ private
54
+
55
+ def encode_none data, prev # :nodoc:
56
+ data
57
+ end
58
+
59
+ def encode_sub data, prev # :nodoc:
60
+ # Filt(x) = Orig(x) - Orig(a)
61
+ d = data.dup
62
+ d.size.times.reverse_each do |i|
63
+ next if i < @pixel_size
64
+ x = d.getbyte i
65
+ a = d.getbyte i - @pixel_size
66
+ d.setbyte i, (x - a) & 0xff
67
+ end
68
+ d
69
+ end
70
+
71
+ def encode_up data, prev # :nodoc:
72
+ # Filt(x) = Orig(x) - Orig(b)
73
+ return data if prev.nil?
74
+ d = data.dup
75
+ d.size.times.reverse_each do |i|
76
+ x = d.getbyte i
77
+ b = prev.getbyte i
78
+ d.setbyte i, (x - b) & 0xff
79
+ end
80
+ d
81
+ end
82
+
83
+ def encode_average data, prev # :nodoc:
84
+ # Filt(x) = Orig(x) - floor((Orig(a) + Orig(b)) / 2)
85
+ d = data.dup
86
+ d.size.times.reverse_each do |i|
87
+ x = d.getbyte i
88
+ a = i >= @pixel_size ? d.getbyte(i - @pixel_size) : 0
89
+ b = !prev.nil? ? prev.getbyte(i) : 0
90
+ d.setbyte i, (x - ((a + b) / 2)) & 0xff
91
+ end
92
+ d
93
+ end
94
+
95
+ def encode_paeth data, prev # :nodoc:
96
+ # Filt(x) = Orig(x) - PaethPredictor(Orig(a), Orig(b), Orig(c))
97
+ #
98
+ # PaethPredictor(a, b, c)
99
+ # p = a + b - c
100
+ # pa = abs(p - a)
101
+ # pb = abs(p - b)
102
+ # pc = abs(p - c)
103
+ # if pa <= pb and pa <= pc then Pr = a
104
+ # else if pb <= pc then Pr = b
105
+ # else Pr = c
106
+ # return Pr
107
+ d = data.dup
108
+ d.size.times.reverse_each do |i|
109
+ x = d.getbyte i
110
+ is_a_exist = i >= @pixel_size
111
+ is_b_exist = !prev.nil?
112
+ a = is_a_exist ? d.getbyte(i - @pixel_size) : 0
113
+ b = is_b_exist ? prev.getbyte(i) : 0
114
+ c = is_a_exist && is_b_exist ? prev.getbyte(i - @pixel_size) : 0
115
+ p = a + b - c
116
+ pa = (p - a).abs
117
+ pb = (p - b).abs
118
+ pc = (p - c).abs
119
+ pr = pa <= pb && pa <= pc ? a : pb <= pc ? b : c
120
+ d.setbyte i, (x - pr) & 0xff
121
+ end
122
+ d
123
+ end
124
+
125
+ def decode_none data, prev # :nodoc:
126
+ data
127
+ end
128
+
129
+ def decode_sub data, prev # :nodoc:
130
+ # Recon(x) = Filt(x) + Recon(a)
131
+ d = data.dup
132
+ d.size.times do |i|
133
+ next if i < @pixel_size
134
+ x = d.getbyte i
135
+ a = d.getbyte i - @pixel_size
136
+ d.setbyte i, (x + a) & 0xff
137
+ end
138
+ d
139
+ end
140
+
141
+ def decode_up data, prev # :nodoc:
142
+ # Recon(x) = Filt(x) + Recon(b)
143
+ return data if prev.nil?
144
+ d = data.dup
145
+ d.size.times do |i|
146
+ x = d.getbyte i
147
+ b = prev.getbyte i
148
+ d.setbyte i, (x + b) & 0xff
149
+ end
150
+ d
151
+ end
152
+
153
+ def decode_average data, prev # :nodoc:
154
+ # Recon(x) = Filt(x) + floor((Recon(a) + Recon(b)) / 2)
155
+ d = data.dup
156
+ d.size.times do |i|
157
+ x = d.getbyte i
158
+ a = i >= @pixel_size ? d.getbyte(i - @pixel_size) : 0
159
+ b = !prev.nil? ? prev.getbyte(i) : 0
160
+ d.setbyte i, (x + ((a + b) / 2)) & 0xff
161
+ end
162
+ d
163
+ end
164
+
165
+ def decode_paeth data, prev # :nodoc:
166
+ # Recon(x) = Filt(x) + PaethPredictor(Recon(a), Recon(b), Recon(c))
167
+ d = data.dup
168
+ d.size.times do |i|
169
+ x = d.getbyte i
170
+ is_a_exist = i >= @pixel_size
171
+ is_b_exist = !prev.nil?
172
+ a = is_a_exist ? d.getbyte(i - @pixel_size) : 0
173
+ b = is_b_exist ? prev.getbyte(i) : 0
174
+ c = is_a_exist && is_b_exist ? prev.getbyte(i - @pixel_size) : 0
175
+ p = a + b - c
176
+ pa = (p - a).abs
177
+ pb = (p - b).abs
178
+ pc = (p - c).abs
179
+ pr = (pa <= pb && pa <= pc) ? a : (pb <= pc ? b : c)
180
+ d.setbyte i, (x + pr) & 0xff
181
+ end
182
+ d
183
+ end
184
+ end
185
+ end
@@ -0,0 +1,141 @@
1
+ module PNGlitch
2
+
3
+ # Scanline is the class that represents a particular PNG image scanline.
4
+ #
5
+ # It consists of a filter type and a filtered pixel data.
6
+ #
7
+ class Scanline
8
+
9
+ attr_reader :index, :filter_type, :prev_filter_type, :filter_codec
10
+
11
+ #
12
+ # Instanciate.
13
+ #
14
+ def initialize io, start_at, width, sample_size, at
15
+ @index = at
16
+ @io = io
17
+ @start_at = start_at
18
+ @data_size = width * sample_size
19
+ @sample_size = sample_size
20
+
21
+ pos = @io.pos
22
+ @io.pos = @start_at
23
+ @filter_type = @io.read(1).unpack('C').first
24
+ @io.pos = pos
25
+
26
+ @data = nil
27
+ @prev_filter_type = nil
28
+ @filter_codec = { encoder: nil, decoder: nil }
29
+
30
+ if block_given?
31
+ @callback = Proc.new
32
+ end
33
+ end
34
+
35
+ #
36
+ # Returns data of the scanline.
37
+ #
38
+ def data
39
+ if @data.nil?
40
+ pos = @io.pos
41
+ @io.pos = @start_at + 1
42
+ @data = @io.read @data_size
43
+ @io.pos = pos
44
+ end
45
+ @data
46
+ end
47
+
48
+ #
49
+ # Replaces data with given Regexp +pattern+ and +replacement+.
50
+ #
51
+ # It is same as <tt>scanline.replace_data(scanline.data.gsub(pattern, replacement))</tt>.
52
+ # When the data size has changed, the data will be chopped or padded with null string
53
+ # in original size.
54
+ #
55
+ def gsub! pattern, replacement
56
+ self.replace_data self.data.gsub(pattern, replacement)
57
+ end
58
+
59
+ #
60
+ # Replace the data with +new_data+.
61
+ #
62
+ # When its size has changed, the data will be chopped or padded with null string
63
+ # in original size.
64
+ #
65
+ def replace_data new_data
66
+ @data = new_data
67
+ save
68
+ end
69
+
70
+ #
71
+ # Replace the filter type with +new_filter+, and it will not compute the filters again.
72
+ #
73
+ # It means the scanline might get wrong filter type. It will be the efficient way to
74
+ # break the PNG image.
75
+ #
76
+ def graft new_filter
77
+ @filter_type = new_filter
78
+ save
79
+ end
80
+
81
+ #
82
+ # Replace the filter type with +new_filter+, and it will compute the filters again.
83
+ #
84
+ # This operation will be a legal way to change filter types.
85
+ #
86
+ def change_filter new_filter
87
+ @prev_filter_type = @filter_type
88
+ @filter_type = new_filter
89
+ save
90
+ end
91
+
92
+ #
93
+ # Registers a custom filter function to encode data.
94
+ #
95
+ # With this operation, it will be able to change filter encoding behavior despite
96
+ # the specified filter type value. It takes a Proc object or a block.
97
+ #
98
+ def register_filter_encoder encoder = nil, &block
99
+ if !encoder.nil? && encoder.is_a?(Proc)
100
+ @filter_codec[:encoder] = encoder
101
+ elsif block_given?
102
+ @filter_codec[:encoder] = block
103
+ end
104
+ save
105
+ end
106
+
107
+ #
108
+ # Registers a custom filter function to decode data.
109
+ #
110
+ # With this operation, it will be able to change filter decoding behavior despite
111
+ # the specified filter type value. It takes a Proc object or a block.
112
+ #
113
+ def register_filter_decoder decoder = nil, &block
114
+ @filter_decoder = decoder
115
+ if !decoder.nil? && decoder.is_a?(Proc)
116
+ @filter_codec[:decoder] = decoder
117
+ elsif block_given?
118
+ @filter_codec[:decoder] = block
119
+ end
120
+ save
121
+ end
122
+
123
+
124
+ #
125
+ # Save the changes.
126
+ #
127
+ def save
128
+ pos = @io.pos
129
+ @io.pos = @start_at
130
+ @io << [@filter_type].pack('C')
131
+ @io << self.data.slice(0, @data_size).ljust(@data_size, "\0")
132
+ @io.pos = pos
133
+ @callback.call(self) unless @callback.nil?
134
+ self
135
+ end
136
+
137
+ alias data= replace_data
138
+ alias filter_type= change_filter
139
+
140
+ end
141
+ end
@@ -0,0 +1,31 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'pnglitch'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "pnglitch"
8
+ spec.version = PNGlitch::VERSION
9
+ spec.authors = ["ucnv"]
10
+ spec.email = ["ucnvvv@gmail.com"]
11
+ spec.summary = %q{A Ruby library to glitch PNG images.}
12
+ spec.description = <<-EOL.gsub(/^\s*/, '')
13
+ PNGlitch is a Ruby library to destroy your PNG images.
14
+ With normal data-bending technique, a glitch against PNG will easily fail
15
+ because of the checksum function. We provide a fail-proof destruction for it.
16
+ Using this library you will see beautiful and various PNG artifacts.
17
+ EOL
18
+ spec.homepage = "https://github.com/ucnv/pnglitch"
19
+ spec.license = "MIT"
20
+
21
+ spec.files = `git ls-files -z`.split("\x0")
22
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
23
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
24
+ spec.require_paths = ["lib"]
25
+
26
+ spec.required_ruby_version = '>= 2.0.0'
27
+
28
+ spec.add_development_dependency "bundler", "~> 1.7"
29
+ spec.add_development_dependency "rake", "~> 10.0"
30
+ spec.add_development_dependency "rspec"
31
+ end
Binary file
Binary file
Binary file
Binary file
Binary file
@@ -0,0 +1,26 @@
1
+ require 'spec_helper'
2
+
3
+ describe PNGlitch::Filter do
4
+ types = [:none, :sub, :up, :average, :paeth]
5
+ dir = Pathname(File.dirname(__FILE__)).join('fixtures')
6
+ tests = {}
7
+ types.each do |t|
8
+ data = File.binread(dir.join('filter_' + t.to_s))
9
+ tests[t] = data.scan %r|[\s\S]{1,#{data.size / 2}}|
10
+ end
11
+
12
+ types.each do |type|
13
+ context "with #{type} type" do
14
+ let(:filter) { PNGlitch::Filter.new type, 3 }
15
+ it 'should encode correctly' do
16
+ expect(filter.encode(tests[:none][0])).to eq tests[type][0]
17
+ expect(filter.encode(tests[:none][1], tests[:none][0])).to eq tests[type][1]
18
+ end
19
+ it 'should decode correctly' do
20
+ expect(filter.decode(tests[type][1], tests[:none][0])).to eq tests[:none][1]
21
+ expect(filter.decode(tests[type][0])).to eq tests[:none][0]
22
+ end
23
+ end
24
+ end
25
+
26
+ end
@@ -0,0 +1,603 @@
1
+ require 'spec_helper'
2
+
3
+ describe PNGlitch do
4
+ before :context do
5
+ @tmpdir = Pathname(File.dirname(__FILE__)).join('fixtures').join('out')
6
+ FileUtils.mkdir @tmpdir unless File.exist? @tmpdir
7
+ end
8
+
9
+ after :context do
10
+ FileUtils.remove_entry_secure @tmpdir
11
+ end
12
+
13
+ after :example do
14
+ FileUtils.rm Dir.glob(@tmpdir.join('*'))
15
+ end
16
+
17
+ let(:infile) { Pathname(File.dirname(__FILE__)).join('fixtures').join('in.png') }
18
+ let(:outdir) { Pathname(@tmpdir) }
19
+ let(:outfile) { outdir.join('out.png') }
20
+
21
+ describe '.open' do
22
+ subject(:png) { PNGlitch.open infile }
23
+ it { is_expected.to be_a PNGlitch::Base }
24
+
25
+ it 'can close' do
26
+ expect { png.close }.not_to raise_error
27
+ expect {
28
+ png.compressed_data.read
29
+ }.to raise_error IOError
30
+ end
31
+
32
+ context 'when it takes a block' do
33
+ it 'should be closed after the block' do
34
+ png = nil
35
+ PNGlitch.open(infile) { |p| png = p }
36
+ expect {
37
+ png.compressed_data.read
38
+ }.to raise_error IOError
39
+ end
40
+
41
+ it 'also can execute with DSL style' do
42
+ types, h = ()
43
+ out = outfile
44
+ expect {
45
+ PNGlitch.open(infile) do
46
+ types = filter_types
47
+ h = height
48
+ output out
49
+ end
50
+ }.not_to raise_error
51
+ expect(types.size).to eq h
52
+ end
53
+
54
+ it 'should return a value of last call in the block' do
55
+ e = 0
56
+ v = PNGlitch.open infile do |p|
57
+ e = p.height
58
+ p.height
59
+ end
60
+ expect(v).to be == e
61
+
62
+ e = 0
63
+ v = PNGlitch.open infile do
64
+ e = height
65
+ height
66
+ end
67
+ expect(v).to be == e
68
+ end
69
+ end
70
+
71
+ context('when decompressed data is unexpected size') do
72
+ it 'should raise error' do
73
+ bomb = infile.dirname.join('bomb.png')
74
+ expect {
75
+ png = PNGlitch.open bomb
76
+ png.close
77
+ }.to raise_error PNGlitch::DataSizeError
78
+ end
79
+
80
+ it 'can avoid the error' do
81
+ bomb = infile.dirname.join('bomb.png')
82
+ expect {
83
+ png = PNGlitch.open bomb, limit_of_decompressed_data_size: 100 * 1024 ** 2
84
+ png.close
85
+ }.not_to raise_error
86
+ end
87
+ end
88
+
89
+ context('when it is not PNG file') do
90
+ it 'should raise error' do
91
+ file = infile.dirname.join('filter_none')
92
+ expect {
93
+ png = PNGlitch.open file
94
+ png.close
95
+ }.to raise_error PNGlitch::FormatError
96
+ end
97
+ end
98
+ end
99
+
100
+ describe '.output' do
101
+ context 'when nothing change' do
102
+ it 'makes an output to be same as input' do
103
+ PNGlitch.open infile do |png|
104
+ png.output outfile
105
+ end
106
+ a = open outfile
107
+ b = open infile
108
+ expect(a.read.unpack('a*').first).to eq(b.read.unpack('a*').first)
109
+ a.close
110
+ b.close
111
+ end
112
+ end
113
+
114
+ context 'when it generates wrong PNG' do
115
+ it 'can even read it again (if it has right compression)' do
116
+ out1 = outfile
117
+ out2 = outdir.join('out2.png')
118
+ PNGlitch.open infile do
119
+ glitch {|d| d.gsub /\d/, '' }
120
+ output out1
121
+ end
122
+ expect {
123
+ PNGlitch.open outfile do
124
+ compress
125
+ output out2
126
+ end
127
+ }.not_to raise_error
128
+ end
129
+
130
+ it 'cannot read broken compression' do
131
+ out1 = outfile
132
+ out2 = outdir.join('out2.png')
133
+ PNGlitch.open infile do
134
+ glitch_after_compress {|d| d.gsub /\d/, 'a' }
135
+ output out1
136
+ end
137
+ expect {
138
+ PNGlitch.open outfile do
139
+ compress
140
+ output out2
141
+ end
142
+ }.to raise_error Zlib::DataError
143
+ end
144
+ end
145
+ end
146
+
147
+ describe '.compress' do
148
+ it 'should be lossless' do
149
+ png = PNGlitch.open infile
150
+ before = png.filtered_data.read
151
+ png.compress
152
+ after = Zlib::Inflate.inflate(png.compressed_data.read)
153
+ png.close
154
+ expect(before).to eq after
155
+ end
156
+ end
157
+
158
+ describe '.glitch' do
159
+ it 'makes a result that is readable as PNG' do
160
+ png = PNGlitch.open infile
161
+ png.glitch do |data|
162
+ data.gsub /\d/, 'a'
163
+ end
164
+ png.output outfile
165
+ png.close
166
+ expect {
167
+ ChunkyPNG::Image.from_file outfile
168
+ }.not_to raise_error
169
+ end
170
+
171
+ it 'makes a result has an intended size of data' do
172
+ png1 = PNGlitch.open infile
173
+ a = png1.filtered_data.size
174
+ png1.glitch do |data|
175
+ data.gsub /\d/, 'a'
176
+ end
177
+ png1.output outfile
178
+ png2 = PNGlitch.open outfile
179
+ b = png2.filtered_data.size
180
+ png2.close
181
+ expect(b).to eq a
182
+
183
+ png1 = PNGlitch.open infile
184
+ a = png1.filtered_data.size
185
+ png1.glitch do |data|
186
+ data[(data.size / 2), 100] = ''
187
+ data
188
+ end
189
+ png1.output outfile
190
+ png2 = PNGlitch.open outfile
191
+ b = png2.filtered_data.size
192
+ png2.close
193
+ expect(b).to eq(a - 100)
194
+ end
195
+ end
196
+
197
+ describe '.glitch_after_compress' do
198
+ it 'should not fail' do
199
+ png = PNGlitch.open infile
200
+ png.glitch_after_compress do |data|
201
+ data[rand(data.size)] = 'x'
202
+ data
203
+ end
204
+ png.output outfile
205
+ png.close
206
+ expect(outfile).to exist
207
+ end
208
+
209
+ context 'when manipulation after glitch_after_compress' do
210
+ it 'warn' do
211
+ png = PNGlitch.open infile
212
+ png.glitch_after_compress do |data|
213
+ data[rand(data.size)] = 'x'
214
+ data
215
+ end
216
+ expect{
217
+ png.glitch do |data|
218
+ data[0] = 'x'
219
+ end
220
+ }.to output.to_stderr
221
+ png.close
222
+ end
223
+ end
224
+ end
225
+
226
+ describe '.glitch_as_io' do
227
+ it 'can generate a same result with glitch method' do
228
+ out1 = outdir.join 'a.png'
229
+ out2 = outdir.join 'b.png'
230
+ pos = []
231
+ PNGlitch.open infile do |p|
232
+ p.glitch_as_io do |io|
233
+ 10.times do |i|
234
+ pos << [rand(io.size), i.to_s]
235
+ end
236
+ pos.each do |x|
237
+ io.pos = x[0]
238
+ io << x[1]
239
+ end
240
+ end
241
+ p.output out1
242
+ end
243
+ PNGlitch.open infile do |p|
244
+ p.glitch do |data|
245
+ pos.each do |x|
246
+ data[x[0]] = x[1]
247
+ end
248
+ data
249
+ end
250
+ p.output out2
251
+ end
252
+
253
+ a = File.read out1
254
+ b = File.read out2
255
+ expect(a.b).to eq b.b
256
+ end
257
+ end
258
+
259
+ describe '.glitch_after_compress_as_io' do
260
+ it 'can generate a same result with glitch_after_compress method' do
261
+ out1 = outdir.join 'a.png'
262
+ out2 = outdir.join 'b.png'
263
+ pos = []
264
+ PNGlitch.open infile do |p|
265
+ p.glitch_after_compress_as_io do |io|
266
+ 10.times do |i|
267
+ pos << [rand(io.size), i.to_s]
268
+ end
269
+ pos.each do |x|
270
+ io.pos = x[0]
271
+ io << x[1]
272
+ end
273
+ end
274
+ p.output out1
275
+ end
276
+ PNGlitch.open infile do |p|
277
+ p.glitch_after_compress do |data|
278
+ pos.each do |x|
279
+ data[x[0]] = x[1]
280
+ end
281
+ data
282
+ end
283
+ p.output out2
284
+ end
285
+
286
+ a = File.read out1
287
+ b = File.read out2
288
+ expect(a.b).to eq b.b
289
+ end
290
+ end
291
+
292
+ describe '.filter_types' do
293
+ it 'should be same size of image height' do
294
+ png = PNGlitch.open infile
295
+ types = png.filter_types
296
+ height = png.height
297
+ png.close
298
+ expect(types.size).to eq height
299
+ end
300
+ end
301
+
302
+ describe '.each_scanline' do
303
+ it 'returns Enumerator' do
304
+ png = PNGlitch.open infile
305
+ expect(png.each_scanline).to be_a Enumerator
306
+ end
307
+
308
+ it 'can exchange filter types' do
309
+ png = PNGlitch.open infile
310
+ png.each_scanline do |line|
311
+ expect(line).to be_a PNGlitch::Scanline
312
+ line.graft rand(4)
313
+ end
314
+ png.output outfile
315
+ png.close
316
+ expect {
317
+ ChunkyPNG::Image.from_file outfile
318
+ }.not_to raise_error
319
+ end
320
+
321
+ it 'can rewite scanline data' do
322
+ png = PNGlitch.open infile
323
+ png.each_scanline do |line|
324
+ line.data = line.data.gsub /\d/, 'a'
325
+ end
326
+ png.output outfile
327
+ png.close
328
+ expect {
329
+ ChunkyPNG::Image.from_file outfile
330
+ }.not_to raise_error
331
+ end
332
+
333
+ it 'can change filter types and re-filter' do
334
+ png = PNGlitch.open infile
335
+ png.each_scanline do |line|
336
+ line.change_filter rand(4)
337
+ end
338
+ png.output outfile
339
+ png.close
340
+ expect {
341
+ ChunkyPNG::Image.from_file outfile
342
+ }.not_to raise_error
343
+
344
+ if system('which convert > /dev/null')
345
+ out1 = outdir.join('a.png')
346
+ out2 = outdir.join('b.png')
347
+ fx = 4
348
+ png = PNGlitch.open infile
349
+ png.each_scanline do |line|
350
+ line.change_filter fx
351
+ end
352
+ png.output out1
353
+ png.close
354
+ system('convert -quality %d %s %s' % [fx, infile, out2])
355
+ png1 = PNGlitch.open out1
356
+ png2 = PNGlitch.open out2
357
+ d1 = png1.filtered_data.read
358
+ d2 = png2.filtered_data.read
359
+ f1 = png1.filter_types
360
+ f2 = png2.filter_types
361
+ png1.close
362
+ png2.close
363
+ expect(f1).to eq(f2)
364
+ expect(d1).to eq(d2)
365
+ end
366
+ end
367
+
368
+ it 'can apply custom filter method' do
369
+ lines = []
370
+ sample_size = nil
371
+ original_filter = 0
372
+ PNGlitch.open infile do |png|
373
+ target = png.scanline_at 100
374
+ original_filter = target.filter_type
375
+ lines[1] = target.data
376
+ sample_size = png.sample_size
377
+ png.each_scanline do |l|
378
+ l.change_filter 0
379
+ end
380
+ lines[0] = png.scanline_at(99).data
381
+ end
382
+
383
+ enc = lambda do |data, prev|
384
+ d = data.dup
385
+ d.size.times.reverse_each do |i|
386
+ x = d.getbyte i
387
+ a = i >= sample_size ? d.getbyte(i - sample_size - 1) : 0
388
+ b = !prev.nil? ? prev.getbyte(i - 1) : 0
389
+ d.setbyte i, (x - ((a + b) / 2)) & 0xff
390
+ end
391
+ d
392
+ end
393
+ decoded = PNGlitch::Filter.new(original_filter, sample_size).decode(lines[1], lines[0])
394
+ encoded = enc.call(decoded, lines[0])
395
+
396
+ PNGlitch.open infile do |png|
397
+ png.each_scanline.with_index do |s, i|
398
+ if i == 100
399
+ s.register_filter_encoder enc
400
+ end
401
+ end
402
+ png.output outfile
403
+ end
404
+ PNGlitch.open outfile do |png|
405
+ expect(png.scanline_at(100).data).to eq encoded
406
+ end
407
+
408
+ # ==================================
409
+
410
+ dec = lambda do |data, prev|
411
+ d = data.dup
412
+ d.size.times do |i|
413
+ x = d.getbyte i
414
+ a = i >= sample_size ? d.getbyte(i - sample_size - 2) : 0
415
+ b = !prev.nil? ? prev.getbyte(i - 1) : 0
416
+ d.setbyte i, (x + ((a + b) / 2)) & 0xff
417
+ end
418
+ d
419
+ end
420
+ decoded = dec.call(lines[1], lines[0])
421
+ encoded = PNGlitch::Filter.new(original_filter, sample_size).encode(decoded, lines[0])
422
+
423
+ PNGlitch.open infile do |png|
424
+ png.each_scanline.with_index do |s, i|
425
+ if i == 100
426
+ s.register_filter_decoder dec
427
+ end
428
+ end
429
+ png.output outfile
430
+ end
431
+ PNGlitch.open outfile do |png|
432
+ expect(png.scanline_at(100).data).to eq encoded
433
+ end
434
+
435
+ # ==================================
436
+
437
+ decoded = dec.call(lines[1], lines[0])
438
+ encoded = enc.call(decoded, lines[0])
439
+
440
+ PNGlitch.open infile do |png|
441
+ png.each_scanline.with_index do |s, i|
442
+ if i == 100
443
+ s.register_filter_encoder enc
444
+ s.register_filter_decoder dec
445
+ end
446
+ end
447
+ png.output outfile
448
+ end
449
+ PNGlitch.open outfile do |png|
450
+ expect(png.scanline_at(100).data).to eq encoded
451
+ end
452
+ end
453
+
454
+ context 'with wrong sized data' do
455
+ it 'should raise no errors' do
456
+ expect {
457
+ PNGlitch.open infile do |png|
458
+ pos = png.filtered_data.pos = png.filtered_data.size * 4 / 5
459
+ png.filtered_data.truncate pos
460
+ png.each_scanline do |line|
461
+ line.gsub! /\d/, 'x'
462
+ line.filter_type = rand(4)
463
+ end
464
+ png.output outfile
465
+ end
466
+ }.not_to raise_error
467
+ expect {
468
+ PNGlitch.open infile do |png|
469
+ png.filtered_data.pos = png.filtered_data.size * 4 / 5
470
+ chunk = png.filtered_data.read
471
+ png.filtered_data.rewind
472
+ 10.times do
473
+ png.filtered_data << chunk
474
+ end
475
+ png.each_scanline do |line|
476
+ line.gsub! /\d/, 'x'
477
+ line.filter_type = rand(4)
478
+ end
479
+ png.output outfile
480
+ end
481
+ }.not_to raise_error
482
+ end
483
+ end
484
+
485
+ context 'when re-filtering with same filters' do
486
+ it 'becomes a same result' do
487
+ png = PNGlitch.open infile
488
+ png.filtered_data.rewind
489
+ a = png.filtered_data.read
490
+ filters = png.filter_types
491
+ png.each_scanline.with_index do |l, i|
492
+ l.change_filter PNGlitch::Filter::UP
493
+ end
494
+ b = png.filtered_data.read
495
+ png.each_scanline.with_index do |l, i|
496
+ l.change_filter filters[i]
497
+ end
498
+ png.filtered_data.rewind
499
+ c = png.filtered_data.read
500
+ png.output outfile
501
+ png.close
502
+ expect(c).to eq(a)
503
+ expect(b).not_to eq(a)
504
+ end
505
+ end
506
+ end
507
+
508
+ describe '.scanline_at' do
509
+ it 'should reflect the changes to the instance' do
510
+ PNGlitch.open infile do |png|
511
+ line = png.scanline_at 100
512
+ f = line.filter_type
513
+ line.filter_type = (f + 1) % 5
514
+ png.output outfile
515
+ end
516
+
517
+ png1 = PNGlitch.open infile
518
+ line1_100 = png1.scanline_at(100).data
519
+ line1_95 = png1.scanline_at(95).data
520
+ png2 = PNGlitch.open outfile
521
+ line2_100 = png2.scanline_at(100).data
522
+ line2_95 = png2.scanline_at(95).data
523
+ png1.close
524
+ png2.close
525
+ expect(line2_100).not_to eq(line1_100)
526
+ expect(line2_95).to eq(line1_95)
527
+ end
528
+
529
+ it 'can apply custom filter method' do
530
+ lines = []
531
+ sample_size = nil
532
+ original_filter = 0
533
+ PNGlitch.open infile do |png|
534
+ target = png.scanline_at 100
535
+ original_filter = target.filter_type
536
+ lines[1] = target.data
537
+ sample_size = png.sample_size
538
+ png.each_scanline do |l|
539
+ l.change_filter 0
540
+ end
541
+ lines[0] = png.scanline_at(99).data
542
+ end
543
+
544
+ enc = lambda do |data, prev|
545
+ d = data.dup
546
+ d.size.times.reverse_each do |i|
547
+ x = d.getbyte i
548
+ a = i >= sample_size ? d.getbyte(i - sample_size - 1) : 0
549
+ b = !prev.nil? ? prev.getbyte(i - 2) : 0
550
+ d.setbyte i, (x - ((a + b) / 2)) & 0xff
551
+ end
552
+ d
553
+ end
554
+ decoded = PNGlitch::Filter.new(original_filter, sample_size).decode(lines[1], lines[0])
555
+ encoded = enc.call(decoded, lines[0])
556
+
557
+ PNGlitch.open infile do |png|
558
+ l = png.scanline_at 100
559
+ l.register_filter_encoder enc
560
+ png.output outfile
561
+ end
562
+ PNGlitch.open outfile do |png|
563
+ expect(png.scanline_at(100).data).to eq encoded
564
+ end
565
+ end
566
+
567
+ it 'should finalize the instance' do
568
+ PNGlitch.open infile do
569
+ lines = scanline_at 1..100
570
+ end
571
+ GC.start
572
+ count = ObjectSpace.each_object(PNGlitch::Scanline).count
573
+ expect(count).to be < 100
574
+
575
+ png = PNGlitch.open infile
576
+ lines = png.scanline_at 1..100
577
+ png.close
578
+ lines = nil
579
+ GC.start
580
+ count = ObjectSpace.each_object(PNGlitch::Scanline).count
581
+ expect(count).to be < 100
582
+ end
583
+ end
584
+
585
+ describe '.width and .height' do
586
+ it 'destroy the dimension of the image' do
587
+ w, h = ()
588
+ out = outdir.join('t.png') #outfile
589
+ PNGlitch.open infile do |p|
590
+ w = p.width
591
+ h = p.height
592
+ p.width = w - 10
593
+ p.height = h + 10
594
+ p.output out
595
+ end
596
+ p = PNGlitch.open out
597
+ expect(p.width).to equal w - 10
598
+ expect(p.height).to equal h + 10
599
+ p.close
600
+ end
601
+ end
602
+
603
+ end