qttk 0.1.0
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.
- checksums.yaml +7 -0
- data/.gitignore +2 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +36 -0
- data/README.markdown +131 -0
- data/Rakefile +17 -0
- data/TODO.markdown +236 -0
- data/bin/qt +7 -0
- data/etc/gutenprint/gp-tool.rb +56 -0
- data/etc/gutenprint/gutenprint-filter.c +400 -0
- data/etc/gutenprint/gutenprint.rb +86 -0
- data/etc/gutenprint/stp-test +326 -0
- data/etc/images/3551599565_db282cf840_o.jpg +0 -0
- data/etc/images/4843122063_d582c569e9_o.jpg +0 -0
- data/etc/images/4843128953_83c1770907_o.jpg +0 -0
- data/lib/quadtone.rb +56 -0
- data/lib/quadtone/cgats.rb +137 -0
- data/lib/quadtone/cluster_calculator.rb +81 -0
- data/lib/quadtone/color.rb +83 -0
- data/lib/quadtone/color/cmyk.rb +112 -0
- data/lib/quadtone/color/device_n.rb +23 -0
- data/lib/quadtone/color/gray.rb +46 -0
- data/lib/quadtone/color/lab.rb +150 -0
- data/lib/quadtone/color/qtr.rb +71 -0
- data/lib/quadtone/color/rgb.rb +71 -0
- data/lib/quadtone/color/xyz.rb +80 -0
- data/lib/quadtone/curve.rb +138 -0
- data/lib/quadtone/curve_set.rb +196 -0
- data/lib/quadtone/descendants.rb +9 -0
- data/lib/quadtone/environment.rb +5 -0
- data/lib/quadtone/extensions/math.rb +11 -0
- data/lib/quadtone/extensions/pathname3.rb +11 -0
- data/lib/quadtone/printer.rb +106 -0
- data/lib/quadtone/profile.rb +217 -0
- data/lib/quadtone/quad_file.rb +59 -0
- data/lib/quadtone/renderer.rb +139 -0
- data/lib/quadtone/run.rb +10 -0
- data/lib/quadtone/sample.rb +32 -0
- data/lib/quadtone/separator.rb +36 -0
- data/lib/quadtone/target.rb +277 -0
- data/lib/quadtone/tool.rb +61 -0
- data/lib/quadtone/tools/add_printer.rb +73 -0
- data/lib/quadtone/tools/characterize.rb +43 -0
- data/lib/quadtone/tools/chart.rb +31 -0
- data/lib/quadtone/tools/check.rb +16 -0
- data/lib/quadtone/tools/dir.rb +15 -0
- data/lib/quadtone/tools/edit.rb +23 -0
- data/lib/quadtone/tools/init.rb +82 -0
- data/lib/quadtone/tools/install.rb +15 -0
- data/lib/quadtone/tools/linearize.rb +28 -0
- data/lib/quadtone/tools/list.rb +19 -0
- data/lib/quadtone/tools/print.rb +38 -0
- data/lib/quadtone/tools/printer_options.rb +40 -0
- data/lib/quadtone/tools/rename.rb +17 -0
- data/lib/quadtone/tools/render.rb +43 -0
- data/lib/quadtone/tools/rewrite.rb +15 -0
- data/lib/quadtone/tools/separate.rb +71 -0
- data/lib/quadtone/tools/show.rb +15 -0
- data/lib/quadtone/tools/test.rb +26 -0
- data/qttk.gemspec +34 -0
- 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
         | 
    
        data/lib/quadtone/run.rb
    ADDED
    
    
| @@ -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
         |