jpeg2moro 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/History.txt ADDED
@@ -0,0 +1,4 @@
1
+ === 0.0.1 2010-11-22
2
+
3
+ * 1 major enhancement:
4
+ * Initial release
data/Manifest.txt ADDED
@@ -0,0 +1,13 @@
1
+ History.txt
2
+ Manifest.txt
3
+ README.rdoc
4
+ Rakefile
5
+ bin/jpeg2moro
6
+ lib/jpeg2moro.rb
7
+ script/console
8
+ script/destroy
9
+ script/generate
10
+ spec/jpeg2moro_spec.rb
11
+ spec/spec.opts
12
+ spec/spec_helper.rb
13
+ tasks/rspec.rake
data/README.rdoc ADDED
@@ -0,0 +1,64 @@
1
+ = jpeg2moro
2
+
3
+ * http://github.com/2moro/jpeg2moro
4
+
5
+ == DESCRIPTION:
6
+
7
+ JPEG2moro is a method of encoding arbitrary data (including an alpha channel) within the JPEG Application Segment APP10.
8
+
9
+ == FEATURES/PROBLEMS:
10
+
11
+ * converts image files to jpeg2moro format
12
+
13
+ == SYNOPSIS:
14
+
15
+ # command line
16
+
17
+ # create a jpeg with transparency from a png file (creates image.jpg)
18
+ > ruby/scripts/jpeg2moro source.png
19
+
20
+ # specify alpha channel bit depth (1 bit) and output file output.jpg
21
+ > ruby/scripts/jpeg2moro source.png -a 1 -o output.jpg
22
+
23
+ # programmatically read from file
24
+
25
+ jpeg = JPEG2moro.new("source.png")
26
+ jpeg.save("output.jpg", :alpha_depth => 8)
27
+
28
+ # programmatically read from string
29
+
30
+ jpeg = JPEG2moro.with_data(File.read("source.png"))
31
+ jpeg.save("output.jpg", :alpha_depth => 8)
32
+
33
+ == REQUIREMENTS:
34
+
35
+ * rmagick
36
+
37
+ == INSTALL:
38
+
39
+ * sudo gem install jpeg2moro
40
+
41
+ == LICENSE:
42
+
43
+ (The MIT License)
44
+
45
+ Copyright (c) 2010 2moro mobile
46
+
47
+ Permission is hereby granted, free of charge, to any person obtaining
48
+ a copy of this software and associated documentation files (the
49
+ 'Software'), to deal in the Software without restriction, including
50
+ without limitation the rights to use, copy, modify, merge, publish,
51
+ distribute, sublicense, and/or sell copies of the Software, and to
52
+ permit persons to whom the Software is furnished to do so, subject to
53
+ the following conditions:
54
+
55
+ The above copyright notice and this permission notice shall be
56
+ included in all copies or substantial portions of the Software.
57
+
58
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
59
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
60
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
61
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
62
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
63
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
64
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,24 @@
1
+ require 'rubygems'
2
+ gem 'hoe', '>= 2.1.0'
3
+ require 'hoe'
4
+ #require 'fileutils'
5
+ #require './lib/jpeg2moro'
6
+
7
+ Hoe.plugin :newgem
8
+ # Hoe.plugin :website
9
+ # Hoe.plugin :cucumberfeatures
10
+
11
+ # Generate all the Rake tasks
12
+ # Run 'rake -T' to see list of generated tasks (from gem root directory)
13
+ $hoe = Hoe.spec 'jpeg2moro' do
14
+ self.developer '2moro mobile', 'support@2moro.com.au'
15
+ self.rubyforge_name = self.name # TODO this is default value
16
+ self.extra_deps = [['rmagick','>= 2.13.1']]
17
+ end
18
+
19
+ require 'newgem/tasks'
20
+ #Dir['tasks/**/*.rake'].each { |t| load t }
21
+
22
+ # TODO - want other tests/tasks run by default? Add them to the list
23
+ # remove_task :default
24
+ # task :default => [:spec, :features]
data/bin/jpeg2moro ADDED
@@ -0,0 +1,102 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+
4
+ $:.unshift File.join(File.dirname(__FILE__), '..', 'lib')
5
+
6
+ require 'jpeg2moro'
7
+ require 'getoptlong'
8
+
9
+ ARG_SPEC = [ [ '--help', '-h', GetoptLong::NO_ARGUMENT ],
10
+ [ '--test', '-n', GetoptLong::NO_ARGUMENT ],
11
+ [ '--output', '-o', GetoptLong::REQUIRED_ARGUMENT ],
12
+ [ '--debug', '-d', GetoptLong::NO_ARGUMENT ],
13
+ [ '--alpha-depth', '-a', GetoptLong::REQUIRED_ARGUMENT ],
14
+ [ '--quality', '-q', GetoptLong::REQUIRED_ARGUMENT ],
15
+ [ '--verbose', '-v', GetoptLong::NO_ARGUMENT ]
16
+ ]
17
+
18
+ private
19
+
20
+ # parse command line arguments
21
+ def command_line_options
22
+ opts = GetoptLong.new(*ARG_SPEC)
23
+ options = {}
24
+ opts.each do |opt,arg|
25
+ opt[0,2] = ''
26
+ opt = opt.gsub(/-/, '_').to_sym
27
+ case opt
28
+ when :help
29
+ puts usage
30
+ exit 0
31
+ else
32
+ options[opt] = arg ? arg : true
33
+ end
34
+ end
35
+ options
36
+ end
37
+
38
+ def usage
39
+ usage = <<EOM
40
+ jpeg2moro - converts image files to jpeg2moro format
41
+ usage: jpeg2moro [OPTIONS] source_file [source_file ...]
42
+
43
+ options:
44
+ --help, -h: this help message
45
+ --output, -o: specify output filename
46
+ --debug, -d: turn on debugging output and file dumping
47
+ --alpha-depth, -a: set depth for alpha channel, default #{JPEG2moro::DEFAULT_ALPHA_DEPTH}
48
+ --quality, -q: jpeg compression level (0-100), default #{JPEG2moro::DEFAULT_QUALITY}
49
+ --verbose, -v: verbose mode
50
+
51
+ EOM
52
+ end
53
+
54
+ def output_filename(input)
55
+ file = File.basename(input)
56
+ file = file.sub(/\.[^.]+$/, '')
57
+ file + ".jpg"
58
+ end
59
+
60
+ begin
61
+ opts = command_line_options
62
+ input_files = ARGV
63
+
64
+ #opts[:input] = params.pop if !opts[:input] && $stdin.tty?
65
+
66
+ JPEG2moro.debug = opts[:debug]
67
+
68
+ if input_files.length == 0
69
+ puts usage
70
+ elsif opts[:output] && input_files.length > 1
71
+ puts "ERROR: output file option can only be used with one input file"
72
+ puts usage
73
+ exit 1
74
+ end
75
+
76
+ output_opts = {}
77
+ [:alpha_depth, :quality].each do |i|
78
+ output_opts[i] = opts[i] if opts[i]
79
+ end
80
+
81
+ # convert input files to jpeg2moro format
82
+ input_files.each do |input_file|
83
+ unless File.exist?(input_file)
84
+ puts "error: '#{input_file}' file not found"
85
+ next
86
+ end
87
+
88
+ output_file = opts[:output] || output_filename(input_file)
89
+ if output_file == input_file
90
+ puts "error: input file '#{input_file}' would overwrite itself, skipping"
91
+ next
92
+ end
93
+
94
+ jpeg = JPEG2moro.new(input_file)
95
+ jpeg.save(output_file, output_opts)
96
+
97
+ if opts[:verbose]
98
+ puts "%s -> %s" % [File.basename(input_file), File.basename(output_file)]
99
+ end
100
+ end
101
+
102
+ end
data/lib/jpeg2moro.rb ADDED
@@ -0,0 +1,201 @@
1
+
2
+ require 'rubygems'
3
+ require 'zlib'
4
+ gem 'rmagick'
5
+ require 'RMagick'
6
+ include Magick
7
+
8
+ class JPEG2moro
9
+ DEBUG = 1
10
+ VERSION = "0.0.1"
11
+
12
+ DEFAULT_ALPHA_DEPTH = 8
13
+ DEFAULT_QUALITY = 70
14
+
15
+ class << self
16
+ attr_accessor :debug
17
+ end
18
+
19
+ def initialize(options = {})
20
+ if options.kind_of?(Hash)
21
+ @file = options[:file]
22
+ @data = options[:data]
23
+ else
24
+ @file = options
25
+ end
26
+
27
+ @data = File.read(@file) if @file
28
+ debug "read %d bytes" % [@data.length]
29
+ end
30
+
31
+ def self.with_data(data)
32
+ return JPEG2moro.new(:data => data)
33
+ end
34
+
35
+ def to_s
36
+ @data
37
+ end
38
+
39
+ # save output_file with the given options
40
+ def save(output_file, options = {})
41
+ output = convert(options)
42
+ File.open(output_file, "w") { |f| f.print output }
43
+ end
44
+
45
+ private
46
+
47
+ # convert data to jpeg2moro format
48
+ def convert(options = {})
49
+ @convert_options = options.clone
50
+ @convert_options[:alpha_depth] ||= DEFAULT_ALPHA_DEPTH
51
+
52
+ # convert image data to jpg format
53
+ img = Image.from_blob(@data).first
54
+ jpeg_data = img.to_blob { |i|
55
+ i.quality = (options[:quality] || DEFAULT_QUALITY).to_i
56
+ i.format = 'jpg'
57
+ }
58
+ # insert alpha channel into jpeg data
59
+ insert_opacity_header(jpeg_data)
60
+
61
+ # return jpeg2moro object (contains alpha channel)
62
+ return JPEG2moro.new(:data => jpeg_data)
63
+ end
64
+
65
+ def debug(msg)
66
+ puts msg if self.class.debug
67
+ end
68
+
69
+ def debug_dump(data, filename)
70
+ if self.class.debug
71
+ File.open(filename, "w") { |f| f.print data }
72
+ end
73
+ end
74
+
75
+ def standalone_marker(marker)
76
+ (marker >= 0xd0 && marker <= 0xd7) || # RST
77
+ marker == 0xd8 || marker == 0xd9 || # SOI/EOI
78
+ marker == 0x01 # TEM
79
+ end
80
+
81
+ # find position to insert opacity information
82
+ # (just before SOS or EOI marker)
83
+ def insert_position(jpeg_data)
84
+ # parse jpeg data
85
+ data = jpeg_data
86
+ pos = 0
87
+ while pos < data.length
88
+ marker1 = data[pos]
89
+ marker2 = data[pos + 1]
90
+
91
+ return -1 if marker1 != 0xff # parse error
92
+ if marker2 == 0xff
93
+ # packed marker
94
+ pos += 1
95
+ next
96
+ end
97
+
98
+ if marker2 == 0xda || marker2 == 0xd9
99
+ # SOS (start of scan) or EOI (end of image)
100
+ # insert before this marker
101
+ debug "found insert position: %d (marker %x)" % [pos, marker2]
102
+ return pos
103
+ end
104
+
105
+ pos += 2
106
+ next if standalone_marker(marker2)
107
+
108
+ mlength = data[pos, 2].unpack('C2').reverse.pack('C2').unpack('S').first
109
+ pos += mlength
110
+
111
+ debug "marker: %x, length: %d" % [marker2, mlength]
112
+ end
113
+ end
114
+
115
+ # create a 'chunk' named name, with the given data.
116
+ # (4 bytes) Length
117
+ # (4 bytes) Chunk type (e.g. "ALPH" for alpha channel)
118
+ # (length bytes) Chunk data
119
+ def chunk(name, data)
120
+ raise "chunk name must be four characters long" unless name.length == 4
121
+ [data.length].pack("N") + name + data # TODO: not sure if this is unsigned
122
+ end
123
+
124
+ # create one or more app10 application segments containing the given data.
125
+ # if data exceeds the maximum length of the app segment, it is split into multiple
126
+ # segments. returns array of app10 segments.
127
+ # (2 bytes) APP10 marker (0xFFE9)
128
+ # (2 bytes) Length of segment (including 2 byte length parameter)
129
+ # (length - 2 bytes) Chunk data
130
+ def app10_segments(data)
131
+ i = 0
132
+ len = data.length
133
+ ret = []
134
+ while i < len
135
+ seglen = len - i > 65535 ? 65535 : len - i
136
+ segdata = data[i, seglen]
137
+ debug "segment data length: " + segdata.length.to_s
138
+
139
+ mlength = segdata.length + 2
140
+ header = [0xff, 0xe9].pack("C*") # APP10 marker
141
+ header += [mlength].pack("S").unpack("C*").reverse.pack("C*")
142
+ header += segdata
143
+ ret.push header
144
+ i += seglen
145
+ end
146
+ ret
147
+ end
148
+
149
+ # create opacity header
150
+ def create_opacity_header
151
+ img = Image.from_blob(@data).first # original image data
152
+ bit_depth = @convert_options[:alpha_depth]
153
+ bit_depth = bit_depth.to_i
154
+
155
+ unless [1,8,16].include?(bit_depth)
156
+ raise "unsupported alpha bit depth: " + @convert_options[:alpha_depth].to_s
157
+ end
158
+
159
+ debug "using bit depth: %d" % [bit_depth]
160
+
161
+ img.set_channel_depth(AlphaChannel, bit_depth)
162
+ alpha = img.separate(OpacityChannel)
163
+ #alpha.set_channel_depth(DefaultChannels, 1)
164
+ #alpha.display
165
+ storage = bit_depth == 16 ? ShortPixel : CharPixel
166
+ data = alpha.export_pixels_to_str(0, 0, alpha.columns, alpha.rows,
167
+ "A", storage)
168
+
169
+ debug_dump(alpha.to_blob, "alpha.png")
170
+
171
+ if bit_depth == 1
172
+ # pack into bit string
173
+ data = data.unpack('C*').map { |i| i == 0xff ? 1 : 0 }.join
174
+ data = [data].pack("B*")
175
+ end
176
+
177
+ debug_dump(data, "pixel.dat")
178
+
179
+ deflated = Zlib::Deflate.deflate(data, 9)
180
+ #deflated = deflated[2..-5] # strip header/footer?
181
+ debug "deflated size: %d bytes" % [deflated.length]
182
+
183
+ debug_dump(deflated, "compressed.dat")
184
+
185
+ # create alpha chunk, pack into app10 segment(s)
186
+ alpha_data = [bit_depth].pack("C") + deflated
187
+ alpha_chunk = chunk("ALPH", alpha_data)
188
+ segments = app10_segments(alpha_chunk)
189
+ header = segments.join
190
+
191
+ debug_dump(header, "header.dat")
192
+
193
+ header
194
+ end
195
+
196
+ def insert_opacity_header(jpeg_data)
197
+ offset = insert_position(jpeg_data)
198
+ header = create_opacity_header
199
+ jpeg_data.insert(offset, header)
200
+ end
201
+ end
data/script/console ADDED
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env ruby
2
+ # File: script/console
3
+ irb = RUBY_PLATFORM =~ /(:?mswin|mingw)/ ? 'irb.bat' : 'irb'
4
+
5
+ libs = " -r irb/completion"
6
+ # Perhaps use a console_lib to store any extra methods I may want available in the cosole
7
+ # libs << " -r #{File.dirname(__FILE__) + '/../lib/console_lib/console_logger.rb'}"
8
+ libs << " -r #{File.dirname(__FILE__) + '/../lib/jpeg2moro.rb'}"
9
+ puts "Loading jpeg2moro gem"
10
+ exec "#{irb} #{libs} --simple-prompt"
data/script/destroy ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+ APP_ROOT = File.expand_path(File.join(File.dirname(__FILE__), '..'))
3
+
4
+ begin
5
+ require 'rubigen'
6
+ rescue LoadError
7
+ require 'rubygems'
8
+ require 'rubigen'
9
+ end
10
+ require 'rubigen/scripts/destroy'
11
+
12
+ ARGV.shift if ['--help', '-h'].include?(ARGV[0])
13
+ RubiGen::Base.use_component_sources! [:rubygems, :newgem, :newgem_theme, :test_unit]
14
+ RubiGen::Scripts::Destroy.new.run(ARGV)
data/script/generate ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+ APP_ROOT = File.expand_path(File.join(File.dirname(__FILE__), '..'))
3
+
4
+ begin
5
+ require 'rubigen'
6
+ rescue LoadError
7
+ require 'rubygems'
8
+ require 'rubigen'
9
+ end
10
+ require 'rubigen/scripts/generate'
11
+
12
+ ARGV.shift if ['--help', '-h'].include?(ARGV[0])
13
+ RubiGen::Base.use_component_sources! [:rubygems, :newgem, :newgem_theme, :test_unit]
14
+ RubiGen::Scripts::Generate.new.run(ARGV)
@@ -0,0 +1,11 @@
1
+ require File.dirname(__FILE__) + '/spec_helper.rb'
2
+
3
+ # Time to add your specs!
4
+ # http://rspec.info/
5
+ describe "Place your specs here" do
6
+
7
+ it "find this spec in spec directory" do
8
+ # violated "Be sure to write your specs"
9
+ end
10
+
11
+ end
data/spec/spec.opts ADDED
@@ -0,0 +1 @@
1
+ --colour
@@ -0,0 +1,8 @@
1
+ begin
2
+ require 'rubygems' unless ENV['NO_RUBYGEMS']
3
+ gem 'rspec'
4
+ require 'rspec'
5
+ end
6
+
7
+ $:.unshift(File.dirname(__FILE__) + '/../lib')
8
+ require 'jpeg2moro'
data/tasks/rspec.rake ADDED
@@ -0,0 +1,16 @@
1
+ begin
2
+ require 'rubygems' unless ENV['NO_RUBYGEMS']
3
+ require 'rspec'
4
+ rescue LoadError
5
+ puts <<-EOS
6
+ To use rspec for testing you must install rspec gem:
7
+ gem install rspec
8
+ EOS
9
+ exit(0)
10
+ end
11
+
12
+ desc "Run the specs under spec/models"
13
+ RSpec::Core::RakeTask.new do |t|
14
+ # t.rspec_opts = ['--options', "spec/spec.opts"]
15
+ t.pattern = 'spec/**/*_spec.rb'
16
+ end
metadata ADDED
@@ -0,0 +1,129 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: jpeg2moro
3
+ version: !ruby/object:Gem::Version
4
+ hash: 29
5
+ prerelease: false
6
+ segments:
7
+ - 0
8
+ - 0
9
+ - 1
10
+ version: 0.0.1
11
+ platform: ruby
12
+ authors:
13
+ - 2moro mobile
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2010-11-23 00:00:00 +10:30
19
+ default_executable:
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ name: rmagick
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ hash: 57
30
+ segments:
31
+ - 2
32
+ - 13
33
+ - 1
34
+ version: 2.13.1
35
+ type: :runtime
36
+ version_requirements: *id001
37
+ - !ruby/object:Gem::Dependency
38
+ name: rubyforge
39
+ prerelease: false
40
+ requirement: &id002 !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ">="
44
+ - !ruby/object:Gem::Version
45
+ hash: 7
46
+ segments:
47
+ - 2
48
+ - 0
49
+ - 4
50
+ version: 2.0.4
51
+ type: :development
52
+ version_requirements: *id002
53
+ - !ruby/object:Gem::Dependency
54
+ name: hoe
55
+ prerelease: false
56
+ requirement: &id003 !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ hash: 19
62
+ segments:
63
+ - 2
64
+ - 7
65
+ - 0
66
+ version: 2.7.0
67
+ type: :development
68
+ version_requirements: *id003
69
+ description: JPEG2moro is a method of encoding arbitrary data (including an alpha channel) within the JPEG Application Segment APP10.
70
+ email:
71
+ - support@2moro.com.au
72
+ executables:
73
+ - jpeg2moro
74
+ extensions: []
75
+
76
+ extra_rdoc_files:
77
+ - History.txt
78
+ - Manifest.txt
79
+ files:
80
+ - History.txt
81
+ - Manifest.txt
82
+ - README.rdoc
83
+ - Rakefile
84
+ - bin/jpeg2moro
85
+ - lib/jpeg2moro.rb
86
+ - script/console
87
+ - script/destroy
88
+ - script/generate
89
+ - spec/jpeg2moro_spec.rb
90
+ - spec/spec.opts
91
+ - spec/spec_helper.rb
92
+ - tasks/rspec.rake
93
+ has_rdoc: true
94
+ homepage: http://github.com/2moro/jpeg2moro
95
+ licenses: []
96
+
97
+ post_install_message:
98
+ rdoc_options:
99
+ - --main
100
+ - README.rdoc
101
+ require_paths:
102
+ - lib
103
+ required_ruby_version: !ruby/object:Gem::Requirement
104
+ none: false
105
+ requirements:
106
+ - - ">="
107
+ - !ruby/object:Gem::Version
108
+ hash: 3
109
+ segments:
110
+ - 0
111
+ version: "0"
112
+ required_rubygems_version: !ruby/object:Gem::Requirement
113
+ none: false
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ hash: 3
118
+ segments:
119
+ - 0
120
+ version: "0"
121
+ requirements: []
122
+
123
+ rubyforge_project: jpeg2moro
124
+ rubygems_version: 1.3.7
125
+ signing_key:
126
+ specification_version: 3
127
+ summary: JPEG2moro is a method of encoding arbitrary data (including an alpha channel) within the JPEG Application Segment APP10.
128
+ test_files: []
129
+