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