File Coverage

lib/SVG/Estimate.pm
Criterion Covered Total %
statement 123 131 93.8
branch 27 32 84.3
condition n/a
subroutine 25 25 100.0
pod 4 6 66.6
total 179 194 92.2


line stmt bran cond sub pod time code
1 6     6   4479 use strict;
  6         13  
  6         172  
2 6     6   27 use warnings;
  6         14  
  6         282  
3             package SVG::Estimate;
4             $SVG::Estimate::VERSION = '1.0114';
5 6     6   2996 use XML::Hash::LX;
  6         284899  
  6         39  
6 6     6   370 use XML::LibXML;
  6         15  
  6         23  
7 6     6   4231 use File::Slurp;
  6         98575  
  6         435  
8 6     6   3599 use List::MoreUtils qw/any/;
  6         75128  
  6         41  
9 6     6   9532 use Moo;
  6         67937  
  6         34  
10 6     6   11369 use SVG::Estimate::Line;
  6         19  
  6         249  
11 6     6   2429 use SVG::Estimate::Rect;
  6         19  
  6         217  
12 6     6   2501 use SVG::Estimate::Circle;
  6         40  
  6         234  
13 6     6   2571 use SVG::Estimate::Ellipse;
  6         18  
  6         190  
14 6     6   39 use SVG::Estimate::Polyline;
  6         11  
  6         129  
15 6     6   28 use SVG::Estimate::Polygon;
  6         8  
  6         183  
16 6     6   2534 use SVG::Estimate::Path;
  6         23  
  6         227  
17 6     6   43 use Data::Dumper;
  6         12  
  6         7729  
