CooCoo 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 +16 -0
- data/CooCoo.gemspec +47 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +88 -0
- data/README.md +123 -0
- data/Rakefile +81 -0
- data/bin/cuda-dev-info +25 -0
- data/bin/cuda-free +28 -0
- data/bin/cuda-free-trend +7 -0
- data/bin/ffi-gen +267 -0
- data/bin/spec_runner_html.sh +42 -0
- data/bin/trainer +198 -0
- data/bin/trend-cost +13 -0
- data/examples/char-rnn.rb +405 -0
- data/examples/cifar/cifar.rb +94 -0
- data/examples/img-similarity.rb +201 -0
- data/examples/math_ops.rb +57 -0
- data/examples/mnist.rb +365 -0
- data/examples/mnist_classifier.rb +293 -0
- data/examples/mnist_dream.rb +214 -0
- data/examples/seeds.rb +268 -0
- data/examples/seeds_dataset.txt +210 -0
- data/examples/t10k-images-idx3-ubyte +0 -0
- data/examples/t10k-labels-idx1-ubyte +0 -0
- data/examples/train-images-idx3-ubyte +0 -0
- data/examples/train-labels-idx1-ubyte +0 -0
- data/ext/buffer/Rakefile +50 -0
- data/ext/buffer/buffer.pre.cu +727 -0
- data/ext/buffer/matrix.pre.cu +49 -0
- data/lib/CooCoo.rb +1 -0
- data/lib/coo-coo.rb +18 -0
- data/lib/coo-coo/activation_functions.rb +344 -0
- data/lib/coo-coo/consts.rb +5 -0
- data/lib/coo-coo/convolution.rb +298 -0
- data/lib/coo-coo/core_ext.rb +75 -0
- data/lib/coo-coo/cost_functions.rb +91 -0
- data/lib/coo-coo/cuda.rb +116 -0
- data/lib/coo-coo/cuda/device_buffer.rb +240 -0
- data/lib/coo-coo/cuda/device_buffer/ffi.rb +109 -0
- data/lib/coo-coo/cuda/error.rb +51 -0
- data/lib/coo-coo/cuda/host_buffer.rb +117 -0
- data/lib/coo-coo/cuda/runtime.rb +157 -0
- data/lib/coo-coo/cuda/vector.rb +315 -0
- data/lib/coo-coo/data_sources.rb +2 -0
- data/lib/coo-coo/data_sources/xournal.rb +25 -0
- data/lib/coo-coo/data_sources/xournal/bitmap_stream.rb +197 -0
- data/lib/coo-coo/data_sources/xournal/document.rb +377 -0
- data/lib/coo-coo/data_sources/xournal/loader.rb +144 -0
- data/lib/coo-coo/data_sources/xournal/renderer.rb +101 -0
- data/lib/coo-coo/data_sources/xournal/saver.rb +99 -0
- data/lib/coo-coo/data_sources/xournal/training_document.rb +78 -0
- data/lib/coo-coo/data_sources/xournal/training_document/constants.rb +15 -0
- data/lib/coo-coo/data_sources/xournal/training_document/document_maker.rb +89 -0
- data/lib/coo-coo/data_sources/xournal/training_document/document_reader.rb +105 -0
- data/lib/coo-coo/data_sources/xournal/training_document/example.rb +37 -0
- data/lib/coo-coo/data_sources/xournal/training_document/sets.rb +76 -0
- data/lib/coo-coo/debug.rb +8 -0
- data/lib/coo-coo/dot.rb +129 -0
- data/lib/coo-coo/drawing.rb +4 -0
- data/lib/coo-coo/drawing/cairo_canvas.rb +100 -0
- data/lib/coo-coo/drawing/canvas.rb +68 -0
- data/lib/coo-coo/drawing/chunky_canvas.rb +101 -0
- data/lib/coo-coo/drawing/sixel.rb +214 -0
- data/lib/coo-coo/enum.rb +17 -0
- data/lib/coo-coo/from_name.rb +58 -0
- data/lib/coo-coo/fully_connected_layer.rb +205 -0
- data/lib/coo-coo/generation_script.rb +38 -0
- data/lib/coo-coo/grapher.rb +140 -0
- data/lib/coo-coo/image.rb +286 -0
- data/lib/coo-coo/layer.rb +67 -0
- data/lib/coo-coo/layer_factory.rb +26 -0
- data/lib/coo-coo/linear_layer.rb +59 -0
- data/lib/coo-coo/math.rb +607 -0
- data/lib/coo-coo/math/abstract_vector.rb +121 -0
- data/lib/coo-coo/math/functions.rb +39 -0
- data/lib/coo-coo/math/interpolation.rb +7 -0
- data/lib/coo-coo/network.rb +264 -0
- data/lib/coo-coo/neuron.rb +112 -0
- data/lib/coo-coo/neuron_layer.rb +168 -0
- data/lib/coo-coo/option_parser.rb +18 -0
- data/lib/coo-coo/platform.rb +17 -0
- data/lib/coo-coo/progress_bar.rb +11 -0
- data/lib/coo-coo/recurrence/backend.rb +99 -0
- data/lib/coo-coo/recurrence/frontend.rb +101 -0
- data/lib/coo-coo/sequence.rb +187 -0
- data/lib/coo-coo/shell.rb +2 -0
- data/lib/coo-coo/temporal_network.rb +291 -0
- data/lib/coo-coo/trainer.rb +21 -0
- data/lib/coo-coo/trainer/base.rb +67 -0
- data/lib/coo-coo/trainer/batch.rb +82 -0
- data/lib/coo-coo/trainer/batch_stats.rb +27 -0
- data/lib/coo-coo/trainer/momentum_stochastic.rb +59 -0
- data/lib/coo-coo/trainer/stochastic.rb +47 -0
- data/lib/coo-coo/transformer.rb +272 -0
- data/lib/coo-coo/vector_layer.rb +194 -0
- data/lib/coo-coo/version.rb +3 -0
- data/lib/coo-coo/weight_deltas.rb +23 -0
- data/prototypes/convolution.rb +116 -0
- data/prototypes/linear_drop.rb +51 -0
- data/prototypes/recurrent_layers.rb +79 -0
- data/www/images/screamer.png +0 -0
- data/www/images/screamer.xcf +0 -0
- data/www/index.html +82 -0
- metadata +373 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
require 'coo-coo/data_sources/xournal/document'
|
|
2
|
+
require 'coo-coo/data_sources/xournal/loader'
|
|
3
|
+
require 'coo-coo/data_sources/xournal/saver'
|
|
4
|
+
require 'coo-coo/data_sources/xournal/renderer'
|
|
5
|
+
require 'coo-coo/data_sources/xournal/training_document'
|
|
6
|
+
|
|
7
|
+
module CooCoo
|
|
8
|
+
module DataSources
|
|
9
|
+
module Xournal
|
|
10
|
+
# Load a Xournal from a file.
|
|
11
|
+
# @param path [String] The file's path.
|
|
12
|
+
# @return [Document]
|
|
13
|
+
def self.from_file(path)
|
|
14
|
+
Loader.from_file(path)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Load Xournal from an XML string.
|
|
18
|
+
# @param xml [String] Unprocessed XML
|
|
19
|
+
# @return [Document]
|
|
20
|
+
def self.from_xml(xml)
|
|
21
|
+
Loader.from_xml(xml)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
require 'pathname'
|
|
2
|
+
require 'coo-coo/math'
|
|
3
|
+
require 'coo-coo/data_sources/xournal/training_document'
|
|
4
|
+
require 'coo-coo/data_sources/xournal/renderer'
|
|
5
|
+
require 'coo-coo/drawing/cairo_canvas'
|
|
6
|
+
|
|
7
|
+
module CooCoo
|
|
8
|
+
module DataSources
|
|
9
|
+
module Xournal
|
|
10
|
+
class BitmapStream
|
|
11
|
+
attr_reader :training_documents
|
|
12
|
+
attr_reader :labels
|
|
13
|
+
attr_reader :example_width, :example_height
|
|
14
|
+
attr_accessor :canvas_klass
|
|
15
|
+
attr_accessor :pen_scale
|
|
16
|
+
attr_reader :use_color
|
|
17
|
+
attr_accessor :shuffle
|
|
18
|
+
|
|
19
|
+
def initialize(options = Hash.new)
|
|
20
|
+
@training_documents = Array.new
|
|
21
|
+
@document_paths = Array.new
|
|
22
|
+
@pen_scale = options.fetch(:pen_scale, 1.0)
|
|
23
|
+
@example_width = options.fetch(:width, 28)
|
|
24
|
+
@example_height = options.fetch(:height, 28)
|
|
25
|
+
@num_labels = options[:num_labels]
|
|
26
|
+
if options[:labels]
|
|
27
|
+
@labels = File.read(options[:labels]).split("\n")
|
|
28
|
+
else
|
|
29
|
+
@labels = Array.new
|
|
30
|
+
end
|
|
31
|
+
@canvas_klass = options.fetch(:canvas, Drawing::CairoCanvas)
|
|
32
|
+
@use_color = options.fetch(:use_color, false)
|
|
33
|
+
@shuffle = options.fetch(:shuffle, 16)
|
|
34
|
+
|
|
35
|
+
options[:training_documents].each do |td|
|
|
36
|
+
add_training_document(td)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def add_training_document(path_or_td)
|
|
41
|
+
td = case path_or_td
|
|
42
|
+
when String then TrainingDocument.from_file(path_or_td)
|
|
43
|
+
when Pathname then TrainingDocument.from_file(path_or_td.to_s)
|
|
44
|
+
when TrainingDocument then path_or_td
|
|
45
|
+
else raise ArgumentError.new("#{path_or_td.inspect} is not a String, Pathname, or TrainingDocument")
|
|
46
|
+
end
|
|
47
|
+
process_training_document(td)
|
|
48
|
+
@document_paths << path_or_td unless td == path_or_td
|
|
49
|
+
@training_documents << td
|
|
50
|
+
self
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def process_training_document(td)
|
|
54
|
+
td.labels.each do |l|
|
|
55
|
+
add_label(l)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def size
|
|
60
|
+
training_documents.reduce(0) do |total, td|
|
|
61
|
+
total + td.each_example.reduce(0) do |subtotal, ex|
|
|
62
|
+
subtotal + ex.size
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def input_size
|
|
68
|
+
example_width * example_height * (@use_color ? 3 : 1)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def output_size
|
|
72
|
+
Math.max(@labels.size, @num_labels)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def add_label(label)
|
|
76
|
+
@labels << label unless @labels.find_index(label)
|
|
77
|
+
self
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def encode_label(label)
|
|
81
|
+
i = @labels.find_index(label)
|
|
82
|
+
v = Vector.zeros(output_size)
|
|
83
|
+
v[i] = 1.0
|
|
84
|
+
v
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def decode_output(output)
|
|
88
|
+
@labels[output.each.with_index.max[1]]
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def encode_strokes_to_canvas(strokes, canvas)
|
|
92
|
+
canvas.fill_color = 'white'
|
|
93
|
+
canvas.stroke_color = 'white'
|
|
94
|
+
canvas.rect(0, 0, @example_width, @example_height)
|
|
95
|
+
ren = Renderer.new
|
|
96
|
+
|
|
97
|
+
strokes.each do |stroke|
|
|
98
|
+
ren.render_stroke(canvas, stroke, 0, 0, 1, 1, @example_width, @example_height)
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def encode_strokes(strokes, return_canvas = false)
|
|
103
|
+
canvas = @canvas_klass.new(@example_width, @example_height)
|
|
104
|
+
if pen_scale != 1.0
|
|
105
|
+
strokes = strokes.collect { |s| s.scale(1.0, 1.0, pen_scale) }
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
encode_strokes_to_canvas(strokes, canvas)
|
|
109
|
+
|
|
110
|
+
if return_canvas
|
|
111
|
+
canvas.flush
|
|
112
|
+
else
|
|
113
|
+
canvas.to_vector(!@use_color) / 256.0
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def each(yield_canvas = false, &block)
|
|
118
|
+
return to_enum(__method__, yield_canvas) unless block_given?
|
|
119
|
+
|
|
120
|
+
training_documents.each do |td|
|
|
121
|
+
stroke_set = 0
|
|
122
|
+
|
|
123
|
+
loop do
|
|
124
|
+
td.each_example.each_slice(shuffle) do |slice|
|
|
125
|
+
examples = slice.collect do |ex|
|
|
126
|
+
strokes = ex.stroke_sets[stroke_set]
|
|
127
|
+
[ ex.label, strokes ] unless strokes.nil? || strokes.empty?
|
|
128
|
+
end.reject(&:nil?)
|
|
129
|
+
|
|
130
|
+
raise StopIteration if examples.empty?
|
|
131
|
+
|
|
132
|
+
examples.shuffle.each do |(label, strokes)|
|
|
133
|
+
yield(encode_label(label), encode_strokes(strokes, yield_canvas))
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
stroke_set += 1
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
if $0 != __FILE__
|
|
147
|
+
require 'ostruct'
|
|
148
|
+
|
|
149
|
+
@options = OpenStruct.new
|
|
150
|
+
@options.training_documents = Array.new
|
|
151
|
+
@options.labels_path = nil
|
|
152
|
+
@options.width = 28
|
|
153
|
+
@options.height = 28
|
|
154
|
+
@options.shuffle = 128
|
|
155
|
+
|
|
156
|
+
require 'coo-coo/option_parser'
|
|
157
|
+
|
|
158
|
+
@opts = CooCoo::OptionParser.new do |o|
|
|
159
|
+
o.banner = "Xournal Training Document Bitmap Stream Generator"
|
|
160
|
+
|
|
161
|
+
o.on('--data-path PATH', String, 'Adds a Xournal training document to be loaded.') do |p|
|
|
162
|
+
@options.training_documents += Dir.glob(p).to_a
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
o.on('--data-labels PATH', String, 'Predefined list of labels to preset the one hot encoding.') do |p|
|
|
166
|
+
@options.labels = p
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
o.on('--data-num-labels NUMBER', Integer, 'Minimum number of labels in the model') do |n|
|
|
170
|
+
@options.num_labels = n.to_i
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
o.on('--data-width NUMBER', Integer, 'Width in pixels of the generated bitmaps.') do |n|
|
|
174
|
+
n = n.to_i
|
|
175
|
+
raise ArgumentError.new('data-width must be > 0') if n <= 0
|
|
176
|
+
@options.width = n
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
o.on('--data-height NUMBER', Integer, 'Height in pixels of the generated bitmaps.') do |n|
|
|
180
|
+
n = n.to_i
|
|
181
|
+
raise ArgumentError.new('data-height must be > 0') if n <= 0
|
|
182
|
+
@options.height = n
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
o.on('--data-shuffle NUMBER', Integer, 'Number of examples to shuffle before yielding.') do |n|
|
|
186
|
+
n = n.to_i
|
|
187
|
+
raise ArgumentError.new('data-shuffle must be > 0') if n <= 0
|
|
188
|
+
@options.shuffle = n
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def training_set
|
|
193
|
+
CooCoo::DataSources::Xournal::BitmapStream.new(@options.to_h)
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
[ method(:training_set), @opts ]
|
|
197
|
+
end
|
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
module CooCoo
|
|
2
|
+
module DataSources
|
|
3
|
+
module Xournal
|
|
4
|
+
Colors = %w(black blue red green gray lightblue lightgreen magenta orange yellow white)
|
|
5
|
+
|
|
6
|
+
# The root of a Xournal document. Each document contains multiple
|
|
7
|
+
# {Page pages} which contain {Layer layers} with actual ink {Stroke strokes}, {Text text}, and {Image images}.
|
|
8
|
+
#
|
|
9
|
+
# More information on what is allowed can be found at:
|
|
10
|
+
# {http://xournal.sourceforge.net/manual.html#file-format}
|
|
11
|
+
class Document
|
|
12
|
+
VERSION = '0.4.8'
|
|
13
|
+
|
|
14
|
+
attr_accessor :title
|
|
15
|
+
attr_accessor :version
|
|
16
|
+
attr_reader :pages
|
|
17
|
+
|
|
18
|
+
def initialize(title = "Untitled Document", version = VERSION)
|
|
19
|
+
@title = title
|
|
20
|
+
@version = version
|
|
21
|
+
@pages = Array.new
|
|
22
|
+
yield(self) if block_given?
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def add_page(page)
|
|
26
|
+
@pages << page
|
|
27
|
+
self
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def delete_page_at(page_num)
|
|
31
|
+
@pages.delete_at(page_num)
|
|
32
|
+
self
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def delete_page(page)
|
|
36
|
+
@pages.delete(page)
|
|
37
|
+
self
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def each_page(&block)
|
|
41
|
+
@pages.each(&block)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def size
|
|
45
|
+
@pages.size
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def save(*args)
|
|
49
|
+
Saver.save(self, *args)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
class Page
|
|
54
|
+
attr_accessor :width, :height, :background
|
|
55
|
+
attr_reader :layers
|
|
56
|
+
|
|
57
|
+
def initialize(width, height, background = Background::Default)
|
|
58
|
+
@width = width
|
|
59
|
+
@height = height
|
|
60
|
+
@background = background
|
|
61
|
+
@layers = Array.new
|
|
62
|
+
yield(self) if block_given?
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def add_layer(layer)
|
|
66
|
+
@layers << layer
|
|
67
|
+
self
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def delete_layer_at(layer)
|
|
71
|
+
@layers.delete_at(layer)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def delete_layer(layer)
|
|
75
|
+
@layers.delete(layer)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def each_layer(&block)
|
|
79
|
+
@layers.each(&block)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
class Background
|
|
84
|
+
attr_accessor :color
|
|
85
|
+
attr_accessor :style
|
|
86
|
+
|
|
87
|
+
Styles = [ 'plain', 'lined', 'ruled', 'graph' ]
|
|
88
|
+
|
|
89
|
+
def initialize(color = 'white', style = 'plain')
|
|
90
|
+
self.color = color
|
|
91
|
+
self.style = style
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def style=(s)
|
|
95
|
+
raise ArgumentError.new("Invalid style #{s}") unless s == nil || Styles.include?(s)
|
|
96
|
+
@style = s
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
Default = self.new
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
class PixmapBackground
|
|
103
|
+
attr_accessor :domain
|
|
104
|
+
attr_accessor :filename
|
|
105
|
+
|
|
106
|
+
Domains = [ 'absolute', 'attach', 'clone' ]
|
|
107
|
+
|
|
108
|
+
def initialize(filename, domain = 'attach')
|
|
109
|
+
self.filename = filename
|
|
110
|
+
self.domain = domain
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def domain=(d)
|
|
114
|
+
raise ArgumentError.new("Invalid domain #{d}") unless d == nil || Domains.include?(d)
|
|
115
|
+
@domain = d
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
class PDFBackground
|
|
120
|
+
attr_accessor :domain
|
|
121
|
+
attr_accessor :filename
|
|
122
|
+
attr_accessor :page_number
|
|
123
|
+
|
|
124
|
+
Domains = [ 'absolute', 'attach' ]
|
|
125
|
+
|
|
126
|
+
def initialize(filename, page_number = nil, domain = 'attach')
|
|
127
|
+
self.filename = filename
|
|
128
|
+
self.domain = domain
|
|
129
|
+
self.page_number = page_number
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def domain=(d)
|
|
133
|
+
raise ArgumentError.new("Invalid domain #{d}") unless d == nil || Domains.include?(d)
|
|
134
|
+
@domain = d
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
class Layer
|
|
139
|
+
attr_reader :children
|
|
140
|
+
|
|
141
|
+
def initialize
|
|
142
|
+
@children = Array.new
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def each(&block)
|
|
146
|
+
@children.each(&block)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def delete_child_at(n)
|
|
150
|
+
@children.delete_at(n)
|
|
151
|
+
self
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def delete_child(child)
|
|
155
|
+
@children.delete(child)
|
|
156
|
+
self
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def add_stroke(stroke)
|
|
160
|
+
@children << stroke
|
|
161
|
+
self
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def strokes
|
|
165
|
+
@children.select { |c| c.kind_of?(Stroke) }
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def each_stroke(&block)
|
|
169
|
+
strokes.each(&block)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def add_text(text)
|
|
173
|
+
@children << text
|
|
174
|
+
self
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def text
|
|
178
|
+
@children.select { |c| c.kind_of?(Text) }
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def each_text(&block)
|
|
182
|
+
text.each(&block)
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def add_image(img)
|
|
186
|
+
@children << img
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def images
|
|
190
|
+
@children.select { |c| c.kind_of?(Image) }
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def each_image(&block)
|
|
194
|
+
images.each(&block)
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
class Stroke
|
|
199
|
+
attr_reader :samples
|
|
200
|
+
attr_accessor :tool
|
|
201
|
+
attr_accessor :color
|
|
202
|
+
|
|
203
|
+
Tools = [ 'pen', 'highlighter', 'eraser' ]
|
|
204
|
+
DefaultTool = Tools.first
|
|
205
|
+
|
|
206
|
+
def initialize(tool = 'pen', color = 'black', samples = nil)
|
|
207
|
+
self.tool = tool
|
|
208
|
+
@color = color
|
|
209
|
+
@samples = samples || Array.new
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def tool=(t)
|
|
213
|
+
raise ArgumentError.new("Invalid tool: #{t.inspect}") unless t == nil || Tools.include?(t)
|
|
214
|
+
@tool = t
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def add_sample(x, y, w = 1)
|
|
218
|
+
@samples << Sample.new(x, y, w)
|
|
219
|
+
self
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def delete_sample_at(n)
|
|
223
|
+
@samples.delete_at(n)
|
|
224
|
+
self
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def delete_sample(sample)
|
|
228
|
+
@samples.delete(sample)
|
|
229
|
+
self
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def size
|
|
233
|
+
@samples.size
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def each_sample(&block)
|
|
237
|
+
@samples.each(&block)
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def translate(dx, dy)
|
|
241
|
+
self.class.new(tool, color, samples.collect { |s| s.translate(dx, dy) })
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def scale(sx, sy, sw = 1.0)
|
|
245
|
+
self.class.new(tool, color, samples.collect { |s| s.scale(sx, sy, sw) })
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def minmax
|
|
249
|
+
xmin = nil
|
|
250
|
+
xmax = nil
|
|
251
|
+
ymin = nil
|
|
252
|
+
ymax = nil
|
|
253
|
+
|
|
254
|
+
xmin, xmax = @samples.collect(&:x).minmax
|
|
255
|
+
ymin, ymax = @samples.collect(&:y).minmax
|
|
256
|
+
|
|
257
|
+
[ [ xmin, ymin ], [ xmax, ymax ] ]
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
class Sample
|
|
262
|
+
attr_accessor :width, :x, :y
|
|
263
|
+
|
|
264
|
+
def initialize(x, y, width = nil)
|
|
265
|
+
@x = x
|
|
266
|
+
@y = y
|
|
267
|
+
@width = width
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
def translate(dx, dy)
|
|
271
|
+
self.class.new(x + dx, y + dy, width)
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
def scale(sx, sy, sw)
|
|
275
|
+
self.class.new(x * sx, y * sy, width * sw)
|
|
276
|
+
end
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
class Text
|
|
280
|
+
attr_accessor :text, :size, :x, :y, :color, :font
|
|
281
|
+
|
|
282
|
+
def initialize(text, x, y, size = 12, color = 'black', font = 'Sans')
|
|
283
|
+
@text = text
|
|
284
|
+
@x = x
|
|
285
|
+
@y = y
|
|
286
|
+
@size = size
|
|
287
|
+
@color = color
|
|
288
|
+
@font = font
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
def left
|
|
292
|
+
x
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def top
|
|
296
|
+
y
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
def right
|
|
300
|
+
x + width
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
def width
|
|
304
|
+
# TODO but how?
|
|
305
|
+
@text.length * @size
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
def bottom
|
|
309
|
+
y + height
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
def height
|
|
313
|
+
@size * @text.count("\n")
|
|
314
|
+
end
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
class Image
|
|
318
|
+
attr_accessor :left, :right, :top, :bottom
|
|
319
|
+
attr_accessor :data, :raw_data
|
|
320
|
+
|
|
321
|
+
def initialize(left, top, right, bottom, data = nil)
|
|
322
|
+
@left = left.to_f
|
|
323
|
+
@top = top.to_f
|
|
324
|
+
@right = right.to_f
|
|
325
|
+
@bottom = bottom.to_f
|
|
326
|
+
self.data = data
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
def data=(data)
|
|
330
|
+
case data
|
|
331
|
+
when String then
|
|
332
|
+
data = Base64.decode64(data)
|
|
333
|
+
@data = decode_image(data) rescue nil
|
|
334
|
+
@raw_data = data
|
|
335
|
+
when ChunkyPNG::Image then
|
|
336
|
+
@data = data
|
|
337
|
+
@raw_data = nil
|
|
338
|
+
when nil then @data = @raw_data = nil
|
|
339
|
+
else @raw_data = data
|
|
340
|
+
end
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
def raw_data
|
|
344
|
+
@raw_data || @data.to_blob
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
def sized_data(zx = 1.0, zy = 1.0)
|
|
348
|
+
if zx == 1.0 && zy == 1.0
|
|
349
|
+
@sized_data ||= @data.resample_bilinear(width, height)
|
|
350
|
+
else
|
|
351
|
+
@data.resample_bilinear((width * zx).to_i, (height * zy).to_i)
|
|
352
|
+
end
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
def width
|
|
356
|
+
(right - left).to_i
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
def height
|
|
360
|
+
(bottom - top).to_i
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
def decode_image(data)
|
|
364
|
+
ChunkyPNG::Image.from_string(data)
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
def data_string
|
|
368
|
+
Base64.encode64(raw_data)
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
def raw_data
|
|
372
|
+
@raw_data ||= @data.to_s
|
|
373
|
+
end
|
|
374
|
+
end
|
|
375
|
+
end
|
|
376
|
+
end
|
|
377
|
+
end
|