File Coverage

blib/lib/Text/Convert/PETSCII.pm
Criterion Covered Total %
statement 140 144 97.2
branch 94 98 95.9
condition 67 81 82.7
subroutine 13 13 100.0
pod 8 8 100.0
total 322 344 93.6


line stmt bran cond sub pod time code
1             package Text::Convert::PETSCII;
2              
3             =head1 NAME
4              
5             Text::Convert::PETSCII - ASCII/PETSCII text converter
6              
7             =head1 SYNOPSIS
8              
9             use Text::Convert::PETSCII qw/:all/;
10              
11             # Convert an ASCII string to a PETSCII string:
12             my $petscii_string = ascii_to_petscii($ascii_string);
13              
14             # Convert a PETSCII string to an ASCII string:
15             my $ascii_string = petscii_to_ascii($petscii_string);
16              
17             # Convert CBM screen codes to a PETSCII string:
18             my $petscii_string = screen_codes_to_petscii($screen_codes);
19              
20             # Convert a PETSCII string to CBM screen codes:
21             my $screen_codes = petscii_to_screen_codes($petscii_string);
22              
23             # Set mode for writing PETSCII character's representation to a file handle:
24             set_petscii_write_mode($write_mode);
25              
26             # Write PETSCII single character's textual representation to a file handle:
27             write_petscii_char($file_handle, $petscii_char);
28              
29             # Validate whether given PETSCII string text may normally be printed out:
30             my $is_printable = is_printable_petscii_string($petscii_string);
31              
32             # Validate whether given text may be considered a valid PETSCII string:
33             my $is_valid = is_valid_petscii_string($text_string);
34              
35             =head1 DESCRIPTION
36              
37             This package provides two basic methods for converting text format between ASCII and PETSCII character sets. PETSCII stands for the "PET Standard Code of Information Interchange" and is also known as CBM ASCII. PETSCII character set has been widely used in Commodore Business Machines (CBM)'s 8-bit home computers, starting with the PET from 1977 and including the VIC-20, C64, CBM-II, Plus/4, C16, C116 and C128.
38              
39             =head1 METHODS
40              
41             =cut
42              
43 3     3   160928 use base qw(Exporter);
  3         7  
  3         2159  
44             our %EXPORT_TAGS = ();
45             $EXPORT_TAGS{'convert'} = [ qw(&ascii_to_petscii &petscii_to_ascii) ];
46             $EXPORT_TAGS{'display'} = [ qw(&set_petscii_write_mode &write_petscii_char) ];
47             $EXPORT_TAGS{'screen'} = [ qw(&screen_codes_to_petscii &petscii_to_screen_codes) ];
48             $EXPORT_TAGS{'validate'} = [ qw(&is_printable_petscii_string &is_valid_petscii_string) ];
49             $EXPORT_TAGS{'all'} = [ @{$EXPORT_TAGS{'convert'}}, @{$EXPORT_TAGS{'display'}}, @{$EXPORT_TAGS{'screen'}}, @{$EXPORT_TAGS{'validate'}} ];
50             our @EXPORT_OK = ( @{ $EXPORT_TAGS{'all'} } );
51             our @EXPORT = qw();
52              
53             our $VERSION = '0.05';
54              
55 3     3   17 use Carp qw/carp croak/;
  3         6  
  3         248  
56 3     3   3414 use Data::Dumper;
  3         28287  
  3         5762  