18              
19             with 'SVG::Estimate::Role::Round';
20              
21             =head1 NAME
22              
23             SVG::Estimate - Estimates the length of all the vectors in an SVG file.
24              
25             =head1 VERSION
26              
27             version 1.0114
28              
29             =head1 SYNOPSIS
30              
31             my $se = SVG::Estimate->new(
32             file_path => '/path/to/file.svg',
33             );
34              
35             $se->estimate; # performs all the calculations
36              
37             my $length = $se->length;
38              
39             =head1 DESCRIPTION
40              
41             SVG::Estimate is a suite of modules that allow you to accurately estimate the length of the vectors inside of a Scalable Vector Graphics (SVG) file. It is only an estimate because we lack the math to give absolutely precise lengths of really complex curves in a time-efficient manner. Therefore, we take guesses in some cases, though those guesses are still quite accurate (to within about 0.1%). In a battery of tests against our own equipment, our measurements were more accurate than those provided by the equipment itself.
42              
43             This is highly useful for any 2 dimensional CNC machines that use vector files to create tool paths, as you may want to know how long a job will take to quote your customer.
44              
45             =head1 INHERITANCE
46              
47             This class consumes L.
48              
49             =head1 METHODS
50              
51             =head2 new(properties)
52              
53             Constructor.
54              
55             =over
56              
57             =item properties
58              
59             A hash of properties for this class.
60              
61             =over
62              
63             =item file_path
64              
65             The path to the SVG file.
66              
67             =item summarize
68              
69             Have each parse SVG object emit its starting coordinates, ending coordinates, travel length, shape length and total length,
70             all in pixels.
71              
72             =back
73              
74             =back
75              
76             =cut
77              
78             has file_path => (
79             is => 'ro',
80             required => 1,
81             );
82              
83             =head2 length()
84              
85             Returns the length in user units (pixels) of the SVG. This is equivalent of adding C and C together. B The number of user units within any given element could be variable depending upon how the vector was specified and how the SVG editor exports its documents. For example, if you have a line that is 1 inch long in Adobe Illustrator it will export that as 72 user units, and a 1 inch line in Inkscape will export that as 90 user units.
86              
87             =cut
88              
89             has length => (
90             is => 'rw',
91             default => sub { 0 },
92             );
93              
94             =head2 travel_length()
95              
96             Returns the length of tool travel in user units that a toolhead would have to move to get into position for the next shape.
97              
98             =cut
99              
100             has travel_length => (
101             is => 'rw',
102             default => sub { 0 },
103             );
104              
105             =head2 shape_length()
106              
107             Returns the length of the vectors (in user units) that make up the shapes in this document.
108              
109             =cut
110              
111             has shape_length => (
112             is => 'rw',
113             default => sub { 0 },
114             );
115              
116             =head2 shape_count()
117              
118             The count of all the shapes in this document.
119              
120             =cut
121              
122             has shape_count => (
123             is => 'rw',
124             default => sub { 0 },
125             );
126              
127             =head2 cursor()
128              
129             Returns a point (an array ref with 2 values) of where the toolhead will be at the end of estimation.
130              
131             =cut
132              
133             has cursor => (
134             is => 'rw',
135             default => sub { [0,0] },
136             );
137              
138             =head2 min_x()
139              
140             Returns the left most x value of the bounding box for this document.
141              
142             =cut
143              
144             has min_x => (
145             is => 'rwp',
146             default => sub { 1e10 },
147             );
148              
149             =head2 max_x()
150              
151             Returns the right most x value of the bounding box for this document.
152              
153             =cut
154              
155             has max_x => (
156             is => 'rwp',
157             default => sub { -1e10 },
158             );
159              
160             =head2 min_y()
161              
162             Returns the top most y value of the bounding box for this document.
163              
164             =cut
165              
166             has min_y => (
167             is => 'rwp',
168             default => sub { 1e10 },
169             );
170              
171             =head2 max_y()
172              
173             Returns the bottom most y value of the bounding box for this document.
174              
175             =cut
176              
177             has max_y => (
178             is => 'rwp',
179             default => sub { -1e10 },
180             );
181              
182             has summarize => (
183             is => 'ro',
184             default => sub { 0 },
185             );
186              
187             has transform_stack => (
188             is => 'rwp',
189             default => sub { [] },
190             trigger => 1,
191             );
192              
193             sub _trigger_transform_stack {
194 27     27   253 my $self = shift;
195 27         38 my $cts = join ' ', map { $_ } @{ $self->transform_stack };
  16         54  
  27         64  
196 27         434 $self->transformer->extract_transforms($cts);
197             }
198              
199             sub push_transform {
200 14     14 0 22 my $self = shift;
201 14         37 my $stack = $self->transform_stack;
202 14         18 push @{ $stack }, @_;
  14         31  
203 14         312 $self->_set_transform_stack($stack);
204             }
205              
206             sub pop_transform {
207 13     13 0 23 my $self = shift;
208 13         23 my $stack = $self->transform_stack;
209 13         22 my $element = pop @{ $stack };
  13         24  
210 13         235 $self->_set_transform_stack($stack);
211 13         904 return $element;
212             }
213              
214             has transformer => (
215             is => 'lazy',
216             );
217              
218             sub _build_transformer {
219 19     19   706 return Image::SVG::Transform->new();
220             }
221              
222             =head2 read_svg()
223              
224             Reads in the SVG document specified by C in the constructor.
225              
226             =cut
227              
228             sub read_svg {
229 20     20 1 31 my $self = shift;
230 20         116 my $xml = read_file($self->file_path);
231 20         3646 my $doc = XML::LibXML->load_xml(string => $xml, load_ext_dtd => 0);
232 20         12537 my $hash = xml2hash($doc, order => 1);
233 20         78377 return $hash;
234             }
235              
236             =head2 estimate()
237              
238             Performs all the calculations on this document. B before C is run, none of the measurements will produce valid values.
239              
240             =cut
241              
242             sub estimate {
243 20     20 1 2662 my $self = shift;
244 20         49 my $hash = $self->read_svg();
245 20         82 $self->sum($hash->{svg});
246 18         879 return $self;
247             }
248              
249             =head2 sum(elements)
250              
251             This is used by C to do calculations on the various elements of the document. It recurses over a list of elements. This method is likely only useful to you if you want to evaluate only a section of a document.
252              
253             =over
254              
255             =item elements
256              
257             An array reference of SVG elements as parsed by L.
258              
259             =back
260              
261             =cut
262              
263             sub sum {
264 127     127 1 204 my ($self, $elements) = @_;
265 127         168 my $length = 0;
266 127         138 my $shape_length = 0;
267 127         132 my $travel_length = 0;
268 127         146 my $shape_count = 0;
269 127         131 my $has_transform = 0; ##Flag for g/svg element having a transform
270             ##xml2hash rules
271             ## * Just one element, you get a hash
272             ## * Two elements, you get an array of hashes
273             ## * One element, container has properties, you get an array of hashes
274 127 100       287 if (ref $elements eq 'ARRAY') {
    50          
275 93         111 foreach my $element (@{$elements}) {
  93         171  
276 700         946 my @keys = keys %{$element};
  700         2492  
277 700 100   90   3038 if (any { $keys[0] eq $_} qw(g svg)) {
  1327 100       3287  
278 73         238 $self->sum($element->{$keys[0]});
279             }
280 1975     206   2395 elsif (any {$keys[0] eq $_} qw(line ellipse rect circle polygon polyline path)) {
281 597         658 $shape_count++;
282 597         1526 my $class = 'SVG::Estimate::'.ucfirst($keys[0]);
283 597         1515 my %params = $self->parse_params($element->{$keys[0]});
284             ##Have to pass this into Path with all its Commands
285 597 50       1598 if ($self->summarize) {
286 0         0 $params{summarize} = 1;
287             }
288             ##Handle transforms on an element
289 597 100       1058 if (exists $params{transform}) {
290 9         25 $self->push_transform($params{transform});
291 9 50       1239 if ($self->summarize) {
292 0         0 print "transform stack: ". join(' ', @{ $self->transform_stack });
  0         0  
293 0         0 print "\n";
294             }
295             }
296 597         11704 $params{transformer} = $self->transformer;
297 597         21097 my $shape = $class->new(%params);
298 595 50       14089 if ($self->summarize) {
299 0         0 $shape->summarize_myself;
300             }
301 595         1239 $shape_length += $shape->shape_length;
302 595         1544 $travel_length += $shape->travel_length;
303 595         1221 $length += $shape->length;
304 595         10649 $self->cursor($shape->draw_end);
305 595 100       5701 $self->_set_min_x($shape->min_x) if $shape->min_x < $self->min_x;
306 595 100       1531 $self->_set_max_x($shape->max_x) if $shape->max_x > $self->max_x;
307 595 100       1494 $self->_set_min_y($shape->min_y) if $shape->min_y < $self->min_y;
308 595 100       1511 $self->_set_max_y($shape->max_y) if $shape->max_y > $self->max_y;
309 595 100       7725 if (exists $params{transform}) {
310 9         23 $self->pop_transform;
311             }
312             }
313             ##Handle transforms on a containing svg or g element
314             else {
315 30 100       163 if (exists $element->{'-transform'}) {
316 5         21 $self->push_transform($element->{'-transform'});
317 5         1431 $has_transform = 1;
318 5 50       40 if ($self->summarize) {
319 0         0 print "transform stack: ". join(' ', @{ $self->transform_stack });
  0         0  
320 0         0 print "\n";
321             }
322             }
323             }
324             }
325             }
326             ##Resubmit hashes (which should only have one key/value pair) as an array
327             elsif (ref $elements eq 'HASH') {
328 34         106 $self->sum([ $elements ]);
329             }
330 124         318 $self->length($self->length + $length);
331 124         233 $self->shape_length($self->shape_length + $shape_length);
332 124         243 $self->travel_length($self->travel_length + $travel_length);
333 124         315 $self->shape_count($self->shape_count + $shape_count);
334 124 100       358 $self->pop_transform if $has_transform;
335             }
336              
337             =head2 parse_params ( in )
338              
339             Removes the C<-> added to attributes by L and returns a hash with the fixed paramter names.
340              
341             =over
342              
343             =item in
344              
345             A hash reference of parameters from L with the preceeding C<-> on each key.
346              
347             =back
348              
349             =cut
350              
351             sub parse_params {
352 597     597 1 957 my ($self, $in) = @_;
353 597         1690 my %out = (start_point => $self->cursor);
354 597         678 foreach my $key (keys %{$in}) {
  597         2860  
355 4567         7208 my $newkey = substr($key, 1);
356 4567         9274 $out{$newkey} = $in->{$key};
357             }
358 597         3913 return %out;
359             }
360              
361             =head1 PREREQS
362              
363             L
364             L
365             L
366             L
367             L
368             L
369             L
370             L
371             L
372             L
373             L
374             L
375              
376             =head1 SUPPORT
377              
378             =over
379              
380             =item Repository
381              
382             L
383              
384             =item Bug Reports
385              
386             L
387              
388             =back
389              
390             =head1 AUTHOR
391              
392             This module was created by JT Smith and Colin Kuskie .
393              
394             =head1 LEGAL
395              
396             SVG::Estimate is Copyright 2016 Plain Black Corporation (L) and is licensed under the same terms as Perl itself.
397              
398             =cut
399              
400             1;