File Coverage

blib/lib/EBook/Ishmael/CharDet.pm
Criterion Covered Total %
statement 141 162 87.0
branch 36 60 60.0
condition 4 12 33.3
subroutine 31 31 100.0
pod 1 9 11.1
total 213 274 77.7


line stmt bran cond sub pod time code
1             package EBook::Ishmael::CharDet;
2 18     18   132181 use 5.016;
  18         64  
3             our $VERSION = '2.03';
4 18     18   98 use strict;
  18         32  
  18         484  
5 18     18   79 use warnings;
  18         43  
  18         920  
6              
7 18     18   150 use Exporter 'import';
  18         42  
  18         1064  
8             our @EXPORT = qw(chardet);
9              
10 18     18   8642 use EBook::Ishmael::CharDet::Constants qw(:CONSTANTS);
  18         61  
  18         3193  
11 18     18   8958 use EBook::Ishmael::CharDet::Big5;
  18         72  
  18         1132  
12 18     18   9648 use EBook::Ishmael::CharDet::CP1250;
  18         85  
  18         1215  
13 18     18   10128 use EBook::Ishmael::CharDet::CP1251;
  18         75  
  18         1234  
14 18     18   9925 use EBook::Ishmael::CharDet::CP1252;
  18         72  
  18         1244  
15 18     18   9455 use EBook::Ishmael::CharDet::CP1253;
  18         94  
  18         1341  
16 18     18   9739 use EBook::Ishmael::CharDet::CP1254;
  18         74  
  18         1299  
17 18     18   10346 use EBook::Ishmael::CharDet::CP1255;
  18         78  
  18         1306  
18 18     18   10076 use EBook::Ishmael::CharDet::CP1256;
  18         88  
  18         1283  
19 18     18   10153 use EBook::Ishmael::CharDet::EUCJP;
  18         81  
  18         1395  
20 18     18   10580 use EBook::Ishmael::CharDet::EUCKR;
  18         77  
  18         1401  
21 18     18   10601 use EBook::Ishmael::CharDet::GB2312;
  18         74  
  18         1141  
22 18     18   10141 use EBook::Ishmael::CharDet::HZ;
  18         66  
  18         810  
23 18     18   9337 use EBook::Ishmael::CharDet::ISO2022JP;
  18         62  
  18         780  
24 18     18   9087 use EBook::Ishmael::CharDet::ISO2022KR;
  18         83  
  18         706  
25 18     18   9756 use EBook::Ishmael::CharDet::ISO88595;
  18         96  
  18         1276  
26 18     18   9830 use EBook::Ishmael::CharDet::ShiftJIS;
  18         78  
  18         1288  
27 18     18   9763 use EBook::Ishmael::CharDet::UTF8;
  18         64  
  18         26947  
