pnglitch 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 5ef1538aec8847d189241145512fe084c43eca6d
4
+ data.tar.gz: 010aa625e9d831f09bac45ea3c6ca88ef9f83545
5
+ SHA512:
6
+ metadata.gz: b356a88db6db41d9729dacd936277b2ffeed2af5714f2c3884821e0654c71071bc379d848e6415473ea4aeea1b5a0cf2a83c100b6820cf211003ae3e4c3fcfd1
7
+ data.tar.gz: ec2db449bb83b8574b2f42a0073540995552c8732a9a453f5c6019382b1bc722a2fe183b1b6ba976290932f7b3935296becbf43467d4d071191fea619f60be85
@@ -0,0 +1,20 @@
1
+ *.sw?
2
+ .DS_Store
3
+ *.gem
4
+ *.rbc
5
+ .bundle
6
+ .config
7
+ .yardoc
8
+ Gemfile.lock
9
+ InstalledFiles
10
+ _yardoc
11
+ coverage
12
+ doc/
13
+ lib/bundler/man
14
+ pkg
15
+ rdoc
16
+ spec/reports
17
+ test/tmp
18
+ test/version_tmp
19
+ tmp
20
+ Guardfile
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
@@ -0,0 +1,4 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.0.0
4
+ - 2.1.4
data/Gemfile ADDED
@@ -0,0 +1,13 @@
1
+ source 'https://rubygems.org'
2
+ gemspec
3
+
4
+ group :test do
5
+ gem 'guard'
6
+ gem 'guard-rspec'
7
+ gem 'chunky_png'
8
+ end
9
+
10
+ group :doc do
11
+ gem 'yard'
12
+ end
13
+
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 ucnv
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,42 @@
1
+ # PNGlitch
2
+
3
+ [![Build Status](https://travis-ci.org/ucnv/pnglitch.svg?branch=master)](https://travis-ci.org/ucnv/pnglitch)
4
+
5
+
6
+ PNGlitch is a Ruby library to destroy your PNG images.
7
+
8
+ With normal data-bending technique, a glitch against PNG will easily fail
9
+ because of the checksum function. We provide a fail-proof destruction for it.
10
+ Using this library you will see beautiful and various PNG artifacts.
11
+
12
+ ## Usage
13
+
14
+ ```ruby
15
+ PNGlitch.open('/path/to/your/image.png') do |p|
16
+ p.glitch do |data|
17
+ data.gsub /\d/, 'x'
18
+ end
19
+ p.save '/path/to/broken/image.png'
20
+ end
21
+ ```
22
+ ## Installation
23
+
24
+ Add this line to your application's Gemfile:
25
+
26
+ gem 'pnglitch'
27
+
28
+ And then execute:
29
+
30
+ $ bundle
31
+
32
+ Or install it yourself as:
33
+
34
+ $ gem install pnglitch
35
+
36
+ ## Contributing
37
+
38
+ 1. Fork it ( http://github.com/ucnv/pnglitch/fork )
39
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
40
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
41
+ 4. Push to the branch (`git push origin my-new-feature`)
42
+ 5. Create new Pull Request
@@ -0,0 +1,11 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
7
+
8
+ require 'yard'
9
+ YARD::Rake::YardocTask.new do |t|
10
+ end
11
+
@@ -0,0 +1,57 @@
1
+ #!/usr/bin/env ruby
2
+ require 'optparse'
3
+ require 'pnglitch'
4
+
5
+ program_name = File.basename($0)
6
+ usage = <<USAGE
7
+ Usage:
8
+ #{program_name} <infile> [--filter=<n>] <outfile>
9
+
10
+ Options:
11
+ -f, --filter=<n> Fix all filter types as passed value before glitching.
12
+ A number (0..4) or a type name (none|sub|up|average|paeth).
13
+ --version Show version.
14
+ -h, --help Show this screen.
15
+
16
+ USAGE
17
+
18
+ filter = nil
19
+
20
+ ARGV.options do |o|
21
+ o.program_name = program_name
22
+ o.on('-f', '--filter=N', String) do |n|
23
+ filter = PNGlitch::Filter.guess n
24
+ end
25
+
26
+ o.on_tail('--version') do
27
+ puts PNGlitch::VERSION
28
+ exit
29
+ end
30
+
31
+ o.on_tail('-h', '--help') do
32
+ puts usage
33
+ exit
34
+ end
35
+
36
+ o.parse!
37
+ end
38
+
39
+ begin
40
+ raise 'Wrong options' if ARGV.size < 2
41
+ infile, outfile = ARGV
42
+ PNGlitch.open infile do |png|
43
+ unless filter.nil?
44
+ png.each_scanline do |line|
45
+ line.change_filter filter
46
+ end
47
+ end
48
+ png.glitch do |data|
49
+ data.gsub /\d/, 'x'
50
+ end
51
+ png.save outfile
52
+ end
53
+ rescue => e
54
+ puts e.message
55
+ puts
56
+ puts usage
57
+ end
@@ -0,0 +1,249 @@
1
+ require 'pathname'
2
+ require 'stringio'
3
+ require 'tempfile'
4
+ require 'zlib'
5
+ require 'pnglitch/errors'
6
+ require 'pnglitch/filter'
7
+ require 'pnglitch/scanline'
8
+ require 'pnglitch/base'
9
+
10
+ # PNGlitch is a Ruby library to manipulate PNG images, solely for the purpose to "glitch" them.
11
+ #
12
+ # Since PNG has CRC checksum in the spec, the viewer applications always detect it
13
+ # and reject to display broken PNGs. It is why a simple glitching with a text or binary editor
14
+ # gets easily failed, differently from images like JPEG.
15
+ # This library provides the fix to take the total failure of glitching out, and to keep
16
+ # your PNG undead.
17
+ #
18
+ # Also it provides options to generate varied glitch results.
19
+ #
20
+ # = Usage
21
+ #
22
+ # == Simple glitch
23
+ #
24
+ # png = PNGlitch.open '/path/to/your/image.png'
25
+ # png.glitch do |data|
26
+ # data.gsub /\d/, 'x'
27
+ # end
28
+ # png.save '/path/to/broken/image.png'
29
+ # png.close
30
+ #
31
+ # The code above can be written with a block like below:
32
+ #
33
+ # PNGlitch.open('/path/to/your/image.png') do |png|
34
+ # png.glitch do |data|
35
+ # data.gsub /\d/, 'x'
36
+ # end
37
+ # png.save '/path/to/broken/image.png'
38
+ # end
39
+ #
40
+ # The +glitch+ method treats the decompressed data into one String instance. It will be
41
+ # very convenient, but please note that it could take a huge size of memory. For example,
42
+ # a normal PNG image in 3264 x 2448 pixels makes over 23 MB of decompressed data.
43
+ # In case that the memory usage becomes a concern, it can be written to use IO instead of
44
+ # String.
45
+ #
46
+ # PNGlitch.open('/path/to/your/image.png') do |png|
47
+ # buf = 2 ** 18
48
+ # png.glitch_as_io do |io|
49
+ # until io.eof? do
50
+ # d = io.read(buf)
51
+ # io.pos -= d.size
52
+ # io.print(d.gsub(/\d/, 'x'))
53
+ # end
54
+ # end
55
+ # png.save '/path/to/broken/image.png'
56
+ # end
57
+ #
58
+ # PNGlitch also provides to manipulate with each scanline.
59
+ #
60
+ # PNGlitch.open('/path/to/your/image.png') do |png|
61
+ # png.each_scanline do |scanline|
62
+ # scanline.gsub! /\d/, 'x'
63
+ # end
64
+ # png.save '/path/to/broken/image.png'
65
+ # end
66
+ #
67
+ # Depe:nding a viewer application, the result of the first example using +glitch+ can
68
+ # detected as unopenable file, because of breaking the filter type bytes (Most applications
69
+ # will ignore it, but I found the library in java.awt get failed). The operation with
70
+ # +each_scanline+ will be more careful for memory usage and the file itself.
71
+ # It is a polite way, but is slower than the rude +glitch+.
72
+ #
73
+ #
74
+ # == Scanlines and filter types
75
+ #
76
+ # Scanline consists of data of pixels and a filter type value.
77
+ #
78
+ # To change the data of pixels, use +Scanline#replace_data+.
79
+ #
80
+ # png.each_scanline do |scanline|
81
+ # data = scanline.data
82
+ # scanline.replace_data(data.gsub(/\d/, 'x'))
83
+ # end
84
+ #
85
+ # Or +Scanline#gsub!+ works like +String#gsub!+
86
+ #
87
+ # png.each_scanline do |scanline|
88
+ # scanline.gsub! /\d/, 'x'
89
+ # end
90
+ #
91
+ # Filter is a tiny function for optimizing PNG compression. It can be set different types
92
+ # with each scanline. The five filter types are defined in the spec, are named +None+, +Up+,
93
+ # +Sub+, +Average+ and +Paeth+ (+None+ means no filter, this filter type makes "raw" data).
94
+ # Internally five digits (0-4) become the references.
95
+ #
96
+ # The filter types must be the most important factor behind the representation of glitch
97
+ # results. Each filter has different effect.
98
+ #
99
+ # Generally in PNG file, scanlines has a variety of filter types on each. As in a
100
+ # convertion by image processing applications (like Photoshop or ImageMagick) they try to
101
+ # apply proper filter types with each scanlines.
102
+ #
103
+ # You can check the values like:
104
+ #
105
+ # puts png.filter_types
106
+ #
107
+ # With +each_scanline+, we can reach the filters particularly.
108
+ #
109
+ # png.each_scanline do |scanline|
110
+ # scanline.change_filter 3
111
+ # end
112
+ #
113
+ # The example above puts all filter types in 3 (type +Average+). +change_filter+ will
114
+ # apply new filter type values correctly. It computes filters and makes the PNG well
115
+ # formatted, and any glitch won't get happened. It also means the output image would have
116
+ # completely looks the same as the input one.
117
+ #
118
+ # However glitches might reveal the difference of filter types.
119
+ #
120
+ # PNGlitch.open(infile) do |png|
121
+ # png.each_scanline do |scanline|
122
+ # scanline.change_filter 3
123
+ # end
124
+ # png.glitch do |data|
125
+ # data.gsub /\d/, 'x'
126
+ # end
127
+ # png.save outfile1
128
+ # end
129
+ #
130
+ # PNGlitch.open(infile) do |png|
131
+ # png.each_scanline do |scanline|
132
+ # scanline.change_filter 4
133
+ # end
134
+ # png.glitch do |data|
135
+ # data.gsub /\d/, 'x'
136
+ # end
137
+ # png.save outfile2
138
+ # end
139
+ #
140
+ # With the results of the example above, obviously we can recognize the filter types make
141
+ # a big difference. The filter is distinct and interesting thing in PNG glitching.
142
+ # To put all filter type in a same value before glitching, we could see the signature
143
+ # taste of each filter type. (Note that +change_filter+ will be a little bit slow, image
144
+ # processing libraries like ImageMagick also have the option to put all filter type in
145
+ # same ones and they will process faster.)
146
+ #
147
+ # This library provides a simple method to change the filter type so that generating all
148
+ # possible effects in PNG glitch.
149
+ #
150
+ # PNGlitch also provides to make the filter types wrong. Following example swaps the
151
+ # filter types but remains data unchanged. It means to put a wrong filter type applied.
152
+ #
153
+ # png.each_scanline do |scanline|
154
+ # scanline.graft rand(4)
155
+ # end
156
+ #
157
+ # Additionally, it is possible to break the filter function. Registering a (wrong) filter
158
+ # function like below, we can make glitches with an algorithmic touch.
159
+ #
160
+ # png.each_scanline do |scanline|
161
+ # scanline.register_filter_encoder do |data, prev|
162
+ # d = data.dup
163
+ # d.size.times.reverse_each do |i|
164
+ # x = d.getbyte(i)
165
+ # v = prev ? prev.getbyte((i - 5).abs) : 0
166
+ # d.setbyte(i, (x - v) & 0xff)
167
+ # end
168
+ # d
169
+ # end
170
+ # end
171
+ #
172
+ # == States
173
+ #
174
+ # Put very simply, the encoding process of PNG is like:
175
+ #
176
+ # +----------+ +---------------+ +-----------------+ +----------------+
177
+ # | Raw data | -> | Filtered data | -> | Compressed data | -> | Formatted file |
178
+ # +----------+ +---------------+ +-----------------+ +----------------+
179
+ #
180
+ # It shows that there are two states between raw data and a result file, and it means there
181
+ # are two states possible to glitch. This library provides to choose of the state to glitch.
182
+ #
183
+ # All examples cited thus far are operations to "filtered data". On the other hand, PNGlitch
184
+ # can touch the "compressed data" through +glitch_after_compress+ method:
185
+ #
186
+ # png.glitch_after_compress do |data|
187
+ # data[rand(data.size)] = 'x'
188
+ # data
189
+ # end
190
+ #
191
+ # Glitch against the compressed data makes slightly different pictures from other results.
192
+ # But sometimes this scratch would break the compression and make the file unopenable.
193
+ #
194
+ module PNGlitch
195
+ VERSION = '0.0.1'
196
+
197
+ class << self
198
+
199
+ #
200
+ # Opens a passed PNG file and returns Base instance.
201
+ #
202
+ # png = PNGltch.open infile
203
+ # png.glitch do |data|
204
+ # data.gsub /\d/, 'x'
205
+ # end
206
+ # png.close
207
+ #
208
+ # +open+ will generate Tempfile internally and you should make sure to call +close+
209
+ # method on the end of operations for removing tempfiles.
210
+ #
211
+ # Same as File.open, it can take a block and will automatically close.
212
+ # An example bellow is same as above one.
213
+ #
214
+ # PNGlitch.open(infile) do |png|
215
+ # png.glitch do |data|
216
+ # data.gsub /\d/, 'x'
217
+ # end
218
+ # end
219
+ #
220
+ # Under normal conditions, the size of the decompressed data of PNG image becomes
221
+ # <tt>(1 + image_width * sample_size) * image_height</tt> in bytes (mostly over 4 times
222
+ # the amount of pixels).
223
+ # To avoid the attack known as "zip bomb", PNGlitch will throw an error when
224
+ # decompressed data goes over twice the expected size. If it's sure that the passed
225
+ # file is safe, the upper limit of decompressed data size can be set in +open+'s option.
226
+ # Like:
227
+ #
228
+ # PNGlitch.open(infile, limit_of_decompressed_data_size: 1 * 1024 ** 3)
229
+ #
230
+ def open file, options = {}
231
+ base = Base.new file, options[:limit_of_decompressed_data_size]
232
+ if block_given?
233
+ begin
234
+ block = Proc.new
235
+ if block.arity == 0
236
+ base.instance_eval &block
237
+ else
238
+ block.call base
239
+ end
240
+ ensure
241
+ base.close
242
+ end
243
+ else
244
+ base
245
+ end
246
+ end
247
+ end
248
+
249
+ end
@@ -0,0 +1,479 @@
1
+ module PNGlitch
2
+
3
+ # Base is the class that represents the interface for PNGlitch functions.
4
+ #
5
+ # It will be initialized through PNGlitch#open and be a mainly used instance.
6
+ #
7
+ class Base
8
+
9
+ attr_reader :width, :height, :sample_size, :is_compressed_data_modified
10
+ attr_accessor :head_data, :tail_data, :compressed_data, :filtered_data
11
+
12
+ #
13
+ # Instanciate the class with the passed +file+
14
+ #
15
+ def initialize file, limit_of_decompressed_data_size = nil
16
+ path = Pathname.new file
17
+ @head_data = StringIO.new
18
+ @tail_data = StringIO.new
19
+ @compressed_data = Tempfile.new 'compressed', :encoding => 'ascii-8bit'
20
+ @filtered_data = Tempfile.new 'filtered', :encoding => 'ascii-8bit'
21
+ @idat_chunk_size = nil
22
+
23
+ open(path, 'rb') do |io|
24
+ idat_sizes = []
25
+ @head_data << io.read(8) # signature
26
+ while bytes = io.read(8)
27
+ length, type = bytes.unpack 'Na*'
28
+ if type == 'IHDR'
29
+ ihdr = {
30
+ width: io.read(4).unpack('N').first,
31
+ height: io.read(4).unpack('N').first,
32
+ bit_depth: io.read(1).unpack('C').first,
33
+ color_type: io.read(1).unpack('C').first,
34
+ compression_method: io.read(1).unpack('C').first,
35
+ filter_method: io.read(1).unpack('C').first,
36
+ interlace_method: io.read(1).unpack('C').first,
37
+ }
38
+ @width = ihdr[:width]
39
+ @height = ihdr[:height]
40
+ @sample_size = {0 => 1, 2 => 3, 3 => 1, 4 => 2, 6 => 4}[ihdr[:color_type]]
41
+ io.pos -= 13
42
+ end
43
+ if type == 'IDAT'
44
+ @compressed_data << io.read(length)
45
+ idat_sizes << length
46
+ io.pos += 4 # crc
47
+ else
48
+ target_io = @compressed_data.pos == 0 ? @head_data : @tail_data
49
+ target_io << bytes
50
+ target_io << io.read(length + 4)
51
+ end
52
+ end
53
+ @idat_chunk_size = idat_sizes.first if idat_sizes.size > 1
54
+ end
55
+ if @compressed_data.size == 0
56
+ raise FormatError.new path.to_s
57
+ end
58
+ @head_data.rewind
59
+ @tail_data.rewind
60
+ @compressed_data.rewind
61
+ decompressed_size = 0
62
+ expected_size = (1 + @width * @sample_size) * @height
63
+ expected_size = limit_of_decompressed_data_size unless limit_of_decompressed_data_size.nil?
64
+ z = Zlib::Inflate.new
65
+ z.inflate(@compressed_data.read) do |chunk|
66
+ decompressed_size += chunk.size
67
+ # raise error when the data size goes over 2 times the usually expected size
68
+ if decompressed_size > expected_size * 2
69
+ z.close
70
+ self.close
71
+ raise DataSizeError.new path.to_s, decompressed_size, expected_size
72
+ end
73
+ @filtered_data << chunk
74
+ end
75
+ z.close
76
+ @compressed_data.rewind
77
+ @filtered_data.rewind
78
+ @is_compressed_data_modified = false
79
+ end
80
+
81
+ #
82
+ # Explicit file close.
83
+ #
84
+ # It will close tempfiles that used internally.
85
+ #
86
+ def close
87
+ @compressed_data.close
88
+ @filtered_data.close
89
+ self
90
+ end
91
+
92
+ #
93
+ # Returns an array of each scanline's filter type value.
94
+ #
95
+ def filter_types
96
+ types = []
97
+ wrap_with_rewind(@filtered_data) do
98
+ until @filtered_data.eof? do
99
+ byte = @filtered_data.read 1
100
+ types << byte.unpack('C').first
101
+ @filtered_data.pos += @width * @sample_size
102
+ end
103
+ end
104
+ types
105
+ end
106
+
107
+ #
108
+ # Manipulates the filtered (decompressed) data as String.
109
+ #
110
+ # To set a glitched result, return the modified value in the block.
111
+ #
112
+ # Example:
113
+ #
114
+ # p = PNGlitch.open 'path/to/your/image.png'
115
+ # p.glitch do |data|
116
+ # data.gsub /\d/, 'x'
117
+ # end
118
+ # p.save 'path/to/broken/image.png'
119
+ # p.close
120
+ #
121
+ # This operation has the potential to damage filter type bytes. The damage will be a cause of
122
+ # glitching but some viewer applications might deny to process those results.
123
+ # To be polite to the filter types, use +each_scanline+ instead.
124
+ #
125
+ # Since this method sets the decompressed data into String, it may use a massive amount of
126
+ # memory. To decrease the memory usage, treat the data as IO through +glitch_as_io+ instead.
127
+ #
128
+ def glitch &block # :yield: data
129
+ warn_if_compressed_data_modified
130
+
131
+ wrap_with_rewind(@filtered_data) do
132
+ result = yield @filtered_data.read
133
+ @filtered_data.rewind
134
+ @filtered_data << result
135
+ truncate_io @filtered_data
136
+ end
137
+ compress
138
+ self
139
+ end
140
+
141
+ #
142
+ # Manipulates the filtered (decompressed) data as IO.
143
+ #
144
+ def glitch_as_io &block # :yield: data
145
+ warn_if_compressed_data_modified
146
+
147
+ wrap_with_rewind(@filtered_data) do
148
+ yield @filtered_data
149
+ end
150
+ compress
151
+ self
152
+ end
153
+
154
+ #
155
+ # Manipulates the after-compressed data as String.
156
+ #
157
+ # To set a glitched result, return the modified value in the block.
158
+ #
159
+ # It will raise an error when the data goes over the limit. In such case, please treat
160
+ # the data as IO through +glitch_after_compress_as_io+ instead.
161
+ #
162
+ # Once the compressed data is glitched, PNGlitch will warn about modifications to
163
+ # filtered (decompressed) data because this method does not decompress the glitched
164
+ # compressed data again. It means that calling +glitch+ after +glitch_after_compress+
165
+ # will make the result overwritten and forgotten.
166
+ #
167
+ # This operation will often destroy PNG image completely.
168
+ #
169
+ def glitch_after_compress &block # :yield: data
170
+ wrap_with_rewind(@compressed_data) do
171
+ result = yield @compressed_data.read
172
+ @compressed_data.rewind
173
+ @compressed_data << result
174
+ truncate_io @compressed_data
175
+ end
176
+ @is_compressed_data_modified = true
177
+ self
178
+ end
179
+
180
+ #
181
+ # Manipulates the after-compressed data as IO.
182
+ #
183
+ def glitch_after_compress_as_io &block # :yield: data
184
+ wrap_with_rewind(@compressed_data) do
185
+ yield @compressed_data
186
+ end
187
+ @is_compressed_data_modified = true
188
+ self
189
+ end
190
+
191
+ #
192
+ # (Re-)computes the filtering methods to each scanlines.
193
+ #
194
+ # On each scanline, it will compute them and apply the results as the filtered data.
195
+ #
196
+ def apply_filters prev_filters = nil, filter_codecs = nil
197
+ prev_filters = filter_types if prev_filters.nil?
198
+ filter_codecs = [] if filter_codecs.nil?
199
+ current_filters = []
200
+ prev = nil
201
+ line_size = @width * @sample_size
202
+ wrap_with_rewind(@filtered_data) do
203
+ # decode all scanlines
204
+ prev_filters.each_with_index do |type, i|
205
+ byte = @filtered_data.read 1
206
+ current_filters << byte.unpack('C').first
207
+ line = @filtered_data.read line_size
208
+ filter = Filter.new type, @sample_size
209
+ if filter_codecs[i] && filter_codecs[i][:decoder]
210
+ filter.decoder = filter_codecs[i][:decoder]
211
+ end
212
+ decoded = filter.decode line, prev
213
+ @filtered_data.pos -= line_size
214
+ @filtered_data << decoded
215
+ prev = decoded
216
+ end
217
+ # encode all
218
+ filter_codecs.reverse!
219
+ data_amount = @filtered_data.pos # should be eof
220
+ current_filters.reverse_each.with_index do |type, i|
221
+ pos = data_amount - ((1 + line_size) * i)
222
+ posa = pos - line_size
223
+ @filtered_data.pos = posa
224
+ line = @filtered_data.read line_size
225
+ posb = pos - (1 + line_size) - line_size
226
+ prev = nil
227
+ unless posb < 0
228
+ @filtered_data.pos = posb
229
+ prev = @filtered_data.read line_size
230
+ end
231
+ filter = Filter.new type, @sample_size
232
+ if filter_codecs[i] && filter_codecs[i][:encoder]
233
+ filter.encoder = filter_codecs[i][:encoder]
234
+ end
235
+ encoded = filter.encode line, prev
236
+ @filtered_data.pos = posa
237
+ @filtered_data << encoded
238
+ end
239
+ end
240
+ end
241
+
242
+ #
243
+ # Re-compress the filtered data.
244
+ #
245
+ # All arguments are for Zlib. See the document of Zlib::Deflate.new for more detail.
246
+ #
247
+ def compress(
248
+ level = Zlib::DEFAULT_COMPRESSION,
249
+ window_bits = Zlib::MAX_WBITS,
250
+ mem_level = Zlib::DEF_MEM_LEVEL,
251
+ strategy = Zlib::DEFAULT_STRATEGY
252
+ )
253
+ wrap_with_rewind(@compressed_data, @filtered_data) do
254
+ z = Zlib::Deflate.new level, window_bits, mem_level, strategy
255
+ until @filtered_data.eof? do
256
+ buffer_size = 2 ** 16
257
+ flush = Zlib::NO_FLUSH
258
+ flush = Zlib::FINISH if @filtered_data.size - @filtered_data.pos < buffer_size
259
+ @compressed_data << z.deflate(@filtered_data.read(buffer_size), flush)
260
+ end
261
+ z.finish
262
+ z.close
263
+ truncate_io @compressed_data
264
+ end
265
+ @is_compressed_data_modified = false
266
+ self
267
+ end
268
+
269
+ #
270
+ # Process each scanlines.
271
+ #
272
+ # It takes a block with a parameter. The parameter is an instance of
273
+ # PNGlitch::Scanline and it provides ways to edit the filter type and the data
274
+ # of the scanlines. Normally it iterates the number of the PNG image height.
275
+ #
276
+ # Here is some examples:
277
+ #
278
+ # pnglitch.each_scanline do |line|
279
+ # line.gsub!(/\w/, '0') # replace all alphabetical chars in data
280
+ # end
281
+ #
282
+ # pnglicth.each_scanline do |line|
283
+ # line.change_filter 3 # change all filter to 3, data will get re-filtering (it's not be a glitch)
284
+ # end
285
+ #
286
+ # pnglicth.each_scanline do |line|
287
+ # line.graft 3 # change all filter to 3 and data remains (it will be a glitch)
288
+ # end
289
+ #
290
+ # See PNGlitch::Scanline for more details.
291
+ #
292
+ # This method is safer than +glitch+ but will be a little bit slow.
293
+ #
294
+ # -----
295
+ #
296
+ # Please note that +each_scanline+ will apply the filters after the loop. It means
297
+ # a following example doesn't work as expected.
298
+ #
299
+ # pnglicth.each_scanline do |line|
300
+ # line.change_filter 3
301
+ # line.gsub! /\d/, 'x' # wants to glitch after changing filters.
302
+ # end
303
+ #
304
+ # To glitch after applying the new filter types, it should be called separately like:
305
+ #
306
+ # pnglicth.each_scanline do |line|
307
+ # line.change_filter 3
308
+ # end
309
+ # pnglicth.each_scanline do |line|
310
+ # line.gsub! /\d/, 'x'
311
+ # end
312
+ #
313
+ def each_scanline # :yield: scanline
314
+ return enum_for :each_scanline unless block_given?
315
+
316
+ prev_filters = self.filter_types
317
+ is_refilter_needed = false
318
+ filter_codecs = []
319
+ wrap_with_rewind(@filtered_data) do
320
+ pos = 0
321
+ at = 0
322
+ until @filtered_data.eof? do
323
+ scanline = Scanline.new @filtered_data, pos, @width, @sample_size, at
324
+ yield scanline
325
+ unless scanline.prev_filter_type.nil?
326
+ is_refilter_needed = true
327
+ else
328
+ prev_filters[at] = scanline.filter_type # forget the prev filter when "graft"
329
+ end
330
+ filter_codecs << scanline.filter_codec
331
+ if !filter_codecs.last[:encoder].nil? || !filter_codecs.last[:decoder].nil?
332
+ is_refilter_needed = true
333
+ end
334
+ at += 1
335
+ pos += @width * @sample_size + 1
336
+ @filtered_data.pos = pos
337
+ end
338
+ end
339
+ apply_filters(prev_filters, filter_codecs) if is_refilter_needed
340
+ compress
341
+ end
342
+
343
+ #
344
+ # Access particular scanline(s) at passed +index_or_range+.
345
+ #
346
+ # It returns a single Scanline or an array of Scanline.
347
+ #
348
+ def scanline_at index_or_range
349
+ base = self
350
+ prev_filters = self.filter_types
351
+ filter_codecs = nil
352
+ scanlines = []
353
+ index_or_range = self.filter_types.size - 1 if index_or_range == -1
354
+ range = index_or_range.is_a?(Range) ? index_or_range : [index_or_range]
355
+ pos = 0
356
+ self.filter_types.each.with_index do |filter, at|
357
+ if range.include? at
358
+ s = Scanline.new(@filtered_data, pos, @width, @sample_size, at) do |scanline|
359
+ is_refilter_needed = false
360
+ unless scanline.prev_filter_type.nil?
361
+ is_refilter_needed = true
362
+ else
363
+ prev_filters[at] = scanline.filter_type
364
+ end
365
+ codec = scanline.filter_codec
366
+ if !codec[:encoder].nil? || !codec[:decoder].nil?
367
+ filter_codecs = Array.new(prev_filters.size) if filter_codecs.nil?
368
+ filter_codecs[at] = codec
369
+ is_refilter_needed = true
370
+ end
371
+ base.apply_filters(prev_filters, filter_codecs) if is_refilter_needed
372
+ base.compress
373
+ end
374
+ scanlines << s
375
+ end
376
+ pos += @width * @sample_size + 1
377
+ end
378
+ scanlines.size <= 1 ? scanlines.first : scanlines
379
+ end
380
+
381
+ #
382
+ # Rewrites the width value.
383
+ #
384
+ def width= w
385
+ @head_data.pos = 8
386
+ while bytes = @head_data.read(8)
387
+ length, type = bytes.unpack 'Na*'
388
+ if type == 'IHDR'
389
+ @head_data << [w].pack('N')
390
+ @head_data.pos -= 4
391
+ data = @head_data.read length
392
+ @head_data << [Zlib.crc32(data, Zlib.crc32(type))].pack('N')
393
+ break
394
+ end
395
+ end
396
+ @head_data.rewind
397
+ end
398
+
399
+ #
400
+ # Rewrites the height value.
401
+ #
402
+ def height= h
403
+ @head_data.pos = 8
404
+ while bytes = @head_data.read(8)
405
+ length, type = bytes.unpack 'Na*'
406
+ if type == 'IHDR'
407
+ @head_data.pos += 4
408
+ @head_data << [h].pack('N')
409
+ @head_data.pos -= 8
410
+ data = @head_data.read length
411
+ @head_data << [Zlib.crc32(data, Zlib.crc32(type))].pack('N')
412
+ @head_data.rewind
413
+ break
414
+ end
415
+ end
416
+ @head_data.rewind
417
+ end
418
+
419
+ #
420
+ # Save to the +file+.
421
+ #
422
+ def save file
423
+ @head_data.rewind
424
+ @tail_data.rewind
425
+ @compressed_data.rewind
426
+ open(file, 'w') do |io|
427
+ io << @head_data.read
428
+ chunk_size = @idat_chunk_size || @compressed_data.size
429
+ type = 'IDAT'
430
+ until @compressed_data.eof? do
431
+ data = @compressed_data.read(chunk_size)
432
+ io << [data.size].pack('N')
433
+ io << type
434
+ io << data
435
+ io << [Zlib.crc32(data, Zlib.crc32(type))].pack('N')
436
+ end
437
+ io << @tail_data.read
438
+ end
439
+ self
440
+ end
441
+
442
+ alias output save
443
+
444
+ private
445
+
446
+ # Truncates IO's data from current position.
447
+ def truncate_io io
448
+ eof = io.pos
449
+ io.truncate eof
450
+ end
451
+
452
+ # Rewinds given IOs before and after the block.
453
+ def wrap_with_rewind *io, &block
454
+ io.each do |i|
455
+ i.rewind
456
+ end
457
+ yield
458
+ io.each do |i|
459
+ i.rewind
460
+ end
461
+ end
462
+
463
+ # Makes warning
464
+ def warn_if_compressed_data_modified # :nodoc:
465
+ if @is_compressed_data_modified
466
+ trace = caller_locations 1, 2
467
+ message = <<-EOL.gsub(/^\s*/, '')
468
+ WARNING: `#{trace.first.label}' is called after a modification to the compressed data.
469
+ With this operation, your changes on the compressed data will be reverted.
470
+ Note that a modification to the compressed data does not reflect to the
471
+ filtered (decompressed) data.
472
+ It\'s happened around #{trace.last.to_s}
473
+ EOL
474
+ message = ["\e[33m", message, "\e[0m"].join if STDOUT.tty? # color yellow
475
+ warn ["\n", message, "\n"].join
476
+ end
477
+ end
478
+ end
479
+ end