mork 0.16.1 → 0.18.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 +4 -4
- data/README.md +31 -0
- data/lib/mork/fonts/OFL.txt +88 -0
- data/lib/mork/fonts/OpenSans-Regular.ttf +0 -0
- data/lib/mork/grid_omr.rb +7 -0
- data/lib/mork/magicko.rb +33 -5
- data/lib/mork/mimage.rb +5 -1
- data/lib/mork/sheet_omr.rb +88 -0
- data/lib/mork/sheet_pdf.rb +34 -14
- data/lib/mork/version.rb +1 -1
- data/script/bench_sheet_pdf.rb +40 -0
- data/spec/mork/grid_omr_spec.rb +14 -0
- data/spec/mork/magicko_spec.rb +12 -0
- data/spec/mork/sheet_omr_spec.rb +52 -0
- data/spec/mork/sheet_pdf_spec.rb +37 -1
- metadata +5 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 46d443ec81a0831a6048e8a7a7b6a16eb35d6a7d9a1a0a13287427d44d7e9b77
|
|
4
|
+
data.tar.gz: 15788c1658c1c07dce5fc1e0c66a345afb1a15a28a3b85bca787437a6500b13e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: cd65ed345abadaad63c283965c49c9af3c5b7b57564a8f0cabae07bc7d5bc32e8c167156c5703ad03b1b2ea4ff6bb0b363f976258589b2d37639cc3a39e4c1cc
|
|
7
|
+
data.tar.gz: bf1ae484be720c8a60f4e4f2cb540d5f9e235f89fd772089cb5bd5968762d54a3a1a1e4fb99725c29653633dbbec1732e3d8da953009fc583e4d734e86665a76
|
data/README.md
CHANGED
|
@@ -64,6 +64,14 @@ Response sheets are created through the `Mork::SheetPDF` class. Two pieces of in
|
|
|
64
64
|
s = SheetPDF.new content, layout
|
|
65
65
|
```
|
|
66
66
|
|
|
67
|
+
Mork uses its bundled Open Sans Regular font for all text in generated PDFs.
|
|
68
|
+
Prawn embeds only the glyphs used by each document, so no font needs to be
|
|
69
|
+
installed on the host that creates or prints the PDF. Open Sans covers extended
|
|
70
|
+
Latin, Greek, Cyrillic, and Hebrew, but it does not cover every Unicode script;
|
|
71
|
+
Arabic, Indic scripts, CJK, and emoji are not supported by Mork's default font.
|
|
72
|
+
The bundled font is distributed under the SIL Open Font License in
|
|
73
|
+
`lib/mork/fonts/OFL.txt`.
|
|
74
|
+
|
|
67
75
|
Let’s look at each argument in turn.
|
|
68
76
|
|
|
69
77
|
### content
|
|
@@ -296,6 +304,29 @@ s.save 'marked_choices_and_outlines.jpg'
|
|
|
296
304
|
system 'open marked_choices_and_outlines.jpg' # this works in macOS
|
|
297
305
|
```
|
|
298
306
|
|
|
307
|
+
For single-answer tests, Mork also provides a higher-level method that compares the detected responses against an answer key and applies the overlays automatically:
|
|
308
|
+
|
|
309
|
+
```ruby
|
|
310
|
+
correct = [3, 0, 2, 1, 2] # one zero-based correct choice per question
|
|
311
|
+
s.overlay_corrections correct
|
|
312
|
+
s.save 'corrections.jpg'
|
|
313
|
+
system 'open corrections.jpg' # this works in macOS
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
`overlay_corrections` applies:
|
|
317
|
+
|
|
318
|
+
- a green outline on the marked cell when the response is correct
|
|
319
|
+
- a red outline on the expected cell and a red cross on the marked cell when the response is incorrect
|
|
320
|
+
- a red outline on the expected cell when the response is blank or invalid
|
|
321
|
+
|
|
322
|
+
By default, `overlay_corrections` uses the unique marked response for each question, so if a responder marks more than one choice, the response is treated as invalid and no red cross is applied. To cross all marked cells for incorrect or multi-marked responses:
|
|
323
|
+
|
|
324
|
+
```ruby
|
|
325
|
+
s.overlay_corrections correct, marked: :all
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
The answer key must contain one entry per configured question, and each entry must be either a zero-based choice index, a one-element array containing that index, or `nil` to skip overlaying a question.
|
|
329
|
+
|
|
299
330
|
Scoring can only be performed if the sheet gets properly registered, which in turn depends on the quality of the scanned image.
|
|
300
331
|
|
|
301
332
|
### Improving sheet registration and marking
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
Copyright 2020 The Open Sans Project Authors (https://github.com/googlefonts/opensans)
|
|
2
|
+
|
|
3
|
+
-----------------------------------------------------------
|
|
4
|
+
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
|
5
|
+
-----------------------------------------------------------
|
|
6
|
+
|
|
7
|
+
PREAMBLE
|
|
8
|
+
The goals of the Open Font License (OFL) are to stimulate worldwide
|
|
9
|
+
development of collaborative font projects, to support the font
|
|
10
|
+
creation efforts of academic and linguistic communities, and to
|
|
11
|
+
provide a free and open framework in which fonts may be shared and
|
|
12
|
+
improved in partnership with others.
|
|
13
|
+
|
|
14
|
+
The OFL allows the licensed fonts to be used, studied, modified and
|
|
15
|
+
redistributed freely as long as they are not sold by themselves. The
|
|
16
|
+
fonts, including any derivative works, can be bundled, embedded,
|
|
17
|
+
redistributed and/or sold with any software provided that any reserved
|
|
18
|
+
names are not used by derivative works. The fonts and derivatives,
|
|
19
|
+
however, cannot be released under any other type of license. The
|
|
20
|
+
requirement for fonts to remain under this license does not apply to
|
|
21
|
+
any document created using the fonts or their derivatives.
|
|
22
|
+
|
|
23
|
+
DEFINITIONS
|
|
24
|
+
"Font Software" refers to the set of files released by the Copyright
|
|
25
|
+
Holder(s) under this license and clearly marked as such. This may
|
|
26
|
+
include source files, build scripts and documentation.
|
|
27
|
+
|
|
28
|
+
"Reserved Font Name" refers to any names specified as such after the
|
|
29
|
+
copyright statement(s).
|
|
30
|
+
|
|
31
|
+
"Original Version" refers to the collection of Font Software
|
|
32
|
+
components as distributed by the Copyright Holder(s).
|
|
33
|
+
|
|
34
|
+
"Modified Version" refers to any derivative made by adding to,
|
|
35
|
+
deleting, or substituting -- in part or in whole -- any of the
|
|
36
|
+
components of the Original Version, by changing formats or by porting
|
|
37
|
+
the Font Software to a new environment.
|
|
38
|
+
|
|
39
|
+
"Author" refers to any designer, engineer, programmer, technical
|
|
40
|
+
writer or other person who contributed to the Font Software.
|
|
41
|
+
|
|
42
|
+
PERMISSION & CONDITIONS
|
|
43
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
44
|
+
a copy of the Font Software, to use, study, copy, merge, embed,
|
|
45
|
+
modify, redistribute, and sell modified and unmodified copies of the
|
|
46
|
+
Font Software, subject to the following conditions:
|
|
47
|
+
|
|
48
|
+
1) Neither the Font Software nor any of its individual components, in
|
|
49
|
+
Original or Modified Versions, may be sold by itself.
|
|
50
|
+
|
|
51
|
+
2) Original or Modified Versions of the Font Software may be bundled,
|
|
52
|
+
redistributed and/or sold with any software, provided that each copy
|
|
53
|
+
contains the above copyright notice and this license. These can be
|
|
54
|
+
included either as stand-alone text files, human-readable headers or
|
|
55
|
+
in the appropriate machine-readable metadata fields within text or
|
|
56
|
+
binary files as long as those fields can be easily viewed by the user.
|
|
57
|
+
|
|
58
|
+
3) No Modified Version of the Font Software may use the Reserved Font
|
|
59
|
+
Name(s) unless explicit written permission is granted by the
|
|
60
|
+
corresponding Copyright Holder. This restriction only applies to the
|
|
61
|
+
primary font name as presented to the users.
|
|
62
|
+
|
|
63
|
+
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
|
64
|
+
Software shall not be used to promote, endorse or advertise any
|
|
65
|
+
Modified Version, except to acknowledge the contribution(s) of the
|
|
66
|
+
Copyright Holder(s) and the Author(s) or with their explicit written
|
|
67
|
+
permission.
|
|
68
|
+
|
|
69
|
+
5) The Font Software, modified or unmodified, in part or in whole,
|
|
70
|
+
must be distributed entirely under this license, and must not be
|
|
71
|
+
distributed under any other license. The requirement for fonts to
|
|
72
|
+
remain under this license does not apply to any document created using
|
|
73
|
+
the Font Software.
|
|
74
|
+
|
|
75
|
+
TERMINATION
|
|
76
|
+
This license becomes null and void if any of the above conditions are
|
|
77
|
+
not met.
|
|
78
|
+
|
|
79
|
+
DISCLAIMER
|
|
80
|
+
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
81
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
|
82
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
|
83
|
+
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
|
84
|
+
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
85
|
+
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
|
86
|
+
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
87
|
+
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
|
88
|
+
OTHER DEALINGS IN THE FONT SOFTWARE.
|
|
Binary file
|
data/lib/mork/grid_omr.rb
CHANGED
|
@@ -5,6 +5,8 @@ require 'deep_merge/rails_compat'
|
|
|
5
5
|
module Mork
|
|
6
6
|
# @private
|
|
7
7
|
class GridOMR < Grid
|
|
8
|
+
OVERLAY_STROKE_WIDTH_MM = 0.5
|
|
9
|
+
|
|
8
10
|
def initialize(options=nil)
|
|
9
11
|
super options
|
|
10
12
|
end
|
|
@@ -15,6 +17,10 @@ module Mork
|
|
|
15
17
|
self
|
|
16
18
|
end
|
|
17
19
|
|
|
20
|
+
def overlay_stroke_width_px
|
|
21
|
+
[(pixels_per_unit * OVERLAY_STROKE_WIDTH_MM).round, 1].max
|
|
22
|
+
end
|
|
23
|
+
|
|
18
24
|
def barcode_areas(bits)
|
|
19
25
|
[].tap do |areas|
|
|
20
26
|
bits.each_with_index do |b, i|
|
|
@@ -61,6 +67,7 @@ module Mork
|
|
|
61
67
|
|
|
62
68
|
def cx() @px / reg_frame_width end
|
|
63
69
|
def cy() @py / reg_frame_height end
|
|
70
|
+
def pixels_per_unit() (ppu_x + ppu_y) / 2.0 end
|
|
64
71
|
def ppu_x() @px / page_width end
|
|
65
72
|
def ppu_y() @py / page_height end
|
|
66
73
|
|
data/lib/mork/magicko.rb
CHANGED
|
@@ -8,10 +8,12 @@ module Mork
|
|
|
8
8
|
class Magicko
|
|
9
9
|
attr_reader :width
|
|
10
10
|
attr_reader :height
|
|
11
|
+
attr_reader :overlay_stroke_width
|
|
11
12
|
|
|
12
13
|
def initialize(path)
|
|
13
14
|
@path = path
|
|
14
15
|
@cmd = []
|
|
16
|
+
@overlay_stroke_width = 3
|
|
15
17
|
# a density is required for processing PDF or other vector-based images;
|
|
16
18
|
# a default of 150 dpi seems sensible. It should not affect bitmaps.
|
|
17
19
|
density = 150
|
|
@@ -39,6 +41,10 @@ module Mork
|
|
|
39
41
|
end
|
|
40
42
|
end
|
|
41
43
|
|
|
44
|
+
def overlay_stroke_width=(width)
|
|
45
|
+
@overlay_stroke_width = [width.to_i, 1].max
|
|
46
|
+
end
|
|
47
|
+
|
|
42
48
|
def valid?
|
|
43
49
|
@valid
|
|
44
50
|
end
|
|
@@ -71,14 +77,36 @@ module Mork
|
|
|
71
77
|
end
|
|
72
78
|
|
|
73
79
|
def outline(coords, rounded)
|
|
74
|
-
|
|
75
|
-
|
|
80
|
+
outline_with_color(coords, rounded, 'green')
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def outline_green(coords, rounded)
|
|
84
|
+
outline_with_color(coords, rounded, 'green')
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def outline_red(coords, rounded)
|
|
88
|
+
outline_with_color(coords, rounded, 'red')
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def check(coords, rounded)
|
|
92
|
+
check_with_color(coords, 'red')
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def check_red(coords, rounded)
|
|
96
|
+
check_with_color(coords, 'red')
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def outline_with_color(coords, rounded, color)
|
|
100
|
+
return if coords.empty?
|
|
101
|
+
@cmd << [:stroke, color]
|
|
102
|
+
@cmd << [:strokewidth, overlay_stroke_width]
|
|
76
103
|
@cmd << [:fill, 'none']
|
|
77
104
|
coords.each { |c| @cmd << [:draw, shape(c, rounded)] }
|
|
78
105
|
end
|
|
79
106
|
|
|
80
|
-
def
|
|
81
|
-
|
|
107
|
+
def check_with_color(coords, color)
|
|
108
|
+
return if coords.empty?
|
|
109
|
+
@cmd << [:stroke, color]
|
|
82
110
|
@cmd << [:strokewidth, '3']
|
|
83
111
|
coords.each do |c|
|
|
84
112
|
@cmd << [:draw, "line #{c.cross1}"]
|
|
@@ -115,7 +143,7 @@ module Mork
|
|
|
115
143
|
def join(p)
|
|
116
144
|
@cmd << [:fill, 'none']
|
|
117
145
|
@cmd << [:stroke, 'green']
|
|
118
|
-
@cmd << [:strokewidth,
|
|
146
|
+
@cmd << [:strokewidth, overlay_stroke_width]
|
|
119
147
|
pts = [
|
|
120
148
|
p[0][:x], p[0][:y],
|
|
121
149
|
p[1][:x], p[1][:y],
|
data/lib/mork/mimage.rb
CHANGED
|
@@ -14,6 +14,7 @@ module Mork
|
|
|
14
14
|
@mack = Magicko.new path
|
|
15
15
|
|
|
16
16
|
@grom = grom.set_page_size @mack.width, @mack.height
|
|
17
|
+
@mack.overlay_stroke_width = @grom.overlay_stroke_width_px
|
|
17
18
|
@choxq = [(0...@grom.max_choices_per_question).to_a] * grom.max_questions
|
|
18
19
|
@rm = {} # registration mark centers
|
|
19
20
|
@valid = register
|
|
@@ -86,7 +87,10 @@ module Mork
|
|
|
86
87
|
fail ArgumentError, 'Invalid overlay argument “where”'
|
|
87
88
|
end
|
|
88
89
|
round = where != :barcode
|
|
89
|
-
@mack.
|
|
90
|
+
unless @mack.respond_to?(what)
|
|
91
|
+
fail ArgumentError, 'Invalid overlay argument “what”'
|
|
92
|
+
end
|
|
93
|
+
@mack.public_send what, areas, round
|
|
90
94
|
end
|
|
91
95
|
|
|
92
96
|
# write the underlying MiniMagick::Image to disk;
|
data/lib/mork/sheet_omr.rb
CHANGED
|
@@ -163,6 +163,47 @@ module Mork
|
|
|
163
163
|
@mim.overlay what, where
|
|
164
164
|
end
|
|
165
165
|
|
|
166
|
+
# Applies correctness-aware overlays to the image using an answer key.
|
|
167
|
+
#
|
|
168
|
+
# Correct responses receive a green outline on the marked cell. Incorrect
|
|
169
|
+
# responses receive a red outline on the expected cell and a red cross on
|
|
170
|
+
# the given cell. Blank or invalid responses receive only the red outline
|
|
171
|
+
# on the expected cell.
|
|
172
|
+
#
|
|
173
|
+
# @param correct_choices [Array<Integer, Array<Integer>, nil>] one entry per
|
|
174
|
+
# question. Each entry can be an integer choice index, a 1-element array
|
|
175
|
+
# containing that index, or nil to skip that question.
|
|
176
|
+
# @param marked [Symbol] `:unique` treats multi-marked responses as invalid
|
|
177
|
+
# and does not cross them out; `:all` crosses all marked cells for
|
|
178
|
+
# incorrect responses.
|
|
179
|
+
def overlay_corrections(correct_choices, marked: :unique)
|
|
180
|
+
return if not_registered
|
|
181
|
+
|
|
182
|
+
expected = normalize_correct_choices(correct_choices)
|
|
183
|
+
actual = marked_responses(marked)
|
|
184
|
+
|
|
185
|
+
green_outlines = Array.new(expected.length) { [] }
|
|
186
|
+
red_outlines = Array.new(expected.length) { [] }
|
|
187
|
+
red_crosses = Array.new(expected.length) { [] }
|
|
188
|
+
|
|
189
|
+
expected.each_with_index do |choice, question|
|
|
190
|
+
next if choice.nil?
|
|
191
|
+
|
|
192
|
+
marked_cells = actual[question]
|
|
193
|
+
if marked_cells == [choice]
|
|
194
|
+
green_outlines[question] = [choice]
|
|
195
|
+
next
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
red_outlines[question] = [choice]
|
|
199
|
+
red_crosses[question] = marked_cells if marked_cells.any?
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
@mim.overlay :outline_green, green_outlines
|
|
203
|
+
@mim.overlay :outline_red, red_outlines
|
|
204
|
+
@mim.overlay :check_red, red_crosses
|
|
205
|
+
end
|
|
206
|
+
|
|
166
207
|
# Saves a copy of the source image after registration;
|
|
167
208
|
# the output image will also contain any previously applied overlays.
|
|
168
209
|
#
|
|
@@ -187,6 +228,53 @@ module Mork
|
|
|
187
228
|
private #
|
|
188
229
|
# ============================================================#
|
|
189
230
|
|
|
231
|
+
def normalize_correct_choices(correct_choices)
|
|
232
|
+
unless correct_choices.is_a?(Array)
|
|
233
|
+
fail ArgumentError, 'Correct choices must be an array'
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
if correct_choices.length != @mim.choxq.length
|
|
237
|
+
fail ArgumentError, 'Correct choices must match the configured number of questions'
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
correct_choices.map.with_index do |choice, question|
|
|
241
|
+
normalized =
|
|
242
|
+
case choice
|
|
243
|
+
when nil
|
|
244
|
+
nil
|
|
245
|
+
when Integer
|
|
246
|
+
choice
|
|
247
|
+
when Array
|
|
248
|
+
if choice.length > 1
|
|
249
|
+
fail ArgumentError, 'Each question supports a single correct choice'
|
|
250
|
+
end
|
|
251
|
+
choice.first
|
|
252
|
+
else
|
|
253
|
+
fail ArgumentError, 'Correct choices must contain integers, 1-element arrays, or nil'
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
next if normalized.nil?
|
|
257
|
+
|
|
258
|
+
max_choice = @mim.choxq[question].length
|
|
259
|
+
if normalized.negative? || normalized >= max_choice
|
|
260
|
+
fail ArgumentError, 'Correct choice exceeds the configured number of choices'
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
normalized
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
def marked_responses(mode)
|
|
268
|
+
case mode
|
|
269
|
+
when :unique
|
|
270
|
+
marked_choices.map { |choices| choices.length == 1 ? choices : [] }
|
|
271
|
+
when :all
|
|
272
|
+
marked_choices
|
|
273
|
+
else
|
|
274
|
+
fail ArgumentError, 'Invalid marked argument'
|
|
275
|
+
end
|
|
276
|
+
end
|
|
277
|
+
|
|
190
278
|
def not_registered
|
|
191
279
|
unless valid?
|
|
192
280
|
puts "---=={ Unregistered image. Reason: '#{@mim.status.inspect}' }==---"
|
data/lib/mork/sheet_pdf.rb
CHANGED
|
@@ -6,6 +6,10 @@ module Mork
|
|
|
6
6
|
# Generating response sheets as PDF files.
|
|
7
7
|
# See the README file for usage
|
|
8
8
|
class SheetPDF < Prawn::Document
|
|
9
|
+
DEFAULT_FONT_FAMILY = 'Open Sans'
|
|
10
|
+
DEFAULT_FONT_PATH = File.expand_path('fonts/OpenSans-Regular.ttf', __dir__)
|
|
11
|
+
private_constant :DEFAULT_FONT_FAMILY, :DEFAULT_FONT_PATH
|
|
12
|
+
|
|
9
13
|
include Extensions
|
|
10
14
|
def initialize(content, layout=nil, duplex=false)
|
|
11
15
|
@content =
|
|
@@ -24,6 +28,7 @@ module Mork
|
|
|
24
28
|
else raise ArgumentError, 'Invalid initialization parameter'
|
|
25
29
|
end
|
|
26
30
|
super my_page_params
|
|
31
|
+
use_default_font
|
|
27
32
|
@duplex = duplex
|
|
28
33
|
process
|
|
29
34
|
end
|
|
@@ -44,6 +49,15 @@ module Mork
|
|
|
44
49
|
private
|
|
45
50
|
###########################################################################
|
|
46
51
|
|
|
52
|
+
def use_default_font
|
|
53
|
+
font_families.update(
|
|
54
|
+
DEFAULT_FONT_FAMILY => {
|
|
55
|
+
normal: { file: DEFAULT_FONT_PATH, subset: true }
|
|
56
|
+
}
|
|
57
|
+
)
|
|
58
|
+
font DEFAULT_FONT_FAMILY
|
|
59
|
+
end
|
|
60
|
+
|
|
47
61
|
def my_page_params
|
|
48
62
|
{
|
|
49
63
|
page_size: @grip.page_size,
|
|
@@ -63,9 +77,7 @@ module Mork
|
|
|
63
77
|
start_new_page if i>0
|
|
64
78
|
barcode(content[:barcode] || 0)
|
|
65
79
|
header(content[:header] || [])
|
|
66
|
-
unless equal_choice_number?
|
|
67
|
-
questions_and_choices ch_len[i]
|
|
68
|
-
end
|
|
80
|
+
choice_cells(ch_len[i]) unless equal_choice_number?
|
|
69
81
|
start_new_page if @duplex
|
|
70
82
|
end
|
|
71
83
|
end
|
|
@@ -73,11 +85,9 @@ module Mork
|
|
|
73
85
|
def make_repeaters
|
|
74
86
|
pages = @duplex ? :odd : :all
|
|
75
87
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
end
|
|
80
|
-
end
|
|
88
|
+
repeat(pages) { question_numbers }
|
|
89
|
+
|
|
90
|
+
repeat(pages) { choice_cells(ch_len.first) } if equal_choice_number?
|
|
81
91
|
|
|
82
92
|
repeat(pages) do
|
|
83
93
|
calibration_cell_repeater
|
|
@@ -129,14 +139,19 @@ module Mork
|
|
|
129
139
|
end
|
|
130
140
|
end
|
|
131
141
|
|
|
132
|
-
def
|
|
133
|
-
|
|
142
|
+
def question_numbers
|
|
143
|
+
ch_len.first.length.times do |i|
|
|
134
144
|
text_box "#{i+1}",
|
|
135
145
|
at: @grip.qnum_xy(i),
|
|
136
146
|
width: @grip.qnum_width,
|
|
137
147
|
height: @grip.height_of_cell,
|
|
138
148
|
align: :right,
|
|
139
149
|
valign: :center
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def choice_cells(n_ch)
|
|
154
|
+
n_ch.each_with_index do |n, i|
|
|
140
155
|
stamp_at "s#{n}", @grip.item_xy(i)
|
|
141
156
|
end
|
|
142
157
|
end
|
|
@@ -194,11 +209,16 @@ module Mork
|
|
|
194
209
|
end
|
|
195
210
|
|
|
196
211
|
def equal_choice_number?
|
|
197
|
-
return
|
|
198
|
-
|
|
199
|
-
|
|
212
|
+
return @equal_choice_number unless @equal_choice_number.nil?
|
|
213
|
+
|
|
214
|
+
first_choices = ch_len.first
|
|
215
|
+
return @equal_choice_number = false unless ch_len.all? { |c| c.length == first_choices.length }
|
|
216
|
+
|
|
217
|
+
first_choices.each_with_index do |c, i|
|
|
218
|
+
return @equal_choice_number = false unless ch_len.all? { |x| c == x[i] }
|
|
200
219
|
end
|
|
201
|
-
|
|
220
|
+
|
|
221
|
+
@equal_choice_number = true
|
|
202
222
|
end
|
|
203
223
|
|
|
204
224
|
def ensure_presence_of_choices
|
data/lib/mork/version.rb
CHANGED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
require 'benchmark'
|
|
2
|
+
require 'mork'
|
|
3
|
+
|
|
4
|
+
def build_content(pages:, unequal:)
|
|
5
|
+
Array.new(pages) do |i|
|
|
6
|
+
choices =
|
|
7
|
+
if unequal
|
|
8
|
+
Array.new(120) { |j| 1 + ((i + j) % 5) }
|
|
9
|
+
else
|
|
10
|
+
[5] * 120
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
{
|
|
14
|
+
barcode: i,
|
|
15
|
+
choices: choices,
|
|
16
|
+
header: { title: "Test #{i + 1}" }
|
|
17
|
+
}
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def measure(label)
|
|
22
|
+
started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
23
|
+
result = yield
|
|
24
|
+
elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - started_at
|
|
25
|
+
[label, elapsed, result]
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
puts "ruby=#{RUBY_VERSION} prawn=#{Prawn::VERSION}"
|
|
29
|
+
puts "pages\tshape\tbuild_s\trender_s\tbytes"
|
|
30
|
+
|
|
31
|
+
[1, 20, 100].each do |pages|
|
|
32
|
+
[[false, 'equal'], [true, 'unequal']].each do |unequal, shape|
|
|
33
|
+
content = build_content(pages: pages, unequal: unequal)
|
|
34
|
+
|
|
35
|
+
_, build_s, pdf = measure('build') { Mork::SheetPDF.new(content) }
|
|
36
|
+
_, render_s, bytes = measure('render') { pdf.to_pdf.bytesize }
|
|
37
|
+
|
|
38
|
+
puts [pages, shape, format('%.6f', build_s), format('%.6f', render_s), bytes].join("\t")
|
|
39
|
+
end
|
|
40
|
+
end
|
data/spec/mork/grid_omr_spec.rb
CHANGED
|
@@ -56,6 +56,20 @@ module Mork
|
|
|
56
56
|
end
|
|
57
57
|
end
|
|
58
58
|
|
|
59
|
+
describe '#overlay_stroke_width_px' do
|
|
60
|
+
it 'keeps the default visual width on the reference scan' do
|
|
61
|
+
grom = GridOMR.new 'spec/samples/jdoe/layout.yml'
|
|
62
|
+
grom.set_page_size 1240, 1754
|
|
63
|
+
expect(grom.overlay_stroke_width_px).to eq 3
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
it 'scales with scan resolution' do
|
|
67
|
+
grom = GridOMR.new 'spec/samples/jdoe/layout.yml'
|
|
68
|
+
grom.set_page_size 2480, 3508
|
|
69
|
+
expect(grom.overlay_stroke_width_px).to eq 6
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
59
73
|
describe '#paper_white_area' do
|
|
60
74
|
it 'returns the coordinates of the white area used for barcode calibration' do
|
|
61
75
|
expect(@grom.paper_white_area).to have_coords(93, 2260, 25, 21)
|
data/spec/mork/magicko_spec.rb
CHANGED
|
@@ -38,5 +38,17 @@ module Mork
|
|
|
38
38
|
expect(ma.registered_bytes(pp).length).to eq sh.height*sh.width
|
|
39
39
|
end
|
|
40
40
|
end
|
|
41
|
+
|
|
42
|
+
describe 'overlay stroke width' do
|
|
43
|
+
it 'defaults to the legacy 3-pixel width' do
|
|
44
|
+
expect(ma.overlay_stroke_width).to eq 3
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
it 'uses the configured width for green outlines' do
|
|
48
|
+
ma.overlay_stroke_width = 6
|
|
49
|
+
ma.outline [co], false
|
|
50
|
+
expect(ma.instance_variable_get(:@cmd)).to include([:strokewidth, 6])
|
|
51
|
+
end
|
|
52
|
+
end
|
|
41
53
|
end
|
|
42
54
|
end
|
data/spec/mork/sheet_omr_spec.rb
CHANGED
|
@@ -148,6 +148,58 @@ module Mork
|
|
|
148
148
|
omr.overlay :outline
|
|
149
149
|
omr.save "spec/out/JD-outline-marked.jpeg"
|
|
150
150
|
end
|
|
151
|
+
|
|
152
|
+
it 'applies correctness-aware overlays and saves the result' do
|
|
153
|
+
omr.set_choices [5] * 5
|
|
154
|
+
omr.overlay_corrections [0, 0, 2, 3, 1]
|
|
155
|
+
omr.save "spec/out/JD-corrections.jpeg"
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
context 'applying correctness-aware overlays' do
|
|
160
|
+
let(:mim) { omr.instance_variable_get(:@mim) }
|
|
161
|
+
|
|
162
|
+
before do
|
|
163
|
+
omr.set_choices [5] * 4
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
it 'batches green outlines, red outlines, and red crosses by correctness' do
|
|
167
|
+
allow(omr).to receive(:marked_choices).and_return([[1], [2], [], [3, 4]])
|
|
168
|
+
|
|
169
|
+
expect(mim).to receive(:overlay).with(:outline_green, [[1], [], [], []]).ordered
|
|
170
|
+
expect(mim).to receive(:overlay).with(:outline_red, [[], [0], [2], [3]]).ordered
|
|
171
|
+
expect(mim).to receive(:overlay).with(:check_red, [[], [2], [], []]).ordered
|
|
172
|
+
|
|
173
|
+
omr.overlay_corrections [1, 0, 2, 3]
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
it 'can cross all marked cells for incorrect responses' do
|
|
177
|
+
allow(omr).to receive(:marked_choices).and_return([[1, 2], [0]])
|
|
178
|
+
|
|
179
|
+
omr.set_choices [5] * 2
|
|
180
|
+
|
|
181
|
+
expect(mim).to receive(:overlay).with(:outline_green, [[], []]).ordered
|
|
182
|
+
expect(mim).to receive(:overlay).with(:outline_red, [[3], [1]]).ordered
|
|
183
|
+
expect(mim).to receive(:overlay).with(:check_red, [[1, 2], [0]]).ordered
|
|
184
|
+
|
|
185
|
+
omr.overlay_corrections [3, 1], marked: :all
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
it 'raises an ArgumentError if the answer key length does not match the questions' do
|
|
189
|
+
expect { omr.overlay_corrections([0, 1, 2]) }.to raise_error(ArgumentError)
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
it 'raises an ArgumentError if a correct choice exceeds the configured choices' do
|
|
193
|
+
expect { omr.overlay_corrections([0, 1, 5, 2]) }.to raise_error(ArgumentError)
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
it 'raises an ArgumentError if a question has multiple correct choices' do
|
|
197
|
+
expect { omr.overlay_corrections([0, [1, 2], 2, 3]) }.to raise_error(ArgumentError)
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
it 'raises an ArgumentError if the marked mode is invalid' do
|
|
201
|
+
expect { omr.overlay_corrections([0, 1, 2, 3], marked: :invalid) }.to raise_error(ArgumentError)
|
|
202
|
+
end
|
|
151
203
|
end
|
|
152
204
|
|
|
153
205
|
context 'requesting invalid responses and choices' do
|
data/spec/mork/sheet_pdf_spec.rb
CHANGED
|
@@ -48,6 +48,26 @@ module Mork
|
|
|
48
48
|
expect(sp.instance_variable_get('@content').first).to be_a Hash
|
|
49
49
|
end
|
|
50
50
|
|
|
51
|
+
it 'uses the bundled Open Sans font with subsetting' do
|
|
52
|
+
pdf = SheetPDF.new({})
|
|
53
|
+
|
|
54
|
+
expect(pdf.font.family).to eq 'Open Sans'
|
|
55
|
+
expect(pdf.font.options[:subset]).to be true
|
|
56
|
+
expect(pdf.font.options[:file]).to end_with 'lib/mork/fonts/OpenSans-Regular.ttf'
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
it 'embeds Open Sans and renders supported Unicode across pages' do
|
|
60
|
+
content = [
|
|
61
|
+
{ header: { title: 'Extended Latin: Ž; Greek: Ω' } },
|
|
62
|
+
{ header: { title: 'Cyrillic: Ж; Hebrew: א' } }
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
bytes = SheetPDF.new(content).to_pdf
|
|
66
|
+
|
|
67
|
+
expect(bytes).to include '/FontFile2'
|
|
68
|
+
expect(bytes).to include 'OpenSans-Regular'
|
|
69
|
+
end
|
|
70
|
+
|
|
51
71
|
it 'creates a minimal PDF sheet' do
|
|
52
72
|
s = SheetPDF.new({})
|
|
53
73
|
s.save dest 'minimal'
|
|
@@ -106,6 +126,23 @@ module Mork
|
|
|
106
126
|
SheetPDF.new(c).save dest('uneq')
|
|
107
127
|
end
|
|
108
128
|
|
|
129
|
+
it 'reuses question number repeaters for multi-page unequal batches' do
|
|
130
|
+
choices = Array.new(120) { |i| 1 + (i % 5) }
|
|
131
|
+
c = 3.times.map do |x|
|
|
132
|
+
{
|
|
133
|
+
barcode: x,
|
|
134
|
+
choices: choices.rotate(x),
|
|
135
|
+
header: { title: "Test #{x+1}" }
|
|
136
|
+
}
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
pdf = SheetPDF.new(c)
|
|
140
|
+
|
|
141
|
+
expect(pdf.page_count).to eq 3
|
|
142
|
+
expect(pdf.repeaters.length).to eq 2
|
|
143
|
+
expect { pdf.to_pdf }.not_to raise_error
|
|
144
|
+
end
|
|
145
|
+
|
|
109
146
|
it 'creates 20 PDF sheets' do
|
|
110
147
|
c = 20.times.map do |x|
|
|
111
148
|
content.merge({ header: {title: "Test #{x+1}"}, barcode: x})
|
|
@@ -130,4 +167,3 @@ module Mork
|
|
|
130
167
|
end
|
|
131
168
|
end
|
|
132
169
|
end
|
|
133
|
-
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: mork
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.18.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Giuseppe Bertini
|
|
@@ -183,6 +183,8 @@ files:
|
|
|
183
183
|
- lib/mork.rb
|
|
184
184
|
- lib/mork/coord.rb
|
|
185
185
|
- lib/mork/extensions.rb
|
|
186
|
+
- lib/mork/fonts/OFL.txt
|
|
187
|
+
- lib/mork/fonts/OpenSans-Regular.ttf
|
|
186
188
|
- lib/mork/grid.rb
|
|
187
189
|
- lib/mork/grid_const.rb
|
|
188
190
|
- lib/mork/grid_omr.rb
|
|
@@ -195,6 +197,7 @@ files:
|
|
|
195
197
|
- lib/mork/version.rb
|
|
196
198
|
- mork.code-workspace
|
|
197
199
|
- mork.gemspec
|
|
200
|
+
- script/bench_sheet_pdf.rb
|
|
198
201
|
- spec/mork/coord_spec.rb
|
|
199
202
|
- spec/mork/grid_omr_spec.rb
|
|
200
203
|
- spec/mork/grid_spec.rb
|
|
@@ -283,7 +286,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
283
286
|
- !ruby/object:Gem::Version
|
|
284
287
|
version: '0'
|
|
285
288
|
requirements: []
|
|
286
|
-
rubygems_version:
|
|
289
|
+
rubygems_version: 4.0.4
|
|
287
290
|
specification_version: 4
|
|
288
291
|
summary: Optical mark recognition of multiple-choice tests and surveys
|
|
289
292
|
test_files: []
|