CooCoo 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (105) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +16 -0
  3. data/CooCoo.gemspec +47 -0
  4. data/Gemfile +4 -0
  5. data/Gemfile.lock +88 -0
  6. data/README.md +123 -0
  7. data/Rakefile +81 -0
  8. data/bin/cuda-dev-info +25 -0
  9. data/bin/cuda-free +28 -0
  10. data/bin/cuda-free-trend +7 -0
  11. data/bin/ffi-gen +267 -0
  12. data/bin/spec_runner_html.sh +42 -0
  13. data/bin/trainer +198 -0
  14. data/bin/trend-cost +13 -0
  15. data/examples/char-rnn.rb +405 -0
  16. data/examples/cifar/cifar.rb +94 -0
  17. data/examples/img-similarity.rb +201 -0
  18. data/examples/math_ops.rb +57 -0
  19. data/examples/mnist.rb +365 -0
  20. data/examples/mnist_classifier.rb +293 -0
  21. data/examples/mnist_dream.rb +214 -0
  22. data/examples/seeds.rb +268 -0
  23. data/examples/seeds_dataset.txt +210 -0
  24. data/examples/t10k-images-idx3-ubyte +0 -0
  25. data/examples/t10k-labels-idx1-ubyte +0 -0
  26. data/examples/train-images-idx3-ubyte +0 -0
  27. data/examples/train-labels-idx1-ubyte +0 -0
  28. data/ext/buffer/Rakefile +50 -0
  29. data/ext/buffer/buffer.pre.cu +727 -0
  30. data/ext/buffer/matrix.pre.cu +49 -0
  31. data/lib/CooCoo.rb +1 -0
  32. data/lib/coo-coo.rb +18 -0
  33. data/lib/coo-coo/activation_functions.rb +344 -0
  34. data/lib/coo-coo/consts.rb +5 -0
  35. data/lib/coo-coo/convolution.rb +298 -0
  36. data/lib/coo-coo/core_ext.rb +75 -0
  37. data/lib/coo-coo/cost_functions.rb +91 -0
  38. data/lib/coo-coo/cuda.rb +116 -0
  39. data/lib/coo-coo/cuda/device_buffer.rb +240 -0
  40. data/lib/coo-coo/cuda/device_buffer/ffi.rb +109 -0
  41. data/lib/coo-coo/cuda/error.rb +51 -0
  42. data/lib/coo-coo/cuda/host_buffer.rb +117 -0
  43. data/lib/coo-coo/cuda/runtime.rb +157 -0
  44. data/lib/coo-coo/cuda/vector.rb +315 -0
  45. data/lib/coo-coo/data_sources.rb +2 -0
  46. data/lib/coo-coo/data_sources/xournal.rb +25 -0
  47. data/lib/coo-coo/data_sources/xournal/bitmap_stream.rb +197 -0
  48. data/lib/coo-coo/data_sources/xournal/document.rb +377 -0
  49. data/lib/coo-coo/data_sources/xournal/loader.rb +144 -0
  50. data/lib/coo-coo/data_sources/xournal/renderer.rb +101 -0
  51. data/lib/coo-coo/data_sources/xournal/saver.rb +99 -0
  52. data/lib/coo-coo/data_sources/xournal/training_document.rb +78 -0
  53. data/lib/coo-coo/data_sources/xournal/training_document/constants.rb +15 -0
  54. data/lib/coo-coo/data_sources/xournal/training_document/document_maker.rb +89 -0
  55. data/lib/coo-coo/data_sources/xournal/training_document/document_reader.rb +105 -0
  56. data/lib/coo-coo/data_sources/xournal/training_document/example.rb +37 -0
  57. data/lib/coo-coo/data_sources/xournal/training_document/sets.rb +76 -0
  58. data/lib/coo-coo/debug.rb +8 -0
  59. data/lib/coo-coo/dot.rb +129 -0
  60. data/lib/coo-coo/drawing.rb +4 -0
  61. data/lib/coo-coo/drawing/cairo_canvas.rb +100 -0
  62. data/lib/coo-coo/drawing/canvas.rb +68 -0
  63. data/lib/coo-coo/drawing/chunky_canvas.rb +101 -0
  64. data/lib/coo-coo/drawing/sixel.rb +214 -0
  65. data/lib/coo-coo/enum.rb +17 -0
  66. data/lib/coo-coo/from_name.rb +58 -0
  67. data/lib/coo-coo/fully_connected_layer.rb +205 -0
  68. data/lib/coo-coo/generation_script.rb +38 -0
  69. data/lib/coo-coo/grapher.rb +140 -0
  70. data/lib/coo-coo/image.rb +286 -0
  71. data/lib/coo-coo/layer.rb +67 -0
  72. data/lib/coo-coo/layer_factory.rb +26 -0
  73. data/lib/coo-coo/linear_layer.rb +59 -0
  74. data/lib/coo-coo/math.rb +607 -0
  75. data/lib/coo-coo/math/abstract_vector.rb +121 -0
  76. data/lib/coo-coo/math/functions.rb +39 -0
  77. data/lib/coo-coo/math/interpolation.rb +7 -0
  78. data/lib/coo-coo/network.rb +264 -0
  79. data/lib/coo-coo/neuron.rb +112 -0
  80. data/lib/coo-coo/neuron_layer.rb +168 -0
  81. data/lib/coo-coo/option_parser.rb +18 -0
  82. data/lib/coo-coo/platform.rb +17 -0
  83. data/lib/coo-coo/progress_bar.rb +11 -0
  84. data/lib/coo-coo/recurrence/backend.rb +99 -0
  85. data/lib/coo-coo/recurrence/frontend.rb +101 -0
  86. data/lib/coo-coo/sequence.rb +187 -0
  87. data/lib/coo-coo/shell.rb +2 -0
  88. data/lib/coo-coo/temporal_network.rb +291 -0
  89. data/lib/coo-coo/trainer.rb +21 -0
  90. data/lib/coo-coo/trainer/base.rb +67 -0
  91. data/lib/coo-coo/trainer/batch.rb +82 -0
  92. data/lib/coo-coo/trainer/batch_stats.rb +27 -0
  93. data/lib/coo-coo/trainer/momentum_stochastic.rb +59 -0
  94. data/lib/coo-coo/trainer/stochastic.rb +47 -0
  95. data/lib/coo-coo/transformer.rb +272 -0
  96. data/lib/coo-coo/vector_layer.rb +194 -0
  97. data/lib/coo-coo/version.rb +3 -0
  98. data/lib/coo-coo/weight_deltas.rb +23 -0
  99. data/prototypes/convolution.rb +116 -0
  100. data/prototypes/linear_drop.rb +51 -0
  101. data/prototypes/recurrent_layers.rb +79 -0
  102. data/www/images/screamer.png +0 -0
  103. data/www/images/screamer.xcf +0 -0
  104. data/www/index.html +82 -0
  105. metadata +373 -0