28              
29             # Based encoding detection algorithm off of the chardet python library:
30             # - https://chardet.readthedocs.io/en/latest/
31             # These particular documents were especially useful:
32             # - https://chardet.readthedocs.io/en/latest/how-it-works.html
33             # - https://www-archive.mozilla.org/projects/intl/universalcharsetdetection
34              
35             # TODO: Optimize performance
36              
37             # TODO: UTF16{BE,LE} support?
38             # TODO: UTF32{BE,LE} support?
39             # TODO: ISO-8859-* support?
40              
41             sub has_high_bit {
42              
43 90     90 0 264 my ($str) = @_;
44              
45 90         2707 return $str =~ /[\x80-\xff]/;
46              
47             }
48              
49             sub chardet_bom {
50              
51 90     90 0 314 my ($str) = @_;
52              
53 90 50       1941 if ($str =~ /^\xef\xbb\xbf/) {
    50          
    50          
    50          
    50          
54 0         0 return 'UTF-8';
55             } elsif ($str =~ /^\xfe\xff/) {
56 0         0 return 'UTF-16BE';
57             } elsif ($str =~ /^\xff\xfe/) {
58 0         0 return 'UTF-16LE';
59             } elsif ($str =~ /^\x00\x00\xfe\xff/) {
60 0         0 return 'UTF-32BE';
61             # This could be wrong, as this could also be UTF-16LE starting with a
62             # null character, but I believe its most likely to be a UTF-32.
63             } elsif ($str =~ /^\xff\xfe\x00\x00/) {
64 0         0 return 'UTF-32LE';
65             }
66              
67 90         281 return undef;
68              
69             }
70              
71             sub chardet_utf16 {
72              
73 90     90 0 224 my ($str) = @_;
74              
75 90 100       481 if (length($str) % 2 != 0) {
76 51         134 return undef;
77             }
78              
79 39 50       139 my $len = length $str > 1024 ? 1024 : length $str;
80              
81 39         75 my $leading_null = 0; # UTF-16BE
82 39         80 my $trailling_null = 0; # UTF-16LE
83 39         75 my $none = 0;
84 39         221 for (my $i = 0; $i < $len; $i += 2) {
85 19968         29824 my $char = substr $str, $i, 2;
86 19968         26149 my $got_null = 0;
87 19968 50       36365 if ($char =~ /^\0/) {
88 0         0 $leading_null++;
89 0         0 $got_null = 1;
90             }
91 19968 50       36287 if ($char =~ /\0$/) {
92 0         0 $trailling_null++;
93 0         0 $got_null = 1;
94             }
95 19968 50       35802 if (!$got_null) {
96 19968         38393 $none++;
97             }
98             }
99              
100 39 50 33     284 if ($leading_null > $trailling_null && $leading_null > $none) {
    50 33        
101 0         0 return 'UTF-16BE';
102             } elsif ($trailling_null > $leading_null && $trailling_null > $none) {
103 0         0 return 'UTF-16LE';
104             } else {
105 39         183 return undef;
106             }
107              
108             }
109              
110             sub chardet_utf32 {
111              
112 90     90 0 246 my ($str) = @_;
113              
114 90 100       418 if (length($str) % 4 != 0) {
115 71         173 return undef;
116             }
117              
118 19 50       80 my $len = length $str > 1024 ? 1024 : length $str;
119              
120 19         52 my $leading_null = 0; # UTF-32BE
121 19         39 my $trailling_null = 0; # UTF-32LE
122 19         35 my $none = 0;
123 19         72 for (my $i = 0; $i < $len; $i += 4) {
124 4864         7333 my $char = substr $str, $i, 4;
125 4864         6362 my $got_null = 0;
126 4864 50       8971 if ($char =~ /^\0\0\0/) {
127 0         0 $leading_null++;
128 0         0 $got_null = 1;
129             }
130 4864 50       9059 if ($char =~ /\0\0\0$/) {
131 0         0 $trailling_null++;
132 0         0 $got_null = 1;
133             }
134 4864 50       8311 if (!$got_null) {
135 4864         9247 $none++;
136             }
137             }
138              
139 19 50 33     159 if ($leading_null > $trailling_null && $leading_null > $none) {
    50 33        
140 0         0 return 'UTF-32BE';
141             } elsif ($trailling_null > $leading_null && $trailling_null > $none) {
142 0         0 return 'UTF-32LE';
143             } else {
144 19         68 return undef;
145             }
146              
147             }
148              
149             sub chardet_with {
150              
151 157     157 0 588 my ($str, @guessers) = @_;
152              
153 157 50       516 my $len = length $str > 8192 ? 8192 : length $str;
154              
155 157         453 for (my $i = 0; $i < $len; $i += 16) {
156 63023         119203 my $c = substr $str, $i, 16;
157 63023         152948 for my $j (0 .. $#guessers) {
158 346011 100       778511 if ($j > $#guessers) {
159 91         204 last;
160             }
161 345920         957163 my $take = $guessers[$j]->take($c);
162 345920 100       1036838 if ($take == TAKE_MUST_BE) {
    100          
163 39         218 return ([ $guessers[$j]->encoding, 1.0 ]);
164             } elsif ($take == TAKE_BAD) {
165 218         613 splice @guessers, $j, 1;
166             }
167             }
168 62984 50       226417 if (!@guessers) {
169 0         0 return ();
170             }
171             }
172              
173 118         555 return map { [ $_->encoding, $_->confidence ] } @guessers;
  644         2491  
174              
175             }
176              
177             sub chardet_7bit {
178              
179 23     23 0 69 my ($str) = @_;
180              
181 23         399 my @guesses = chardet_with(
182             $str,
183             EBook::Ishmael::CharDet::HZ->new,
184             EBook::Ishmael::CharDet::ISO2022JP->new,
185             EBook::Ishmael::CharDet::ISO2022KR->new,
186             );
187 23         327 push @guesses, [ 'ASCII', 0.75 ];
188              
189 23         221 return @guesses;
190              
191             }
192              
193             sub chardet_multibyte {
194              
195 67     67 0 204 my ($str) = @_;
196              
197 67         919 return chardet_with(
198             $str,
199             EBook::Ishmael::CharDet::UTF8->new,
200             EBook::Ishmael::CharDet::Big5->new,
201             EBook::Ishmael::CharDet::ShiftJIS->new,
202             EBook::Ishmael::CharDet::EUCJP->new,
203             EBook::Ishmael::CharDet::EUCKR->new,
204             EBook::Ishmael::CharDet::GB2312->new,
205             );
206              
207             }
208              
209             sub chardet_singlebyte {
210              
211 67     67 0 230 my ($str) = @_;
212              
213 67         1225 return chardet_with(
214             $str,
215             EBook::Ishmael::CharDet::CP1250->new,
216             EBook::Ishmael::CharDet::CP1251->new,
217             EBook::Ishmael::CharDet::CP1252->new,
218             EBook::Ishmael::CharDet::CP1253->new,
219             EBook::Ishmael::CharDet::CP1254->new,
220             EBook::Ishmael::CharDet::CP1255->new,
221             EBook::Ishmael::CharDet::CP1256->new,
222             EBook::Ishmael::CharDet::ISO88595->new,
223             );
224              
225             }
226              
227             sub chardet {
228              
229 90     90 1 93222 my ($str) = @_;
230              
231 90         429 my $char = chardet_bom($str);
232 90 50       616 if (defined $char) {
233 0         0 return $char;
234             }
235              
236 90         386 $char = chardet_utf16($str);
237 90 50       423 if (defined $char) {
238 0         0 return $char;
239             }
240              
241 90         423 $char = chardet_utf32($str);
242 90 50       348 if (defined $char) {
243 0         0 return $char;
244             }
245              
246 90         214 my @chars;
247              
248 90 100       400 if (!has_high_bit($str)) {
249 23         137 @chars = sort { $b->[1] <=> $a->[1] } chardet_7bit($str);
  91         266  
250 23 50       311 return @chars > 0 ? $chars[0]->[0] : 'ASCII';
251             }
252              
253 67         353 @chars = chardet_multibyte($str);
254 67         1316 push @chars, chardet_singlebyte($str);
255 67         1217 @chars = sort { $b->[1] <=> $a->[1] } @chars;
  1465         2630  
256              
257 67 50       1404 return @chars > 0 ? $chars[0]->[0] : undef;
258              
259             }
260              
261             1;
262              
263             =head1 NAME
264              
265             =encoding UTF-8
266              
267             EBook::Ishmael::CharDet - Guess the character encoding of given text
268              
269             =head1 SYNOPSIS
270              
271             use EBook::Ishmael::CharDet;
272              
273             use Encode qw(encode);
274              
275             # $encoding should be 'CP1250'
276             my $encoding = chardet(encode('CP1250', 'Obecná veřejná'));
277              
278             =head1 DESCRIPTION
279              
280             B is a module that provides the C subroutine
281             which guesses character encoding of given text. This is a private module,
282             please consult the L manual for user documentation.
283              
284             =head1 SUBROUTINES
285              
286             =over 4
287              
288             =item $encoding = chardet($text)
289              
290             Guesses the encoding for the encoded text C<$text> through a series of
291             heuristics. If C cannot come to a conclusion, C is returned.
292              
293             The follow encodings are supported so far:
294              
295             =over 2
296              
297             =item ASCII
298              
299             =item UTF-8
300              
301             =item UTF-16BE
302              
303             =item UTF-16LE
304              
305             =item UTF-32BE
306              
307             =item UTF-32LE
308              
309             =item GB2312
310              
311             =item CP1250
312              
313             =item CP1251
314              
315             =item CP1252
316              
317             =item CP1253
318              
319             =item CP1254
320              
321             =item CP1255
322              
323             =item CP1256
324              
325             =item HZ
326              
327             =item ISO-2022-JP
328              
329             =item ISO-2022-KR
330              
331             =item ISO-8859-5
332              
333             =item EUC-JP
334              
335             =item EUC-KR
336              
337             =item Big5
338              
339             =item Shift_JIS
340              
341             =back
342              
343             =back
344              
345             =head1 AUTHOR
346              
347             Written by Samuel Young, Esamyoung12788@gmail.comE.
348              
349             This project's source can be found on its
350             L. Comments and pull
351             requests are welcome!
352              
353             =head1 COPYRIGHT
354              
355             Copyright (C) 2025-2026 Samuel Young
356              
357             This program is free software: you can redistribute it and/or modify
358             it under the terms of the GNU General Public License as published by
359             the Free Software Foundation, either version 3 of the License, or
360             (at your option) any later version.
361              
362             =cut