pnglitch 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +20 -0
- data/.rspec +2 -0
- data/.travis.yml +4 -0
- data/Gemfile +13 -0
- data/LICENSE.txt +22 -0
- data/README.md +42 -0
- data/Rakefile +11 -0
- data/bin/pnglitch +57 -0
- data/lib/pnglitch.rb +249 -0
- data/lib/pnglitch/base.rb +479 -0
- data/lib/pnglitch/errors.rb +37 -0
- data/lib/pnglitch/filter.rb +185 -0
- data/lib/pnglitch/scanline.rb +141 -0
- data/pnglitch.gemspec +31 -0
- data/spec/fixtures/bomb.png +0 -0
- data/spec/fixtures/filter_average +0 -0
- data/spec/fixtures/filter_none +0 -0
- data/spec/fixtures/filter_paeth +0 -0
- data/spec/fixtures/filter_sub +0 -0
- data/spec/fixtures/filter_up +0 -0
- data/spec/fixtures/in.png +0 -0
- data/spec/pnglitch_filter_spec.rb +26 -0
- data/spec/pnglitch_spec.rb +603 -0
- data/spec/spec_helper.rb +8 -0
- metadata +126 -0
@@ -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
|
data/pnglitch.gemspec
ADDED
@@ -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
|
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
|