jpeg2moro 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.
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
+