mork 0.0.1 → 0.0.2
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.
- data/.gitignore +3 -0
- data/Gemfile +1 -2
- data/Guardfile +10 -0
- data/config/grids.yml +42 -0
- data/lib/mork.rb +8 -4
- data/lib/mork/grid.rb +307 -0
- data/lib/mork/grid_const.rb +14 -0
- data/lib/mork/mimage.rb +86 -0
- data/lib/mork/mimage_list.rb +29 -0
- data/lib/mork/npatch.rb +51 -0
- data/lib/mork/report.rb +39 -0
- data/lib/mork/sheet.rb +144 -0
- data/lib/mork/sheet_pdf.rb +97 -0
- data/lib/mork/version.rb +1 -1
- data/mork.gemspec +9 -2
- data/spec/mork/grid_spec.rb +85 -0
- data/spec/mork/mimage_list_spec.rb +37 -0
- data/spec/mork/mimage_spec.rb +48 -0
- data/spec/mork/npatch_spec.rb +52 -0
- data/spec/mork/report_spec.rb +13 -0
- data/spec/mork/sheet_pdf_spec.rb +34 -0
- data/spec/mork/sheet_spec.rb +149 -0
- data/spec/samples/code_sample.pdf +85 -0
- data/spec/samples/code_sample.png +0 -0
- data/spec/samples/code_zero.pdf +79 -0
- data/spec/samples/info.yml +53 -0
- data/spec/samples/reg_mark.jpg +0 -0
- data/spec/samples/sample.pages +0 -0
- data/spec/samples/sample01.jpg +0 -0
- data/spec/samples/two_pages.pdf +0 -0
- data/spec/spec_helper.rb +34 -0
- metadata +199 -31
data/.gitignore
CHANGED
data/Gemfile
CHANGED
data/Guardfile
ADDED
data/config/grids.yml
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
default:
|
2
|
+
page_size:
|
3
|
+
width: 210.0
|
4
|
+
height: 297.0
|
5
|
+
header:
|
6
|
+
name:
|
7
|
+
top: 3
|
8
|
+
left: 7.5
|
9
|
+
width: 120
|
10
|
+
size: 14
|
11
|
+
code:
|
12
|
+
top: 3
|
13
|
+
left: 170
|
14
|
+
width: 25
|
15
|
+
size: 12
|
16
|
+
title:
|
17
|
+
top: 12
|
18
|
+
left: 7.5
|
19
|
+
width: 180.0
|
20
|
+
size: 12
|
21
|
+
date:
|
22
|
+
top: 18
|
23
|
+
left: 7.5
|
24
|
+
width: 180.0
|
25
|
+
size: 12
|
26
|
+
responses:
|
27
|
+
columns: 4
|
28
|
+
column_width: 50.0
|
29
|
+
rows: 40
|
30
|
+
# from the top-left registration mark
|
31
|
+
# to the center of the first choice cell
|
32
|
+
first_x: 10.5
|
33
|
+
first_y: 35.5
|
34
|
+
# between choices
|
35
|
+
x_spacing: 7.0
|
36
|
+
# between rows
|
37
|
+
y_spacing: 6.0
|
38
|
+
# darkened area
|
39
|
+
cell_width: 6.0
|
40
|
+
cell_height: 4.0
|
41
|
+
# the maximum number of choices per question
|
42
|
+
max_cells: 5
|
data/lib/mork.rb
CHANGED
@@ -1,5 +1,9 @@
|
|
1
1
|
require "mork/version"
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
2
|
+
require 'mork/sheet'
|
3
|
+
require 'mork/grid'
|
4
|
+
require 'mork/grid_const'
|
5
|
+
require 'mork/mimage'
|
6
|
+
require 'mork/mimage_list'
|
7
|
+
require 'mork/report'
|
8
|
+
require 'mork/npatch'
|
9
|
+
require 'mork/sheet_pdf'
|
data/lib/mork/grid.rb
ADDED
@@ -0,0 +1,307 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
|
3
|
+
module Mork
|
4
|
+
# The Grid is a set of expectations on what the response sheet should look like
|
5
|
+
# It knows nothing about the actual scanned image
|
6
|
+
# All returned values are in the arbitrary units given in the configuration file
|
7
|
+
class Grid
|
8
|
+
|
9
|
+
def initialize(type = :default, fname="config/grids.yml")
|
10
|
+
# from the full YAML file, only get the requested grid type
|
11
|
+
c = YAML.load_file(fname)
|
12
|
+
@params = c[type.to_s]
|
13
|
+
end
|
14
|
+
|
15
|
+
def set_page_size(x, y)
|
16
|
+
@px = x.to_f
|
17
|
+
@py = y.to_f
|
18
|
+
end
|
19
|
+
|
20
|
+
def default_chlist
|
21
|
+
[@params["responses"]["max_cells"]] * max_questions
|
22
|
+
end
|
23
|
+
|
24
|
+
def max_questions
|
25
|
+
columns * rows
|
26
|
+
end
|
27
|
+
|
28
|
+
def max_choices_per_question
|
29
|
+
@params["responses"]["max_cells"]
|
30
|
+
end
|
31
|
+
|
32
|
+
def code_bits
|
33
|
+
CODE_BITS
|
34
|
+
end
|
35
|
+
|
36
|
+
def header
|
37
|
+
@params["header"]
|
38
|
+
end
|
39
|
+
|
40
|
+
def reg_mark_search_area(n)
|
41
|
+
rmp = REG_MARGIN + REG_SEARCH
|
42
|
+
rmm = REG_MARGIN - REG_SEARCH
|
43
|
+
case n
|
44
|
+
when :top_left
|
45
|
+
reg_mark_sa rmm, rmm
|
46
|
+
when :top_right
|
47
|
+
reg_mark_sa page_width - rmp, rmm
|
48
|
+
when :bottom_right
|
49
|
+
reg_mark_sa page_width - rmp, page_height - rmp
|
50
|
+
when :bottom_left
|
51
|
+
reg_mark_sa rmm, page_height - rmp
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def reg_mark_sa(x, y)
|
56
|
+
cw = @px / page_width
|
57
|
+
ch = @py / page_height
|
58
|
+
{
|
59
|
+
x: (cw * x).round,
|
60
|
+
y: (ch * y).round,
|
61
|
+
w: (cw * REG_SEARCH * 2).round,
|
62
|
+
h: (ch * REG_SEARCH * 2).round
|
63
|
+
}
|
64
|
+
end
|
65
|
+
|
66
|
+
def choice_cell_area(q, c)
|
67
|
+
{
|
68
|
+
x: (cx * cell_x(q, c)).round,
|
69
|
+
y: (cy * cell_y(q) ).round,
|
70
|
+
w: (cx * cell_width ).round,
|
71
|
+
h: (cy * cell_height ).round
|
72
|
+
}
|
73
|
+
end
|
74
|
+
|
75
|
+
def white_calibration_area
|
76
|
+
code_cell_area -1
|
77
|
+
end
|
78
|
+
|
79
|
+
def black_calibration_area
|
80
|
+
code_cell_area 0
|
81
|
+
end
|
82
|
+
|
83
|
+
def code_bit_area(i)
|
84
|
+
code_cell_area i+1
|
85
|
+
end
|
86
|
+
|
87
|
+
# ============
|
88
|
+
# = to Prawn =
|
89
|
+
# ============
|
90
|
+
def pdf_page_size
|
91
|
+
[page_width.mm, page_height.mm]
|
92
|
+
end
|
93
|
+
|
94
|
+
def pdf_margins
|
95
|
+
REG_MARGIN.mm
|
96
|
+
end
|
97
|
+
|
98
|
+
def pdf_reg_marks
|
99
|
+
r = REG_RADIUS.mm
|
100
|
+
[
|
101
|
+
{ p: [0, 0 ], r: r },
|
102
|
+
{ p: [0, reg_frame_height.mm], r: r },
|
103
|
+
{ p: [reg_frame_width.mm, reg_frame_height.mm], r: r },
|
104
|
+
{ p: [reg_frame_width.mm, 0 ], r: r }
|
105
|
+
]
|
106
|
+
end
|
107
|
+
|
108
|
+
def pdf_dark_calibration_area
|
109
|
+
pdf_code_cell_area 0
|
110
|
+
end
|
111
|
+
|
112
|
+
def pdf_code_areas_for(code)
|
113
|
+
a = []
|
114
|
+
CODE_BITS.times do |bit|
|
115
|
+
a << pdf_code_cell_area(bit+1) if code[bit] == 1
|
116
|
+
end
|
117
|
+
a
|
118
|
+
end
|
119
|
+
|
120
|
+
def pdf_code_bit_areas
|
121
|
+
(1..CODE_BITS).collect do |bit|
|
122
|
+
pdf_code_cell_area bit
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def pdf_code_cell_area(i)
|
127
|
+
{
|
128
|
+
p: [code_cell_x(i).mm, (reg_frame_height - code_y).mm],
|
129
|
+
w: CODE_WIDTH.mm,
|
130
|
+
h: CODE_HEIGHT.mm * 2
|
131
|
+
}
|
132
|
+
end
|
133
|
+
|
134
|
+
def pdf_choice_cell_area(q, c)
|
135
|
+
{
|
136
|
+
p: [cell_x(q, c).mm, (reg_frame_height - cell_y(q)).mm],
|
137
|
+
w: cell_width.mm,
|
138
|
+
h: cell_height.mm
|
139
|
+
}
|
140
|
+
end
|
141
|
+
|
142
|
+
def pdf_choice_letter_xy(q, c)
|
143
|
+
[
|
144
|
+
cell_x(q, c).mm + 2.mm,
|
145
|
+
(reg_frame_height - cell_y(q)).mm - 3.mm
|
146
|
+
]
|
147
|
+
end
|
148
|
+
|
149
|
+
def pdf_qnum_xy(q)
|
150
|
+
[
|
151
|
+
cell_x(q, 0).mm - pdf_qnum_width - Q_NUM_GAP.mm,
|
152
|
+
(reg_frame_height - cell_y(q)).mm - 0.5.mm
|
153
|
+
]
|
154
|
+
end
|
155
|
+
|
156
|
+
def pdf_qnum_width
|
157
|
+
Q_NUM_WIDTH.mm
|
158
|
+
end
|
159
|
+
|
160
|
+
def pdf_header_xy(k)
|
161
|
+
[
|
162
|
+
header[k.to_s]["left"].to_f.mm,
|
163
|
+
(reg_frame_height - header[k.to_s]["top"].to_f).mm
|
164
|
+
]
|
165
|
+
end
|
166
|
+
|
167
|
+
def pdf_header_width(k)
|
168
|
+
header[k.to_s]["width"].to_f.mm
|
169
|
+
end
|
170
|
+
|
171
|
+
def pdf_header_size(k)
|
172
|
+
header[k.to_s]["size"].to_f
|
173
|
+
end
|
174
|
+
|
175
|
+
private
|
176
|
+
|
177
|
+
def cx
|
178
|
+
@px / reg_frame_width
|
179
|
+
end
|
180
|
+
|
181
|
+
def cy
|
182
|
+
@py / reg_frame_height
|
183
|
+
end
|
184
|
+
|
185
|
+
# x, y = cell_xy(q,c)
|
186
|
+
#
|
187
|
+
# the distances from the registration frame of the left and top edges
|
188
|
+
# of the c-th choice cell of the q-th question
|
189
|
+
def cell_xy(q, c)
|
190
|
+
return cell_x(q, c), cell_y(q)
|
191
|
+
end
|
192
|
+
|
193
|
+
# cell_y(q)
|
194
|
+
#
|
195
|
+
# the distance from the registration frame to the top edge
|
196
|
+
# of all choice cells in the q-th question
|
197
|
+
def cell_y(q)
|
198
|
+
first_y + response_spacing * (q % rows) - cell_height / 2
|
199
|
+
end
|
200
|
+
|
201
|
+
# cell_x(q,c)
|
202
|
+
#
|
203
|
+
# the distance from the registration frame to the left edge
|
204
|
+
# of the c-th choice cell of the q-th question
|
205
|
+
def cell_x(q,c)
|
206
|
+
first_x + column_width * (q / rows) + cell_spacing * c - cell_width / 2
|
207
|
+
end
|
208
|
+
|
209
|
+
def cell_width
|
210
|
+
@params["responses"]["cell_width"].to_f
|
211
|
+
end
|
212
|
+
|
213
|
+
def cell_height
|
214
|
+
@params["responses"]["cell_height"].to_f
|
215
|
+
end
|
216
|
+
|
217
|
+
def cell_spacing
|
218
|
+
@params["responses"]["x_spacing"].to_f
|
219
|
+
end
|
220
|
+
|
221
|
+
def response_spacing
|
222
|
+
@params["responses"]["y_spacing"].to_f
|
223
|
+
end
|
224
|
+
|
225
|
+
def column_width
|
226
|
+
@params["responses"]["column_width"].to_f
|
227
|
+
end
|
228
|
+
|
229
|
+
def row_spacing
|
230
|
+
@params["responses"]["y_spacing"].to_f
|
231
|
+
end
|
232
|
+
|
233
|
+
def first_x
|
234
|
+
@params["responses"]["first_x"].to_f
|
235
|
+
end
|
236
|
+
|
237
|
+
def first_y
|
238
|
+
@params["responses"]["first_y"].to_f
|
239
|
+
end
|
240
|
+
|
241
|
+
def rows
|
242
|
+
@params["responses"]["rows"]
|
243
|
+
end
|
244
|
+
|
245
|
+
def columns
|
246
|
+
@params["responses"]["columns"]
|
247
|
+
end
|
248
|
+
|
249
|
+
# ==============
|
250
|
+
# = sheet code =
|
251
|
+
# ==============
|
252
|
+
def code_cell_area(i)
|
253
|
+
{
|
254
|
+
x: (cx * code_cell_x(i) ).round,
|
255
|
+
y: (cy * code_y ).round,
|
256
|
+
w: (cx * CODE_WIDTH ).round,
|
257
|
+
h: (cy * CODE_HEIGHT).round
|
258
|
+
}
|
259
|
+
end
|
260
|
+
|
261
|
+
def code_cell_x(i)
|
262
|
+
CODE_LEFT + CODE_SPACING * i
|
263
|
+
end
|
264
|
+
|
265
|
+
def code_y
|
266
|
+
reg_frame_height - CODE_HEIGHT
|
267
|
+
end
|
268
|
+
|
269
|
+
# ======================
|
270
|
+
# = registration sides =
|
271
|
+
# ======================
|
272
|
+
def reg_frame_width
|
273
|
+
page_width - REG_MARGIN * 2
|
274
|
+
end
|
275
|
+
|
276
|
+
def reg_frame_height
|
277
|
+
page_height - REG_MARGIN * 2
|
278
|
+
end
|
279
|
+
|
280
|
+
def page_width
|
281
|
+
@params["page_size"]["width"].to_f
|
282
|
+
end
|
283
|
+
|
284
|
+
def page_height
|
285
|
+
@params["page_size"]["height"].to_f
|
286
|
+
end
|
287
|
+
|
288
|
+
# ============
|
289
|
+
# = Header
|
290
|
+
# ============
|
291
|
+
def name_x
|
292
|
+
@params["header"]["name"]["top"].to_f
|
293
|
+
end
|
294
|
+
|
295
|
+
def name_y
|
296
|
+
@params["header"]["name"]["left"].to_f
|
297
|
+
end
|
298
|
+
|
299
|
+
def name_w
|
300
|
+
@params["header"]["name"]["width"].to_f
|
301
|
+
end
|
302
|
+
|
303
|
+
def name_size
|
304
|
+
@params["header"]["name"]["size"]
|
305
|
+
end
|
306
|
+
end
|
307
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module Mork
|
2
|
+
REG_MARGIN = 7.5
|
3
|
+
REG_RADIUS = 2.5
|
4
|
+
REG_SEARCH = 5.0
|
5
|
+
CODE_LEFT = 7.5
|
6
|
+
CODE_WIDTH = 2.0
|
7
|
+
CODE_HEIGHT = 2.5
|
8
|
+
CODE_SPACING = 2.5
|
9
|
+
CODE_BITS = 64
|
10
|
+
Q_NUM_GAP = 2.0 # distance between right side of q num and left side of first choice cell
|
11
|
+
Q_NUM_WIDTH = 8.0 # width of question number text box
|
12
|
+
Q_NUM_SIZE = 10
|
13
|
+
CH_LETTER_SZ = 8
|
14
|
+
end
|
data/lib/mork/mimage.rb
ADDED
@@ -0,0 +1,86 @@
|
|
1
|
+
require 'RMagick'
|
2
|
+
|
3
|
+
module Mork
|
4
|
+
# The class Mimage is a wrapper for the core image library, currently RMagick
|
5
|
+
class Mimage
|
6
|
+
def initialize(img, page=0)
|
7
|
+
if img.class == String
|
8
|
+
if File.extname(img) == '.pdf'
|
9
|
+
@image = Magick::Image.read(img) { self.density = 200 }[page]
|
10
|
+
else
|
11
|
+
@image = Magick::ImageList.new(img)[page]
|
12
|
+
end
|
13
|
+
elsif img.class == Magick::ImageList
|
14
|
+
@image = img[page]
|
15
|
+
elsif img.class == Magick::Image
|
16
|
+
@image = img
|
17
|
+
else
|
18
|
+
raise "Invalid initialization argument"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
# =============
|
23
|
+
# = Highlight =
|
24
|
+
# =============
|
25
|
+
def highlight!(c)
|
26
|
+
m = Magick::Image.new(c[:w], c[:h]) { self.background_color = "red" }
|
27
|
+
@image.composite! m, c[:x], c[:y], Magick::CopyCompositeOp
|
28
|
+
end
|
29
|
+
|
30
|
+
# ============
|
31
|
+
# = Cropping =
|
32
|
+
# ============
|
33
|
+
def crop(c)
|
34
|
+
Mimage.new @image.crop(c[:x], c[:y], c[:w], c[:h])
|
35
|
+
end
|
36
|
+
|
37
|
+
def crop!(c)
|
38
|
+
@image.crop!(c[:x], c[:y], c[:w], c[:h])
|
39
|
+
self
|
40
|
+
end
|
41
|
+
|
42
|
+
# ============
|
43
|
+
# = Blurring =
|
44
|
+
# ============
|
45
|
+
def blur(a, b)
|
46
|
+
Mimage.new @image.blur_image(a, b)
|
47
|
+
end
|
48
|
+
|
49
|
+
def blur!(a, b)
|
50
|
+
@image = @image.blur_image(a, b)
|
51
|
+
self
|
52
|
+
end
|
53
|
+
|
54
|
+
# ==============
|
55
|
+
# = Stretching =
|
56
|
+
# ==============
|
57
|
+
def stretch(points)
|
58
|
+
Mimage.new @image.distort(Magick::PerspectiveDistortion, points)
|
59
|
+
end
|
60
|
+
|
61
|
+
def stretch!(points)
|
62
|
+
@image = @image.distort(Magick::PerspectiveDistortion, points)
|
63
|
+
self
|
64
|
+
end
|
65
|
+
|
66
|
+
# returns the raw pixels from the entire image or from the area
|
67
|
+
# defined in opts
|
68
|
+
def pixels(opts = {})
|
69
|
+
c = {x: 0, y: 0, w: width, h: height}.merge(opts)
|
70
|
+
@image.export_pixels(c[:x], c[:y], c[:w], c[:h], "I")
|
71
|
+
end
|
72
|
+
|
73
|
+
def width
|
74
|
+
@image.columns
|
75
|
+
end
|
76
|
+
|
77
|
+
def height
|
78
|
+
@image.rows
|
79
|
+
end
|
80
|
+
|
81
|
+
# write the underlying Magick::Image to disk
|
82
|
+
def write(fname)
|
83
|
+
@image.write fname
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|