File Coverage

lib/SVG/Estimate.pm
Criterion Covered Total %
statement 7 9 77.7
branch n/a
condition n/a
subroutine 3 3 100.0
pod n/a
total 10 12 83.3


line stmt bran cond sub pod time code
1 6     6   2318 use strict;
  6         5  
  6         134  
2 6     6   19 use warnings;
  6         4  
  6         225  
3             package SVG::Estimate;
4             $SVG::Estimate::VERSION = '1.0107';
5 6     6   2508 use XML::Hash::LX;
  0            
  0            
6             use XML::LibXML;
7             use File::Slurp;
8             use List::MoreUtils qw/any/;
9             use Moo;
10             use SVG::Estimate::Line;
11             use SVG::Estimate::Rect;
12             use SVG::Estimate::Circle;
13             use SVG::Estimate::Ellipse;
14             use SVG::Estimate::Polyline;
15             use SVG::Estimate::Polygon;
16             use SVG::Estimate::Path;
17             use Data::Dumper;
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.0107
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             my $self = shift;
195             my $cts = join ' ', map { $_ } @{ $self->transform_stack };
196             $self->transformer->extract_transforms($cts);
197             }
198              
199             sub push_transform {
200             my $self = shift;
201             my $stack = $self->transform_stack;
202             push @{ $stack }, @_;
203             $self->_set_transform_stack($stack);
204             }
205              
206             sub pop_transform {
207             my $self = shift;
208             my $stack = $self->transform_stack;
209             my $element = pop @{ $stack };
210             $self->_set_transform_stack($stack);
211             return $element;
212             }
213              
214             has transformer => (
215             is => 'lazy',
216             );
217              
218             sub _build_transformer {
219             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             my $self = shift;
230             my $xml = read_file($self->file_path);
231             my $doc = XML::LibXML->load_xml(string => $xml, load_ext_dtd => 0);
232             my $hash = xml2hash($doc, order => 1);
233             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             my $self = shift;
244             my $hash = $self->read_svg();
245             $self->sum($hash->{svg});
246             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             my ($self, $elements) = @_;
265             my $length = 0;
266             my $shape_length = 0;
267             my $travel_length = 0;
268             my $shape_count = 0;
269             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             if (ref $elements eq 'ARRAY') {
275             foreach my $element (@{$elements}) {
276             my @keys = keys %{$element};
277             if (any { $keys[0] eq $_} qw(g svg)) {
278             $self->sum($element->{$keys[0]});
279             }
280             elsif (any {$keys[0] eq $_} qw(line ellipse rect circle polygon polyline path)) {
281             $shape_count++;
282             my $class = 'SVG::Estimate::'.ucfirst($keys[0]);
283             my %params = $self->parse_params($element->{$keys[0]});
284             ##Have to pass this into Path with all its Commands
285             if ($self->summarize) {
286             $params{summarize} = 1;
287             }
288             ##Handle transforms on an element
289             if (exists $params{transform}) {
290             $self->push_transform($params{transform});
291             if ($self->summarize) {
292             print "transform stack: ". join(' ', @{ $self->transform_stack });
293             print "\n";
294             }
295             }
296             $params{transformer} = $self->transformer;
297             my $shape = $class->new(%params);
298             if ($self->summarize) {
299             $shape->summarize_myself;
300             }
301             $shape_length += $shape->shape_length;
302             $travel_length += $shape->travel_length;
303             $length += $shape->length;
304             $self->cursor($shape->draw_end);
305             $self->_set_min_x($shape->min_x) if $shape->min_x < $self->min_x;
306             $self->_set_max_x($shape->max_x) if $shape->max_x > $self->max_x;
307             $self->_set_min_y($shape->min_y) if $shape->min_y < $self->min_y;
308             $self->_set_max_y($shape->max_y) if $shape->max_y > $self->max_y;
309             if (exists $params{transform}) {
310             $self->pop_transform;
311             }
312             }
313             ##Handle transforms on a containing svg or g element
314             else {
315             if (exists $element->{'-transform'}) {
316             $self->push_transform($element->{'-transform'});
317             $has_transform = 1;
318             if ($self->summarize) {
319             print "transform stack: ". join(' ', @{ $self->transform_stack });
320             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             $self->sum([ $elements ]);
329             }
330             $self->length($self->length + $length);
331             $self->shape_length($self->shape_length + $shape_length);
332             $self->travel_length($self->travel_length + $travel_length);
333             $self->shape_count($self->shape_count + $shape_count);
334             $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             my ($self, $in) = @_;
353             my %out = (start_point => $self->cursor);
354             foreach my $key (keys %{$in}) {
355             my $newkey = substr($key, 1);
356             $out{$newkey} = $in->{$key};
357             }
358             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;