qttk 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (61) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +2 -0
  3. data/Gemfile +6 -0
  4. data/Gemfile.lock +36 -0
  5. data/README.markdown +131 -0
  6. data/Rakefile +17 -0
  7. data/TODO.markdown +236 -0
  8. data/bin/qt +7 -0
  9. data/etc/gutenprint/gp-tool.rb +56 -0
  10. data/etc/gutenprint/gutenprint-filter.c +400 -0
  11. data/etc/gutenprint/gutenprint.rb +86 -0
  12. data/etc/gutenprint/stp-test +326 -0
  13. data/etc/images/3551599565_db282cf840_o.jpg +0 -0
  14. data/etc/images/4843122063_d582c569e9_o.jpg +0 -0
  15. data/etc/images/4843128953_83c1770907_o.jpg +0 -0
  16. data/lib/quadtone.rb +56 -0
  17. data/lib/quadtone/cgats.rb +137 -0
  18. data/lib/quadtone/cluster_calculator.rb +81 -0
  19. data/lib/quadtone/color.rb +83 -0
  20. data/lib/quadtone/color/cmyk.rb +112 -0
  21. data/lib/quadtone/color/device_n.rb +23 -0
  22. data/lib/quadtone/color/gray.rb +46 -0
  23. data/lib/quadtone/color/lab.rb +150 -0
  24. data/lib/quadtone/color/qtr.rb +71 -0
  25. data/lib/quadtone/color/rgb.rb +71 -0
  26. data/lib/quadtone/color/xyz.rb +80 -0
  27. data/lib/quadtone/curve.rb +138 -0
  28. data/lib/quadtone/curve_set.rb +196 -0
  29. data/lib/quadtone/descendants.rb +9 -0
  30. data/lib/quadtone/environment.rb +5 -0
  31. data/lib/quadtone/extensions/math.rb +11 -0
  32. data/lib/quadtone/extensions/pathname3.rb +11 -0
  33. data/lib/quadtone/printer.rb +106 -0
  34. data/lib/quadtone/profile.rb +217 -0
  35. data/lib/quadtone/quad_file.rb +59 -0
  36. data/lib/quadtone/renderer.rb +139 -0
  37. data/lib/quadtone/run.rb +10 -0
  38. data/lib/quadtone/sample.rb +32 -0
  39. data/lib/quadtone/separator.rb +36 -0
  40. data/lib/quadtone/target.rb +277 -0
  41. data/lib/quadtone/tool.rb +61 -0
  42. data/lib/quadtone/tools/add_printer.rb +73 -0
  43. data/lib/quadtone/tools/characterize.rb +43 -0
  44. data/lib/quadtone/tools/chart.rb +31 -0
  45. data/lib/quadtone/tools/check.rb +16 -0
  46. data/lib/quadtone/tools/dir.rb +15 -0
  47. data/lib/quadtone/tools/edit.rb +23 -0
  48. data/lib/quadtone/tools/init.rb +82 -0
  49. data/lib/quadtone/tools/install.rb +15 -0
  50. data/lib/quadtone/tools/linearize.rb +28 -0
  51. data/lib/quadtone/tools/list.rb +19 -0
  52. data/lib/quadtone/tools/print.rb +38 -0
  53. data/lib/quadtone/tools/printer_options.rb +40 -0
  54. data/lib/quadtone/tools/rename.rb +17 -0
  55. data/lib/quadtone/tools/render.rb +43 -0
  56. data/lib/quadtone/tools/rewrite.rb +15 -0
  57. data/lib/quadtone/tools/separate.rb +71 -0
  58. data/lib/quadtone/tools/show.rb +15 -0
  59. data/lib/quadtone/tools/test.rb +26 -0
  60. data/qttk.gemspec +34 -0
  61. metadata +215 -0