57              
58             require XSLoader;
59             XSLoader::load(__PACKAGE__, $VERSION);
60              
61             our $WRITE_MODE = 'unshifted';
62              
63             =head2 ascii_to_petscii
64              
65             Convert an ASCII string to a PETSCII string:
66              
67             my $petscii_string = ascii_to_petscii($ascii_string);
68              
69             Input data is handled as a stream of bytes. When original ASCII string contains any non-ASCII character, a relevant warning will be triggered, providing detailed information about invalid character's integer code and its position within the source string.
70              
71             =cut
72              
73             sub ascii_to_petscii {
74 11     11 1 9158 my ($str_ascii) = @_;
75 11         72 my $str_petscii = '';
76 11         19 my $position = 1;
77 11         77 while ($str_ascii =~ s/^(.)(.*)$/$2/) {
78 22         39 my $c = ord $1;
79 22         26 my $code = $c & 0x7f;
80 22 100       80 if ($c != $code) {
81 2         342 carp sprintf qq{Invalid ASCII code at position %d of converted text string: "0x%02x" (convertible codes include bytes between 0x00 and 0x7f)}, $position, $c;
82             }
83 22 100 100     135 if ($code >= ord 'A' && $code <= ord 'Z') {
    100 66        
84 3         5 $code += 32;
85             } elsif ($code >= ord 'a' && $code <= ord 'z') {
86 13         13 $code -= 32;
87             }
88 22         32 $str_petscii .= chr $code;
89 22         145 $position++;
90             }
91 11         38 return $str_petscii;
92             }
93              
94             =head2 petscii_to_ascii
95              
96             Convert a PETSCII string to an ASCII string:
97              
98             my $ascii_string = petscii_to_ascii($petscii_string);
99              
100             Input data is handled as a stream of bytes. Note that integer codes between 0x80 and 0xff despite of being valid PETSCII codes are not convertible into any ASCII equivalents, therefore they trigger a relevant warning, providing detailed information about invalid character's integer code and its position within the source string.
101              
102             =cut
103              
104             sub petscii_to_ascii {
105 5     5 1 1851 my ($str_petscii) = @_;
106 5         6 my $str_ascii = '';
107 5         6 my $position = 1;
108 5         28 while ($str_petscii =~ s/^(.)(.*)$/$2/) {
109 23         27 my $c = ord $1;
110 23         25 my $code = $c & 0x7f;
111 23 100       38 if ($c != $code) {
112 3         450 carp sprintf qq{Invalid PETSCII code at position %d of converted text string: "0x%02x" (convertible codes include bytes between 0x00 and 0x7f)}, $position, $c;
113             }
114 23 100 100     106 if ($code >= ord 'A' && $code <= ord 'Z') {
    100 66        
    50          
115 12         10 $code += 32;
116             } elsif ($code >= ord 'a' && $code <= ord 'z') {
117 8         8 $code -= 32;
118             } elsif ($code == 0x7f) {
119 0         0 $code = 0x3f;
120             }
121 23         25 $str_ascii .= chr $code;
122 23         85 $position++;
123             }
124 5         13 return $str_ascii;
125             }
126              
127             =head2 screen_codes_to_petscii
128              
129             Convert CBM screen codes to a PETSCII string:
130              
131             my $petscii_string = screen_codes_to_petscii($screen_codes);
132              
133             Input screen codes are expected to be a scalar value that is handled as a stream of bytes. And so is a returned value.
134              
135             =cut
136              
137             sub screen_codes_to_petscii {
138 17     17 1 5368 my ($screen_codes) = @_;
139              
140 17         19 my $reverse_flag = 0;
141              
142 17         16 my $petscii_string;
143              
144 17         40 for my $screen_char (split //, $screen_codes) {
145              
146 42         36 my $screen_code = ord ($screen_char);
147              
148             # RVS ON:
149 42 100       59 if ($screen_code & 0x80) {
150 11 100       19 unless ($reverse_flag) {
151 1         2 $reverse_flag = 1;
152 1         2 $petscii_string .= chr (0x12);
153             }
154 11         12 $screen_code ^= 0x80;
155             }
156             # RVS OFF:
157             else {
158 31 100       63 if ($reverse_flag) {
159 1         1 $reverse_flag = 0;
160 1         2 $petscii_string .= chr (0x92);
161             }
162             }
163              
164             # $20 .. $3f ("SPACE ($20)" .. "?"):
165 42         35 my $petscii_byte = $screen_code;
166             # $00 .. $1f ("@" .. "left arrow"):
167 42 100 66     241 if ($petscii_byte >= 0x00 && $petscii_byte < 0x20) {
    100 100        
    100 66        
168 12         25 $petscii_byte += 0x40;
169             }
170             # $40 .. $5f ("horizontal line" .. "top-right triangle"):
171             elsif ($petscii_byte >= 0x40 && $petscii_byte < 0x60) {
172 9         10 $petscii_byte += 0x20;
173             }
174             # $60 .. $7f ("SPACE ($60)" .. "racing square"):
175             elsif ($petscii_byte >= 0x60 && $petscii_byte < 0x80) {
176 3         4 $petscii_byte += 0x40;
177             }
178              
179 42         71 $petscii_string .= chr ($petscii_byte);
180             }
181              
182 17         93 return $petscii_string;
183             }
184              
185             =head2 petscii_to_screen_codes
186              
187             Convert a PETSCII string to CBM screen codes:
188              
189             my $screen_codes = petscii_to_screen_codes($petscii_string);
190              
191             Input PETSCII string is expected to be a scalar value that is handled as a stream of bytes. And so is a returned value.
192              
193             =cut
194              
195             sub petscii_to_screen_codes {
196 28     28 1 7650 my ($petscii_string) = @_;
197              
198 28         30 my $reverse_flag = 0x00;
199              
200 28         27 my $screen_codes;
201              
202 28         72 for my $petscii_char (split //, $petscii_string) {
203              
204 59         59 my $petscii_byte = ord ($petscii_char);
205              
206             # RVS ON:
207 59 100       95 if ($petscii_byte == 0x12) {
208 1         2 $reverse_flag = 0x80;
209 1         2 next;
210             }
211              
212             # RVS OFF:
213 58 100       85 if ($petscii_byte == 0x92) {
214 1         2 $reverse_flag = 0x00;
215 1         1 next;
216             }
217              
218             # $c0 .. $df are the same as $60 .. $7f
219 57 100 66     312 if ($petscii_byte >= 0xc0 && $petscii_byte < 0xe0) {
    50 33        
    50 100        
    100          
220 1         1 $petscii_byte -= 0x60;
221             }
222             # $e0 .. $fe are the same as $a0 .. $be
223             elsif ($petscii_byte >= 0xe0 && $petscii_byte < 0xff) {
224 0         0 $petscii_byte -= 0x40;
225             }
226             # $ff is the same as $7e
227             elsif ($petscii_byte == 0xff) {
228 0         0 $petscii_byte = 0x7e;
229             }
230             # $95 .. $9b are the same as $75 .. $7b
231             elsif ($petscii_byte >= 0x95 && $petscii_byte < 0x9c) {
232 2         4 $petscii_byte -= 0x20;
233             }
234              
235             # Skip all non-printable characters:
236 57 100 66     298 if ($petscii_byte >= 0x00 && $petscii_byte < 0x20 || $petscii_byte >= 0x80 && $petscii_byte < 0xa0) {
      100        
      66        
237 11         22 next;
238             }
239              
240             # $20 .. $3f ("SPACE ($20)" .. "?"):
241 46         51 my $screen_code = $petscii_byte;
242             # $40 .. $5f ("@" .. "left arrow"):
243 46 100 100     293 if ($screen_code >= 0x40 && $screen_code < 0x60) {
    100 100        
    100 66        
244 12         12 $screen_code -= 0x40;
245             }
246             # $60 .. $7f ("horizontal line" .. "top-right triangle"):
247             elsif ($screen_code >= 0x60 && $screen_code < 0x80) {
248 12         12 $screen_code -= 0x20;
249             }
250             # $a0 .. $bf ("SPACE ($A0)" .. "racing square"):
251             elsif ($screen_code >= 0xa0 && $screen_code < 0xc0) {
252 4         5 $screen_code -= 0x40;
253             }
254              
255 46         86 $screen_codes .= chr ($screen_code | $reverse_flag);
256             }
257              
258 28         114 return $screen_codes;
259             }
260              
261             =head2 set_petscii_write_mode
262              
263             Set mode for writing PETSCII character's textual representation to a file handle:
264              
265             set_petscii_write_mode('shifted');
266             set_petscii_write_mode('unshifted');
267              
268             There are two modes available. A "shifted" mode, also known as a "text" mode, refers to mode, in which lowercase letters occupy the range 0x41 .. 0x5a, and uppercase letters occupy the range 0xc1 .. 0xda. In "unshifted" mode, codes 0x60 .. 0x7f and 0xa0 .. 0xff are allocated to CBM-specific block graphics characters.
269              
270             If not set explicitly, writing PETSCII char defaults to "unshifted" mode.
271              
272             =cut
273              
274             sub set_petscii_write_mode {
275 9     9 1 14984 my ($petscii_write_mode) = @_;
276 9 50       34 if (not defined $petscii_write_mode) {
277 0         0 carp q{Failed to set PETSCII write mode: no mode to set has been specified};
278             }
279 9         26 _petscii_write_mode($petscii_write_mode);
280             }
281              
282             sub _petscii_write_mode {
283 36     36   46 my ($petscii_write_mode) = @_;
284 36 100       74 if (defined $petscii_write_mode) {
285 9 100       14 unless (grep { $petscii_write_mode eq $_ } qw/shifted unshifted/) {
  18         48  
286 1         138 carp sprintf q{Failed to set PETSCII write mode, invalid PETSCII write mode: "%s"}, $petscii_write_mode;
287 1         5 return;
288             }
289 8         17 $WRITE_MODE = $petscii_write_mode;
290             }
291 35         106 return $WRITE_MODE;
292             }
293              
294             =head2 write_petscii_char
295              
296             Write PETSCII character's textual representation to a file handle:
297              
298             write_petscii_char($fh, $petscii_char);
299              
300             C<$fh> is expected to be an opened file handle that PETSCII character's textual representation may be written to, and C<$petscii_char> is expected to either be an integer code (between 0x20 and 0x7f as well as between 0xa0 and 0xff, since control codes between 0x00 and 0x1f and between 0x80 and 0x9f are not printable by design) or a character byte (the actual single byte with PETSCII data to be processed, same rules for possible printable characters apply).
301              
302             =cut
303              
304             sub write_petscii_char {
305 33     33 1 34387 my ($fh, $chr_petscii) = @_;
306              
307             # Check if character provided is actually a character or a numerical index:
308 33         49 my $screen_code = undef;
309 33 100       161 if (_is_integer($chr_petscii)) {
    100          
310 21 100 100     222 if ($chr_petscii < 0x20 or $chr_petscii > 0xff or ($chr_petscii >= 0x80 and $chr_petscii <= 0x9f)) {
      66        
      66        
311 2         291 carp sprintf q{Value out of range: "0x%02x" (PETSCII character set supports printable characters in the range of 0x20 to 0x7f and 0xa0 to 0xff)}, $chr_petscii;
312             }
313             else {
314 19         46 $screen_code = _petscii_to_screen_code($chr_petscii);
315             }
316             }
317             elsif (_is_string($chr_petscii)) {
318 10 100       37 if (length $chr_petscii == 0) {
    100          
319 1         146 carp q{PETSCII character byte missing, nothing to be printed out};
320             }
321             elsif (length $chr_petscii > 1) {
322 1         254 carp sprintf q{PETSCII character string too long: %d bytes (currently writing only a single character is supported)}, length $chr_petscii;
323             }
324             else {
325 8         20 $screen_code = _petscii_to_screen_code(ord $chr_petscii);
326             }
327             }
328             else {
329 2         18 my $invalid_data = Data::Dumper->new([$chr_petscii])->Terse(1)->Indent(0)->Dump();
330 2         590 carp qq{Not a valid PETSCII character to write: ${invalid_data} (expected integer code or character byte)};
331             }
332              
333             # Print out character's textual representation based on the calculated screen code:
334 33 100       202 if (defined $screen_code) {
335 27 100       58 my $shifted_mode = _petscii_write_mode() eq 'shifted' ? 1 : 0;
336 27         118 my @font_data = _get_font_data($screen_code, $shifted_mode);
337 27         69 foreach my $font_line (@font_data) {
338 216         548 for (my $i = 0; $i < 8; $i++) {
339 1728 100       3088 my $font_pixel = $font_line & 0x80 ? 1 : 0;
340 1728 100       2578 if ($font_pixel) {
341 541         4934 print q{*};
342             }
343             else {
344 1187         11542 print q{-};
345             }
346 1728         4030 $font_line <<= 1;
347             }
348 216         2108 print qq{\n};
349             }
350             }
351              
352 33         194 return;
353             }
354              
355             =head2 is_printable_petscii_string
356              
357             Validate whether given PETSCII string text may normally be printed out:
358              
359             my $is_printable = is_printable_petscii_string($petscii_string);
360              
361             Returns true value upon successful validation, and false otherwise. False value will also be immediately returned when text string that is given as an argument is not a PETSCII string at all.
362              
363             =cut
364              
365             sub is_printable_petscii_string {
366 12     12 1 7064 my ($text_string) = @_;
367              
368 12 100       36 return 0 unless is_valid_petscii_string($text_string);
369              
370 9 100       30 return 1 if length $text_string == 0;
371              
372 8 100       36 unless ($text_string =~ m/^[^\x20-\x7f\xa0-\xff]*$/g) {
373 4         23 return 1;
374             }
375              
376 4         41 return 0;
377             }
378              
379             =head2 is_valid_petscii_string
380              
381             Validate whether given text may be considered a valid PETSCII string:
382              
383             my $is_valid = is_valid_petscii_string($text_string);
384              
385             Returns true value upon successful validation, and false otherwise.
386              
387             =cut
388              
389             sub is_valid_petscii_string {
390 24     24 1 7291 my ($text_string) = @_;
391              
392 24 100       172 return 0 unless defined $text_string;
393 22 100       81 return 0 if ref $text_string;
394              
395 20 100       196 return 1 if length $text_string == 0;
396              
397 18 100       105 unless ($text_string =~ m/^[^\x00-\xff]*$/g) {
398 16         74 return 1;
399             }
400              
401 2         18 return 0;
402             }
403              
404             # TODO: Consider adding this method to the public interface of current package:
405             sub _petscii_to_screen_code {
406 30     30   1617 my ($num_petscii) = @_;
407 30 100 100     204 if ($num_petscii < 0x20 or $num_petscii > 0xff or ($num_petscii >= 0x80 and $num_petscii <= 0x9f)) {
      66        
      66        
408 2         301 croak sprintf q{Invalid PETSCII integer code: "0x%02x" (PETSCII character set supports printable characters in the range of 0x20 to 0x7f and 0xa0 to 0xff)}, $num_petscii;
409             }
410 28         31 my $screen_code = $num_petscii;
411 28 100 100     268 if ($num_petscii >= 64 && $num_petscii <= 95) {
    100 100        
    100 100        
    100 100        
    100 100        
    100          
412 10         16 $screen_code -= 64;
413             }
414             elsif ($num_petscii >= 96 && $num_petscii <= 127) {
415 4         7 $screen_code -= 32;
416             }
417             elsif ($num_petscii >= 160 && $num_petscii <= 191) {
418 2         4 $screen_code -= 64;
419             }
420             elsif ($num_petscii >= 192 && $num_petscii <= 223) {
421 4         8 $screen_code -= 128;
422             }
423             elsif ($num_petscii >= 224 && $num_petscii <= 254) {
424 2         4 $screen_code -= 128;
425             }
426             elsif ($num_petscii == 255) {
427 1         2 $screen_code -= 161;
428             }
429 28         92 return $screen_code;
430             }
431              
432             =head1 BUGS
433              
434             There are no known bugs at the moment. Please report any bugs or feature requests.
435              
436             =head1 EXPORT
437              
438             No method is exported into the caller's namespace by default.
439              
440             Selected methods may be exported into the caller's namespace explicitly by using the following tags in the import list:
441              
442             =over
443              
444             =item *
445             C tag adds L and L subroutines to the list of symbols to be imported into the caller's namespace
446              
447             =item *
448             C tag adds L and L subroutines to the list of symbols to be imported into the caller's namespace
449              
450             =item *
451             C tag adds L and subroutines to the list of symbols to be imported into the caller's namespace
452              
453             =item *
454             C tag adds L and subroutines to the list of symbols to be imported into the caller's namespace
455              
456             =item *
457             C tag adds all subroutines listed by C, C, C, and C tags to the list of exported symbols
458              
459             =back
460              
461             =head1 AUTHOR
462              
463             Pawel Krol, Epawelkrol@cpan.orgE.
464              
465             =head1 VERSION
466              
467             Version 0.05 (2013-03-08)
468              
469             =head1 COPYRIGHT AND LICENSE
470              
471             Copyright 2011, 2013 by Pawel Krol .
472              
473             This library is free open source software; you can redistribute it and/or modify it under the same terms as Perl itself, either Perl version 5.8.6 or, at your option, any later version of Perl 5 you may have available.
474              
475             PLEASE NOTE THAT IT COMES WITHOUT A WARRANTY OF ANY KIND!
476              
477             =cut
478              
479             1;