| 1 |
use strict; |
|---|
| 2 |
package FuzzyOcr::Hashing; |
|---|
| 3 |
|
|---|
| 4 |
use base 'Exporter'; |
|---|
| 5 |
our @EXPORT_OK = qw(check_image_hash_db |
|---|
| 6 |
add_image_hash_db |
|---|
| 7 |
calc_image_hash); |
|---|
| 8 |
|
|---|
| 9 |
use lib qw(..); |
|---|
| 10 |
use FuzzyOcr::Config qw(get_thresholds get_config set_config get_tmpdir get_mysql_ddb); |
|---|
| 11 |
use FuzzyOcr::Misc qw(save_execute); |
|---|
| 12 |
use FuzzyOcr::Logging qw(debuglog errorlog warnlog infolog); |
|---|
| 13 |
use Fcntl; |
|---|
| 14 |
use Fcntl ':flock'; |
|---|
| 15 |
|
|---|
| 16 |
|
|---|
| 17 |
|
|---|
| 18 |
sub within_threshold { |
|---|
| 19 |
my $thresref = get_thresholds(); |
|---|
| 20 |
my %Threshold = %$thresref; |
|---|
| 21 |
|
|---|
| 22 |
my $digest = shift; |
|---|
| 23 |
my $record = shift; |
|---|
| 24 |
|
|---|
| 25 |
my ($dimg,$dkey) = split('::',$digest); |
|---|
| 26 |
my ($rimg,$rkey) = split('::',$record); |
|---|
| 27 |
my ($ds, $dh, $dw, $dcn) = split(':',$dimg); |
|---|
| 28 |
my ($rs, $rh, $rw, $rcn) = split(':',$rimg); |
|---|
| 29 |
return(0) unless $rs; |
|---|
| 30 |
return(0) unless $rh; |
|---|
| 31 |
return(0) unless $rw; |
|---|
| 32 |
return(0) unless $rcn; |
|---|
| 33 |
return(0) unless $rkey; |
|---|
| 34 |
return(0) if ((abs($ds - $rs ) / $rs ) > $Threshold{s}); |
|---|
| 35 |
return(0) if ((abs($dh - $rh ) / $rh ) > $Threshold{h}); |
|---|
| 36 |
return(0) if ((abs($dw - $rw ) / $rw ) > $Threshold{w}); |
|---|
| 37 |
return(0) if ((abs($dcn - $rcn) / $rcn) > $Threshold{cn}); |
|---|
| 38 |
|
|---|
| 39 |
my @rcf = split('::',$rkey); |
|---|
| 40 |
my @dcf = split('::',$dkey); |
|---|
| 41 |
|
|---|
| 42 |
my (@dcfs, @rcfs); |
|---|
| 43 |
foreach (@dcf) { push @dcfs,split(':',$_); } |
|---|
| 44 |
foreach (@rcf) { push @rcfs,split(':',$_); } |
|---|
| 45 |
|
|---|
| 46 |
my $total = scalar(@rcfs); |
|---|
| 47 |
if ($total == scalar(@dcfs)) { |
|---|
| 48 |
my $match = 0; |
|---|
| 49 |
foreach (0 .. ($total-1)) { |
|---|
| 50 |
$match++ if (abs($dcfs[$_] - $rcfs[$_]) <= $Threshold{c}); |
|---|
| 51 |
} |
|---|
| 52 |
infolog("image matched <$match> of <$total> colors"); |
|---|
| 53 |
return(1) if ($match == $total); |
|---|
| 54 |
} |
|---|
| 55 |
return(0); |
|---|
| 56 |
} |
|---|
| 57 |
|
|---|
| 58 |
sub check_image_hash_db { |
|---|
| 59 |
my $conf = get_config(); |
|---|
| 60 |
if ($conf->{focr_enable_image_hashing} == 0) { |
|---|
| 61 |
warnlog("Image Hashing is disabled"); |
|---|
| 62 |
return (0,''); |
|---|
| 63 |
} |
|---|
| 64 |
my $digest = $_[0]; |
|---|
| 65 |
my $dbfile = $_[1] || $conf->{focr_db_hash}; |
|---|
| 66 |
my $fname = $_[2]; |
|---|
| 67 |
my $ctype = $_[3]; |
|---|
| 68 |
my $ftype = $_[4] || 0; |
|---|
| 69 |
my ($img, $key) = split('::', $digest,2); |
|---|
| 70 |
return (0,'') unless defined $key; |
|---|
| 71 |
my $now = time; |
|---|
| 72 |
my $hash = $digest; |
|---|
| 73 |
my $ret = 0; my $txt = 'Exact'; |
|---|
| 74 |
my $dinfo; |
|---|
| 75 |
|
|---|
| 76 |
if ($conf->{focr_enable_image_hashing} == 3) { |
|---|
| 77 |
unless (defined $conf->{focr_mysql_ddb}) { |
|---|
| 78 |
warnlog("Connection to MySQL server unavailable"); |
|---|
| 79 |
return (0,''); |
|---|
| 80 |
} |
|---|
| 81 |
my $ddb = $conf->{focr_mysql_ddb}; |
|---|
| 82 |
my $db = $conf->{focr_mysql_db}; |
|---|
| 83 |
my $sql = qq(select * from $db.$dbfile where $dbfile.key='$key'); |
|---|
| 84 |
my @data = $ddb->selectrow_array($sql); |
|---|
| 85 |
my $next = 0; |
|---|
| 86 |
my $when = 0; |
|---|
| 87 |
my $match = 0; |
|---|
| 88 |
if ((scalar(@data)>0) and ($img eq $data[1])) { |
|---|
| 89 |
$match++; |
|---|
| 90 |
infolog("Found[$dbfile]: Score='$data[8]' Info: '$data[9]'"); |
|---|
| 91 |
$next = $data[5]; $next++; |
|---|
| 92 |
$when = $data[7]; $data[8] += 0; |
|---|
| 93 |
$ret = $data[8] == 0 ? 0.001 : $data[8]; |
|---|
| 94 |
$dinfo = $data[9] || ''; |
|---|
| 95 |
if ($data[2] eq '') { |
|---|
| 96 |
infolog("Updating $txt info File-Name:'$fname'"); |
|---|
| 97 |
$ddb->do(qq(update $db.$dbfile set $dbfile.fname=? where $dbfile.key='$key'),undef,$fname); |
|---|
| 98 |
} |
|---|
| 99 |
if ($data[3] eq '') { |
|---|
| 100 |
infolog("Updating $txt info Content-Type:'$ctype'"); |
|---|
| 101 |
$ddb->do(qq(update $db.$dbfile set $dbfile.ctype=? where $dbfile.key='$key'),undef,$ctype); |
|---|
| 102 |
} |
|---|
| 103 |
if ($data[4] != $ftype) { |
|---|
| 104 |
infolog("Updating $txt info File-Type:'$ftype'"); |
|---|
| 105 |
$ddb->do(qq(update $db.$dbfile set $dbfile.ftype=? where $dbfile.key='$key'),undef,$ftype); |
|---|
| 106 |
} |
|---|
| 107 |
} |
|---|
| 108 |
unless ($match) { |
|---|
| 109 |
my $then = time - ($conf->{focr_db_max_days}*86400); |
|---|
| 110 |
$sql = qq(select * from $db.$dbfile order by $dbfile.check); |
|---|
| 111 |
my $sth = $ddb->prepare($sql); $sth->execute; |
|---|
| 112 |
while (my @row = $sth->fetchrow_array) { |
|---|
| 113 |
my $hash2 = $row[1] || "0:0:0:0"; |
|---|
| 114 |
$hash2 .= "::$row[0]"; |
|---|
| 115 |
if (within_threshold($digest,$hash2)) { |
|---|
| 116 |
$txt = 'Approx'; |
|---|
| 117 |
$key = $row[0]; |
|---|
| 118 |
$next = $row[5] + 1; |
|---|
| 119 |
$when = $row[7] || $now; |
|---|
| 120 |
$ret = $dbfile eq $conf->{focr_mysql_hash} ? $row[8] : $row[5]; |
|---|
| 121 |
$dinfo = $row[9] || ''; |
|---|
| 122 |
infolog("Found[$dbfile]: Score='$row[8]' Info: '$row[9]'"); |
|---|
| 123 |
last; |
|---|
| 124 |
} |
|---|
| 125 |
} |
|---|
| 126 |
|
|---|
| 127 |
$sql = qq(delete from $db.$dbfile where $dbfile.check < $then); |
|---|
| 128 |
debuglog($sql,2); |
|---|
| 129 |
$ddb->do($sql); |
|---|
| 130 |
} |
|---|
| 131 |
if ($ret > 0) { |
|---|
| 132 |
if ($dbfile eq $conf->{focr_mysql_hash}) { |
|---|
| 133 |
infolog("Found Score <$ret> for $txt Image Hash"); |
|---|
| 134 |
} |
|---|
| 135 |
infolog("Matched [$next] time(s). Prev match: ".fmt_time($now - $when)); |
|---|
| 136 |
$sql = qq(update $db.$dbfile set $dbfile.match='$next',$dbfile.check='$now' where $dbfile.key='$key'); |
|---|
| 137 |
$ddb->do($sql); |
|---|
| 138 |
debuglog($sql); |
|---|
| 139 |
} |
|---|
| 140 |
return ($ret,$dinfo); |
|---|
| 141 |
} |
|---|
| 142 |
elsif ($conf->{focr_enable_image_hashing} == 2) { |
|---|
| 143 |
import MLDBM qw(DB_File Storable); |
|---|
| 144 |
my %DB = (); my $dbm; my $sdbm; |
|---|
| 145 |
$sdbm = tie %DB, 'MLDBM::Sync', $dbfile, O_CREAT|O_RDWR or $ret++; |
|---|
| 146 |
if ($ret>0) { |
|---|
| 147 |
warnlog("No Image Hash database found at \"$dbfile\", or permissions wrong."); |
|---|
| 148 |
return (0,''); |
|---|
| 149 |
} |
|---|
| 150 |
$sdbm->Lock; |
|---|
| 151 |
if (defined $DB{$key}) { |
|---|
| 152 |
$dbm = $DB{$key}; |
|---|
| 153 |
if ($img eq $dbm->{basic}) { |
|---|
| 154 |
$ret = $dbm->{score} || 0.001; |
|---|
| 155 |
$dinfo = $dbm->{dinfo} || ''; |
|---|
| 156 |
$dbm->{fname} = $fname; |
|---|
| 157 |
$dbm->{ctype} = $ctype; |
|---|
| 158 |
infolog("Updating $txt info File:'$fname' Type:'$ctype'"); |
|---|
| 159 |
$DB{$key} = $dbm; |
|---|
| 160 |
} |
|---|
| 161 |
} |
|---|
| 162 |
if ($ret == 0) { |
|---|
| 163 |
my $then = time - ($conf->{focr_db_max_days}*86400); |
|---|
| 164 |
foreach my $k (keys %DB) { |
|---|
| 165 |
$dbm = $DB{$k}; |
|---|
| 166 |
$hash = $dbm->{basic} ? $dbm->{basic} : "0:0:0:0::$k"; |
|---|
| 167 |
if (within_threshold($digest,$hash)) { |
|---|
| 168 |
$ret = $dbfile eq $conf->{focr_db_hash} ? $dbm->{score} : $dbm->{match}; |
|---|
| 169 |
$txt = 'Approx'; $dinfo = $dbm->{dinfo} || ''; |
|---|
| 170 |
infolog("Found in: <$dbfile>"); |
|---|
| 171 |
last; |
|---|
| 172 |
} |
|---|
| 173 |
|
|---|
| 174 |
$dbm->{check} = $now - 1 unless defined $dbm->{check}; |
|---|
| 175 |
if ($dbm->{check} < $then) { |
|---|
| 176 |
infolog("Expiring <$k> older than $conf->{focr_db_max_days} days"); |
|---|
| 177 |
delete $DB{$k}; |
|---|
| 178 |
} |
|---|
| 179 |
} |
|---|
| 180 |
} |
|---|
| 181 |
if ($ret>0) { |
|---|
| 182 |
$dbm->{match}++; |
|---|
| 183 |
if ($dbfile eq $conf->{focr_db_hash}) { |
|---|
| 184 |
$ret = sprintf("%0.3f",$dbm->{score}); |
|---|
| 185 |
infolog("Found Score <$ret> for $txt Image Hash"); |
|---|
| 186 |
} |
|---|
| 187 |
infolog("Matched [$dbm->{match}] time(s). Prev match: ".fmt_time(time - $dbm->{check})); |
|---|
| 188 |
$dbm->{check} = time; |
|---|
| 189 |
$DB{$key} = $dbm; |
|---|
| 190 |
} |
|---|
| 191 |
$sdbm->UnLock; |
|---|
| 192 |
undef $sdbm; |
|---|
| 193 |
untie %DB; |
|---|
| 194 |
return ($ret,$dinfo); |
|---|
| 195 |
} |
|---|
| 196 |
elsif ($conf->{focr_enable_image_hashing} == 1) { |
|---|
| 197 |
$ret = open HASH, $conf->{focr_digest_db}; |
|---|
| 198 |
unless($ret) { |
|---|
| 199 |
warnlog("No Image Hash database found at \"$conf->{focr_digest_db}\", or permissions wrong."); |
|---|
| 200 |
return (0,''); |
|---|
| 201 |
} |
|---|
| 202 |
while (<HASH>) { |
|---|
| 203 |
chomp; |
|---|
| 204 |
($ret,$hash) = split('::',$_,2); |
|---|
| 205 |
if (within_threshold($digest,$hash)) { |
|---|
| 206 |
infolog("Found Score <$ret> for Hash <$hash>"); |
|---|
| 207 |
return ($ret,''); |
|---|
| 208 |
} |
|---|
| 209 |
} |
|---|
| 210 |
close HASH; |
|---|
| 211 |
return (0,''); |
|---|
| 212 |
} |
|---|
| 213 |
} |
|---|
| 214 |
|
|---|
| 215 |
sub add_image_hash_db { |
|---|
| 216 |
my $conf = get_config(); |
|---|
| 217 |
return if ($conf->{focr_enable_image_hashing} == 0); |
|---|
| 218 |
my $digest = $_[0]; |
|---|
| 219 |
my $score = $_[1]; |
|---|
| 220 |
my $ret = 0; |
|---|
| 221 |
|
|---|
| 222 |
if ($conf->{focr_enable_image_hashing} == 3) { |
|---|
| 223 |
unless (defined $conf->{focr_mysql_ddb}) { |
|---|
| 224 |
warnlog("Connection to MySQL server unavailable"); |
|---|
| 225 |
return; |
|---|
| 226 |
} |
|---|
| 227 |
my $db = $conf->{focr_mysql_db}; |
|---|
| 228 |
my $ddb = $conf->{focr_mysql_ddb}; |
|---|
| 229 |
my $table = $_[2] || $conf->{focr_mysql_hash}; |
|---|
| 230 |
my $fname = $_[3] || ''; |
|---|
| 231 |
my $ctype = $_[4] || 'image'; |
|---|
| 232 |
my $ftype = $_[5] || 0; |
|---|
| 233 |
my $dinfo = $_[6] || ''; |
|---|
| 234 |
infolog("Adding Hash to table: \"$db.$table\" with score \"$score\""); |
|---|
| 235 |
my $sql; |
|---|
| 236 |
my ($img,$key) = split('::',$digest,2); |
|---|
| 237 |
if (defined $key) { |
|---|
| 238 |
$sql = "select basic from $db.$table where $table.key='$key'"; |
|---|
| 239 |
my @data = $ddb->selectrow_array($sql); |
|---|
| 240 |
if (scalar(@data)>0) { |
|---|
| 241 |
if ($conf->{focr_mysql_update_hash}) { |
|---|
| 242 |
infolog("Hash already in $db.$table updating..."); |
|---|
| 243 |
$sql = "update $db.$table set "; |
|---|
| 244 |
my @params; |
|---|
| 245 |
unless ($data[1] eq $img) { |
|---|
| 246 |
$sql .= "basic=?,"; push @params,$img; |
|---|
| 247 |
} |
|---|
| 248 |
unless ($data[2] eq $fname) { |
|---|
| 249 |
$sql .= "fname=?,"; push @params,$fname; |
|---|
| 250 |
} |
|---|
| 251 |
unless ($data[3] eq $ctype) { |
|---|
| 252 |
$sql .= "ctype=?,"; push @params,$ctype; |
|---|
| 253 |
} |
|---|
| 254 |
unless ($data[4] == $ftype) { |
|---|
| 255 |
$sql .= "ftype=?,"; push @params,$ftype; |
|---|
| 256 |
} |
|---|
| 257 |
unless ($data[8] == $score) { |
|---|
| 258 |
$sql .= "score=?,"; push @params,$score; |
|---|
| 259 |
} |
|---|
| 260 |
unless ($data[9] == $dinfo) { |
|---|
| 261 |
$sql .= "dinfo=?,"; push @params,$dinfo; |
|---|
| 262 |
} |
|---|
| 263 |
$sql =~ s/,$//; |
|---|
| 264 |
$sql .= " where $table.key='$key'"; |
|---|
| 265 |
$ddb->do($sql,undef,@params); |
|---|
| 266 |
foreach my $p (@params) { $sql =~ s/\?/$p/; } |
|---|
| 267 |
debuglog($sql); |
|---|
| 268 |
} else { |
|---|
| 269 |
infolog("Hash already in $db.$table skipping..."); |
|---|
| 270 |
} |
|---|
| 271 |
} else { |
|---|
| 272 |
my @params = ( |
|---|
| 273 |
$key, $img, $fname, $ctype, $ftype, |
|---|
| 274 |
($table eq $conf->{focr_mysql_hash} ? 0 : 1), |
|---|
| 275 |
time, time, $score, $dinfo); |
|---|
| 276 |
$sql = "insert into $db.$table values (?,?,?,?,?,?,?,?,?,?)"; |
|---|
| 277 |
$ddb->do($sql,undef,@params); |
|---|
| 278 |
foreach my $p (@params) { $sql =~ s/\?/$p/; } |
|---|
| 279 |
debuglog($sql); |
|---|
| 280 |
} |
|---|
| 281 |
} |
|---|
| 282 |
} |
|---|
| 283 |
elsif ($conf->{focr_enable_image_hashing} == 2) { |
|---|
| 284 |
import MLDBM qw(DB_File Storable); |
|---|
| 285 |
my $dbfile = $_[2] || $conf->{focr_db_hash}; |
|---|
| 286 |
my %DB = (); my $sdbm; |
|---|
| 287 |
$sdbm = tie %DB, 'MLDBM::Sync', $dbfile, O_CREAT|O_RDWR or $ret++; |
|---|
| 288 |
if ($ret>0) { |
|---|
| 289 |
warnlog("Unable to open/create Image Hash database at \"$dbfile\", check permissions."); |
|---|
| 290 |
return; |
|---|
| 291 |
} |
|---|
| 292 |
$sdbm->Lock; |
|---|
| 293 |
infolog("Adding Hash to \"$dbfile\" with score \"$score\""); |
|---|
| 294 |
my ($img,$key) = split('::',$digest,2); |
|---|
| 295 |
if (defined $key) { |
|---|
| 296 |
my $dbm = $DB{$key}; |
|---|
| 297 |
$dbm->{fname} = $_[3]; |
|---|
| 298 |
$dbm->{ctype} = $_[4]; |
|---|
| 299 |
$dbm->{ftype} = $_[5]; |
|---|
| 300 |
$dbm->{dinfo} = $_[6]; |
|---|
| 301 |
$dbm->{basic} = $img; |
|---|
| 302 |
$dbm->{score} = $score; |
|---|
| 303 |
$dbm->{input} = |
|---|
| 304 |
$dbm->{check} = time; |
|---|
| 305 |
$dbm->{match} = $dbfile eq $conf->{focr_db_hash} ? 0 : 1; |
|---|
| 306 |
$DB{$key} = $dbm; |
|---|
| 307 |
} |
|---|
| 308 |
$sdbm->UnLock; |
|---|
| 309 |
undef $sdbm; |
|---|
| 310 |
untie %DB; |
|---|
| 311 |
} |
|---|
| 312 |
elsif ($conf->{focr_enable_image_hashing} == 1) { |
|---|
| 313 |
if (-e $conf->{focr_digest_db}) { |
|---|
| 314 |
$ret = open DB, ">>$conf->{focr_digest_db}"; |
|---|
| 315 |
} else { |
|---|
| 316 |
$ret = open DB, ">$conf->{focr_digest_db}"; |
|---|
| 317 |
} |
|---|
| 318 |
unless ($ret) { |
|---|
| 319 |
warnlog("Unable to open/create Image Hash database at \"$conf->{focr_digest_db}\", check permissions."); |
|---|
| 320 |
return; |
|---|
| 321 |
} |
|---|
| 322 |
infolog("Adding Hash to \"$conf->{focr_digest_db}\""); |
|---|
| 323 |
flock( DB, LOCK_EX ); |
|---|
| 324 |
seek( DB, 0, 2 ); |
|---|
| 325 |
print DB "${score}::${digest}\n"; |
|---|
| 326 |
close(DB); |
|---|
| 327 |
} |
|---|
| 328 |
debuglog("Digest: $digest"); |
|---|
| 329 |
} |
|---|
| 330 |
|
|---|
| 331 |
sub calc_image_hash { |
|---|
| 332 |
my $conf = get_config(); |
|---|
| 333 |
my $imgdir = get_tmpdir(); |
|---|
| 334 |
my $thresref = get_thresholds(); |
|---|
| 335 |
my %Threshold = %$thresref; |
|---|
| 336 |
my $pfile = $_[0]; |
|---|
| 337 |
my $pic = $_[1]; |
|---|
| 338 |
my $hash; |
|---|
| 339 |
|
|---|
| 340 |
foreach my $a (qw/ppmhist/) { |
|---|
| 341 |
unless (defined $conf->{"focr_bin_$a"}) { |
|---|
| 342 |
errorlog("calc_image_hash cannot exec $a"); |
|---|
| 343 |
return (1, ''); |
|---|
| 344 |
} |
|---|
| 345 |
} |
|---|
| 346 |
|
|---|
| 347 |
unless (-r $pfile) { |
|---|
| 348 |
errorlog("Cannot read $pfile"); |
|---|
| 349 |
return(1, ''); |
|---|
| 350 |
} |
|---|
| 351 |
|
|---|
| 352 |
my ($r, @stdout_data) = save_execute( |
|---|
| 353 |
"$conf->{focr_bin_ppmhist} -noheader $pfile", undef, |
|---|
| 354 |
">$imgdir/ppmhist.info", |
|---|
| 355 |
">/dev/null", 1); |
|---|
| 356 |
|
|---|
| 357 |
if ($r) { |
|---|
| 358 |
chomp $r; |
|---|
| 359 |
errorlog("$conf->{focr_bin_ppmhist}: ". |
|---|
| 360 |
($r<0) ? 'Timed out' : 'Error' |
|---|
| 361 |
." [$r], skipping..."); |
|---|
| 362 |
return (1, ''); |
|---|
| 363 |
} |
|---|
| 364 |
|
|---|
| 365 |
my $cnt = 0; |
|---|
| 366 |
my $c = scalar(@stdout_data); |
|---|
| 367 |
my $s = (stat($pfile))[7] || 0; |
|---|
| 368 |
$hash = sprintf "%d:%d:%d:%d",$s, |
|---|
| 369 |
defined $pic->{height} ? $pic->{height} : 0, |
|---|
| 370 |
defined $pic->{width} ? $pic->{width} : 0, |
|---|
| 371 |
$c; |
|---|
| 372 |
if ($Threshold{max_hash}) { |
|---|
| 373 |
foreach (@stdout_data) { |
|---|
| 374 |
$_ =~ s/ +/ /g; |
|---|
| 375 |
my(@d) = split(' ', $_); |
|---|
| 376 |
$hash .= sprintf("::%d:%d:%d:%d:%d",@d); |
|---|
| 377 |
if ($cnt++ ge $Threshold{max_hash}) { |
|---|
| 378 |
last; |
|---|
| 379 |
} |
|---|
| 380 |
} |
|---|
| 381 |
} |
|---|
| 382 |
debuglog("Got: <$hash>"); |
|---|
| 383 |
return(0, $hash); |
|---|
| 384 |
} |
|---|
| 385 |
|
|---|
| 386 |
sub fmt_time { |
|---|
| 387 |
my $when = $_[0]; |
|---|
| 388 |
my $ret; |
|---|
| 389 |
|
|---|
| 390 |
if ($when>86400) { |
|---|
| 391 |
my $d = int($when/86400); |
|---|
| 392 |
$when -= $d*86400; |
|---|
| 393 |
$ret = "$d days,"; |
|---|
| 394 |
} |
|---|
| 395 |
if ($when>3600) { |
|---|
| 396 |
my $h = int($when/3600); |
|---|
| 397 |
$when -= $h*3600; |
|---|
| 398 |
$ret .= " $h hrs."; |
|---|
| 399 |
} |
|---|
| 400 |
if ($when>60) { |
|---|
| 401 |
my $m = int($when/60); |
|---|
| 402 |
$when -= $m*60; |
|---|
| 403 |
$ret .= " $m min."; |
|---|
| 404 |
} |
|---|
| 405 |
if ($when>0) { |
|---|
| 406 |
$ret .= " $when sec."; |
|---|
| 407 |
} |
|---|
| 408 |
$ret .= " ago"; |
|---|
| 409 |
return $ret; |
|---|
| 410 |
} |
|---|
| 411 |
|
|---|
| 412 |
1; |
|---|