Skip Menu |

This queue is for tickets about the CAD-Format-STL CPAN distribution.

Report information
The Basics
Id: 83595
Status: new
Priority: 0/
Queue: CAD-Format-STL

People
Owner: Nobody in particular
Requestors: b [...] bathsheba.com
Cc:
AdminCc:

Bug Information
Severity: Critical
Broken in: v0.2.1
Fixed in: (no value)



Subject: Tiny fatal bug in Windows
Thanks for writing this module! I was using it in Windows 7 with ActivePerl to import a lot of STL files, and I saw it fail intermittently with binary format. I tracked this down and found that when bytes 0x0D, i.e. a carriage return, occur in such a file, it fails. Drops the character, reads a lot of gibberish numbers, and eventually fails with an unexpected EOF. Attached is a small STL, 2 triangles, that shows this behavior. I think it's because the perl read function is, in systems that distinguish binary from text files, a text read: it strips carriage returns. Unfortunately Windows is such a system. I found that by calling binmode on the file handle, this is fixed. In STL.pm at line 348 I inserted 'binmode $fh;'. Attached is this file with that change. This is probably not the right place to add that, because it could fail earlier during format detection. But I didn't understand that part very well and I only need to read binary files, so this is the best I could do right now. I'm not a developer and don't feel up to putting a patch on CPAN, especially when I don't think it's quite right. But it would be awesome if this got fixed properly. Thanks for your consideration!
Subject: STL.pm
package CAD::Format::STL; $VERSION = v0.2.1; use warnings; use strict; use Carp; use CAD::Format::STL::part; =head1 NAME CAD::Format::STL - read/write 3D stereolithography files =head1 SYNOPSIS Reading: my $stl = CAD::Format::STL->new->load("foo.stl"); # what about the part/multipart? my @facets = $stl->part->facets; Writing: my $stl = CAD::Format::STL->new; my $part = $stl->add_part("my part"); $part->add_facets(@faces); $stl->save("foo.stl"); # or $stl->save(binary => "foo.stl"); Streaming read/write: my $reader = CAD::Format::STL->reader("foo.stl"); my $writer = CAD::Format::STL->writer(binary => "bar.stl"); while(my $part = $reader->next_part) { my $part_name = $part->name; $writer->start_solid($part_name); while(my @data = $part->facet) { my ($normal, @vertices) = @data; my @v1 = @{$vertices[0]}; my @v2 = @{$vertices[0]}; my @v3 = @{$vertices[0]}; # that's just for illustration $writer->facet(\@v1, \@v2, \@v3); # note the omitted normal } $writer->end_solid; } =begin design The reader auto-detects whether it is binary (but assumes ascii when seek can't go backwards.) The reader and writer both take 1, 2, or {1,2}+2n arguments. This package and/or the reader/writer are subclassable (though getting $self->reader to instantiate a subclass implies that you have subclassed $self.) A cached_facet (or raw_facet) method is necessary to ensure uniform tranformation of shared points (and optimize the computation.) This would return the normal and points as a list of scalars rather than arrays, with a later call to unpack_point() or something. The caller needs to be able to handle the caching (or else there is a callback for non-cached (or an override for unpack_point().) Maybe $self->set_writer() and set_reader() immutable object methods? =end design =head1 ABOUT This module provides object-oriented methods to read and write the STL (Stereo Lithography) file format in both binary and ASCII forms. The STL format is a simple set of 3D triangles. =cut use Class::Accessor::Classy; lo 'parts'; no Class::Accessor::Classy; =head1 Constructor =head2 new my $stl = CAD::Format::STL->new; =cut sub new { my $package = shift; my $class = ref($package) || $package; my $self = {parts => []}; bless($self, $class); return($self); } # end subroutine new definition ######################################################################## =head2 add_part Create a new part in the stl. my $part = $stl->add_part("name"); Optionally, add the faces directly: my $part = $stl->add_part("name", @faces); =cut sub add_part { my $self = shift; my ($name, @faces) = @_; my $part = CAD::Format::STL::part->new($name, @faces); push(@{$self->{parts}}, $part); return($part); } # end subroutine add_part definition ######################################################################## =head2 part Get the part at $index. Negative indices are valid. my $part = $stl->part($index); Throws an error if there is no such part. =cut sub part { my $self = shift; my ($index) = @_; @{$self->{parts}} or croak("file has no parts"); $index ||= 0; exists($self->{parts}[$index]) or croak("no part $index"); return($self->{parts}[$index]); } # end subroutine part definition ######################################################################## =head1 I/O Methods =head2 load Load an STL file (auto-detects binary/ascii) $stl = $stl->load("filename.stl"); Optionally, explicitly declare binary mode: $stl = $stl->load(binary => "filename.stl"); The $self object is returned to allow e.g. chaining to C<new()>. The filename may also be a filehandle. =cut sub load { my $self = shift; my ($file, @and) = @_; my $mode; if(@and) { (@and > 1) and croak('too many arguments to load()'); $mode = $file; ($file) = @and; } # allow filehandle unless((ref($file) || '') eq 'GLOB') { open(my $fh, '<', $file) or die "cannot open '$file' for reading $!"; $file = $fh; } # detection unless($mode) { unless(seek($file, 0,0)) { croak('must have explicit mode for non-seekable filehandle'); } # now, detection... $mode = sub { my $fh = shift; seek($fh, 80, 0); my $count = eval { my $buf; read($fh, $buf, 4) or die; unpack('L', $buf); }; $@ and return 'ascii'; # if we hit eof, it can't be binary $count or die "detection failed - no facets?"; my $size = (stat($fh))[7]; # calculate the expected file size my $expect = + 80 # header + 4 # count + $count * ( + 4 # normal, pt,pt,pt (vectors) * 4 # bytes per value * 3 # values per vector + 2 # the trailing 'short' ); return ($size == $expect) ? 'binary' : 'ascii'; }->($file); seek($file, 0, 0) or die "cannot reset filehandle"; } my $method = '_read_' . lc($mode); $self->can($method) or croak("invalid read mode '$mode'"); $self->$method($file); return($self); } # end subroutine load definition ######################################################################## =head2 _read_ascii $self->_read_ascii($filehandle); =cut sub _read_ascii { my $self = shift; my ($fh) = @_; my $getline = sub { while(my $line = <$fh>) { $line =~ s/\s*$//; # allow any eol length($line) or next; return($line); } return; }; my $p_re = qr/([^ ]+)\s+([^ ]+)\s+([^ ]+)$/; my $part; while(my $line = $getline->()) { if($line =~ m/^\s*solid (.*)/) { $part = $self->add_part($1); } elsif($line =~ m/^\s*endsolid (.*)/) { my $name = $1; $part or die "invalid 'endsolid' entry with no current part"; ($name eq $part->name) or die "end of part '$name' should have been '", $part->name, "'"; $part = undef; } elsif($part) { my @n = ($line =~ m/^\s*facet\s+normal\s+$p_re/) or die "how did that happen? ($line)"; #warn "got ", join('|', @n); my @facet = (\@n); my $next = $getline->(); unless($next and ($next =~ m/^\s*outer\s+loop$/)) { die "facet doesn't start with 'outer loop' ($next)"; } push(@facet, do { my @got; while(my $line = $getline->()) { ($line =~ m/^\s*endloop$/) and last; if($line =~ m/^\s*vertex\s+$p_re/) { push(@got, [$1, $2, $3]); } } @got; }); (scalar(@facet) == 4) or die "need three vertices per facet (not $#facet)"; my $end = $getline->(); ($end and ($end =~ m/^\s*endfacet/)) or die "bad endfacet $line"; $part->add_facets([@facet]); } else { die "what? ($line)"; } } $part and die "part '", $part->name, "' was left open"; } # end subroutine _read_ascii definition ######################################################################## =head2 get_<something> These functions are currently only used internally. =over =item get_triangle =item get_ulong =item get_float32 =item get_short =back =cut sub get_triangle { my ($fh) = @_; my ($n, $x, $y, $z) = map({[map({get_float32($fh)} 1..3)]} 1..4); my $scrap = get_short($fh); return($n, $x, $y, $z); } sub get_ulong { my ($fh) = @_; my $buf; read($fh, $buf, 4) or warn "EOF?"; return(unpack('L', $buf)); } sub get_float32 { my ($fh) = @_; my $buf; read($fh, $buf, 4) or warn "EOF?"; return(unpack('f', $buf)); } sub get_short { my ($fh) = @_; my $buf; read($fh, $buf, 2) or warn "EOF?"; return(unpack('S', $buf)); } =head2 _read_binary $self->_read_binary($filehandle); =cut sub _read_binary { my $self = shift; my ($fh) = @_; binmode $fh; $self->parts and die "binary STL files must have only one part"; die "bigfloat" unless(length(pack("f", 1)) == 4); # TODO try to read part name from header (up to \0) my $name = 'a part'; seek($fh, 80, 0); my $triangles = get_ulong($fh); my $part = $self->add_part($name); my $count = 0; while(1) { my @tr = get_triangle($fh); # TODO check that the unit normal is within a thousandth of a radian # (0.001 rad is ~0.06deg) $part->add_facets([@tr]); $count++; eof($fh) and last; } ($count == $triangles) or die "ERROR: got $count facets (expected $triangles)"; } # end subroutine _read_binary definition ######################################################################## =head2 save $stl->save("filename.stl"); $stl->save(binary => "filename.stl"); =cut sub save { my $self = shift; my ($file, @and) = @_; my $mode; if(@and) { (@and > 1) and croak('too many arguments to save()'); $mode = $file; ($file) = @and; } # allow filehandle unless((ref($file) || '') eq 'GLOB') { open(my $fh, '>', $file) or die "cannot open '$file' for writing $!"; $file = $fh; } $mode = 'ascii' unless($mode); my $method = '_write_' . lc($mode); $self->can($method) or croak("invalid write mode '$mode'"); $self->$method($file); } # end subroutine save definition ######################################################################## =head2 _write_binary $self->_write_binary($filehandle); =cut sub _write_binary { my $self = shift; my ($fh) = @_; my ($part, @and) = $self->parts; @and and die 'cannot write binary files with multiple parts'; my $name = $part->name; # utf8 is ok print $fh $name, "\0" x (80 - do {use bytes; length($name)}); my @facets = $part->facets; print $fh pack('L', scalar(@facets)); foreach my $facet (@facets) { print $fh map({map({pack('f', $_)} @$_)} @$facet); print $fh "\0" x 2; } } # end subroutine _write_binary definition ######################################################################## =head2 _write_ascii $self->_write_ascii($filehandle); =cut sub _write_ascii { my $self = shift; my ($fh) = @_; my $spaces = ''; my $print = sub {print $fh $spaces, @_, "\n"}; my @parts = $self->parts or croak("no parts to write"); foreach my $part (@parts) { $print->('solid ', $part->name); $spaces = ' 'x2; foreach my $facet ($part->facets) { my ($n, @pts) = @$facet; $print->(join(' ', 'facet normal', @$n)); $spaces = ' 'x4; $print->('outer loop'); $spaces = ' 'x6; (@pts == 3) or die "invalid facet"; foreach my $pt (@pts) { $print->(join(' ', 'vertex', @$pt)); } $spaces = ' 'x4; $print->('endloop'); $spaces = ' 'x2; $print->('endfacet'); } $spaces = ''; print $fh 'endsolid ', $part->name, "\n"; } } # end subroutine _write_ascii definition ######################################################################## =head1 AUTHOR Eric Wilhelm @ <ewilhelm at cpan dot org> http://scratchcomputing.com/ =head1 BUGS If you found this module on CPAN, please report any bugs or feature requests through the web interface at L<http://rt.cpan.org>. I will be notified, and then you'll automatically be notified of progress on your bug as I make changes. If you pulled this development version from my /svn/, please contact me directly. =head1 COPYRIGHT Copyright (C) 2007 Eric L. Wilhelm, All Rights Reserved. =head1 NO WARRANTY Absolutely, positively NO WARRANTY, neither express or implied, is offered with this software. You use this software at your own risk. In case of loss, no person or entity owes you anything whatsoever. You have been warned. =head1 LICENSE This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself. =cut # vi:ts=2:sw=2:et:sta 1;
Subject: onetri.stl
Download onetri.stl
application/vnd.ms-pki.stl 184b

