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,59 @@
1
+ module Quadtone
2
+
3
+ class QuadFile
4
+
5
+ attr_accessor :curve_set
6
+
7
+ ChannelAliases = {
8
+ 'C' => 'c',
9
+ 'M' => 'm',
10
+ 'Y' => 'y',
11
+ 'K' => 'k',
12
+ 'c' => 'lc',
13
+ 'm' => 'lm',
14
+ 'k' => 'lk',
15
+ }
16
+
17
+ def initialize(profile)
18
+ @profile = profile
19
+ @curve_set = CurveSet.new(channels: [], profile: @profile, type: :separation)
20
+ load(@profile.quad_file_path)
21
+ end
22
+
23
+ # Read QTR quad (curve) file
24
+
25
+ def load(quad_file)
26
+ ;;warn "reading #{quad_file}"
27
+ lines = Pathname.new(quad_file).open.readlines.map { |line| line.chomp.force_encoding('ISO-8859-1') }
28
+ # process header
29
+ channels = parse_channel_list(lines.shift)
30
+ channels.each do |channel|
31
+ samples = (0..255).to_a.map do |input|
32
+ lines.shift while lines.first =~ /^#/
33
+ line = lines.shift
34
+ line =~ /^(\d+)$/ or raise "Unexpected value: #{line.inspect}"
35
+ output = $1.to_i
36
+ Sample.new(input: Color::Gray.new(k: 100 * (input / 255.0)), output: Color::Gray.new(k: 100 * (output / 65535.0)))
37
+ end
38
+ if @profile.inks.include?(channel)
39
+ @curve_set.curves << Curve.new(channel: channel, samples: samples)
40
+ end
41
+ end
42
+ end
43
+
44
+ def parse_channel_list(line)
45
+ # "## QuadToneRIP K,C,M,Y,LC,LM"
46
+ # "## QuadToneRIP KCMY"
47
+ line =~ /^##\s+QuadToneRIP\s+(.*)$/ or raise "Unexpected header line: #{line.inspect}"
48
+ channel_list = $1
49
+ case channel_list
50
+ when /,/
51
+ channel_list.split(',')
52
+ else
53
+ channel_list.chars.map { |c| ChannelAliases[c] }
54
+ end.map { |c| c.downcase.to_sym }
55
+ end
56
+
57
+ end
58
+
59
+ end
@@ -0,0 +1,139 @@
1
+ module Quadtone
2
+
3
+ class Renderer
4
+
5
+ attr_accessor :gamma
6
+ attr_accessor :rotate
7
+ attr_accessor :compress
8
+ attr_accessor :page_size
9
+ attr_accessor :resolution
10
+ attr_accessor :desired_size
11
+
12
+ def initialize(params={})
13
+ @compress = true
14
+ @resolution = 720
15
+ params.each { |k, v| send("#{k}=", v) }
16
+ end
17
+
18
+ def render(input_path)
19
+ @input_path = input_path
20
+
21
+ raise "Page size required" unless @page_size
22
+
23
+ # Scale measurements to specified resolution
24
+
25
+ @page_size.width = (@page_size.width / 72.0 * @resolution).to_i
26
+ @page_size.height = (@page_size.height / 72.0 * @resolution).to_i
27
+ @page_size.imageable_width = (@page_size.imageable_width / 72.0 * @resolution).to_i
28
+ @page_size.imageable_height = (@page_size.imageable_height / 72.0 * @resolution).to_i
29
+ @page_size.margin.left = (@page_size.margin.left / 72.0 * @resolution).to_i
30
+ @page_size.margin.right = (@page_size.margin.right / 72.0 * @resolution).to_i
31
+ @page_size.margin.top = (@page_size.margin.top / 72.0 * @resolution).to_i
32
+ @page_size.margin.bottom = (@page_size.margin.bottom / 72.0 * @resolution).to_i
33
+
34
+ if @desired_size
35
+ @desired_size.width = (@desired_size.width * @resolution).to_i
36
+ @desired_size.height = (@desired_size.height * @resolution).to_i
37
+ if @desired_size.width > @page_size.imageable_width || @desired_size.height > @page_size.imageable_height
38
+ raise "Image too large for page size (#{@page_size.name})"
39
+ end
40
+ else
41
+ @desired_size = HashStruct.new(width: @page_size.imageable_width, height: @page_size.imageable_height)
42
+ end
43
+
44
+ ;;warn "Reading #{@input_path} @ #{@resolution}dpi"
45
+ r = @resolution # have to alias to avoid referring to ImageList object
46
+ image_list = Magick::ImageList.new(@input_path) {
47
+ self.density = r
48
+ }
49
+ output_paths = []
50
+ image_list.each_with_index do |image, image_index|
51
+ @current_image = image
52
+ @current_image_index = image_index
53
+ ;;warn "\t" + "Processing sub-image \##{@current_image_index}"
54
+ show_info
55
+ delete_profiles
56
+ convert_to_16bit
57
+ apply_gamma
58
+ rotate
59
+ resize
60
+ extend_to_page
61
+ crop_to_imageable_area
62
+ show_info
63
+ output_paths << write_to_file
64
+ end
65
+ output_paths
66
+ end
67
+
68
+ def output_path
69
+ params = []
70
+ params << "#{@desired_size.width}x#{@desired_size.height}"
71
+ params << @page_size.name
72
+ params << "@#{@resolution}"
73
+ params << "g#{@gamma}" if @gamma
74
+ @input_path.with_extname(".out-#{params.join('-')}.#{@current_image_index}.png")
75
+ end
76
+
77
+ def show_info
78
+ ;;warn "\t\t" + @current_image.inspect
79
+ end
80
+
81
+ def delete_profiles
82
+ ;;warn "\t\t" + "Deleting profiles"
83
+ @current_image.delete_profile('*')
84
+ end
85
+
86
+ def convert_to_16bit
87
+ ;;warn "\t\t" + "Changing to grayscale"
88
+ @current_image = @current_image.quantize(2 ** 16, Magick::GRAYColorspace)
89
+ end
90
+
91
+ def apply_gamma
92
+ if @gamma
93
+ ;;warn "\t\t" + "Applying gamma #{@gamma}"
94
+ @current_image = @current_image.gamma_correct(@gamma)
95
+ end
96
+ end
97
+
98
+ def rotate
99
+ if @rotate
100
+ ;;warn "\t\t" + "Rotating #{@rotate}°"
101
+ @current_image.rotate!(@rotate)
102
+ elsif (@current_image.columns.to_f / @current_image.rows) > (@page_size.width.to_f / @page_size.height)
103
+ ;;warn "\t\t" + "Auto-rotating 90°"
104
+ @current_image.rotate!(90)
105
+ end
106
+ end
107
+
108
+ def resize
109
+ ;;warn "\t\t" + "Resizing to desired size"
110
+ @current_image.resize_to_fit!(@desired_size.width, @desired_size.height)
111
+ end
112
+
113
+ def extend_to_page
114
+ ;;warn "\t\t" + "Extending canvas to page area"
115
+ @current_image = @current_image.extent(
116
+ @page_size.width,
117
+ @page_size.height,
118
+ -(@page_size.width - @current_image.columns) / 2,
119
+ -(@page_size.height - @current_image.rows) / 2)
120
+ end
121
+
122
+ def crop_to_imageable_area
123
+ x, y, w, h = @page_size.margin.left, @page_size.height - @page_size.margin.top, @page_size.imageable_width, @page_size.imageable_height
124
+ ;;warn "\t\t" + "Cropping to imageable area (x,y = #{x},#{y}, w,h = #{w},#{h})"
125
+ @current_image.crop!(x, y, w, h)
126
+ end
127
+
128
+ def write_to_file
129
+ path = output_path
130
+ ;;warn "\t\t" + "Writing image to #{path}"
131
+ @current_image.write(path) do
132
+ self.compression = @compress ? Magick::ZipCompression : Magick::NoCompression
133
+ end
134
+ path
135
+ end
136
+
137
+ end
138
+
139
+ end
@@ -0,0 +1,10 @@
1
+ module Quadtone
2
+
3
+ def self.run(*args)
4
+ args = args.flatten.compact.map { |a| a.to_s }
5
+ warn "\t* #{args.join(' ')}"
6
+ system(*args)
7
+ raise "Error: #{$?}" unless $? == 0
8
+ end
9
+
10
+ end
@@ -0,0 +1,32 @@
1
+ module Quadtone
2
+
3
+ class Sample
4
+
5
+ attr_accessor :input
6
+ attr_accessor :output
7
+ attr_accessor :error
8
+ attr_accessor :label
9
+
10
+ def initialize(params={})
11
+ params.each { |key, value| send("#{key}=", value) }
12
+ end
13
+
14
+ def input_value
15
+ @input.value
16
+ end
17
+
18
+ def output_value
19
+ @output.value
20
+ end
21
+
22
+ def to_s
23
+ "%s / %s%s" % [
24
+ input,
25
+ output,
26
+ label ? " [#{label}]" : '',
27
+ ]
28
+ end
29
+
30
+ end
31
+
32
+ end
@@ -0,0 +1,36 @@
1
+ module Quadtone
2
+
3
+ class Separator
4
+
5
+ attr_accessor :luts
6
+
7
+ def initialize(curve_set)
8
+ @luts = {}
9
+ curve_set.curves.each do |curve|
10
+ color_map = Magick::Image.new(curve.num_samples, 1) do
11
+ self.colorspace = Magick::GRAYColorspace
12
+ end
13
+ color_map.pixel_interpolation_method = Magick::IntegerInterpolatePixel
14
+ color_map.view(0, 0, curve.num_samples, 1) do |view|
15
+ curve.samples.each_with_index do |sample, i|
16
+ value = ((1 - sample.output.value) * 65535).to_i
17
+ view[0][curve.num_samples - 1 - i] = Magick::Pixel.new(value, value, value)
18
+ end
19
+ end
20
+ @luts[curve.channel] = color_map
21
+ end
22
+ end
23
+
24
+ def separate(image)
25
+ images = {}
26
+ @luts.each do |channel, lut|
27
+ image2 = image.copy.clut_channel(lut)
28
+ image2['Label'] = channel.to_s.upcase
29
+ images[channel] = image2
30
+ end
31
+ images
32
+ end
33
+
34
+ end
35
+
36
+ end
@@ -0,0 +1,277 @@
1
+ module Quadtone
2
+
3
+ class Target
4
+
5
+ attr_accessor :base_dir
6
+ attr_accessor :channels
7
+ attr_accessor :type
8
+ attr_accessor :name
9
+ attr_accessor :ink_limits
10
+ attr_accessor :samples
11
+
12
+ def initialize(params={})
13
+ params.each { |k, v| send("#{k}=", v) }
14
+ raise "Base directory must be specified" unless @base_dir
15
+ raise "Channels must be specified" unless @channels
16
+ raise "Type must be specified" unless @type
17
+ raise "Name must be specified" unless @name
18
+ end
19
+
20
+ def build
21
+ ;;warn "Making target for channels #{@channels.inspect}"
22
+ cleanup_files(:all)
23
+
24
+ resolution = 360
25
+ page_size = HashStruct.new(width: 8, height: 10.5)
26
+ # total_patches = 42
27
+ ;;total_patches = 14
28
+ patches_per_row = 14
29
+ total_rows = total_patches / patches_per_row
30
+ row_height = 165
31
+ target_size = [
32
+ ((total_rows + 1) * row_height).to_f / resolution,
33
+ page_size.height,
34
+ ].map { |n| (n * 25.4).to_i }.join('x')
35
+
36
+ image_list = Magick::ImageList.new
37
+ @channels.each do |channel|
38
+ sub_path = base_file(channel)
39
+ sub_image_path = image_file(channel)
40
+ ;;warn "Making target #{sub_path.inspect} at #{sub_image_path}"
41
+ Quadtone.run('targen',
42
+ # '-v', # Verbose mode [optional level 1..N]
43
+ '-d', 0, # generate grayscale target
44
+ '-e', 0, # White test patches (default 4)
45
+ '-B', 0, # Black test patches (default 4 Grey/RGB, else 0)
46
+ '-s', total_patches, # Single channel steps (default grey 50, color 0)
47
+ sub_path)
48
+ Quadtone.run('printtarg',
49
+ # '-v', # Verbose mode [optional level 1..N]
50
+ '-a', 1.45, # Scale patch size and spacers by factor (e.g. 0.857 or 1.5 etc.)
51
+ '-r', # Don't randomize patch location
52
+ '-i', 'i1', # set instrument to EyeOne (FIXME: make configurable)
53
+ '-t', resolution, # generate 16-bit TIFF @ 360 ppi
54
+ '-m', 0, # Set a page margin in mm (default 6.0 mm)
55
+ '-L', # Suppress any left paper clip border
56
+ '-p', target_size, # Select page size
57
+ sub_path)
58
+ image = Magick::Image.read(sub_image_path).first
59
+ # image.background_color = 'transparent'
60
+ # image = image.transparent('white')
61
+ case @type
62
+ when :characterization
63
+ if @ink_limits && (limit = @ink_limits[channel]) && limit != 1
64
+ ;;warn "\t" + "#{channel.to_s.upcase}: Applying limit of #{limit}"
65
+ levels = [1.0 - limit, 1.0].map { |n| n * Magick::QuantumRange }
66
+ image = image.levelize_channel(*levels)
67
+ end
68
+ # calculate a black RGB pixel for this channel in QTR calibration mode
69
+ black_qtr = Color::QTR.new(channel: channel, value: 0)
70
+ black_rgb = black_qtr.to_rgb
71
+ ;;warn "\t" + "#{channel.to_s.upcase}: Colorizing to #{black_rgb}"
72
+ image = image.colorize(1, 0, 1, black_rgb.to_pixel)
73
+ end
74
+ image_list << image
75
+ end
76
+
77
+ if @type == :linearization || @type == :test
78
+
79
+ width = (page_size.width * resolution).to_i - image_list.first.columns
80
+ height = page_size.height * resolution
81
+
82
+ ;;width = (page_size.width / 2) * resolution
83
+
84
+ linear_scale_height = 1 * resolution
85
+ radial_scale_height = (height - linear_scale_height) / 2
86
+ sample_image_height = (height - linear_scale_height) / 2
87
+
88
+ test_images = Magick::ImageList.new
89
+ test_images << linear_gradation_scale_image([width, linear_scale_height])
90
+ test_images << radial_gradation_scale_image([width, radial_scale_height])
91
+ test_images << sample_image([width, sample_image_height])
92
+ image_list << test_images.append(true)
93
+ end
94
+
95
+ # ;;warn "montaging images"
96
+ # image_list = image_list.montage do
97
+ # self.geometry = Magick::Geometry.new(page_size.width * resolution, page_size.height * resolution)
98
+ # self.tile = Magick::Geometry.new(image_list.length, 1)
99
+ # end
100
+
101
+ # ;;warn "writing target image"
102
+ # image_list.write(image_file) do
103
+ # self.depth = (@type == :characterization) ? 8 : 16
104
+ # self.compression = Magick::ZipCompression
105
+ # end
106
+
107
+ ;;warn "writing target image"
108
+ image_list.append(false).write(image_file) do
109
+ self.depth = (@type == :characterization) ? 8 : 16
110
+ self.compression = Magick::ZipCompression
111
+ end
112
+
113
+ end
114
+
115
+ def linear_gradation_scale_image(size)
116
+ ;;warn "\t" + "generating linear gradation scale of size #{size.inspect}"
117
+ bounds = Magick::Rectangle.new(*size, 0, 0)
118
+ image1 = Magick::Image.new(bounds.width, bounds.height/2, Magick::GradientFill.new(0, 0, 0, bounds.height/2, 'white', 'black'))
119
+ image2 = image1.posterize(21)
120
+ image3 = image1.posterize(256)
121
+ ilist = Magick::ImageList.new
122
+ ilist << image1
123
+ ilist << image2
124
+ ilist << image3
125
+ ilist.append(true)
126
+ end
127
+
128
+ def radial_gradation_scale_image(size)
129
+ ;;warn "\t" + "generating radial gradation scale of size #{size.inspect}"
130
+ bounds = Magick::Rectangle.new(*size, 0, 0)
131
+ image1 = Magick::Image.new(bounds.width, bounds.height/2, Magick::GradientFill.new(bounds.width/2, bounds.height/2, bounds.width/2, bounds.height/2, 'black', 'white'))
132
+ image2 = image1.posterize(21).flip
133
+ ilist = Magick::ImageList.new
134
+ ilist << image1
135
+ ilist << image2
136
+ ilist.append(true)
137
+ end
138
+
139
+ def sample_image(size)
140
+ ;;warn "\t" + "generating sample image of size #{size.inspect}"
141
+ bounds = Magick::Rectangle.new(*size, 0, 0)
142
+ ilist = Magick::ImageList.new(Pathname.new(ENV['HOME']) + 'Desktop' + '121213b.01.tif')
143
+ ilist.first.resize_to_fill(*size)
144
+ end
145
+
146
+ def measure(options={})
147
+ options = HashStruct.new(options)
148
+ channels_to_measure = options.channels || @channels
149
+ channels_to_measure.each_with_index do |channel, i|
150
+ measure_channel(channel, options.merge(disable_calibration: i > 0))
151
+ end
152
+ end
153
+
154
+ def measure_channel(channel, options=HashStruct.new)
155
+ options = HashStruct.new(options)
156
+ if options.remeasure
157
+ pass = options.remeasure
158
+ else
159
+ pass = ti2_files(channel).length
160
+ end
161
+ base = base_file(channel, pass)
162
+ FileUtils.cp(ti2_file(channel), base.with_extname('.ti2')) unless options.remeasure
163
+ ;;warn "Measuring target #{base.inspect}"
164
+ Quadtone.run('chartread',
165
+ # '-v', # Verbose mode [optional level 1..N]
166
+ '-p', # Measure patch by patch rather than strip
167
+ '-n', # Don't save spectral information (default saves spectral)
168
+ '-l', # Save CIE as D50 L*a*b* rather than XYZ
169
+ options.disable_calibration ? '-N' : nil, # Disable initial calibration of instrument if possible
170
+ options.remeasure ? '-r' : nil, # Resume reading partly read chart
171
+ base)
172
+ end
173
+
174
+ def read
175
+ ;;warn "reading samples for #{@channels.join(', ')} from CGATS files"
176
+ @samples = {}
177
+ @channels.each do |channel|
178
+ samples = {}
179
+ ti3_files(channel).map do |file|
180
+ ;;warn "reading #{file}"
181
+ cgats = CGATS.new_from_file(file)
182
+ cgats.sections.first.data.each do |set|
183
+ id = set['SAMPLE_LOC'].gsub(/"/, '')
184
+ samples[id] ||= []
185
+ samples[id] << Sample.new(input: Color::Gray.from_cgats(set), output: Color::Lab.from_cgats(set))
186
+ end
187
+ end
188
+ @samples[channel] = samples.map do |id, samples|
189
+ cc = ClusterCalculator.new(samples: samples, max_clusters: samples.length > 2 ? 2 : 1)
190
+ cc.cluster!
191
+ clusters = cc.clusters.sort_by(&:size).reverse
192
+ # ;;warn "Clusters:"
193
+ # clusters.each do |cluster|
194
+ # warn "\t" + cluster.center.to_s
195
+ # cluster.samples.each do |sample|
196
+ # warn "\t\t" + "#{sample.to_s}"
197
+ # end
198
+ # end
199
+ # ;;
200
+ cluster = clusters.shift
201
+ raise "Too much spread" if cluster.samples.length < 2 && samples.length > 2
202
+ unless clusters.empty?
203
+ warn "Dropped #{clusters.length} out of range sample(s) at patch #{channel}-#{id}"
204
+ end
205
+ output = cluster.center
206
+ Sample.new(input: samples.first.input, output: output, label: "#{channel}-#{id}")
207
+ end
208
+ end
209
+ end
210
+
211
+ # private
212
+
213
+ def base_file(channel=nil, n=nil)
214
+ @base_dir + (@name.to_s + (channel ? "-#{channel}" : '') + (n ? "-#{n}" : ''))
215
+ end
216
+
217
+ def ti1_file(channel)
218
+ base_file(channel).with_extname('.ti1')
219
+ end
220
+
221
+ def ti2_file(channel, n=nil)
222
+ base_file(channel, n).with_extname('.ti2')
223
+ end
224
+
225
+ def ti3_file(channel, n=nil)
226
+ base_file(channel, n).with_extname('.ti3')
227
+ end
228
+
229
+ def image_file(channel=nil)
230
+ base_file(channel).with_extname('.tif')
231
+ end
232
+
233
+ def values_file(channel=nil)
234
+ base_file(channel).with_extname('.txt')
235
+ end
236
+
237
+ def ti_files
238
+ Pathname.glob(base_file.with_extname('*.ti[123]'))
239
+ end
240
+
241
+ def ti2_files(channel)
242
+ Pathname.glob(base_file(channel).with_extname('*.ti2'))
243
+ end
244
+
245
+ def ti3_files(channel)
246
+ Pathname.glob(base_file(channel).with_extname('*.ti3'))
247
+ end
248
+
249
+ def image_files
250
+ Pathname.glob(base_file.with_extname('*.tif'))
251
+ end
252
+
253
+ def cleanup_files(files)
254
+ ;;warn "deleting files: #{files.inspect}"
255
+ files = [files].flatten
256
+ until files.empty?
257
+ file = files.shift
258
+ case file
259
+ when :all
260
+ files << :ti
261
+ files += image_files
262
+ when :ti
263
+ files += ti_files
264
+ when Pathname
265
+ if file.exist?
266
+ # ;;warn "\t" + file
267
+ file.unlink
268
+ end
269
+ else
270
+ raise "Unknown file to cleanup: #{file}"
271
+ end
272
+ end
273
+ end
274
+
275
+ end
276
+
277
+ end