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.
- 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
|