Message body not shown because it is not plain text.

From: b [...] bathsheba.com
On Sun Feb 24 08:30:07 2013, bathsheba wrote: Show quoted text
> Thanks for writing this module! > > I was using it in Windows 7 with ActivePerl to import a lot of STL > files, and I saw it fail intermittently with binary format. I tracked > this down and found that when bytes 0x0D, i.e. a carriage return, occur > in such a file, it fails. Drops the character, reads a lot of gibberish > numbers, and eventually fails with an unexpected EOF. Attached is a > small STL, 2 triangles, that shows this behavior. > > I think it's because the perl read function is, in systems that > distinguish binary from text files, a text read: it strips carriage > returns. Unfortunately Windows is such a system. > > I found that by calling binmode on the file handle, this is fixed. > In STL.pm at line 348 I inserted 'binmode $fh;'. Attached is this file > with that change. > > This is probably not the right place to add that, because it could fail > earlier during format detection. But I didn't understand that part very > well and I only need to read binary files, so this is the best I could > do right now. > > I'm not a developer and don't feel up to putting a patch on CPAN, > especially when I don't think it's quite right. But it would be awesome > if this got fixed properly. > > Thanks for your consideration!
From: b [...] bathsheba.com
PS added binmode again at line 423, in the _write_binary function, to get rid of the same problem when writing.
Subject: STL.pm
package CAD::Format::STL; $VERSION = v0.2.1; use warnings; use strict; use Carp; use CAD::Format::STL::part; =head1 NAME CAD::Format::STL - read/write 3D stereolithography files =head1 SYNOPSIS Reading: my $stl = CAD::Format::STL->new->load("foo.stl"); # what about the part/multipart? my @facets = $stl->part->facets; Writing: my $stl = CAD::Format::STL->new; my $part = $stl->add_part("my part"); $part->add_facets(@faces); $stl->save("foo.stl"); # or $stl->save(binary => "foo.stl"); Streaming read/write: my $reader = CAD::Format::STL->reader("foo.stl"); my $writer = CAD::Format::STL->writer(binary => "bar.stl"); while(my $part = $reader->next_part) { my $part_name = $part->name; $writer->start_solid($part_name); while(my @data = $part->facet) { my ($normal, @vertices) = @data; my @v1 = @{$vertices[0]}; my @v2 = @{$vertices[0]}; my @v3 = @{$vertices[0]}; # that's just for illustration $writer->facet(\@v1, \@v2, \@v3); # note the omitted normal } $writer->end_solid; } =begin design The reader auto-detects whether it is binary (but assumes ascii when seek can't go backwards.) The reader and writer both take 1, 2, or {1,2}+2n arguments. This package and/or the reader/writer are subclassable (though getting $self->reader to instantiate a subclass implies that you have subclassed $self.) A cached_facet (or raw_facet) method is necessary to ensure uniform tranformation of shared points (and optimize the computation.) This would return the normal and points as a list of scalars rather than arrays, with a later call to unpack_point() or something. The caller needs to be able to handle the caching (or else there is a callback for non-cached (or an override for unpack_point().) Maybe $self->set_writer() and set_reader() immutable object methods? =end design =head1 ABOUT This module provides object-oriented methods to read and write the STL (Stereo Lithography) file format in both binary and ASCII forms. The STL format is a simple set of 3D triangles. =cut use Class::Accessor::Classy; lo 'parts'; no Class::Accessor::Classy; =head1 Constructor =head2 new my $stl = CAD::Format::STL->new; =cut sub new { my $package = shift; my $class = ref($package) || $package; my $self = {parts => []}; bless($self, $class); return($self); } # end subroutine new definition ######################################################################## =head2 add_part Create a new part in the stl. my $part = $stl->add_part("name"); Optionally, add the faces directly: my $part = $stl->add_part("name", @faces); =cut sub add_part { my $self = shift; my ($name, @faces) = @_; my $part = CAD::Format::STL::part->new($name, @faces); push(@{$self->{parts}}, $part); return($part); } # end subroutine add_part definition ######################################################################## =head2 part Get the part at $index. Negative indices are valid. my $part = $stl->part($index); Throws an error if there is no such part. =cut sub part { my $self = shift; my ($index) = @_; @{$self->{parts}} or croak("file has no parts"); $index ||= 0; exists($self->{parts}[$index]) or croak("no part $index"); return($self->{parts}[$index]); } # end subroutine part definition ######################################################################## =head1 I/O Methods =head2 load Load an STL file (auto-detects binary/ascii) $stl = $stl->load("filename.stl"); Optionally, explicitly declare binary mode: $stl = $stl->load(binary => "filename.stl"); The $self object is returned to allow e.g. chaining to C<new()>. The filename may also be a filehandle. =cut sub load { my $self = shift; my ($file, @and) = @_; my $mode; if(@and) { (@and > 1) and croak('too many arguments to load()'); $mode = $file; ($file) = @and; } # allow filehandle unless((ref($file) || '') eq 'GLOB') { open(my $fh, '<', $file) or die "cannot open '$file' for reading $!"; $file = $fh; } # detection unless($mode) { unless(seek($file, 0,0)) { croak('must have explicit mode for non-seekable filehandle'); } # now, detection... $mode = sub { my $fh = shift; seek($fh, 80, 0); my $count = eval { my $buf; read($fh, $buf, 4) or die; unpack('L', $buf); }; $@ and return 'ascii'; # if we hit eof, it can't be binary $count or die "detection failed - no facets?"; my $size = (stat($fh))[7]; # calculate the expected file size my $expect = + 80 # header + 4 # count + $count * ( + 4 # normal, pt,pt,pt (vectors) * 4 # bytes per value * 3 # values per vector + 2 # the trailing 'short' ); return ($size == $expect) ? 'binary' : 'ascii'; }->($file); seek($file, 0, 0) or die "cannot reset filehandle"; } my $method = '_read_' . lc($mode); $self->can($method) or croak("invalid read mode '$mode'"); $self->$method($file); return($self); } # end subroutine load definition ######################################################################## =head2 _read_ascii $self->_read_ascii($filehandle); =cut sub _read_ascii { my $self = shift; my ($fh) = @_; my $getline = sub { while(my $line = <$fh>) { $line =~ s/\s*$//; # allow any eol length($line) or next; return($line); } return; }; my $p_re = qr/([^ ]+)\s+([^ ]+)\s+([^ ]+)$/; my $part; while(my $line = $getline->()) { if($line =~ m/^\s*solid (.*)/) { $part = $self->add_part($1); } elsif($line =~ m/^\s*endsolid (.*)/) { my $name = $1; $part or die "invalid 'endsolid' entry with no current part"; ($name eq $part->name) or die "end of part '$name' should have been '", $part->name, "'"; $part = undef; } elsif($part) { my @n = ($line =~ m/^\s*facet\s+normal\s+$p_re/) or die "how did that happen? ($line)"; #warn "got ", join('|', @n); my @facet = (\@n); my $next = $getline->(); unless($next and ($next =~ m/^\s*outer\s+loop$/)) { die "facet doesn't start with 'outer loop' ($next)"; } push(@facet, do { my @got; while(my $line = $getline->()) { ($line =~ m/^\s*endloop$/) and last; if($line =~ m/^\s*vertex\s+$p_re/) { push(@got, [$1, $2, $3]); } } @got; }); (scalar(@facet) == 4) or die "need three vertices per facet (not $#facet)"; my $end = $getline->(); ($end and ($end =~ m/^\s*endfacet/)) or die "bad endfacet $line"; $part->add_facets([@facet]); } else { die "what? ($line)"; } } $part and die "part '", $part->name, "' was left open"; } # end subroutine _read_ascii definition ######################################################################## =head2 get_<something> These functions are currently only used internally. =over =item get_triangle =item get_ulong =item get_float32 =item get_short =back =cut sub get_triangle { my ($fh) = @_; my ($n, $x, $y, $z) = map({[map({get_float32($fh)} 1..3)]} 1..4); my $scrap = get_short($fh); return($n, $x, $y, $z); } sub get_ulong { my ($fh) = @_; my $buf; read($fh, $buf, 4) or warn "EOF?"; return(unpack('L', $buf)); } sub get_float32 { my ($fh) = @_; my $buf; read($fh, $buf, 4) or warn "EOF?"; return(unpack('f', $buf)); } sub get_short { my ($fh) = @_; my $buf; read($fh, $buf, 2) or warn "EOF?"; return(unpack('S', $buf)); } =head2 _read_binary $self->_read_binary($filehandle); =cut sub _read_binary { my $self = shift; my ($fh) = @_; binmode $fh; $self->parts and die "binary STL files must have only one part"; die "bigfloat" unless(length(pack("f", 1)) == 4); # TODO try to read part name from header (up to \0) my $name = 'a part'; seek($fh, 80, 0); my $triangles = get_ulong($fh); my $part = $self->add_part($name); my $count = 0; while(1) { my @tr = get_triangle($fh); # TODO check that the unit normal is within a thousandth of a radian # (0.001 rad is ~0.06deg) $part->add_facets([@tr]); $count++; eof($fh) and last; } ($count == $triangles) or die "ERROR: got $count facets (expected $triangles)"; } # end subroutine _read_binary definition ######################################################################## =head2 save $stl->save("filename.stl"); $stl->save(binary => "filename.stl"); =cut sub save { my $self = shift; my ($file, @and) = @_; my $mode; if(@and) { (@and > 1) and croak('too many arguments to save()'); $mode = $file; ($file) = @and; } # allow filehandle unless((ref($file) || '') eq 'GLOB') { open(my $fh, '>', $file) or die "cannot open '$file' for writing $!"; $file = $fh; } $mode = 'ascii' unless($mode); my $method = '_write_' . lc($mode); $self->can($method) or croak("invalid write mode '$mode'"); $self->$method($file); } # end subroutine save definition ######################################################################## =head2 _write_binary $self->_write_binary($filehandle); =cut sub _write_binary { my $self = shift; my ($fh) = @_; my ($part, @and) = $self->parts; @and and die 'cannot write binary files with multiple parts'; binmode $fh; my $name = $part->name; # utf8 is ok print $fh $name, "\0" x (80 - do {use bytes; length($name)}); my @facets = $part->facets; print $fh pack('L', scalar(@facets)); foreach my $facet (@facets) { print $fh map({map({pack('f', $_)} @$_)} @$facet); print $fh "\0" x 2; } } # end subroutine _write_binary definition ######################################################################## =head2 _write_ascii $self->_write_ascii($filehandle); =cut sub _write_ascii { my $self = shift; my ($fh) = @_; my $spaces = ''; my $print = sub {print $fh $spaces, @_, "\n"}; my @parts = $self->parts or croak("no parts to write"); foreach my $part (@parts) { $print->('solid ', $part->name); $spaces = ' 'x2; foreach my $facet ($part->facets) { my ($n, @pts) = @$facet; $print->(join(' ', 'facet normal', @$n)); $spaces = ' 'x4; $print->('outer loop'); $spaces = ' 'x6; (@pts == 3) or die "invalid facet"; foreach my $pt (@pts) { $print->(join(' ', 'vertex', @$pt)); } $spaces = ' 'x4; $print->('endloop'); $spaces = ' 'x2; $print->('endfacet'); } $spaces = ''; print $fh 'endsolid ', $part->name, "\n"; } } # end subroutine _write_ascii definition ######################################################################## =head1 AUTHOR Eric Wilhelm @ <ewilhelm at cpan dot org> http://scratchcomputing.com/ =head1 BUGS If you found this module on CPAN, please report any bugs or feature requests through the web interface at L<http://rt.cpan.org>. I will be notified, and then you'll automatically be notified of progress on your bug as I make changes. If you pulled this development version from my /svn/, please contact me directly. =head1 COPYRIGHT Copyright (C) 2007 Eric L. Wilhelm, All Rights Reserved. =head1 NO WARRANTY Absolutely, positively NO WARRANTY, neither express or implied, is offered with this software. You use this software at your own risk. In case of loss, no person or entity owes you anything whatsoever. You have been warned. =head1 LICENSE This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself. =cut # vi:ts=2:sw=2:et:sta 1;