pnglitch 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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