+=item csv_from_fixed, FILEREF COUNTREF, [ LENGTH_LISTREF, [ CALLBACKS_LISTREF ] ]
+
+Converts the filehandle referenced by FILEREF from fixed length record
+lines to a CSV file according to the lengths specified in LENGTH_LISTREF.
+The CALLBACKS_LISTREF refers to a correpsonding list of coderefs. Each
+should return the value to be substituted in place of its single argument.
+
+Returns false on success or an error if one occurs.
+
+=cut
+
+sub csv_from_fixed {
+ my( $fhref, $countref, $lengths, $callbacks) = @_;
+
+ eval { require Text::CSV_XS; };
+ return $@ if $@;
+
+ my $ofh = $$fhref;
+ my $unpacker = new Text::CSV_XS;
+ my $total = 0;
+ my $template = join('', map {$total += $_; "A$_"} @$lengths) if $lengths;
+
+ my $dir = "%%%FREESIDE_CACHE%%%/cache.$FS::UID::datasrc";
+ my $fh = new File::Temp( TEMPLATE => "FILE.csv.XXXXXXXX",
+ DIR => $dir,
+ UNLINK => 0,
+ ) or return "can't open temp file: $!\n"
+ if $template;
+
+ while ( defined(my $line=<$ofh>) ) {
+ $$countref++;
+ if ( $template ) {
+ my $column = 0;
+
+ chomp $line;
+ return "unexpected input at line $$countref: $line".
+ " -- expected $total but received ". length($line)
+ unless length($line) == $total;
+
+ $unpacker->combine( map { my $i = $column++;
+ defined( $callbacks->[$i] )
+ ? &{ $callbacks->[$i] }( $_ )
+ : $_
+ } unpack( $template, $line )
+ )
+ or return "invalid data for CSV: ". $unpacker->error_input;
+
+ print $fh $unpacker->string(), "\n"
+ or return "can't write temp file: $!\n";
+ }
+ }
+
+ if ( $template ) { close $$fhref; $$fhref = $fh }
+
+ seek $$fhref, 0, 0;
+ '';
+}
+
+=item ocr_image IMAGE_SCALAR
+
+Runs OCR on the provided image data and returns a list of text lines.
+
+=cut
+
+sub ocr_image {
+ my $logo_data = shift;
+
+ #XXX use conf dir location from Makefile
+ my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
+ my $fh = new File::Temp(
+ TEMPLATE => 'bizcard.XXXXXXXX',
+ SUFFIX => '.png', #XXX assuming, but should handle jpg, gif, etc. too
+ DIR => $dir,
+ UNLINK => 0,
+ ) or die "can't open temp file: $!\n";
+
+ my $filename = $fh->filename;
+
+ print $fh $logo_data;
+ close $fh;
+
+ local($SIG{CHLD}) = sub {};
+
+ run( [qw(ocroscript recognize), $filename], '>'=>"$filename.hocr" )
+ or die "ocroscript recognize failed\n";
+
+ run( [qw(ocroscript hocr-to-text), "$filename.hocr"], '>pipe'=>\*OUT )
+ or die "ocroscript hocr-to-text failed\n";
+
+ my @lines = split(/\n/, <OUT> );
+
+ foreach (@lines) { s/\.c0m\s*$/.com/; }
+
+ @lines;
+}
+
+=item bytes_substr STRING, OFFSET[, LENGTH[, REPLACEMENT] ]
+
+A replacement for "substr" that counts raw bytes rather than logical
+characters. Unlike "bytes::substr", will suppress fragmented UTF-8 characters
+rather than output them. Unlike real "substr", is not an lvalue.
+
+=cut
+
+sub bytes_substr {
+ my ($string, $offset, $length, $repl) = @_;
+ my $bytes = substr(
+ Encode::encode('utf8', $string),
+ $offset,
+ $length,
+ Encode::encode('utf8', $repl)
+ );
+ my $chk = $DEBUG ? Encode::FB_WARN : Encode::FB_QUIET;
+ return Encode::decode('utf8', $bytes, $chk);
+}
+
+=item money_pretty
+
+Accepts a postive or negative numerical value.
+Returns amount formatted for display,
+including money character.
+
+=cut
+
+sub money_pretty {
+ my $amount = shift;
+ my $money_char = $conf->{'money_char'} || '$';
+ $amount = sprintf("%0.2f",$amount);
+ $amount =~ s/^(-?)/$1$money_char/;
+ return $amount;
+}
+