pnglitch 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,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