root/grabbers/ten_website @ 425

Revision 425, 40.1 kB (checked in by lincoln, 6 years ago)

ten_website grabber is alive

  • Property svn:executable set to *
Line 
1#!/usr/bin/perl -w
2
3# network TEN au_tv guide grabber - runs from "Shepherd" master grabber
4#  * grabs data from the network TEN website (http://www.ten.com.au)
5#    for channel TEN only
6#  * this does NOT use any config file - all settings are passed in from shepherd
7#  * idea based on OCR::PerfectCR CPAN module
8
9use strict;
10
11my $progname = "ten_website";
12my $version = "0.01";
13
14use LWP::UserAgent;
15use LWP::ConnCache;
16use XMLTV;
17use POSIX qw(strftime mktime);
18use Getopt::Long;
19use HTML::TreeBuilder;
20use Data::Dumper;
21use Compress::Zlib;
22use GD;
23use Digest::MD5;
24
25#
26# global variables and settings
27#
28
29$| = 1;
30my $script_start_time = time;
31my %stats;
32my $channels, my $opt_channels;
33my $data_cache;
34my $ua;
35my $conn_cache;
36my $prev_url;
37my $d;
38my $opt;
39my %amp = ( nbsp => ' ', qw{ amp & lt < gt > apos ' quot " } );
40
41
42#
43# parse command line
44#
45
46$opt->{days} =          7;                      # default
47$opt->{outputfile} =    "output.xmltv";         # default
48$opt->{cache_file} =    $progname.".cache";     # default
49$opt->{lang} =          "en";
50$opt->{region} =        94;
51
52GetOptions(
53        'log-http'      => \$opt->{log_http},
54        'region=i'      => \$opt->{region},
55        'days=i'        => \$opt->{days},
56        'offset=i'      => \$opt->{offset},
57        'timezone=s'    => \$opt->{timezone},
58        'channels_file=s' => \$opt->{channels_file},
59        'output=s'      => \$opt->{outputfile},
60        'cache-file=s'  => \$opt->{cache_file},
61        'fast'          => \$opt->{fast},
62        'no-cache'      => \$opt->{no_cache},
63        'no-details'    => \$opt->{no_details},
64        'debug+'        => \$opt->{debug},
65        'warper'        => \$opt->{warper},
66        'lang=s'        => \$opt->{lang},
67        'no-hdtv-flags' => \$opt->{no_hdtv_flags},
68        'obfuscate'     => \$opt->{obfuscate},
69
70        'ocr-learn-mode' => \$opt->{ocr_learn_mode},
71
72        'help'          => \$opt->{help},
73        'verbose'       => \$opt->{help},
74        'version'       => \$opt->{version},
75        'ready'         => \$opt->{version},
76        'v'             => \$opt->{help});
77
78&help if ($opt->{help});
79
80if ($opt->{version}) {
81        printf "%s %s\n",$progname,$version;
82        exit(0);
83}
84
85die "no channel file specified, see --help for instructions\n", if (!$opt->{channels_file});
86$opt->{days} = 7 if ($opt->{days} > 7); # limit to a max of 7 days
87
88# check XMLTV version for HDTV compatability
89my @xmltv_version = split(/\./,$XMLTV::VERSION);
90if (($xmltv_version[0] <= 0) && ($xmltv_version[1] <= "5") && ($xmltv_version[2] <= "43")) {
91        &log("XMLTV version ".$XMLTV::VERSION." too old to support HDTV flags. Disabling HDTV flags.");
92        $opt->{no_hdtv_flags} = 1;
93        $stats{disabled_hdtv_flag}++;
94}
95
96#
97# go go go!
98#
99
100&log(sprintf "going to grab %d days%s of data into %s (%s%s%s%s)",
101        $opt->{days},
102        (defined $opt->{offset} ? " (skipping first $opt->{offset} days)" : ""),
103        $opt->{outputfile},
104        (defined $opt->{fast} ? "with haste" : "slowly"),
105        (defined $opt->{warper} ? ", anonymously" : ""),
106        (defined $opt->{no_details} ? ", without details" : ", with details"),
107        (defined $opt->{no_cache} ? ", without caching" : ", with caching"));
108
109# read channels file
110if (-r $opt->{channels_file}) {
111        local (@ARGV, $/) = ($opt->{channels_file});
112        no warnings 'all'; eval <>; die "$@" if $@;
113} else {
114        die "WARNING: channels file $opt->{channels_file} could not be read\n";
115}
116
117die "TEN not listed as a channel in ".$opt->{channels_file}.", nothing to do!\n" if (!defined $channels->{TEN});
118
119&read_cache unless (defined $opt->{no_cache});
120
121&set_ua;
122&setup_charset;
123
124&set_region;
125
126&get_summary_pages;
127&get_detail_pages unless (defined $opt->{no_details});
128
129&write_xmltv;
130
131&write_cache unless (defined $opt->{no_cache});
132
133&print_stats;
134exit(0);
135
136##############################################################################
137# help
138
139sub help
140{
141        print<<EOF
142$progname $version
143
144options are as follows:
145        --help                  show these help options
146        --days=N                fetch 'n' days of data (default: $opt->{days})
147        --output=file           send xml output to file (default: "$opt->{outputfile}")
148        --no-cache              don't use a cache to optimize (reduce) number of web queries
149        --no-details            don't fetch detailed descriptions (default: do)
150        --no-hdtv-flags         don't mark HD programs as being in HDTV (default: do)
151        --cache-file=file       where to store cache (default "$opt->{cache_file}")
152        --fast                  don't run slow - get data as quick as you can - not recommended
153        --anonsocks=(ip:port)   use SOCKS4A server at (ip):(port) (for Tor: recommended)
154
155        --debug                 increase debug level
156        --warper                fetch data using WebWarper web anonymizer service
157        --obfuscate             pretend to be a proxy servicing multiple clients
158        --lang=[s]              set language of xmltv output data (default $opt->{lang})
159
160        --region=N              set region for where to collect data from (default: $opt->{region})
161        --channels_file=file    where to get channel data from
162
163        --ocr-learn-mode        put $progname into OCR learning mode to learn the text
164
165EOF
166;
167
168        exit(0);
169}
170
171##############################################################################
172# populate cache
173
174sub read_cache
175{
176        if (-r $opt->{cache_file}) {
177                local (@ARGV, $/) = ($opt->{cache_file});
178                no warnings 'all'; eval <>; die "$@" if $@;
179        } else {
180                printf "WARNING: no programme cache $opt->{cache_file} - have to fetch all details\n";
181
182                # try to write to it - if directory doesn't exist this will then cause an error
183                &write_cache;
184        }
185}
186
187##############################################################################
188# write out updated cache
189
190sub write_cache
191{
192        if (!(open(F,">$opt->{cache_file}"))) {
193                printf "ERROR: could not write cache file $opt->{cache_file}: $!\n";
194                printf "Please fix this in order to reduce the number of queries for data!\n";
195                exit 1;
196        } else {
197                # cleanup old entries from cache
198                for my $k (keys %{($data_cache->{id_cache})}) {
199                        if ($data_cache->{id_cache}->{$k}->{last_used} < (time-(86400*14))) {
200                                delete $data_cache->{id_cache}->{$k};
201                                $stats{expired_from_cache}++;
202                        }
203                }
204
205                for my $k (keys %{($data_cache->{detail_cache})}) {
206                        if ($data_cache->{detail_cache}->{$k}->{last_used} < (time-(86400*14))) {
207                                delete $data_cache->{detail_cache}->{$k};
208                                $stats{expired_from_cache}++;
209                        }
210                }
211
212                print F Data::Dumper->Dump([$data_cache], ["data_cache"]);
213                close F;
214        }
215}
216
217##############################################################################
218# logic to fetch a page via http
219#  retries up to $retrycount times to get a page with 10 second pauses inbetween
220
221sub get_url
222{
223        my ($url,$retrycount,$referer,$reqtype,$postvars) = @_;
224        my $request;
225        my $response;
226        my $attempts = 0;
227        my ($raw, $page, $base);
228
229        $reqtype = "GET" if (!defined $reqtype);
230
231        $retrycount = 5 if ($retrycount == 0);
232        $url =~ s#^http://#http://webwarper.net/ww/# if (defined $opt->{warper});
233
234        if ($reqtype eq "GET") {
235                $request = HTTP::Request->new(GET => $url);
236        } elsif ($reqtype eq "HEAD") {
237                $request = HTTP::Request->new(HEAD => $url);
238        } elsif ($reqtype eq "POST") {
239                $request = HTTP::Request->new(POST => $url);
240                $request->header('Content-type' => 'application/x-www-form-urlencoded');
241                $request->add_content($postvars);
242        }
243
244        if (defined $referer) {
245                $request->header('Referer' => $referer);
246        } else {
247                $request->header('Referer' => $prev_url) if (defined $prev_url);
248        }
249        $prev_url = $url;
250
251        $request->header('Accept-Encoding' => 'gzip');
252
253        if ($opt->{obfuscate}) {
254                my $randomaddr = sprintf "203.%d.%d.%d",rand(255),rand(255),(rand(254)+1);
255                $request->header('Via' => '1.0 proxy:81 (Squid/2.3.STABLE3)');
256                $request->header('X-Forwarded-For' => $randomaddr);
257        }
258
259        my $status;
260        for (1..$retrycount) {
261                $response = $ua->request($request);
262
263                if ((defined $opt->{log_http}) && (open(F,">>http_log.txt"))) {
264                        printf F "\n----------------------------------------------------\n";
265                        printf F "request: %s %s %s\n",$reqtype,$url,(defined $postvars ? $postvars : "");
266                        printf F "referer: %s\n", ($request->header('Referer') ? $request->header('Referer') : "(none)");
267                        printf F "response: %s\n",$response->status_line;
268                        print F $response->content;
269                        close F;
270                }
271
272                if ($response->is_success) {
273                        if ($response->content =~ /we are unable to process your request/) {
274                                $status = "fail: 999: Service unavailable"; # CPAN's LWP lied to us
275                        } else {
276                                $status = "good";
277                                last;
278                        }
279                } else {
280                        $status = "fail: ".$response->status_line;
281                }
282
283                $stats{http_failed_requests}++;
284                $attempts++;
285
286                my $sleep_for = 60;
287
288                &log("attempt $attempts of $retrycount failed to fetch $url, sleeping for $sleep_for secs: $status");
289
290                $stats{slept_for} += $sleep_for;
291                sleep $sleep_for;
292        }
293        if ($status !~ /^good/) {
294                &log("aborting after $attempts attempts to fetch url $url");
295                return undef;
296        }
297
298        $prev_url = $response->base;
299        $prev_url =~ s#^http://webwarper.net/ww/#http://# if (defined $opt->{warper});
300
301        $stats{bytes_fetched} += do {use bytes; length($response->content)};
302        $stats{http_successful_requests}++;
303
304        if ($reqtype eq "HEAD") {
305                return $response->header("Content-Length");
306        }
307
308        if ($response->header('Content-Encoding') &&
309            $response->header('Content-Encoding') eq 'gzip') {
310                $stats{compressed_pages} += do {use bytes; length($response->content)};
311                $response->content(Compress::Zlib::memGunzip($response->content));
312        }
313        return $response->content;
314}
315
316##############################################################################
317
318sub log
319{
320        my ($entry) = @_;
321        printf "%s [%d] %s\n",$progname,time,$entry;
322}
323
324##############################################################################
325
326sub print_stats
327{
328        printf "STATS: %s v%s completed in %d seconds",$progname, $version, time-$script_start_time;
329        foreach my $key (sort keys %stats) {
330                printf ", %d %s",$stats{$key},$key;
331        }
332        printf "\n";
333}
334
335##############################################################################
336# descend a structure and clean up various things, including stripping
337# leading/trailing spaces in strings, translations of html stuff etc
338#   -- taken & modified from Michael 'Immir' Smith's excellent tv_grab_au
339
340sub cleanup {
341        my $x = shift;
342        if    (ref $x eq "REF")   { cleanup($_) }
343        elsif (ref $x eq "HASH")  { cleanup(\$_) for values %$x }
344        elsif (ref $x eq "ARRAY") { cleanup(\$_) for @$x }
345        elsif (defined $$x) {
346                $$x =~ s/&(#(\d+)|(.*?));/ $2 ? chr($2) : $amp{$3}||' ' /eg;
347                $$x =~ s/[^\x20-\x7f]/ /g;
348                $$x =~ s/(^\s+|\s+$)//g;
349        }
350}
351
352##############################################################################
353
354sub write_xmltv
355{
356        my $writer;
357
358        my %writer_args = ( encoding => 'ISO-8859-1' );
359        if ($opt->{outputfile}) {
360                my $fh = new IO::File(">$opt->{outputfile}") || die "can't open $opt->{outputfile}: $!";
361                $writer_args{OUTPUT} = $fh;
362        }
363
364        $writer = new XMLTV::Writer(%writer_args);
365
366        $writer->start
367          ( { 'source-info-name'   => "$progname $version",
368              'generator-info-name' => "$progname $version"} );
369
370        $writer->write_channel( {
371                'display-name' => [[ "TEN", $opt->{lang} ]], 'id' => $channels->{TEN} } );
372
373        foreach my $prog (@{($d->{progs})}) {
374                # convert epoch starttime into XMLTV starttime
375                next if (!defined $prog->{starttime});
376                $prog->{start} = strftime "%Y%m%d%H%M", localtime($prog->{starttime});
377                delete $prog->{starttime};
378
379                # convert epoch stoptime into XMLTV stoptime
380                next if (!defined $prog->{stoptime});
381                $prog->{stop} = strftime "%Y%m%d%H%M", localtime($prog->{stoptime});
382                delete $prog->{stoptime};
383
384                delete $prog->{details};
385                delete $prog->{id};
386
387                &cleanup($prog);
388                printf "DEBUG: programme xmltv: ".Dumper($prog) if (defined $opt->{debug});
389                $writer->write_programme($prog);
390        }
391
392        $writer->end();
393}
394
395##############################################################################
396
397sub set_ua
398{
399        my @agent_list = (
400                'Mozilla/4.0 (compatible; MSIE 5.5; Windows 98)',
401                'Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1)',
402                'Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; FunWebProducts)',
403                'Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322)',
404                'Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322; .NET CLR 2.0.50727)',
405                'Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; Q312466)',
406                'Mozilla/4.0 (compatible; MSIE 6.0; Windows XP)',
407                'Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en-us) AppleWebKit/85.8.5 (KHTML, like Gecko) Safari/85.8.1',
408                'Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.0.4) Gecko/20060508 Firefox/1.5.0.4',
409                'Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.7.6) Gecko/20050512 Firefox',
410                'Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.0.8) Gecko/20061025 Firefox/1.5.0.8',
411                'Mozilla/5.0 (Windows; U; Windows NT 5.1; de; rv:1.8.1) Gecko/20061010 Firefox/2.0',
412                'Mozilla/5.0 (compatible; Yahoo! Slurp; http://help.yahoo.com/help/us/ysearch/slurp)',
413                'Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en-us) AppleWebKit/412 (KHTML, like Gecko) Safari/412',
414                'Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en-us) AppleWebKit/418.9 (KHTML, like Gecko) Safari/419.3',
415                'Mozilla/5.0 (Macintosh; U; Intel Mac OS X; fr) AppleWebKit/418.9 (KHTML, like Gecko) Safari/419.3',
416                'Opera/9.00 (Windows NT 5.1; U; en)');
417
418        $ua = undef;
419        $conn_cache = LWP::ConnCache->new;
420        $ua = LWP::UserAgent->new('conn_cache' => $conn_cache, 'timeout' => 30, 'agent' => $agent_list[(int(rand($#agent_list+1)))] );
421        $ua->env_proxy;
422        $ua->cookie_jar({ });
423
424
425        $prev_url = undef; # reset referer
426}
427
428##############################################################################
429
430sub urlify
431{
432        my $str = shift;
433        $str =~ s/([^A-Za-z0-9])/sprintf("%%%02X", ord($1))/seg;
434        $str =~ s/%20/+/g;
435        $str =~ s/%2D/-/g;
436        return $str;
437}
438
439##############################################################################
440
441sub translate_category
442{
443        my $genre = shift;
444        my %translation = (
445                'Sport' => 'sports',
446                'Soap Opera' => 'Soap',
447                'Science and Technology' => 'Science/Nature',
448                'Real Life' => 'Reality',
449                'Cartoon' => 'Animation',
450                'Family' => 'Children',
451                'Murder' => 'Crime' );
452
453        return $translation{$genre} if defined $translation{$genre};
454        return $genre;
455}
456
457##############################################################################
458
459sub set_region
460{
461        &log("setting region");
462
463        my $url = sprintf "http://www.ten.com.au/citySelect.aspx?change=true";
464        my $data = &get_url($url,5);
465        if (!$data) {
466                &log("CRITICAL ERROR: Could not set region because of error fetching '$url'");
467                exit(1);
468        }
469
470        my $tree = HTML::TreeBuilder->new_from_content($data);
471        if (!$tree) {
472                &log("CRITICAL ERROR: url '$url' doesn't seem to contain any valid HTML: has the format changed?");
473                exit(1);
474        }
475
476        $opt->{viewstate} = $_->attr('value') if ($_ = $tree->look_down('_tag' => 'input', 'type' => 'hidden', 'name' => '__VIEWSTATE'));
477        if (!defined $opt->{viewstate}) {
478                &log(" has the format changed? could not find 'viewstate'...");
479                $stats{viewstate_not_found}++;
480                $opt->{viewstate} = "";
481        }
482
483        my $reg = 1;                                                    # sydney
484        $reg = 2 if ($opt->{region} =~ /^9/);                           # melbourne
485        $reg = 3 if ($opt->{region} =~ /(75|78|79|114|74|108)/);        # brisbane
486        $reg = 4 if ($opt->{region} =~ /(101|102)/);                    # perth
487        $reg = 5 if ($opt->{region} =~ /(81|82|83|85|86|107)/);         # adelaide
488
489        my $postvars = "__VIEWSTATE=".urlify($opt->{viewstate})."&new_site_id=".$reg."&_ctl1.x=0&_ctl1.y=0";
490        $data = &get_url($url, 5, undef, "POST", $postvars);
491
492        $stats{programmes} = 0 if (!defined $stats{programmes});
493}
494
495##############################################################################
496
497sub get_summary_pages
498{
499        my $starttime = time;
500        my $day_num = 0;
501        my $skip_days = 0;
502
503        $skip_days = $opt->{offset} if (defined $opt->{offset});
504        while ($day_num < $opt->{days}) {
505                my $currtime = $starttime + (60*60*24 * $day_num);
506                $day_num++;
507
508                # skip if --offset applies against this day
509                if ($skip_days > 0) {
510                        $skip_days--;
511                        next;
512                }
513
514                my @timeattr = localtime($currtime); # 0=sec,1=min,2=hour,3=day,4=month,5=year,6=wday,7=yday,8=isdst
515                $timeattr[0] = 0; # zero sec
516                $timeattr[1] = 0; # zero min
517                $timeattr[2] = 0; # midnight
518                my $day_start = mktime(@timeattr);
519
520                &log("fetching day $day_num summary page");
521                &parse_summary_page($day_start, $day_num);
522        }
523}
524
525##############################################################################
526
527sub parse_summary_page
528{
529        my ($day_start, $day_num) = @_;
530        my %seen_prog;
531
532        my $url = "http://www.ten.com.au/programGuide.aspx?section=programGuide";
533        my $postvars = "__VIEWSTATE=".urlify($opt->{viewstate}).
534                "&newDate=".urlify(POSIX::strftime("%Y-%m-%d %H:%M:%S",localtime($day_start))).".000".
535                "&newTime=&newGenre=&newKeyword=&filterButton.x=0&filterButton.y=0&sq=";
536        $postvars .= "&storeDate=".urlify($opt->{storedate}) if (defined $opt->{storedate});
537
538        &log("parse_summary_page debug: day_start $day_start day_num $day_num POST $postvars") if (defined $opt->{debug} && $opt->{debug} > 2);
539
540        my $data = &get_url($url, 5, undef, "POST", $postvars);
541        if (!$data) {
542                &log("url '$url' (POST $postvars) doesn't seem to contain any valid response.");
543                $stats{bad_response}++;
544                return;
545        }
546
547        my $tree = HTML::TreeBuilder->new_from_content($data);
548        if (!$tree) {
549                &log("url '$url' (POST $postvars) doesn't seem to contain any valid HTML: has the format changed?");
550                $stats{bad_html}++;
551                return;
552        }
553
554        if (my $viewstate_field = $tree->look_down('_tag' => 'input', 'type' => 'hidden', 'name' => '__VIEWSTATE')) {
555                my $new_viewstate = $viewstate_field->attr('value');
556                $opt->{viewstate} = $new_viewstate if (defined $new_viewstate);
557        }
558
559        if (my $storedate_field = $tree->look_down('_tag' => 'input', 'type' => 'hidden', 'name' => 'storeDate')) {
560                my $page_date = $storedate_field->attr('value');
561                $opt->{storedate} = $page_date if (defined $page_date);
562        }
563
564        my $progs_in_day = 0;
565
566        my @tree_rows = $tree->look_down('_tag' => 'tr');
567        foreach my $tree_pg (@tree_rows) {
568                my $yellow_row_count = 0;
569                my $prog_bg;
570                my $prog_fg;
571                my $prog_details;
572                my $prog_rating;
573                my $prog_cc;
574                my $prog_hd;
575
576                foreach my $prog_td ($tree_pg->look_down('_tag' => 'td')) {
577                        my $prog_td_class = $prog_td->attr('class');
578                        if ((defined $prog_td_class) && ($prog_td_class eq "pgimgcell") && (!defined $prog_bg)) {
579                                if (my $style_tag = $prog_td->attr('style')) {
580                                        if ($style_tag =~ /^background:url\((.*)\) /) {
581                                                $prog_bg = $1;
582                                                &log("got prog_bg '$prog_bg'") if (defined $opt->{debug} && $opt->{debug} > 1);
583                                        }
584                                }
585
586                                if ((!defined $prog_fg) && (my $img_tag = $prog_td->look_down('_tag' => 'img', 'class' => 'pgimg'))) {
587                                        $prog_fg = $img_tag->attr('src');
588                                        &log("got prog_fg '$prog_fg'") if (defined $opt->{debug} && $opt->{debug} > 1);
589                                }
590
591                                if ((!defined $prog_details) && (my $link_tag = $prog_td->look_down('_tag' => 'a', 'href' => '#'))) {
592                                        my $link_url = $link_tag->attr('onClick');
593                                        if ($link_url =~ /^window\.open\('(.+?)'/) {
594                                                $prog_details = $1;
595                                                &log("got prog_details '$prog_details'") if (defined $opt->{debug} && $opt->{debug} > 1);
596                                        }
597                                }
598                        } elsif ((defined $prog_td_class) && ($prog_td_class eq "yellow")) {
599                                $yellow_row_count++;
600                                if ($yellow_row_count == 1) {           # HD flag
601                                        if ($prog_td->as_text() =~ /HD/) {
602                                                $prog_hd = 1;
603                                                &log("prog is in HD") if (defined $opt->{debug} && $opt->{debug} > 1);
604                                        } else {
605                                                $prog_hd = 0;
606                                        }
607                                } elsif ($yellow_row_count == 2) {      # CC flag
608                                        if ($prog_td->as_text() =~ /Y/) {
609                                                $prog_cc = 1;
610                                                &log("prog has closed captions") if (defined $opt->{debug} && $opt->{debug} > 1);
611                                        } else {
612                                                $prog_cc = 0;
613                                        }
614                                } elsif ($yellow_row_count == 3) {      # rating
615                                        $prog_rating = $prog_td->as_text();
616                                        &log("prog has rating '$prog_rating'") if (defined $opt->{debug} && $opt->{debug} > 1);
617                                }
618                        }
619                }
620
621                &log("finished evaluating row") if (defined $opt->{debug} && $opt->{debug} > 1);
622
623                if ((defined $prog_fg) && (defined $prog_bg) && (defined $prog_details)) {
624                        next if (defined $seen_prog{$prog_details});
625                        $seen_prog{$prog_details}++;
626
627                        $progs_in_day++;
628                        &parse_one_summary_prog($day_start, $day_num, $progs_in_day, $prog_fg, $prog_bg, $prog_details, $prog_rating, $prog_hd, $prog_cc);
629                }
630        }
631
632        &log("WARNING: Only $progs_in_day programmes seen on day $day_num in '$url' (POST $postvars). ".
633          "Data may be bad.") if ($progs_in_day < 10);
634
635        unless (defined $opt->{fast}) {
636                my $sleep_for = 20 + int(rand(5));
637                &log(" .. found $progs_in_day programmes on day $day_num, sleeping for $sleep_for seconds.");
638                sleep $sleep_for;
639                $stats{slept_for} += $sleep_for;
640        }
641}
642
643##############################################################################
644# given a summary-line of a programme, turn it into a prog entry
645
646sub parse_one_summary_prog
647{
648        my ($day_start, $day_num, $progs_in_day, $prog_fg, $prog_bg, $prog_details, $prog_rating, $prog_hd, $prog_cc) = @_;
649        my $prog;
650
651        my $id;
652        $id = $1 if ($prog_bg =~ /id=(\d+)$/);
653
654        my $s;
655
656        if (!defined $data_cache->{id_cache}->{$id}) {
657                $s = &ocr_image($id, "http://www.ten.com.au".$prog_fg, "http://www.ten.com.au".$prog_bg, 3, ($day_num == 1 ? 6 : 14), 0);
658                $data_cache->{id_cache}->{$id}->{ocr_text} = $s;
659        } else {
660                $s = $data_cache->{id_cache}->{$id}->{ocr_text};
661                $stats{used_cached_items}++;
662        }
663        $data_cache->{id_cache}->{$id}->{last_used} = time;
664
665        if ($s =~ /\s*(\d+):(\d+)\s*(A|P)M\s+(.*)$/) {
666                my $prog_hr = $1;
667                $prog_hr = 0 if ($prog_hr == 12);
668
669                $prog->{starttime} = (($prog_hr * 60) + $2) * 60; # seconds
670                $prog->{starttime} += (60*60*12) if (lc($3) eq "p");
671
672                $prog->{title} = [[ $4, $opt->{lang} ]];
673        } elsif ($s =~ /\s*(\d+)\.(\d+)\.(\d+)\s+(\d+):(\d+)\s*(A|P)M\s+(.*)$/) {
674                my $prog_hr = $4;
675                $prog_hr = 0 if ($prog_hr == 12);
676
677                $prog->{starttime} = (($prog_hr * 60) + $5) * 60; # seconds
678                $prog->{starttime} += (60*60*12) if (lc($6) eq "p");
679
680                $prog->{title} = [[ $7, $opt->{lang} ]];
681        }
682
683        if (!defined $prog->{title}) {
684                &log("could not parse progname from OCR string '$s'. Format changed?");
685                $stats{unparsable_name}++;
686                return;
687        }
688        if (!defined $prog->{starttime}) {
689                &log("could not parse start time from OCR string '$s'. Format changed?");
690                $stats{unparsable_time}++;
691                return;
692        }
693
694        if ($prog->{starttime} < (12*60*60)) {
695                $prog->{starttime} += (24*60*60) if (defined $d->{seen_pm}->[$day_num]);
696        } else {
697                $d->{seen_pm}->[$day_num] = 1 if (!defined $d->{seen_pm}->[$day_num]);
698        }
699        $prog->{starttime} += $day_start;
700
701        $prog->{id} = $id;
702        $prog->{details} = $prog_details;
703        $prog->{channel} = $channels->{TEN};
704
705        if ((defined $prog_rating) && ($prog_rating ne "")) {
706                my @ratings;
707                push(@ratings, [$prog_rating, 'ABA', undef]);
708                $prog->{rating} = [ @ratings ];
709        }
710
711        $prog->{subtitles} = [ { 'type' => 'teletext' } ] if ($prog_cc);
712        if ($prog_hd) {
713                $prog->{video}->{aspect} = "16:9";      # widescreen
714                $prog->{video}->{quality} = "HDTV" unless (defined $opt->{no_hdtv_flags});
715        }
716
717        push(@{($d->{progs})},$prog);
718
719        if ($stats{programmes} > 0) {
720                # set previous stoptime based on this starttime
721                $d->{progs}->[($stats{programmes}-1)]->{stoptime} = $prog->{starttime};
722        }
723        $stats{programmes}++;
724
725        &log("parse_one_summary_prog: d".$day_num."p".$progs_in_day." ".$prog_details.": start:".$prog->{starttime}." name:".$prog->{title}->[0]->[0]) if (defined $opt->{debug});
726
727}
728
729##############################################################################
730
731sub get_detail_pages
732{
733        my $prog_count;
734        $stats{used_detailed_cache} = 0;
735        &log("fetching up to ".$stats{programmes}." detail pages...");
736
737        foreach my $prog (@{($d->{progs})}) {
738                my $was_in_cache = 0;
739
740                $prog_count++;
741                my $details = $prog->{details};
742
743                if (($prog_count % 10) == 1) {
744                        &log(" .. at programme ".$prog_count." of ".$stats{programmes}." (".$stats{used_detailed_cache}." from cache)");
745                }
746
747                if (!defined $data_cache->{detail_cache}->{$details}) {
748                        my $url = "http://www.ten.com.au/".$details;
749                        my $data = &get_url($url,5);
750                        my $tree = HTML::TreeBuilder->new_from_content($data) if (defined $data);
751                        if ((!$data) || (!$tree)) {
752                                &log("url '$url' doesn't seem to contain any valid details. Has the format changed?");
753                                $stats{bad_detail_response}++;
754                                return;
755                        }
756
757                        # parse description from first 'class=info' table cell
758                        my $prog_desc = $tree->look_down('_tag' => 'td', 'class' => 'info', 'style' => 'text-align:justify;');
759                        if (defined $prog_desc) {
760                                $data_cache->{detail_cache}->{$details}->{desc} = [[ $prog_desc->as_text(), $opt->{lang} ]];
761                                &log("got desc '".$prog_desc->as_text()."'") if (defined $opt->{debug} && $opt->{debug} > 1);
762                        }
763
764                        my $genre_group = &get_url("http://www.ten.com.au/pgutil/epfront.ashx?cd=2&id=".$prog->{id},3,undef,"HEAD");
765                        if ((defined $genre_group) && ($genre_group ne "")) {
766                                if (defined $d->{gset}->{$genre_group}) {
767                                        $data_cache->{detail_cache}->{$details}->{category} = [[ translate_category($d->{gset}->{$genre_group}), $opt->{lang} ]];
768                                } else {
769                                        $data_cache->{detail_cache}->{$details}->{category} = [[ $genre_group, $opt->{lang} ]];
770                                        &log("unknown genre group '$genre_group' for prog '".$prog->{title}->[0]->[0]."'");
771                                        $stats{unknown_genre}++;
772                                }
773                        }
774
775                        if ($data =~ /EPISODE:/) {
776                                my $s = &ocr_image($prog->{id}, "http://www.ten.com.au/pgutil/epfront.ashx?cd=1&id=".$prog->{id}, "http://www.ten.com.au/pgutil/epback.ashx?cd=1&id=".$prog->{id}, 3, 0, 1);
777                                if ((defined $s) && ($s ne "")) {
778                                        $data_cache->{detail_cache}->{$details}->{'sub-title'} = [[ $s, $opt->{lang} ]];
779                                }
780                        }
781
782                        $stats{used_detailed_cache}++;
783                        $was_in_cache = 1;
784                }
785
786                $data_cache->{detail_cache}->{$details}->{last_used} = time;
787
788                $prog->{desc} = $data_cache->{detail_cache}->{$details}->{desc}
789                    if (defined $data_cache->{detail_cache}->{$details}->{desc});
790
791                $prog->{category} = $data_cache->{detail_cache}->{$details}->{category}
792                    if (defined $data_cache->{detail_cache}->{$details}->{category});
793
794                $prog->{'sub-title'} = $data_cache->{detail_cache}->{$details}->{'sub-title'}
795                    if (defined $data_cache->{detail_cache}->{$details}->{'sub-title'});
796
797                unless ((defined $opt->{fast}) || ($was_in_cache)) {
798                        my $sleep_for = 3 + int(rand(2));
799                        sleep $sleep_for;
800                        $stats{slept_for} += $sleep_for;
801                }
802        }
803}
804
805##############################################################################
806
807sub parse_characters
808{
809        my ($imgname,$i,$space_threshold) = @_;
810
811        my ($width, $height) = $i->getBounds;
812        my $bg = $i->getPixel(0,0);
813
814        # flatten image colours
815        my @bgcolour;
816        foreach my $index (0..$i->colorsTotal) {
817                my @r = $i->rgb($index);
818                my $total = $r[0]+$r[1]+$r[2];
819
820                if ($total > (240*3)) {
821                        $bgcolour[$index] = 1;
822                } else {
823                        $bgcolour[$index] = 0;
824                }
825        }
826
827        my $char_x1 = 0;
828        my $char_x2 = 0;
829        my $last_char_x = 0;
830        my $done = 0;
831        my $charnum = 0;
832        my $s = "";
833
834        while (!$done) {
835                $char_x2++;
836                $done = 1 if ($char_x2 >= ($width-1));
837
838                my $blank_line = 1;     # until proven otherwise
839                my $char_y = 0;
840                while (($char_y < ($height-1)) && ($blank_line)) {
841                        my $index = $i->getPixel($char_x2,$char_y);
842                        $blank_line = 0 if ($bgcolour[($i->getPixel($char_x2,$char_y))] == 0);
843                        $char_y++;
844                }
845
846                if (($blank_line) || ($char_x2 == ($width-1))) {
847                        if (($char_x2 - $char_x1) == 1) {
848                                $char_x1 = $char_x2 + 1;
849                                $char_x2++;
850                                next;
851                        }
852
853                        if (($char_x1 - $last_char_x) >= 3) {
854                                if (($charnum > $space_threshold) || (($char_x1 - $last_char_x) >= 4)) {
855                                        $s .= " ";
856                                        $charnum++;
857                                }
858                        }
859                        $last_char_x = $char_x2;
860
861                        my $str = pack('N',($char_x2-$char_x1));
862                        foreach my $x ($char_x1..$char_x2) {
863                                foreach my $y (0..($height-1)) {
864                                        $str .= pack('N', $bgcolour[($i->getPixel($x, $y))]);
865                                }
866                        }
867                        my $md5 = Digest::MD5::md5_hex($str);
868                        $charnum++;
869
870                        if ((!defined $d->{charset}->{$md5}) || ($d->{charset}->{$md5} eq "?")) {
871                                if (defined $opt->{ocr_learn_mode}) {
872                                        $d->{charset}->{$md5} = "[".$md5."]";
873                                        $s .= "[".$md5."]";
874                                } else {
875                                        $s .= "?";
876                                }
877                        } else {
878                                $s .= $d->{charset}->{$md5};
879                        }
880
881                        $char_x1 = $char_x2+1;
882                        $char_x2++;
883                }
884        }
885
886        return $s;
887}
888
889##############################################################################
890
891sub setup_charset
892{
893        my %charset = qw{
894                87b7de3dbbeda572e883253803f73a78 e caa8c600dd0aecf49f445753963e97b5 : 46854a6efed48426f1018828cca41ac2 o
895                f64db65ec25ba73bdced42fcf01be00a r d0ec43eddec59827259b46c460386ae0 l d4fb05e2702c4199a73794b5def96ec2 G
896                ca1cbc6861523c4608f19365cac6dde0 0 a49667c09e9d4be0b595578d51eeb60d M 1a9de7fb6f1c93f3ffaa15816549e43a 6
897                fbec6375cab7ff5b9d4b4783c7aab13b s b1465cc2781264fff5a55a9e9b3d8064 A b61b8c026407890a23276d41125d7e98 h
898                8e2a682942360201f924e694dc70fa43 T c1e402fec1d35694b1898b1f1dbb16bb w bed2eaba5e16b7246bb1f5b94d44b61c h
899                c0107f886a27e42ce8fd2eca63a5ebcc D 8300a291d7dae2e876126878c98af6d1 S 7edd9f81d7da6577d57da07f93f95b87 E
900                ec5d1b2140213fdbbf3c837400b2d3c5 e 14e3c224bd590504ffa95a1987ac3fbc n 55ced8bf6a2a2482f578bc988b60b5ed a
901                51bc70bfed877b2bf7300a5023a88634 o 5ae0c7cc64eb457ed198ee008fcd52f9 d 8769704a7c47684c74d841673664f942 V
902                763602fa61fe36273a3492f3fbae0ff8 t 0efb2ccf6c4e8b3084e56da89ad6629b 7 70dddd8427594526c8fd308b6151d673 i
903                75419c36d52e0f29143d4ecf3c5fd2fd W 592559bccc3f515e5d2a93622320a1a2 m 4e4073891b344c07deceee07cd6ba348 g
904                9e2f28787475e105da5221e20eb7a137 r f7e8dade3df2070be62dd206ef0cc8f1 5 62982338ad7a6b499056bac67f840d83 N
905                97fd0fdcedad187e8bf877adc5d580c5 2 35f9e067a546f3ae0057065223fe4c33 3 97f15e1d2ad1cb232147d0b6f01c8022 O
906                4c52033ce6a724d184d9c8d23a960d6e P 9fbd3153eb8e55a0a1f453ee33e6bafd i e7a5cbb21f17f35f2f141e63f37a45fb c
907                f7e8dade3df2070be62dd206ef0cc8f1 5 646db1d6726727e809ed1eb7ea11f545 8 c5ed676c18b62bc6885e34bf527e66af Y
908                99bea8c75f15219ca16a7229b3938665 u a9ea989899145834e84daf0abc5964f0 ! 8113592ffa186852672d458f5bd86135 k
909                fec8880342772dd7e83ca9ffeed0e216 l 185a57d42d98c6cbd85135d9e8295501 D 606bf5428471cfd5de3434374c281334 y
910                50692dc12cde0fae151d9a0c2563c81d J f3045893d14fbb5f20e215a38617aee4 0 cf78a362c08ef3b9284ade8113e670d7 R
911                0de173cf09ded97fff935aa24f7f8bfe z cf63706b1f8eaa1c9120e1f9794918c3 T f9f2e0d23af08cb6fbeacb686992f633 v
912                c1777c45a7d53a5d557c5da145bea080 ' 88f4902f74cf89846318c96003466835 p 2b821839a93b75e470d04a5e2c1971b3 J
913                120cfb2dcf74d7900dc22d44bea9db09 H 298b488eb21a879c4cf9007c05283a15 s 08021ebe5ef72c0ed41b438fd794e71e tt
914                b24415f6bccb3a9ad482156a524dbf1e y 6c27fb8ed1d2d451785d957138ca0902 u 5a6e6307a1b18b409618616556a327e5 E
915                8d4430c7857a01d4805b4666c54fe114 b cf9c23550ff1fde3b19b593966fdd391 S 97986e54d74ef7047eebc1169134564f B
916                561fda757040c25038687752394d39a8 M 8015f8d4c3d6574c9ec73b412ece2013 L 7fdc4d50db244ad00f11d7c362f10b8f 9
917                73f0455d71b4156ab2bbebb7fac004ca 4 401ccf9844fe6399f13597cb458abedb a 28de7104f0f94e161104c407071a5e91 m
918                cc282e429660787afc4a292a6e35cb2a F 449bb458f502dbb10cf71673d1bd7ac4 5 eb6c72d1cb3b32bfcf646e2c5dafc4d2 N
919                aa364cab095bc5f46f855c9772619f5e 1 6aadacaa0e0b622fe755be8615f67f87 2 0f87f473885da54c2a7c886ae92f0ddd R
920                17d10978ffc796cc024c68afa3fb463c I 70b21817f2611845e464f8b551c73b71 1 f5a215139fdc4921b4fad687e0899fdc H
921                8b9e1cc11d23773ca68afaea3064902a A 6adf28b9140e9b236394bd6956638630 9 0d2eeea7b20edb640d5556ea8528ba67 K
922                3503cdc59df22be3b6242db35cfe3482 f b5856240a388696d55ea99fad53166ce W 4b8e08032dde00ced51e8435820be5e1 n
923                38b1c7da79cbbac219c590129f40cca1 k 1697c04376dac187f028f240cb0ccc9a C 2e03a06a91a1993a5c6e15b43784e5c3 3
924                840e43645d65217fd0d57914321db2bf : 5e871ec322ade9e74d44285c3ddad972 L e3bcb0065109e004bc6b18b1403fb810 rt
925                9f32b9cd5083733eead4380bb6551ac5 B 148cafcb02f1a203866f583dbdb253af & 519cc9d317d1a6db113c0da6e5560e71 d
926                40ec9716cfe72fe54201dae866e70ec5 V ace17452c10518e97caba9493898c910 U d244b3a33602a55c1ee8cf9c570dced9 -
927                25ee9123a9fdb7c164b29dfaa50d10b7 6 a87bc5bc8b3e5df44df2e2405561dd83 . f6e64e873007d53c7bf7873d639f4678 .
928                de8b17aa3cf358a1e8b9496dd99e20f1 7 21e73997781a1af8c506eded30c6143f 4 4518bf9cb085588761164be21442aa5d F
929                c38b4e845130be00f1a27a023241a500 ! f5a9cba4badf510bbde66e1012647c8c O c67d0abf9dd1bf2352613c243de4649b P
930                453b59cf0cb2813958d5518fc668639c Z 3dad6dcdedabfbb99ef2067f38d6bd67 B b2da7f7ca8c9be23ca445a7df954a4f2 8
931                8a3bf2c9eb10c811e50c91759e6e57cc G f9e0333c0725c22b198bc0c3a7aa4a51 x 61ea6df7256f910d1cb031979d7d1eda C
932                588b076556aa1b58810fe1f97fa77371 Y 8a3bf2c9eb10c811e50c91759e6e57cc G 61ea6df7256f910d1cb031979d7d1eda C
933
934                5892305501d6d7b3c944edcdfac487b0 W cb28d04e3bbe3bfd0bf0086b5b50b50e a d9f38cfa215b61b0baf8d3232ab71e5a c
935                bcfbf5865682d0d691b0ba7ad34b4e5f k 0ff718ec0df83d26df8ef58f27af3e1d y df27299772b1c1fa25bc74e3e0b28519 M
936                e74795b60c312f1fa48d956433cffd67 d e1bcd7c44b8fd705281926db43eae7f2 n c677cf0e0d2124629e224628a01a96fe e
937                df74545eacbaf90dc1206ef81be97bbb s 118dfa4e0e53dbde0a74554c16f4b6e8 A 6ac1a637edb8d167b9b0263b72d30d50 B
938                ab0135e45bdc858357c40d35e2a6d662 l 202c9276948bf52699ef2521988c2ed0 z f65810bb9e22c25d31a442b3ff1ec3e8 i
939                f079edc2a2167e5c3b5a0250130ad3cc g 6ac1a637edb8d167b9b0263b72d30d50 B 238d4f228563b5efcd46fdb0ee0fa367 ttl
940                df74545eacbaf90dc1206ef81be97bbb s acf02f7463a907c98ccfdaf1364e506a ( 761086404df3dd6a879c15722e6b5c72 P
941                f932ec8ddd3f2edd739a715090614687 1 ff8c0771c4dc7c6a1867ada5d47c1446 ) 26e1d6a4efa3a6e7d107a7003924ad9f rt
942                27e56f6930a29f7ccb1f2ed98c2c99be G 168527e83abcaee41f74514b627b651a ra 9ab22051e33a6755e407cc69ea9d02b4 a
943                9ab22051e33a6755e407cc69ea9d02b4 p 0e6e0a842f847b0997de866dcb69fd7d th 98dee45f3aa315b8d6d1c2a83208e158 u
944                90b5c188102f105c0cab2556d27b0788 rd 154690fb8d4578148e1513ab0f921076 P 34065c67fbb12cce0561001cd462d573 a
945                1d58b69f2b50b50daacfb7645a0fdd18 rt 13f9bf707f893bc39e10ce0475e151a7 I 3af52f596fd1c33743a59d7fa816aaa3 ts
946                2e1e8bd83e52ee09bb58297aeb1da158 Th 1e128beba3aff04a49fba2b291603579 Re aa296120499cf1ee8868ec6759895f9b m
947                f74eae3e6c5426b5da01fb1ad236e1a2 Tw faa3822c5ea6489c829cafc96ba86271 o a507f381a52898da1b4c63a3252559ef N
948                0e07a84d610ae1d5f823c02573825438 h 49b35e005120197a73685301f17ddd92 b f66ffe4a80deebe8ffca678d33e33f7b rs
949                2e1e8bd83e52ee09bb58297aeb1da158 Th 28a61bb021be4f7b4d43c3a995207169 re 33f3092e1d836e03bbbe45cf77f46183 S
950                517c4ccbb8292617db5d758e868023a0 M ac8de377a8f7d07007d10ad37eeaa88b r e42460162dfa7d0d9ad67efe32f9505c .
951                7f84b8c690c3b0412a0514e117a04c69 S fd84447f45a91a443e1863fa7a2c830e p 1e92bddfb0b4813630d147a38863543d ri
952                84db1131cd6f3ed6f630e58b879f781f tz 0fd741130b71b082f1eeebda6e2e2811 G a26fbebcec2437f07bad0ad6f6dc2313 o
953                29f53067840a08d6ca5c34834ad14e77 e 673324edd255d182fad9267db821f230 s d3f8a87a788b91db4886c6a4c0e5a82d To
954                11a9bc26a268f7cd5787ccae1a3a7fd6 to a68667571be8a5b2aaf5fd4f4f429d41 D 8d4c375b6b8db04ccee5077e5ba33863 Re
955                ed445642499ca8148938c51518771540 e a1d72e973b08017846fcd70a732b3143 i 70a7a183ec29e18634005ddde569f65d a
956                29dc936fcdb2723b69c638a022135ff2 tch 3961534a0448ed072632dce5dba32d2a e e27d84de85414214f105583f45d406d7 d
957                0dc0ef29925f3ddffb70ce1107ca1b4d ri b46e207278c9048939ff4eb56d1aa847 t 53f78e0dc0417e0f6a455299e15dca0c V
958                ef32aff5c88702eb5ed51c3a6836a583 7 732b43290b91d76547d1e4dd5e85ab8f - ade03db1bcb287d34d4ca9c9bd82c227 r
959                2f49cdc45bf918107fd3001a57d334cc U 96ab55702d9094de2f158ec3a5f1dd00 n a1a6c673257c30fe6b02ed3a5de7acec to
960                222c34badb06b16ff61a3bfdbd2087c5 l b6e528d8cb510fceabfcb1d280e539d9 W bd4a858bb84721b3c83498f9e4e33b20 a
961                222c34badb06b16ff61a3bfdbd2087c5 l a49e3b56b645aa6dc1de7a81898c92ba th b2c89ec08fe126b2e147bc3fceb5b72e S
962                05dd472da0bb30cf7eb463c5eea42aca u ce6488a8ce8ae8a8e81bdc631880780d c 000312319671d8f7f93eb9461828c238 s
963                49ba6d6bfe0d856eb6808ab901bf0ec3 F 207d6b243ade809ae1cad6507711d528 ro 37138974a7027ed973547cce5fba5db7 m
964                c458ef3d193bfddaecd9970d9a57f844 P bd4a858bb84721b3c83498f9e4e33b20 a af722d233b9e8ae897b72d15fd8b5bc4 ti
965                7f76b9fb361c686de8ec1c828c71da4b v 4dcdd7bc37f7b3dae2943ddb8618bbc1 9 8fc445dd8da1ee8f8542ca18a4816109 V
966                5eee84d45d3263e5db81dfcc62d101fa 2
967                };
968        $d->{charset} = \%charset;
969
970
971        my %gset = qw{491 News 508 Children 531 Entertainment 496 Drama 533 Infotainment 507 Religion};
972        $d->{gset} = \%gset;
973}
974
975##############################################################################
976
977sub ocr_image
978{
979        my ($id, $fg_url, $bg_url, $tries, $space_width, $multiline) = @_;
980        $multiline = 0 if (!defined $multiline);
981
982        my $fg_gif_image = &get_url($fg_url, $tries);
983        my $bg_png_image = &get_url($bg_url, $tries);
984
985        my $fg_image = GD::Image->newFromGifData($fg_gif_image);
986        my $bg_image = GD::Image->newFromPngData($bg_png_image);
987
988        $bg_image->copyMerge($fg_image, 0, 0, 0, 0, $fg_image->width, $fg_image->height, 100);
989
990        if (!$multiline) {
991                # remove underline
992                my $white = $bg_image->colorExact(255,255,255);
993                $bg_image->filledRectangle(0, 14, $fg_image->width, $fg_image->height, $white);
994
995                return parse_characters($id, $bg_image, $space_width);
996        }
997
998        return parse_multiline_characters($id, $bg_image);
999}
1000
1001##############################################################################
1002
1003sub parse_multiline_characters
1004{
1005        my ($imgname,$i) = @_;
1006
1007        my ($width, $height) = $i->getBounds;
1008        my $bg = $i->getPixel(0,0);
1009        &log("image bounds: x=$width, y=$height") if ((defined $opt->{debug}) && ($opt->{debug} > 3));
1010
1011        # flatten image colours
1012        my @bgcolour;
1013        foreach my $index (0..$i->colorsTotal) {
1014                my @r = $i->rgb($index);
1015                my $total = $r[0]+$r[1]+$r[2];
1016
1017                if ($total > (240*3)) {
1018                        $bgcolour[$index] = 1;
1019                } else {
1020                        $bgcolour[$index] = 0;
1021                }
1022        }
1023
1024        my $last_char_x;
1025        my $charnum = 0;
1026        my $s = "";
1027
1028        my $done = 0;
1029        my $char_y1 = 0;
1030        my $char_y2 = 0;
1031
1032        while (!$done) {
1033                # 1. find first non-blank horizontal
1034                $char_y1 = $char_y2;
1035                my $blank_y_line = 1;   # until proven otherwise
1036                while (($blank_y_line) && ($char_y1 < ($height-1))) {
1037                        my $char_x = 0;
1038                        while ($char_x < ($width-1)) {
1039                                my $index = $i->getPixel($char_x, $char_y1);
1040                                $blank_y_line = 0 if ($bgcolour[($i->getPixel($char_x,$char_y1))] == 0);
1041                                $char_x++;
1042                        }
1043                        if ($blank_y_line) {
1044                                &log("[1] whole-of-line y $char_y1 was blank!") if ((defined $opt->{debug}) && ($opt->{debug} > 3));
1045                                $char_y1++;
1046                        }
1047                }
1048                if ($blank_y_line) {
1049                        &log("[1] reached end of image without finding anymore non-blank y lines. end of image!") if ((defined $opt->{debug}) && ($opt->{debug} > 3));
1050                        $done = 1;
1051                        next;
1052                }
1053                &log("[1] non-blank horizontal line found: y1=$char_y1") if ((defined $opt->{debug}) && ($opt->{debug} > 3));
1054
1055                # 2. find first blank horizontal
1056                $char_y2 = $char_y1;
1057                my $nonblank_y_line = 0;
1058                while (($nonblank_y_line == 0) && ($char_y2 < ($height-1))) {
1059                        my $char_x = 0;
1060                        $nonblank_y_line = 1;
1061                        while ($char_x < ($width-1)) {
1062                                my $index = $i->getPixel($char_x, $char_y2);
1063                                $nonblank_y_line = 0 if ($bgcolour[($i->getPixel($char_x,$char_y2))] == 0);
1064                                $char_x++;
1065                        }
1066                        if ($nonblank_y_line == 0) {
1067                                &log("[2] whole-of-line y $char_y2 was nonblank!") if ((defined $opt->{debug}) && ($opt->{debug} > 3));
1068                                $char_y2++;
1069                        }
1070                }
1071                &log("[2] blank horizontal line found: y2=$char_y2") if ((defined $opt->{debug}) && ($opt->{debug} > 3));
1072
1073                my $done_line = 0;
1074                my $char_x1 = 0;
1075                my $char_x2 = 0;
1076                while (!$done_line) {
1077                        # 3. find first non-blank vertical between char_y1 and char_y2
1078                        $char_x1 = $char_x2;
1079                        my $blank_x_line = 1;   # until proven otherwise
1080                        while (($blank_x_line) && ($char_x1 < ($width-1))) {
1081                                my $char_y = $char_y1;
1082                                while ($char_y < $char_y2) {
1083                                        my $index = $i->getPixel($char_x1,$char_y);
1084                                        $blank_x_line = 0 if ($bgcolour[($i->getPixel($char_x1,$char_y))] == 0);
1085                                        $char_y++;
1086                                }
1087                                if ($blank_x_line) {
1088                                        &log("[3] whole-of-line x $char_x1 was blank!") if ((defined $opt->{debug}) && ($opt->{debug} > 3));
1089                                        $char_x1++;
1090                                }
1091                        }
1092                        if ($blank_x_line) {
1093                                &log("[3] end of this line (x1 is $char_x1), looking for next line...") if ((defined $opt->{debug}) && ($opt->{debug} > 3));
1094                                $done_line = 1;
1095                                $s .= " " if ($s ne "");
1096                                next;
1097                        }
1098                        &log("[3] non-blank vertical line found: x1=$char_x1") if ((defined $opt->{debug}) && ($opt->{debug} > 3));
1099
1100                        # 4. find first blank vertical between char_y1 and char_y2
1101                        $char_x2 = $char_x1;
1102                        my $nonblank_x_line = 0;
1103                        while (($nonblank_x_line == 0) && ($char_x2 < ($width-1))) {
1104                                my $char_y = $char_y1;
1105                                $nonblank_x_line = 1;
1106                                while ($char_y < $char_y2) {
1107                                        my $index = $i->getPixel($char_x2,$char_y);
1108                                        $nonblank_x_line = 0 if ($bgcolour[($i->getPixel($char_x2,$char_y))] == 0);
1109                                        $char_y++;
1110                                }
1111                                if ($nonblank_x_line == 0) {
1112                                        &log("[4] whole-of-line x $char_x2 wasn't blank!") if ((defined $opt->{debug}) && ($opt->{debug} > 3));
1113                                        $char_x2++;
1114                                }
1115                        }
1116                        &log("blank vertical line found: x2=$char_x2") if ((defined $opt->{debug}) && ($opt->{debug} > 3));
1117
1118                        &log("looking at character between: x1,y1 $char_x1,$char_y1 and x2,y2 $char_x2,$char_y2 ........") if ((defined $opt->{debug}) && ($opt->{debug} > 3));
1119                        # 5. insert spaces
1120                        if ((defined $last_char_x) && (($char_x1 - $last_char_x) >= 3)) {
1121                                $s .= " ";
1122                                $charnum++;
1123                        }
1124                        $last_char_x = $char_x2;
1125
1126                        # 6. md5 char
1127                        my $str = pack('NN',($char_x2-$char_x1),($char_y2-$char_y1));
1128                        foreach my $x ($char_x1..($char_x2-1)) {
1129                                foreach my $y ($char_y1..($char_y2-1)) {
1130                                        $str .= pack('N', $bgcolour[($i->getPixel($x, $y))]);
1131                                }
1132                        }
1133                        my $md5 = Digest::MD5::md5_hex($str);
1134                        $charnum++;
1135
1136                        # 7. insert char
1137                        if ((!defined $d->{charset}->{$md5}) || ($d->{charset}->{$md5} eq "?")) {
1138                                if (defined $opt->{ocr_learn_mode}) {
1139                                        $d->{charset}->{$md5} = "[".$md5."]";
1140                                        $s .= "[".$md5."]";
1141                                } else {
1142                                        $s .= "?";
1143                                }
1144                        } else {
1145                                $s .= $d->{charset}->{$md5};
1146                        }
1147                }
1148        }
1149
1150        &log("multiline ocr got '$s'") if (defined $opt->{debug} && $opt->{debug} > 1);
1151        return $s;
1152}
Note: See TracBrowser for help on using the browser.