1 | #! /usr/bin/perl
|
2 | #
|
3 | # Copyright (C) 2009-2019 Alexis Bienvenue <paamc@passoire.fr>
|
4 | #
|
5 | # This file is part of Auto-Multiple-Choice
|
6 | #
|
7 | # Auto-Multiple-Choice is free software: you can redistribute it
|
8 | # and/or modify it under the terms of the GNU General Public License
|
9 | # as published by the Free Software Foundation, either version 2 of
|
10 | # the License, or (at your option) any later version.
|
11 | #
|
12 | # Auto-Multiple-Choice is distributed in the hope that it will be
|
13 | # useful, but WITHOUT ANY WARRANTY; without even the implied warranty
|
14 | # of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
15 | # General Public License for more details.
|
16 | #
|
17 | # You should have received a copy of the GNU General Public License
|
18 | # along with Auto-Multiple-Choice. If not, see
|
19 | # <http://www.gnu.org/licenses/>.
|
20 |
|
21 | use Getopt::Long;
|
22 |
|
23 | use Gtk3;
|
24 | use Cairo;
|
25 |
|
26 | use List::Util qw(min max sum);
|
27 |
|
28 | use AMC::Path;
|
29 | use AMC::Basic;
|
30 | use AMC::Exec;
|
31 | use AMC::Gui::Avancement;
|
32 | use AMC::NamesFile;
|
33 | use AMC::Data;
|
34 | use AMC::DataModule::capture qw/:zone :position/;
|
35 | use AMC::DataModule::layout qw/:flags/;
|
36 | use AMC::Substitute;
|
37 |
|
38 | use utf8;
|
39 |
|
40 | my $cr_dir = "";
|
41 | my $rep_projet = '';
|
42 | my $rep_projets = '';
|
43 | my $fichnotes = '';
|
44 | my $fich_bareme = '';
|
45 | my $id_file = '';
|
46 |
|
47 | my $seuil = 0.1;
|
48 | my $seuil_up = 1.0;
|
49 |
|
50 | my $data_dir = '';
|
51 |
|
52 | my $taille_max = "1000x1500";
|
53 | my $qualite_jpg = "65";
|
54 |
|
55 | my $debug = '';
|
56 |
|
57 | my $progress = 1;
|
58 | my $progress_id = '';
|
59 |
|
60 | my $line_width = 2;
|
61 | my @o_symbols = ();
|
62 | my $annote_indicatives = '';
|
63 | my $position = 'marge';
|
64 | my $ecart = 1;
|
65 | my $ecart_marge = 1.5;
|
66 | my $pointsize_rel = 60;
|
67 |
|
68 | my $chiffres_significatifs = 4;
|
69 |
|
70 | my $verdict = 'TOTAL : %S/%M => %s/%m';
|
71 | my $verdict_question_cancelled = '"X"';
|
72 | my $verdict_question = '';
|
73 |
|
74 | my $font_name = 'FreeSans';
|
75 | my $rtl = '';
|
76 | my $test_font_size = 100;
|
77 |
|
78 | my $fich_noms = '';
|
79 | my $noms_encodage = 'utf-8';
|
80 | my $csv_build_name = '';
|
81 |
|
82 | my $changes_only = 1;
|
83 |
|
84 | # cle : "a_cocher-cochee"
|
85 | my %symboles = (
|
86 | '0-0' => {qw/type none/},
|
87 | '0-1' => {qw/type circle color red/},
|
88 | '1-0' => {qw/type mark color red/},
|
89 | '1-1' => {qw/type mark color blue/},
|
90 | );
|
91 |
|
92 | @ARGV = unpack_args(@ARGV);
|
93 |
|
94 | GetOptions(
|
95 | "cr=s" => \$cr_dir,
|
96 | "projet=s", \$rep_projet,
|
97 | "projets=s", \$rep_projets,
|
98 | "data=s" => \$data_dir,
|
99 | "id-file=s" => \$id_file,
|
100 | "debug=s" => \$debug,
|
101 | "taille-max=s" => \$taille_max,
|
102 | "qualite=s" => \$qualite_jpg,
|
103 | "progression=s" => \$progress,
|
104 | "progression-id=s" => \$progress_id,
|
105 | "line-width=s" => \$line_width,
|
106 | "symbols=s" => \@o_symbols,
|
107 | "indicatives=s" => \$annote_indicatives,
|
108 | "position=s" => \$position,
|
109 | "pointsize-nl=s" => \$pointsize_rel,
|
110 | "ecart=s" => \$ecart,
|
111 | "ecart-marge=s" => \$ecart_marge,
|
112 | "ch-sign=s" => \$chiffres_significatifs,
|
113 | "verdict=s" => \$verdict,
|
114 | "verdict-question=s" => \$verdict_question,
|
115 | "verdict-question-cancelled=s" => \$verdict_question_cancelled,
|
116 | "fich-noms=s" => \$fich_noms,
|
117 | "noms-encodage=s" => \$noms_encodage,
|
118 | "csv-build-name=s" => \$csv_build_name,
|
119 | "font=s" => \$font_name,
|
120 | "rtl!" => \$rtl,
|
121 | "changes-only!" => \$changes_only,
|
122 | );
|
123 |
|
124 | set_debug($debug);
|
125 |
|
126 | print( ( "*" x 60 ) . "\n" );
|
127 | print "* WARNING: AMC-annote is now obsolete\n* Please move to AMC-annotate\n";
|
128 | print( ( "*" x 60 ) . "\n" );
|
129 |
|
130 | for ( split( /,/, join( ',', @o_symbols ) ) ) {
|
131 | if (/^([01]-[01]):(none|circle|mark|box)(?:\/([\#a-z0-9]+))?$/) {
|
132 | $symboles{$1} = { type => $2, color => $3 };
|
133 | } else {
|
134 | die "Bad symbol syntax: $_";
|
135 | }
|
136 | }
|
137 |
|
138 | my $commandes = AMC::Exec::new("AMC-annote");
|
139 | $commandes->signalise();
|
140 |
|
141 | $cr_dir = $rep_projet . "/cr" if ( !$cr_dir );
|
142 |
|
143 | if ( !-d $cr_dir ) {
|
144 | attention("No CR directory: $cr_dir");
|
145 | die "No CR directory: $cr_dir";
|
146 | }
|
147 |
|
148 | my $noms = '';
|
149 |
|
150 | if ($fich_noms) {
|
151 | $noms = AMC::NamesFile::new(
|
152 | $fich_noms,
|
153 | encodage => $noms_encodage,
|
154 | identifiant => $csv_build_name
|
155 | );
|
156 |
|
157 | debug "Keys in names file: " . join( ", ", $noms->heads() );
|
158 | }
|
159 |
|
160 | # ---
|
161 |
|
162 | sub color_rgb {
|
163 | my ($s) = @_;
|
164 | my $col = Gtk3::Gdk::Color::parse($s);
|
165 | return ( $col->red / 65535, $col->green / 65535, $col->blue / 65535 );
|
166 | }
|
167 |
|
168 | my $avance = AMC::Gui::Avancement::new( $progress, id => $progress_id );
|
169 |
|
170 | my $data = AMC::Data->new($data_dir);
|
171 | my $capture = $data->module('capture');
|
172 | my $scoring = $data->module('scoring');
|
173 | my $assoc = $data->module('association');
|
174 | my $layout = $data->module('layout');
|
175 |
|
176 | $seuil = $scoring->variable_transaction('darkness_threshold');
|
177 | $seuil_up = $scoring->variable_transaction('darkness_threshold_up');
|
178 |
|
179 | #################################
|
180 |
|
181 | sub milieu_cercle {
|
182 | my $zoneid = shift;
|
183 | return (
|
184 | $capture->sql_row(
|
185 | $capture->statement('zoneCenter'),
|
186 | $zoneid, POSITION_BOX
|
187 | )
|
188 | );
|
189 | }
|
190 |
|
191 | sub cercle_coors {
|
192 | my ( $context, $zoneid, $color ) = @_;
|
193 | my ( $x, $y ) = milieu_cercle($zoneid);
|
194 | my $t = sqrt( $capture->zone_dist2( $zoneid, $x, $y ) );
|
195 | $context->set_source_rgb( color_rgb($color) );
|
196 | $context->new_path;
|
197 | $context->arc( $x, $y, $t, 0, 360 );
|
198 | $context->stroke;
|
199 | }
|
200 |
|
201 | sub croix_coors {
|
202 | my ( $context, $zoneid, $color ) = @_;
|
203 | $context->set_source_rgb( color_rgb($color) );
|
204 | $context->new_path;
|
205 | for my $i ( 1, 2 ) {
|
206 | $context->move_to( $capture->zone_corner( $zoneid, $i ) );
|
207 | $context->line_to( $capture->zone_corner( $zoneid, $i + 2 ) );
|
208 | }
|
209 | $context->stroke;
|
210 | }
|
211 |
|
212 | sub boite_coors {
|
213 | my ( $context, $zoneid, $color ) = @_;
|
214 | my @pts = "";
|
215 | $context->set_source_rgb( color_rgb($color) );
|
216 | $context->new_path;
|
217 | $context->move_to( $capture->zone_corner( $zoneid, 1 ) );
|
218 | for my $i ( 2 .. 4 ) {
|
219 | $context->line_to( $capture->zone_corner( $zoneid, $i ) );
|
220 | }
|
221 | $context->close_path;
|
222 | $context->stroke;
|
223 | }
|
224 |
|
225 | my $delta = 1;
|
226 |
|
227 | $capture->begin_read_transaction('PAGE');
|
228 |
|
229 | my $annotate_source_change = $capture->variable('annotate_source_change');
|
230 |
|
231 | my @pages = @{
|
232 | $capture->dbh->selectall_arrayref( $capture->statement('pages'),
|
233 | { Slice => {} } )
|
234 | };
|
235 |
|
236 | $capture->end_transaction('PAGE');
|
237 |
|
238 | $delta = 1 / ( 1 + $#pages ) if ( $#pages >= 0 );
|
239 | $n_processed_pages = 0;
|
240 |
|
241 | my %ok_students = ();
|
242 |
|
243 | # a) first case: these numbers are given by --id-file option
|
244 |
|
245 | if ($id_file) {
|
246 |
|
247 | open( NUMS, $id_file );
|
248 | while (<NUMS>) {
|
249 | chomp;
|
250 | if (/^[0-9]+(:[0-9]+)?$/) {
|
251 | $ok_students{$_} = 1;
|
252 | }
|
253 | }
|
254 | close(NUMS);
|
255 |
|
256 | }
|
257 |
|
258 | my $subst = AMC::Substitute::new(
|
259 | names => $noms,
|
260 | scoring => $scoring,
|
261 | assoc => $assoc,
|
262 | name => '',
|
263 | chsign => $chiffres_significatifs,
|
264 | );
|
265 |
|
266 | print "* Annotation\n";
|
267 |
|
268 | PAGE: for my $p (@pages) {
|
269 | my @spc = map { $p->{$_} } (qw/student page copy/);
|
270 |
|
271 | if ( $id_file && !$ok_students{ studentids_string( $spc[0], $spc[2] ) } ) {
|
272 | next PAGE;
|
273 | }
|
274 |
|
275 | if ( $changes_only && $p->{timestamp_annotate} > $annotate_source_change ) {
|
276 | my $f = $p->{annotated};
|
277 | if ( -f "$cr_dir/corrections/jpg/$f" ) {
|
278 | print "Skipping page " . pageids_string(@spc) . " (up to date)\n";
|
279 | debug "Skipping page " . pageids_string(@spc) . " (up to date)";
|
280 | next PAGE;
|
281 | }
|
282 | }
|
283 |
|
284 | debug "Analyzing " . pageids_string(@spc);
|
285 |
|
286 | my $scan = $p->{src};
|
287 |
|
288 | debug "Scan file: $scan";
|
289 |
|
290 | if ($rep_projet) {
|
291 | $scan = proj2abs(
|
292 | {
|
293 | '%PROJET', $rep_projet,
|
294 | '%PROJETS', $rep_projets,
|
295 | '%HOME' => $ENV{HOME},
|
296 | },
|
297 | $scan
|
298 | );
|
299 | }
|
300 |
|
301 | my $scan_f = $scan;
|
302 |
|
303 | $scan_f =~ s/\[[0-9]+\]$//;
|
304 |
|
305 | if ( -f $scan_f ) {
|
306 |
|
307 | # ONE SCAN FILE
|
308 |
|
309 | # read scan file (converting to PNG)
|
310 | debug "Reading $scan";
|
311 | open( CONV, "-|", magick_module("convert"), $scan, "png:-" );
|
312 | my $surface = Cairo::ImageSurface->create_from_png_stream(
|
313 | sub {
|
314 | my ( $cb_data, $length ) = @_;
|
315 | read CONV, $data, $length;
|
316 | return ($data);
|
317 | }
|
318 | );
|
319 | close(CONV);
|
320 |
|
321 | my $context = Cairo::Context->create($surface);
|
322 | $context->set_line_width($line_width);
|
323 |
|
324 | my $lay = Pango::Cairo::create_layout($context);
|
325 |
|
326 | # adjusts text size...
|
327 | my $l0 = Pango::Cairo::create_layout($context);
|
328 | $l0->set_font_description(
|
329 | Pango::FontDescription->from_string(
|
330 | $font_name . ' ' . $test_font_size
|
331 | )
|
332 | );
|
333 | $l0->set_text('H');
|
334 | my ( $text_x, $text_y ) = $l0->get_pixel_size();
|
335 | my $page_width = $surface->get_width;
|
336 | my $page_height = $surface->get_height;
|
337 | debug "Scan height: $page_height";
|
338 | my $target_y = $page_height / $pointsize_rel;
|
339 | debug "Target TY: $target_y";
|
340 | my $font_size = int( $test_font_size * $target_y / $text_y );
|
341 | debug "Font size: $font_size";
|
342 |
|
343 | $lay->set_font_description(
|
344 | Pango::FontDescription->from_string(
|
345 | $font_name . ' ' . $font_size
|
346 | )
|
347 | );
|
348 | $lay->set_text('H');
|
349 | ( $text_x, $text_y ) = $lay->get_pixel_size();
|
350 |
|
351 | my ( $x_ppem, $y_ppem, $ascender, $descender, $width, $height,
|
352 | $max_advance );
|
353 |
|
354 | my $idf = pageids_string( @spc, path => 1 );
|
355 |
|
356 | print "Annotating $scan (sheet $idf)...\n";
|
357 |
|
358 | my %question = ();
|
359 |
|
360 | $capture->begin_read_transaction('xSTD');
|
361 |
|
362 | # print global mark and name on the page
|
363 |
|
364 | if ( $p->{page} == 1 || $capture->zones_count( @spc, ZONE_NAME ) ) {
|
365 | my $text = $subst->substitute( $verdict, @spc[ 0, 2 ] );
|
366 |
|
367 | $lay->set_text($text);
|
368 | $context->set_source_rgb( color_rgb('red') );
|
369 | if ($rtl) {
|
370 | my ( $tx, $ty ) = $lay->get_pixel_size;
|
371 | $context->move_to( $page_width - $text_x - $tx, $text_y * .7 );
|
372 | } else {
|
373 | $context->move_to( $text_x, $text_y * .7 );
|
374 | }
|
375 | Pango::Cairo::show_layout( $context, $lay );
|
376 | }
|
377 |
|
378 | #########################################
|
379 | # signs around each box
|
380 |
|
381 | my $sth = $capture->statement('pageZones');
|
382 | $sth->execute( @spc, ZONE_BOX );
|
383 | BOX: while ( my $b = $sth->fetchrow_hashref ) {
|
384 |
|
385 | my $p_strategy = $scoring->unalias( $p->{student} );
|
386 | my $q = $b->{id_a};
|
387 | my $r = $b->{id_b};
|
388 | my $indic = $scoring->indicative( $p_strategy, $q );
|
389 |
|
390 | next BOX if ( $indic && !$annote_indicatives );
|
391 |
|
392 | # to be ticked?
|
393 | my $bonne = $scoring->correct_answer( $p_strategy, $q, $r );
|
394 |
|
395 | # ticked on this scan?
|
396 | my $cochee =
|
397 | $capture->ticked( $p->{student}, $p->{copy}, $q, $r, $seuil,
|
398 | $seuil_up );
|
399 |
|
400 | debug "Q=$q R=$r $bonne-$cochee";
|
401 |
|
402 | my $sy = $symboles{"$bonne-$cochee"};
|
403 |
|
404 | if ($debug) {
|
405 | for my $i ( 1 .. 4 ) {
|
406 | debug(
|
407 | sprintf( "Corner $i: (%.2f,%.2f)",
|
408 | $capture->zone_corner( $b->{zoneid}, $i ) )
|
409 | );
|
410 | }
|
411 | }
|
412 |
|
413 | if (
|
414 | !(
|
415 | $layout->get_box_flags( $p->{student}, $q, $r,
|
416 | BOX_ROLE_ANSWER ) & BOX_FLAGS_DONTANNOTATE
|
417 | )
|
418 | )
|
419 | {
|
420 | if ( $sy->{type} eq 'circle' ) {
|
421 | cercle_coors( $context, $b->{zoneid}, $sy->{color} );
|
422 | } elsif ( $sy->{type} eq 'mark' ) {
|
423 | croix_coors( $context, $b->{zoneid}, $sy->{color} );
|
424 | } elsif ( $sy->{type} eq 'box' ) {
|
425 | boite_coors( $context, $b->{zoneid}, $sy->{color} );
|
426 | } elsif ( $sy->{type} eq 'none' ) {
|
427 | } else {
|
428 | debug "Unknown symbol type ($bonne-$cochee): $sy->{type}";
|
429 | }
|
430 | }
|
431 |
|
432 | # pour avoir la moyenne des coors pour marquer la note de
|
433 | # la question
|
434 |
|
435 | $question{$q} = {} if ( !$question{$q} );
|
436 | my @mil = milieu_cercle( $b->{zoneid} );
|
437 | push @{ $question{$q}->{x} }, $mil[0];
|
438 | push @{ $question{$q}->{y} }, $mil[1];
|
439 | }
|
440 |
|
441 | #########################################
|
442 | # write questions scores
|
443 |
|
444 | if ( $position ne 'none' ) {
|
445 | QUEST: for my $q ( keys %question ) {
|
446 | next QUEST if ( $scoring->indicative( $p_strategy, $q ) );
|
447 | my $x;
|
448 |
|
449 | my $result = $scoring->question_result( @spc[ 0, 2 ], $q );
|
450 |
|
451 | my $text;
|
452 |
|
453 | if ( $result->{why} =~ /c/i ) {
|
454 | $text = $verdict_question_cancelled;
|
455 | } else {
|
456 | $text = $verdict_question;
|
457 | }
|
458 |
|
459 | $text =~ s/\%[S]/$result->{score}/g;
|
460 | $text =~ s/\%[M]/$result->{max}/g;
|
461 | $text =~ s/\%[W]/$result->{why}/g;
|
462 | $text =~ s/\%[s]/$subst->format_note($result->{score})/ge;
|
463 | $text =~ s/\%[m]/$subst->format_note($result->{max})/ge;
|
464 |
|
465 | my $te = eval($text);
|
466 | if ($@) {
|
467 | debug "Annotation: $text";
|
468 | debug "Evaluation error $@";
|
469 | } else {
|
470 | $text = $te;
|
471 | }
|
472 |
|
473 | $lay->set_text($text);
|
474 | my ( $tx, $ty ) = $lay->get_pixel_size;
|
475 |
|
476 | # mean of the y coordinate of all boxes
|
477 | my $y = sum( @{ $question{$q}->{y} } ) /
|
478 | ( 1 + $#{ $question{$q}->{y} } ) - $ty / 2;
|
479 |
|
480 | if ( $position eq 'marge' ) {
|
481 |
|
482 | # scores written in one margin
|
483 | if ($rtl) {
|
484 | $x = $page_width - $ecart_marge * $text_x - $tx;
|
485 | } else {
|
486 | $x = $ecart_marge * $text_x;
|
487 | }
|
488 | } elsif ( $position eq 'case' ) {
|
489 |
|
490 | # scores written at the left of the boxes
|
491 | if ($rtl) {
|
492 | $x = max( @{ $question{$q}->{x} } ) + $ecart * $text_x;
|
493 | } else {
|
494 | $x =
|
495 | min( @{ $question{$q}->{x} } ) -
|
496 | $ecart * $text_x -
|
497 | $tx;
|
498 | }
|
499 | } elsif ( $position eq 'marges' ) {
|
500 |
|
501 | # scores written in one of the margins (left or right),
|
502 | # depending on the position of the boxes. This mode is often
|
503 | # used when the subject is in a 2-column layout.
|
504 |
|
505 | # fist extract the y coordinates of the boxes in the left column
|
506 | my $left = 1;
|
507 | my @y = map { $question{$q}->{y}->[$_] } grep {
|
508 | $rtl xor( $question{$q}->{x}->[$_] <= $page_width / 2 )
|
509 | } ( 0 .. $#{ $question{$q}->{x} } );
|
510 | if ( !@y ) {
|
511 |
|
512 | # if empty, use the right column
|
513 | $left = 0;
|
514 | @y = map { $question{$q}->{y}->[$_] } grep {
|
515 | $rtl
|
516 | xor( $question{$q}->{x}->[$_] > $page_width / 2 )
|
517 | } ( 0 .. $#{ $question{$q}->{x} } );
|
518 | }
|
519 |
|
520 | # set the x-position to the right margin
|
521 | if ( $left xor $rtl ) {
|
522 | $x = $ecart_marge * $text_x;
|
523 | } else {
|
524 | $x = $page_width - $ecart_marge * $text_x - $tx;
|
525 | }
|
526 |
|
527 | # set the y-position to the mean of y coordinates of the
|
528 | # boxes in the corresponding column
|
529 | $y = sum(@y) / ( 1 + $#y ) - $ty / 2;
|
530 | } else {
|
531 | debug "Annotation : position invalide : $position";
|
532 | $x = $ecart_marge * $text_x;
|
533 | }
|
534 |
|
535 | $context->set_source_rgb( color_rgb('red') );
|
536 | $context->move_to( $x, $y );
|
537 | Pango::Cairo::show_layout( $context, $lay );
|
538 | }
|
539 | }
|
540 |
|
541 | $capture->end_transaction('xSTD');
|
542 |
|
543 | # WRITE TO FILE
|
544 |
|
545 | $context->show_page;
|
546 |
|
547 | my $out_file = "page-$idf.jpg";
|
548 |
|
549 | debug "Saving annotated scan to $cr_dir/corrections/jpg/$out_file";
|
550 |
|
551 | my @args = ();
|
552 |
|
553 | if ($qualite_jpg) {
|
554 | if ( $qualite_jpg =~ /^[0-9]+$/ ) {
|
555 | push @args, "-quality", $qualite_jpg;
|
556 | } else {
|
557 | debug
|
558 | "WARNING: non-numeric --qualite argument, ignored ($qualite_jpg)";
|
559 | }
|
560 | }
|
561 |
|
562 | if ($taille_max) {
|
563 | if ( $taille_max =~ /^[0-9]*x?[0-9]*$/ ) {
|
564 | push @args, "-geometry", $taille_max;
|
565 | } else {
|
566 | debug
|
567 | "WARNING: malformed --taille-max argument, ignored ($taille_max)";
|
568 | }
|
569 | }
|
570 |
|
571 | open( CONV, "|-", magick_module("convert"),
|
572 | "png:-", @args, "$cr_dir/corrections/jpg/$out_file" );
|
573 | $surface->write_to_png_stream(
|
574 | sub {
|
575 | my ( $cb_data, $data ) = @_;
|
576 | print CONV $data;
|
577 | }
|
578 | );
|
579 | close(CONV);
|
580 |
|
581 | $capture->begin_transaction('ANNf');
|
582 | $capture->set_annotated( @spc, $out_file );
|
583 | $capture->end_transaction('ANNf');
|
584 |
|
585 | $n_processed_pages++;
|
586 |
|
587 | } else {
|
588 | print "No scan for page " . pageids_string(@spc) . ":$scan_f\n";
|
589 | debug "No scan: $scan_f";
|
590 | }
|
591 |
|
592 | $avance->progres($delta);
|
593 | }
|
594 |
|
595 | # stores state parameter to know all sheets have been annotated
|
596 |
|
597 | $capture->begin_transaction('Aend');
|
598 | $capture->variable( 'annotate_source_change', 0 );
|
599 | $capture->end_transaction('Aend');
|
600 |
|
601 | print "VAR: n_processed=$n_processed_pages\n";
|
602 |
|
603 | $avance->fin();
|
604 |
|