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
checksums.yaml
ADDED
@@ -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
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -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.
|
data/README.md
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
# PNGlitch
|
2
|
+
|
3
|
+
[](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
|
data/Rakefile
ADDED
data/bin/pnglitch
ADDED
@@ -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
|
data/lib/pnglitch.rb
ADDED
@@ -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
|