@@ -0,0 +1,196 @@
1
+ module Quadtone
2
+
3
+ class CurveSet
4
+
5
+ attr_accessor :profile
6
+ attr_accessor :channels
7
+ attr_accessor :type
8
+ attr_accessor :curves
9
+
10
+ def initialize(params={})
11
+ @curves = []
12
+ params.each { |key, value| send("#{key}=", value) }
13
+ raise "Profile must be specified" unless @profile
14
+ raise "Channels must be specified" unless @channels
15
+ raise "Type must be specified" unless @type
16
+ @target = Target.new(name: @type.to_s, channels: @channels, base_dir: @profile.dir_path, type: @type, ink_limits: @profile.ink_limits)
17
+ generate_scale
18
+ end
19
+
20
+ def build_target
21
+ @target.build
22
+ end
23
+
24
+ def print_target
25
+ @profile.print_file(@target.image_file, calibrate: (@type == :characterization), print: true)
26
+ end
27
+
28
+ def measure_target(options={})
29
+ @target.measure(options)
30
+ process_target
31
+ chart_target
32
+ end
33
+
34
+ def process_target
35
+ case @type
36
+ when :characterization
37
+ import_from_target
38
+ verify_increasing_values
39
+ set_common_white
40
+ # trim_to_limits
41
+ # @profile.ink_limits = Hash[
42
+ # @curves.map do |curve|
43
+ # [
44
+ # curve.channel,
45
+ # curve.samples.last.input.value
46
+ # ]
47
+ # end
48
+ # ]
49
+ # normalize_curves
50
+ @profile.ink_partitions = partitions
51
+ when :linearization, :test
52
+ import_from_target
53
+ if @type == :linearization
54
+ @profile.linearization = grayscale(21)
55
+ elsif @type == :test
56
+ @profile.grayscale = grayscale(21)
57
+ end
58
+ end
59
+ @profile.save
60
+ @profile.install
61
+ end
62
+
63
+ def chart_target
64
+ import_from_target
65
+ out_file = (@profile.dir_path + @type.to_s).with_extname('.html')
66
+ out_file.open('w') { |io| io.write(to_html) }
67
+ ;;warn "Saved chart to #{out_file.to_s.inspect}"
68
+ system('qlmanage', '-p', out_file.to_s)
69
+ end
70
+
71
+ private
72
+
73
+ def import_from_target
74
+ @target.read
75
+ @curves.each do |curve|
76
+ curve.samples = @target.samples[curve.channel]
77
+ end
78
+ end
79
+
80
+ def generate_scale
81
+ @curves = @channels.map do |channel|
82
+ Curve.new(channel: channel, samples: [
83
+ Sample.new(input: Color::Gray.new(k: 0), output: Color::Lab.new(l: 100)),
84
+ Sample.new(input: Color::Gray.new(k: 1), output: Color::Lab.new(l: 0))
85
+ ])
86
+ end
87
+ end
88
+
89
+ def channels
90
+ @curves.map(&:channel)
91
+ end
92
+
93
+ def verify_increasing_values
94
+ @curves.each { |c| c.verify_increasing_values }
95
+ end
96
+
97
+ def trim_to_limits
98
+ @curves.each { |c| c.trim_to_limit }
99
+ end
100
+
101
+ def normalize_curves
102
+ @curves.each { |c| c.normalize_inputs }
103
+ end
104
+
105
+ # find average shade of paper, and update each curve to have that average as its first value
106
+
107
+ def set_common_white
108
+ samples = samples_with_value(0)
109
+ outputs = samples.map(&:output)
110
+ average, error = Color::Lab.average(outputs)
111
+ raise "too much variance in white samples: average = #{average}, error = #{error}" if error >= 1
112
+ samples.each { |s| s.output = average }
113
+ end
114
+
115
+ def samples_with_value(value)
116
+ @curves.map { |c| c.samples.find { |s| s.input_value == value } }.compact.flatten
117
+ end
118
+
119
+ def partitions
120
+ partitions = {}
121
+ previous_curve = nil
122
+ @curves.sort_by(&:dmax).reverse.each do |curve|
123
+ ;;warn "processing #{curve.channel}"
124
+ last_sample = curve.samples.last
125
+ if previous_curve
126
+ partitions[curve.channel] = previous_curve.input_for_output(last_sample.output.value) * partitions[previous_curve.channel]
127
+ ;;warn "\t" + "value on previous curve for dmax #{last_sample.output.value} = #{partitions[curve.channel]}"
128
+ else
129
+ partitions[curve.channel] = last_sample.input.value
130
+ ;;warn "\t" + "using absolute value of curve for dmax #{last_sample.output.value} = #{partitions[curve.channel]}"
131
+ end
132
+ previous_curve = curve
133
+ end
134
+ partitions
135
+ end
136
+
137
+ def grayscale(steps)
138
+ raise "Can't get gray scale of non-grayscale curveset" if @channels.length > 1
139
+ @curves.first.grayscale(steps)
140
+ end
141
+
142
+ def to_html
143
+ html = Builder::XmlMarkup.new(indent: 2)
144
+ html.div do
145
+ html.ul do
146
+ html.li("Channels: #{@channels.join(', ')}")
147
+ end
148
+ html.h3('Curve set:')
149
+ html.table(border: 1) do
150
+ html.tr do
151
+ [
152
+ 'channel',
153
+ 'ink limit',
154
+ 'density: min',
155
+ 'density: max',
156
+ 'density: range',
157
+ ].each { |s| html.th(s) }
158
+ end
159
+ @curves.each do |curve|
160
+ html.tr do
161
+ dmin, dmax = curve.dynamic_range
162
+ [
163
+ curve.channel.to_s,
164
+ curve.ink_limit.input,
165
+ '%.2f' % dmin,
166
+ '%.2f' % dmax,
167
+ '%.2f' % (dmax - dmin),
168
+ ].each { |s| html.td(s) }
169
+ end
170
+ end
171
+ end
172
+ html << to_svg
173
+ end
174
+ html.target!
175
+ end
176
+
177
+ def to_svg(options={})
178
+ size = options[:size] || 500
179
+ svg = Builder::XmlMarkup.new(indent: 2)
180
+ svg.svg(xmlns: 'http://www.w3.org/2000/svg', version: '1.1') do
181
+ svg.g(width: size, height: size, transform: "translate(0,#{size}) scale(1,-1)") do
182
+ svg.g(stroke: 'blue') do
183
+ svg.rect(x: 0, y: 0, width: size, height: size, fill: 'none', :'stroke-width' => 1)
184
+ svg.line(x1: 0, y1: 0, x2: size, y2: size, :'stroke-width' => 0.5)
185
+ end
186
+ @curves.each do |curve|
187
+ curve.draw_svg(svg, options)
188
+ end
189
+ end
190
+ end
191
+ svg.target!
192
+ end
193
+
194
+ end
195
+
196
+ end
@@ -0,0 +1,9 @@
1
+ # from http://stackoverflow.com/a/2393878
2
+
3
+ class Class
4
+
5
+ def descendants
6
+ ObjectSpace.each_object(::Class).select { |c| c < self }
7
+ end
8
+
9
+ end
@@ -0,0 +1,5 @@
1
+ module Quadtone
2
+
3
+ BaseDir = Pathname.new(ENV['HOME']) + '.qttk'
4
+
5
+ end
@@ -0,0 +1,11 @@
1
+ module Math
2
+
3
+ def deg2rad(deg)
4
+ deg * (PI / 180)
5
+ end
6
+
7
+ def rad2deg(rad)
8
+ rad * (180 / PI)
9
+ end
10
+
11
+ end
@@ -0,0 +1,11 @@
1
+ class Pathname
2
+
3
+ def with_extname(extname)
4
+ Pathname.new(without_extname.to_s + extname)
5
+ end
6
+
7
+ def without_extname
8
+ Pathname.new(to_s.sub(/#{Regexp.quote(self.extname)}$/, ''))
9
+ end
10
+
11
+ end
@@ -0,0 +1,106 @@
1
+ module Quadtone
2
+
3
+ class Printer
4
+
5
+ ImportantOptions = %i{
6
+ MediaType
7
+ Resolution
8
+ ripBlack
9
+ ripSpeed
10
+ stpDither
11
+ }
12
+
13
+ attr_accessor :name
14
+ attr_accessor :options
15
+ attr_accessor :attributes
16
+ attr_accessor :inks
17
+
18
+ def initialize(name)
19
+ @name = name
20
+ @cups_ppd = CupsPPD.new(@name, nil)
21
+ @options = Hash[
22
+ @cups_ppd.options.map { |o| [o.delete(:keyword).to_sym, HashStruct.new(o)] }
23
+ ]
24
+ @attributes = Hash[
25
+ @cups_ppd.attributes.map { |a| [a.delete(:name).to_sym, HashStruct.new(a)] }
26
+ ]
27
+ @cups_printer = CupsPrinter.new(@name)
28
+ get_inks
29
+ end
30
+
31
+ def quadtone?
32
+ @attributes[:Manufacturer].value == 'QuadToneRIP'
33
+ end
34
+
35
+ def get_inks
36
+ # FIXME: It would be nice to get this path programmatically.
37
+ ppd_file = Pathname.new("/etc/cups/ppd/#{@name}.ppd")
38
+ ppd_file.readlines.each do |line|
39
+ if line =~ /^\*%Inks\s*(.*?)\s*$/
40
+ @inks = $1.split(/,/).map(&:downcase).map(&:to_sym)
41
+ break
42
+ end
43
+ end
44
+ end
45
+
46
+ def page_size(name=nil)
47
+ name ||= @cups_ppd.attribute('DefaultPageSize').first[:value]
48
+ page_size = @cups_ppd.page_size(name) or raise "Can't determine page size #{name.inspect}"
49
+ size = HashStruct.new(page_size)
50
+ # change 'length' to 'height', or else there are problems with Hash#length
51
+ size[:height] = size.delete(:length)
52
+ size = HashStruct.new(size)
53
+ size.imageable_width = size.margin.right - size.margin.left
54
+ size.imageable_height = size.margin.top - size.margin.bottom
55
+ size
56
+ end
57
+
58
+ def default_options
59
+ Hash[
60
+ @options.map do |name, option|
61
+ [name, option.default_choice]
62
+ end
63
+ ]
64
+ end
65
+
66
+ def show_attributes
67
+ puts "Attributes:"
68
+ max_field_length = @attributes.keys.map(&:length).max
69
+ @attributes.sort_by { |name, info| name }.each do |name, attribute|
70
+ puts "\t" + "%#{max_field_length}s: %s%s" % [
71
+ name,
72
+ attribute.value.inspect,
73
+ attribute.spec.empty? ? '' : " [#{attribute.spec.inspect}]"
74
+ ]
75
+ end
76
+ end
77
+
78
+ def show_options
79
+ puts "Options:"
80
+ max_field_length = @options.keys.map(&:length).max
81
+ @options.sort_by { |name, option| name }.each do |name, option|
82
+ puts "\t" + "%#{max_field_length}s: %s [%s]" % [
83
+ name,
84
+ option.default_choice.inspect,
85
+ (option.choices.map { |o| o.choice } - [option.default_choice]).map { |o| o.inspect }.join(', ')
86
+ ]
87
+ end
88
+ end
89
+
90
+ def show_inks
91
+ puts "Inks:"
92
+ puts "\t" + @inks.map { |ink| ink.to_s.upcase }.join(', ')
93
+ end
94
+
95
+ def print_file(path, options)
96
+ warn "Printing #{path}"
97
+ warn "Options:"
98
+ options.each do |key, value|
99
+ warn "\t" + "%10s: %s" % [key, value]
100
+ end
101
+ @cups_printer.print_file(path, options)
102
+ end
103
+
104
+ end
105
+
106
+ end
@@ -0,0 +1,217 @@
1
+ module Quadtone
2
+
3
+ class Profile
4
+
5
+ attr_accessor :printer
6
+ attr_accessor :printer_options
7
+ attr_accessor :medium
8
+ attr_accessor :inks
9
+ attr_accessor :ink_partitions
10
+ attr_accessor :ink_limits
11
+ attr_accessor :linearization
12
+ attr_accessor :default_ink_limit
13
+ attr_accessor :gray_highlight
14
+ attr_accessor :gray_shadow
15
+ attr_accessor :gray_overlap
16
+ attr_accessor :gray_gamma
17
+ attr_accessor :characterization_curveset
18
+ attr_accessor :linearization_curveset
19
+ attr_accessor :test_curveset
20
+
21
+ ProfilesDir = BaseDir + 'profiles'
22
+ ProfileName = 'profile.txt'
23
+
24
+ def self.profile_names
25
+ Pathname.glob(ProfilesDir + '*').select { |p| p.directory? && p[0] != '.' }.map(&:basename)
26
+ end
27
+
28
+ def self.load(name)
29
+ profile = Profile.new
30
+ profile.load(name)
31
+ profile
32
+ end
33
+
34
+ def initialize(params={})
35
+ @printer_options = nil
36
+ @default_ink_limit = 1.0
37
+ @ink_limits = {}
38
+ @ink_partitions = {}
39
+ @gray_highlight = 0.06
40
+ @gray_shadow = 0.06
41
+ @gray_overlap = 0.10
42
+ @gray_gamma = 1.0
43
+ params.each { |key, value| send("#{key}=", value) }
44
+ end
45
+
46
+ def name
47
+ [
48
+ @printer.name.gsub(/[^-A-Z0-9]/i, ''),
49
+ @medium.gsub(/[^-A-Z0-9]/i, ''),
50
+ ].flatten.join('-')
51
+ end
52
+
53
+ def load(name)
54
+ inks_by_num = []
55
+ (ProfilesDir + name + ProfileName).readlines.each do |line|
56
+ line.chomp!
57
+ line.sub!(/#.*/, '')
58
+ line.strip!
59
+ next if line.empty?
60
+ key, value = line.split('=', 2)
61
+ case key
62
+ when 'PRINTER'
63
+ @printer = Printer.new(value)
64
+ when 'PRINTER_OPTIONS'
65
+ @printer_options = Hash[ value.split(',').map { |o| o.split('=') } ]
66
+ when 'MEDIUM'
67
+ @medium = value
68
+ when 'GRAPH_CURVE'
69
+ # ignore
70
+ when 'N_OF_INKS'
71
+ # ignore
72
+ when 'INKS'
73
+ @inks = value.split(',').map(&:downcase).map(&:to_sym)
74
+ when 'DEFAULT_INK_LIMIT'
75
+ @default_ink_limit = value.to_f / 100
76
+ when /^LIMIT_(.+)$/
77
+ @ink_limits[$1.downcase.to_sym] = value.to_f / 100
78
+ when 'N_OF_GRAY_PARTS'
79
+ # ignore
80
+ when /^GRAY_INK_(\d+)$/
81
+ i = $1.to_i - 1
82
+ inks_by_num[i] = value.downcase.to_sym
83
+ when /^GRAY_VAL_(\d+)$/
84
+ i = $1.to_i - 1
85
+ ink = inks_by_num[i]
86
+ @ink_partitions[ink] = value.to_f / 100
87
+ when 'GRAY_HIGHLIGHT'
88
+ @gray_highlight = value.to_f / 100
89
+ when 'GRAY_SHADOW'
90
+ @gray_shadow = value.to_f / 100
91
+ when 'GRAY_OVERLAP'
92
+ @gray_overlap = value.to_f / 100
93
+ when 'GRAY_GAMMA'
94
+ @gray_gamma = value.to_f
95
+ when 'LINEARIZE'
96
+ @linearization = value.gsub('"', '').split(/\s+/).map { |v| Color::Lab.new([v.to_f]) }
97
+ else
98
+ warn "Unknown key in QTR profile: #{key.inspect}"
99
+ end
100
+ end
101
+ @characterization_curveset = CurveSet.new(channels: @inks, profile: self, type: :characterization)
102
+ @linearization_curveset = CurveSet.new(channels: [:k], profile: self, type: :linearization)
103
+ end
104
+
105
+ def save
106
+ qtr_profile_path.dirname.mkpath
107
+ qtr_profile_path.open('w') do |io|
108
+ io.puts "PRINTER=#{@printer.name}"
109
+ io.puts "PRINTER_OPTIONS=#{@printer_options.map { |k, v| [k, v].join('=') }.join(',')}" if @printer_options
110
+ io.puts "MEDIUM=#{@medium}"
111
+ io.puts "GRAPH_CURVE=YES"
112
+ io.puts "INKS=#{@inks.join(',')}"
113
+ io.puts "N_OF_INKS=#{@inks.length}"
114
+ io.puts "DEFAULT_INK_LIMIT=#{@default_ink_limit * 100}"
115
+ @ink_limits.each do |ink, limit|
116
+ io.puts "LIMIT_#{ink.upcase}=#{limit * 100}"
117
+ end
118
+ io.puts "N_OF_GRAY_PARTS=#{@ink_partitions.length}"
119
+ @ink_partitions.each_with_index do |partition, i|
120
+ ink, value = *partition
121
+ io.puts "GRAY_INK_#{i+1}=#{ink.upcase}"
122
+ io.puts "GRAY_VAL_#{i+1}=#{value * 100}"
123
+ end
124
+ io.puts "GRAY_HIGHLIGHT=#{@gray_highlight * 100}"
125
+ io.puts "GRAY_SHADOW=#{@gray_shadow * 100}"
126
+ io.puts "GRAY_OVERLAP=#{@gray_overlap * 100}"
127
+ io.puts "GRAY_GAMMA=#{@gray_gamma}"
128
+ io.puts "LINEARIZE=\"#{@linearization.map(&:l).join(' ')}\"" if @linearization
129
+ end
130
+ end
131
+
132
+ def dir_path
133
+ ProfilesDir + name
134
+ end
135
+
136
+ def qtr_profile_path
137
+ dir_path + ProfileName
138
+ end
139
+
140
+ def quad_file_path
141
+ Pathname.new('/Library/Printers/QTR/quadtone') + @printer.name + "#{name}.quad"
142
+ end
143
+
144
+ def ink_limit(ink)
145
+ @ink_limits[ink] || @default_ink_limit
146
+ end
147
+
148
+ def install
149
+ # filename needs to match name of profile for quadprofile to install it properly,
150
+ # so temporarily make a symlink
151
+ tmp_file = Pathname.new('/tmp') + "#{name}.txt"
152
+ qtr_profile_path.symlink(tmp_file)
153
+ system('/Library/Printers/QTR/bin/quadprofile', tmp_file)
154
+ tmp_file.unlink
155
+ end
156
+
157
+ def print_file(input_path, options={})
158
+ options = HashStruct.new(options)
159
+ printer_options = @printer_options.dup
160
+ if options.printer_options
161
+ options.printer_options.each do |key, value|
162
+ printer_options[key.to_s] = value
163
+ end
164
+ end
165
+ if options.calibrate
166
+ printer_options['ColorModel'] = 'QTCAL'
167
+ else
168
+ printer_options['ripCurve1'] = name
169
+ end
170
+ @printer.print_file(input_path, printer_options)
171
+ end
172
+
173
+ def show
174
+ puts "Profile: #{name}"
175
+ puts "Printer: #{@printer.name}"
176
+ puts "Printer options:"
177
+ @printer_options.each do |key, value|
178
+ puts "\t" + "#{key}: #{value}"
179
+ end
180
+ puts "Medium: #{@medium}"
181
+ puts "Inks: #{@inks.join(', ')}"
182
+ puts "Default ink limit: #{@default_ink_limit}"
183
+ puts "Ink limits:"
184
+ @ink_limits.each do |ink, limit|
185
+ puts "\t" + "#{ink.upcase}: #{limit}"
186
+ end
187
+ puts "Gray settings:"
188
+ puts "\t" + "Highlight: #{@gray_highlight}"
189
+ puts "\t" + "Shadow: #{@gray_shadow}"
190
+ puts "\t" + "Overlap: #{@gray_overlap}"
191
+ puts "Gray gamma: #{@gray_gamma}"
192
+ puts "Ink partitions:"
193
+ @ink_partitions.each do |ink, value|
194
+ puts "\t" + "#{ink.upcase}: #{value}"
195
+ end
196
+ puts "Linearization: #{@linearization.join(', ')}" if @linearization
197
+ end
198
+
199
+ def to_html
200
+ html = Builder::XmlMarkup.new(indent: 2)
201
+ html.html do
202
+ html.head do
203
+ end
204
+ html.body do
205
+ html.h1("Profile: #{name}")
206
+ end
207
+ end
208
+ html.target!
209
+ end
210
+
211
+ def check
212
+ #FIXME: check values
213
+ end
214
+
215
+ end
216
+
217
+ end