@@ -0,0 +1,144 @@
1
+ require 'nokogiri'
2
+ require 'zlib'
3
+ require 'chunky_png'
4
+ require 'base64'
5
+ require 'coo-coo/data_sources/xournal/document'
6
+
7
+ module CooCoo
8
+ module DataSources
9
+ module Xournal
10
+ # Loads a {Document}.
11
+ class Loader
12
+ # General catch all class for load errors.
13
+ class Error < RuntimeError
14
+ end
15
+
16
+ # Error while parsing the Xournal.
17
+ class ParseError < Error
18
+ end
19
+
20
+ def initialize(doc)
21
+ @doc = doc || Document.new
22
+ end
23
+
24
+ # Loads a Xournal document from the file at +path+.
25
+ # @return [Document]
26
+ def self.from_file(path)
27
+ doc = nil
28
+
29
+ Zlib::GzipReader.open(path) do |io|
30
+ doc = from_xml(io)
31
+ end
32
+
33
+ doc
34
+ rescue Zlib::GzipFile::Error
35
+ from_regular_file(path)
36
+ end
37
+
38
+ # Loads a {Document} from XML in a String.
39
+ def self.from_xml(data)
40
+ xml = Nokogiri::XML(data)
41
+ root = xml.xpath('//xournal')[0]
42
+ raise ParseError.new("XML root is not 'xournal'") unless root
43
+ title_el = root.xpath("title")
44
+ title = title_el[0].text if title_el.size > 0
45
+
46
+ self.
47
+ new(Document.new(title, root['version'])).
48
+ from_xml(xml)
49
+ end
50
+
51
+ def from_xml(xml)
52
+ xml.xpath("//page").each do |page|
53
+ @doc.add_page(load_page(page))
54
+ end
55
+
56
+ @doc
57
+ end
58
+
59
+ protected
60
+
61
+ def self.from_regular_file(path)
62
+ File.open(path, 'rb') do |f|
63
+ from_xml(f)
64
+ end
65
+ end
66
+
67
+ def load_page(xml)
68
+ w = xml['width'].to_f
69
+ h = xml['height'].to_f
70
+ bg_xml = xml.xpath('background')
71
+ bg = load_background(bg_xml[0]) if bg_xml[0]
72
+ page = Page.new(w, h, bg)
73
+
74
+ xml.xpath('layer').each do |layer|
75
+ page.add_layer(load_layer(layer))
76
+ end
77
+
78
+ page
79
+ end
80
+
81
+ def load_background(xml)
82
+ case xml['type']
83
+ when 'pixmap' then PixmapBackground.new(xml['filename'], xml['domain'])
84
+ when 'pdf' then PDFBackground.new(xml['filename'], xml['pageno'], xml['domain'])
85
+ when 'solid' then Background.new(xml['color'], xml['style'])
86
+ else raise ParseError.new("Unknown background type #{xml['type']}: #{xml}")
87
+ end
88
+ end
89
+
90
+ def load_layer(xml)
91
+ layer = Layer.new
92
+
93
+ xml.children.select(&:element?).each do |elem|
94
+ case elem.name
95
+ when 'stroke' then layer.add_stroke(load_stroke(elem))
96
+ when 'text' then layer.add_text(load_text(elem))
97
+ when 'image' then layer.add_image(load_image(elem))
98
+ else raise ParseError.new("Unknown element: #{elem}")
99
+ end
100
+ end
101
+
102
+ layer
103
+ end
104
+
105
+ def load_image(xml)
106
+ Image.new(xml['left'],
107
+ xml['top'],
108
+ xml['right'],
109
+ xml['bottom'],
110
+ xml.text)
111
+ end
112
+
113
+ def load_text(xml)
114
+ Text.new(xml.text,
115
+ xml['x'].to_f,
116
+ xml['y'].to_f,
117
+ xml['size'].to_f,
118
+ xml['color'],
119
+ xml['font'])
120
+ end
121
+
122
+ def load_stroke(xml)
123
+ tool = xml['tool']
124
+ tool = Stroke::DefaultTool if tool == nil || tool.empty?
125
+ color = xml['color']
126
+ stroke = Stroke.new(tool, color)
127
+ widths = xml['width'].split.collect(&:to_f)
128
+
129
+ width = nil
130
+ xml.children.to_s.
131
+ split.
132
+ collect(&:to_f).
133
+ each_slice(2).
134
+ zip(widths) do |(x, y), w|
135
+ width ||= w if w
136
+ stroke.add_sample(x, y, width)
137
+ end
138
+
139
+ stroke
140
+ end
141
+ end
142
+ end
143
+ end
144
+ end
@@ -0,0 +1,101 @@
1
+ require 'chunky_png'
2
+ require 'cairo'
3
+ require 'coo-coo/drawing'
4
+
5
+ module CooCoo
6
+ module DataSources
7
+ module Xournal
8
+ class Renderer
9
+ def initialize()
10
+ end
11
+
12
+ def render(*args)
13
+ render_to_chunky(*args)
14
+ end
15
+
16
+ def render_to_canvas(canvas, document, page_num, x = 0, y = 0, w = nil, h = nil, zx = 1.0, zy = 1.0)
17
+ page = document.pages[page_num]
18
+ w ||= (page.width - x).ceil.to_i
19
+ h ||= (page.height - y).ceil.to_i
20
+ render_page(canvas, page, x, y, x + w, y + h, zx, zy)
21
+ canvas
22
+ end
23
+
24
+ def render_to_chunky(document, page_num, x = 0, y = 0, w = nil, h = nil, zx = 1.0, zy = 1.0)
25
+ page = document.pages[page_num]
26
+ w ||= (page.width - x).ceil.to_i
27
+ h ||= (page.height - y).ceil.to_i
28
+ img = ChunkyPNG::Image.new((w * zx).to_i, (h * zy).to_i, chunky_color(page.background.color || :white))
29
+ canvas = Drawing::ChunkyCanvas.new(img)
30
+ render_to_canvas(canvas, document, page_num, x, y, w, h, zx, zy)
31
+ img
32
+ end
33
+
34
+ def render_to_cairo(document, page_num, x = 0, y = 0, w = nil, h = nil, zx = 1.0, zy = 1.0)
35
+ page = document.pages[page_num]
36
+ w ||= (page.width - x).ceil.to_i
37
+ h ||= (page.height - y).ceil.to_i
38
+ surface = Cairo::ImageSurface.new((w * zx).to_i, (h * zy).to_i)
39
+ canvas = Drawing::CairoCanvas.new(surface)
40
+ render_to_canvas(canvas, document, page_num, x, y, w, h, zx, zy)
41
+ surface
42
+ end
43
+
44
+ def chunky_color(color)
45
+ color && ChunkyPNG::Color.parse(color)
46
+ end
47
+
48
+ def render_page(canvas, page, min_x, min_y, max_x, max_y, zx, zy)
49
+ render_background(canvas, page.background, min_x, min_y, max_x, max_y, zx, zy)
50
+ page.each_layer do |layer|
51
+ render_layer(canvas, layer, min_x, min_y, max_x, max_y, zx, zy)
52
+ end
53
+ end
54
+
55
+ def render_background(canvas, bg, min_x, min_y, max_x, max_y, zx, zy)
56
+ color = chunky_color(bg.color || :white)
57
+ canvas.stroke_color = canvas.fill_color = color
58
+ canvas.rect(0, 0, ((max_x - min_x) * zx).to_i, ((max_y - min_y) * zy).to_i)
59
+ end
60
+
61
+ def render_layer(canvas, layer, min_x, min_y, max_x, max_y, zx, zy)
62
+ layer.each do |child|
63
+ #next unless child.within?(min_x, min_y, max_x, max_y)
64
+ case child
65
+ when Image then render_image(canvas, child, min_x, min_y, zx, zy)
66
+ when Stroke then render_stroke(canvas, child, min_x, min_y, max_x, max_y, zx, zy)
67
+ when Text then render_text(canvas, child, min_x, min_y, zx, zy)
68
+ end
69
+ end
70
+ end
71
+
72
+ def render_image(canvas, src, min_x, min_y, zx, zy)
73
+ canvas.blit(src.raw_data, ((src.left - min_x) * zx), ((src.top - min_y) * zy), src.width * zx, src.height * zy)
74
+ end
75
+
76
+ def render_stroke(canvas, stroke, min_x, min_y, max_x, max_y, zx, zy)
77
+ points = stroke.each_sample.inject([]) do |acc, sample|
78
+ #next unless sample.within?(min_x, min_y, max_x, max_y)
79
+ acc << [ (sample.x - min_x) * zx,
80
+ (sample.y - min_y) * zy,
81
+ sample.width * zx
82
+ ]
83
+ end
84
+
85
+ canvas.stroke_color = chunky_color(stroke.color)
86
+ canvas.fill_color = chunky_color(stroke.color)
87
+ canvas.stroke(points)
88
+ end
89
+
90
+ def render_text(canvas, text, min_x, min_y, zx, zy)
91
+ canvas.fill_color = chunky_color(text.color)
92
+ canvas.text(text.text,
93
+ (text.x - min_x) * zx,
94
+ (text.y - min_y) * zy,
95
+ text.font,
96
+ text.size * zy)
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,99 @@
1
+ module CooCoo
2
+ module DataSources
3
+ module Xournal
4
+ # Saves a {Document}
5
+ # @todo Keep linked images with the Xournal when it is saved.
6
+ class Saver
7
+ # Saves +doc+ to +io_or_path+ using Xournal's compressed XML format.
8
+ # @param io_or_path [String, IO] File name or an IO
9
+ def self.save(doc, io_or_path)
10
+ new.save(doc, io_or_path)
11
+ end
12
+
13
+ # Saves +doc+ to an XML string.
14
+ def self.to_xml(doc)
15
+ new.to_xml(doc)
16
+ end
17
+
18
+ def initialize
19
+ end
20
+
21
+ def save(doc, io_or_path)
22
+ if io_or_path.respond_to?(:write)
23
+ save_to_io(doc, io_or_path)
24
+ elsif io_or_path.kind_of?(String)
25
+ save_to_file(doc, io_or_path)
26
+ else
27
+ raise ArgumentError.new("Only paths as String and IO are supported outputs. Not #{io_or_path.class}")
28
+ end
29
+ end
30
+
31
+ def to_xml(doc)
32
+ Nokogiri::XML::Builder.new(encoding: 'UTF-8') do |xml|
33
+ xml.xournal(version: doc.version) do
34
+ xml.title(doc.title)
35
+ doc.pages.each do |p|
36
+ page_to_xml(p, xml)
37
+ end
38
+ end
39
+ end.to_xml
40
+ end
41
+
42
+ protected
43
+
44
+ def save_to_file(doc, path)
45
+ Zlib::GzipWriter.open(path) do |f|
46
+ save_to_io(doc, f)
47
+ end
48
+ end
49
+
50
+ def save_to_io(doc, io)
51
+ io.write(to_xml(doc))
52
+ end
53
+
54
+ def page_to_xml(p, xml)
55
+ xml.page(width: p.width, height: p.height) do
56
+ background_to_xml(p.background || Background::Default, xml)
57
+ p.layers.each do |l|
58
+ layer_to_xml(l, xml)
59
+ end
60
+ end
61
+ end
62
+
63
+ def background_to_xml(bg, xml)
64
+ case bg
65
+ when PixmapBackground then xml.background(type: 'pixmap', domain: bg.domain, filename: bg.filename)
66
+ when PDFBackground then xml.background(type: 'pdf', domain: bg.domain, filename: bg.filename, pageno: bg.page_number)
67
+ else xml.background(type: 'solid', color: bg.color, style: bg.style)
68
+ end
69
+ end
70
+
71
+ def layer_to_xml(layer, xml)
72
+ xml.layer do
73
+ layer.each do |child|
74
+ case child
75
+ when Image then image_to_xml(child, xml)
76
+ when Stroke then stroke_to_xml(child, xml)
77
+ when Text then text_to_xml(child, xml)
78
+ else raise ParseError.new("Unknown layer child: #{child.class} #{child.inspect}")
79
+ end
80
+ end
81
+ end
82
+ end
83
+
84
+ def image_to_xml(img, xml)
85
+ xml.image(img.data_string, left: img.left, top: img.top, right: img.right, bottom: img.bottom)
86
+ end
87
+
88
+ def stroke_to_xml(stroke, xml)
89
+ xml.stroke(stroke.samples.collect { |s| [ s.x, s.y ] }.flatten.join(' '),
90
+ tool: stroke.tool, color: stroke.color, width: stroke.samples.collect(&:width).join(' '))
91
+ end
92
+
93
+ def text_to_xml(text, xml)
94
+ xml.text_(text.text, x: text.x, y: text.y, size: text.size, color: text.color, font: text.font)
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,78 @@
1
+ require 'coo-coo/data_sources/xournal/training_document/constants'
2
+ require 'coo-coo/data_sources/xournal/training_document/example'
3
+ require 'coo-coo/data_sources/xournal/training_document/document_maker'
4
+ require 'coo-coo/data_sources/xournal/training_document/document_reader'
5
+ require 'coo-coo/data_sources/xournal/training_document/sets'
6
+
7
+ module CooCoo
8
+ module DataSources
9
+ module Xournal
10
+ # The {TrainingDocument} is the source of strokes for the trainer of
11
+ # the Xournal recognizer. Each {TrainingDocument} has a set of labels
12
+ # and associated strokes. Examples are loaded and stored to Xournal
13
+ # documents formatted into a grid with a label and strokes in each cell.
14
+ class TrainingDocument
15
+ attr_reader :examples
16
+
17
+ # @param examples [Array<Example>]
18
+ def initialize(examples = nil)
19
+ @examples = examples || Hash.new { |h, k| h[k] = Example.new(k) }
20
+ end
21
+
22
+ # @return [Integer] Number of examples
23
+ def size
24
+ @examples.size
25
+ end
26
+
27
+ # @return [Array<String>] of every example's label
28
+ def labels
29
+ @examples.keys
30
+ end
31
+
32
+ # Add an example to the set.
33
+ # @param label [String] The label of the example.
34
+ # @param strokes [Array<Stroke>] Strokes associated with this label.
35
+ # @return self
36
+ def add_example(label, strokes = nil)
37
+ ex = @examples[label]
38
+ ex.add_set(strokes) if strokes && !strokes.empty?
39
+ self
40
+ end
41
+
42
+ # Iterates each {Example}.
43
+ # @return [Enumerator]
44
+ def each_example(&block)
45
+ return to_enum(__method__) unless block_given?
46
+
47
+ @examples.each do |label, ex|
48
+ block.call(ex)
49
+ end
50
+ end
51
+
52
+ # Convert the {Example} set into a {Document}.
53
+ # @param columns [Integer] Number of examples across the page.
54
+ # @param rows [Integer] Number of examples down the page.
55
+ # @param page_width [Float] Width of the page in points.
56
+ # @param page_height [Float] Height of the page in points.
57
+ # @return [Document]
58
+ def to_document(columns, rows, cells_per_example = 4, page_width = 612, page_height = 792)
59
+ DocumentMaker.new(self, columns, rows, cells_per_example, page_width, page_height).make_document
60
+ end
61
+
62
+ # Load {TrainingDocument} from a Xournal file.
63
+ # @param io_or_path [IO, String]
64
+ # @return [TrainingDocument]
65
+ def self.from_file(io_or_path)
66
+ DocumentReader.new.load(Xournal.from_file(io_or_path))
67
+ end
68
+
69
+ # Load a {TrainingDocument} from a {Document}.
70
+ # @param doc [Document]
71
+ # @return [TrainingDocument]
72
+ def self.from_document(doc)
73
+ DocumentReader.new.load(doc)
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,15 @@
1
+ require 'chunky_png'
2
+
3
+ module CooCoo
4
+ module DataSources
5
+ module Xournal
6
+ class TrainingDocument
7
+ VERSION = '1'
8
+ GRID_COLOR = '#00E0FFFF' # FIXME Xournal places alpha up front
9
+ PARSED_GRID_COLOR = ChunkyPNG::Color.parse(GRID_COLOR)
10
+ META_LABEL = "Training Document"
11
+ META_LABEL_REGEX = /^#{META_LABEL}( +\d+)?: *(\d+)( +\d+)( +\d+)?/
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,89 @@
1
+ require 'coo-coo/data_sources/xournal/document'
2
+
3
+ module CooCoo
4
+ module DataSources
5
+ module Xournal
6
+ class TrainingDocument
7
+ class DocumentMaker
8
+ attr_reader :page_width
9
+ attr_reader :page_height
10
+ attr_reader :cells_per_example
11
+ attr_reader :columns
12
+ attr_reader :rows
13
+
14
+ def initialize(training_doc, columns, rows, cells_per_example, page_width, page_height)
15
+ @doc = training_doc
16
+ @columns = columns
17
+ @rows = rows
18
+ @cells_per_example = cells_per_example
19
+ @page_width = page_width
20
+ @page_height = page_height
21
+ end
22
+
23
+ def make_document
24
+ Document.new do |d|
25
+ @doc.each_example.each_slice(@columns * @rows / @cells_per_example).with_index do |labels, page_num|
26
+ d.add_page(make_page(labels, page_num))
27
+ end
28
+
29
+ d.pages.first.layers.last.add_text(Text.new("#{META_LABEL} #{VERSION}: #{@columns} #{@rows} #{@cells_per_example}", 0, @page_height - 14, 12, 'gray'))
30
+ end
31
+ end
32
+
33
+ def make_cell(layer, label, strokes, x, y, grid_w, grid_h)
34
+ layer.add_text(Text.new(label, x + 1, y + 1, 12, 'black', 'Serif'))
35
+ strokes.each do |stroke|
36
+ layer.add_stroke(stroke.scale(grid_w, grid_h, grid_w).translate(x, y))
37
+ end
38
+ end
39
+
40
+ def make_cells(examples, grid_w, grid_h)
41
+ layer = Layer.new
42
+
43
+ examples = examples.collect { |e|
44
+ e.each_set.collect { |s|
45
+ [ e.label, s ]
46
+ } + Array.new(@cells_per_example - e.size, [ e.label, [] ])
47
+ }.flatten(1)
48
+
49
+ examples.each_slice(@columns).with_index do |row, y|
50
+ row.each_with_index do |(label, strokes), x|
51
+ make_cell(layer, label, strokes, x * grid_w, y * grid_h, grid_w, grid_h)
52
+ end
53
+ end
54
+
55
+ layer
56
+ end
57
+
58
+ def make_grid(grid_w, grid_h)
59
+ grid_layer = Layer.new
60
+
61
+ (1...@rows).each do |y|
62
+ (1...@columns).each do |x|
63
+ grid_layer.add_stroke(Stroke.new('pen', GRID_COLOR).
64
+ add_sample(x * grid_w, 0).
65
+ add_sample(x * grid_w, @page_height))
66
+ end
67
+
68
+ grid_layer.add_stroke(Stroke.new('pen', GRID_COLOR).
69
+ add_sample(0, y * grid_h).
70
+ add_sample(@page_width, y * grid_h))
71
+ end
72
+
73
+ grid_layer
74
+ end
75
+
76
+ def make_page(examples, page_num)
77
+ grid_w = @page_width / @columns
78
+ grid_h = @page_height / @rows
79
+
80
+ Page.new(@page_width, @page_height).
81
+ add_layer(make_grid(grid_w, grid_h)).
82
+ add_layer(make_cells(examples, grid_w, grid_h))
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
89
+