summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rwxr-xr-xCAMM.pm672
-rw-r--r--DXF.pm1469
-rw-r--r--GPL-2.txt339
-rw-r--r--GPL-3.txt674
-rw-r--r--LICENSE.txt11
-rw-r--r--README.txt12
-rwxr-xr-xcamm2svg.pl23
-rwxr-xr-xdxf2camm.pl505
-rwxr-xr-xdxf2xml.pl29
-rwxr-xr-xdxf_simplify.pl43
-rwxr-xr-xxml2dxf.pl57
11 files changed, 3834 insertions, 0 deletions
diff --git a/CAMM.pm b/CAMM.pm
new file mode 100755
index 0000000..e7b99f5
--- /dev/null
+++ b/CAMM.pm
@@ -0,0 +1,672 @@
+#!/usr/bin/perl
+
+# Module to read/write CAMM-GL III content
+
+## Copyright (c) 2019-2020 by Thomas Kremer
+## License: GPL ver. 2 or 3
+
+package CAMM;
+
+# This implements (a subset of) the CAMM-GL III instruction set "mode 2".
+
+use strict;
+use warnings;
+
+use Math::Trig qw(pi);
+#use POSIX qw(lround);
+
+use overload '""' => "content";
+
+use constant units_per_mm => 40;
+our $units_per_mm = 40;
+
+# escape character for writing strings:
+#our $escape_char = "\003";
+# examplary slow and fast speed settings:
+our $slow = 2;
+our $fast = 30;
+
+# named arguments:
+# escape_char: string terminator for text (default: "\003" aka END-OF-TEXT)
+# relative, down, speed, char_size, char_slant, tool,
+# force, p: current state of the machine (is not set automatically).
+# outfile: a file to open for writing (overwrites "f")
+# f: a file to write to (overwrites "output")
+# output: a scalar ref to append commands or a ref to a subroutine
+# (or object) to call for writing commands. (default: new scalar ref)
+# no_timeouts: disable timeout detection that checks the tool after inactivity.
+
+sub new {
+ my ($class,%args) = shift;
+ if (defined $args{outfile}) {
+ open($args{f},">",$args{outfile}) or die "cannot open $args{outfile}: $!";
+ }
+ if (defined $args{f}) {
+ my $f = $args{f};
+ $args{output} = sub {
+ print $f @_;
+ };
+ }
+ $args{output} //= \(my $s = "");
+ $args{$$_[0]} //= $$_[1] for (["relative",0],["escape_char","\x03"],["down",0]);
+ return bless \%args, ref $class || $class;
+}
+
+sub copy {
+ my $self = shift;
+ return bless {%$self}, ref $self;
+}
+
+package CAMM::Commands {
+ # This is a list of bare, stateless commands with parameters,
+ # so mostly sprintf-like functions returning CAMM code.
+ # Their state logic is managed by the parent module.
+
+ #our $escape_char = "\003";
+ sub header { # escape_char
+ my $escape_char = $_[0]//"\003";
+ "\003\015\012\015\012\015\012\015\012\015\012".
+ ";IN;PU;PA0,0;IW0,0,47000,64000;VS30;DT$escape_char;\n";
+ # PA isn't a good idea:
+ #";IN;PA0,0;IW0,0,47000,64000;VS30;DT$escape_char;\n";
+ }
+ sub set_escape_char {
+ my $escape_char = $_[0];
+ "DT$escape_char;\n";
+ }
+ sub footer {
+ moveto(0,0);
+ }
+ sub tool_up {
+ "PU;\n";
+ }
+ sub tool_down {
+ "PD;\n";
+ }
+ sub set_relative {
+ "PR;\n";
+ }
+ sub set_absolute {
+ "PA;\n";
+ }
+ sub moveto { # x,y
+ sprintf "PU%.2f,%.2f;\n", @_;
+ }
+ sub lineto { # x,y
+ sprintf "PD%.2f,%.2f;\n", @_;
+ }
+ sub polylineto { # x1,y1,x2,y2,...
+ "PD".join(",",map {sprintf "%.2f",$_} @_).";\n";
+ }
+ sub set_speed { # v=2..30 is normal; cm/sec; max 85cm/sec
+ sprintf "VS%d;\n", $_[0];
+ }
+ sub circle { # r; current point is center.
+ sprintf "CI%.2f;\n", $_[0];
+ }
+ sub arc { # Mx,My,angle; radius is such that it includes the current point.
+ sprintf "AA%.2f,%.2f,%.2f;\n", @_;
+ # FIXED: datasheet says "*1"(float), but other angles are "*3"(float)...
+ }
+ sub arc_relative { # Mx,My,angle; Mx,My are relative to current point.
+ sprintf "AR%.2f,%.2f,%.2f;\n", @_;
+ # FIXED: datasheet says "*1"(float), but other angles are "*3"(float)...
+ }
+ sub moveto_relative { # x,y
+ sprintf "PR;PU%.2f,%.2f;\n", @_;
+ }
+ sub lineto_relative { # x,y
+ sprintf "PR;PD%.2f,%.2f;\n", @_;
+ }
+ sub polylineto_relative { # x1,y1,x2,y2,...
+ "PR;PD".join(",",map {sprintf "%.2f",$_} @_).";\n";
+ }
+ sub set_char_size { # w,h in *cm*
+ sprintf "SI%.5f,%.5f;\n", @_;
+ }
+ sub set_char_slant { # tan(angle)
+ sprintf "SL%.2f;", $_[0];
+ }
+ sub text { # text,escape_char; text must not contain escape_char
+ sprintf "LB%s%s\n", $_[0],($_[1]//"\003"); #$escape_char;
+ }
+ sub tool_change {
+ sprintf "SP%d;\n", $_[0];
+ }
+ sub set_force {
+ sprintf "!FS %d\n", floor($_[0]/10)*10;
+ }
+}
+
+BEGIN {
+ # name ? precondition ! postcondition
+ # -> to invoke <name>, <precondition> has to be guaranteed.
+ # Afterwards, state has changed to establish <postcondition>.
+ my @cmdspec = qw(
+ header!relative=0!down=0
+ footer?relative=0!down=0
+ tool_up!down=0
+ tool_down!down=1
+ moveto?relative=0!down=0
+ lineto?relative=0!down=1
+ polylineto?relative=0!down=1
+ circle?down=1
+ arc?down=1!relative=0
+ arc_relative?down=1!relative=1
+ moveto_relative!relative=1!down=0
+ lineto_relative!relative=1!down=1
+ polylineto_relative!relative=1!down=1
+ );
+ # set_absolute!relative=0
+ # set_relative!relative=1
+ # set_speed
+ # set_char_size
+ # set_char_slant
+ # tool_change
+ # set_force
+
+ my %setters = (
+ speed => \&CAMM::Commands::set_speed,
+ char_size => \&CAMM::Commands::char_size,
+ char_slant => \&CAMM::Commands::char_slant,
+ tool => \&CAMM::Commands::tool_change,
+ force => \&CAMM::Commands::set_force,
+ escape_char => \&CAMM::Commands::set_escape_char,
+ down => sub { $_[0] ?
+ CAMM::Commands::tool_down
+ : CAMM::Commands::tool_up;
+ },
+ relative => sub { $_[0] ?
+ CAMM::Commands::set_relative
+ : CAMM::Commands::set_absolute;
+ },
+ );
+
+ # sub set_escape_char {
+ # my ($self,$c) = @_;
+ # $self->{escape_char} = $c;
+ # local $CAMM::Commands::escape_char = $c;
+ # $self->emit(CAMM::Commands::set_escape_char($c));
+ # }
+
+ # getters, setters.
+ for my $name (keys %setters) {
+ my $sub = sub {
+ return $_[0]->{$name};
+ };
+ my $settersub = sub {
+ $_[0]->set($name,$_[1]);
+ };
+ my $get_name = "get_$name";
+ my $set_name = "set_$name";
+ no strict "refs";
+ *$name = $sub;
+ *$get_name = $sub;
+ *$set_name = $settersub;
+ }
+
+ sub set {
+ my ($self,$name,$value) = @_;
+ # if ($name eq "down") {
+ # $self->emit(
+ # $value ?
+ # CAMM::Commands::tool_down
+ # : CAMM::Commands::tool_up);
+ # } elsif ($name eq "abs") {
+ # $self->emit(
+ # $value ?
+ # CAMM::Commands::set_absolute
+ # : CAMM::Commands::set_relative);
+ # } els
+ if (defined $setters{$name}) {
+ $self->emit($setters{$name}($value));
+ } else {
+ die "unknown variable \"$name\"";
+ }
+ $self->{$name} = $value;
+ }
+
+#my $global_object = __PACKAGE__->new;
+
+ for (@cmdspec) {
+ my @spec = split /(?=[!?])/, $_;
+ my $name = shift @spec;
+ my $command = do {
+ no strict "refs";
+ \&{"CAMM::Commands::$name"};
+ };
+ my @reqs;
+ my @sets;
+ for (@spec) {
+ if (/^([?!])(\w+)=(\d+)/) {
+ my $arr = $1 eq "?" ? \@reqs : \@sets;
+ push @$arr, [$2,0+$3];
+ } else {
+ die "invalid internal spec";
+ }
+ }
+ my $sub = sub {
+ #my $self = (@_ && ref $$_[0] eq __PACKAGE__) ? shift : $global_object;
+ my $self = shift;
+ for (@reqs) {
+ if (($self->{$$_[0]}//"-1") != $$_[1]) {
+ $self->set($$_[0],$$_[1]);
+ }
+ }
+ $self->emit($command->(@_));
+ for (@sets) {
+ $self->{$$_[0]} = $$_[1];
+ }
+ };
+ {
+ no strict "refs";
+ *$name = $sub;
+ }
+ }
+
+}
+
+sub text {
+ #my $self = (@_ && ref $$_[0] eq __PACKAGE__) ? shift : $global_object;
+ my $self = shift;
+ #local $CAMM::Commands::escape_char = $self->{escape_char};
+ $self->emit(CAMM::Commands::text($_[0],$self->{escape_char}));
+}
+
+sub emit {
+ my ($self,$code) = @_;
+ my $out = $self->{output};
+ if (ref $out eq "SCALAR") {
+ $$out .= $code;
+# } elsif (ref $out eq "CODE") {
+ } else {
+ # if we've been idle for a couple of seconds, the tool has been upped
+ # automatically, so we have to down it again. This only applies to
+ # real-time usage and doesn't hurt otherwise, so we just use it in
+ # all direct-io cases.
+ my $lt = \$self->{lasttime};
+ my $t = time;
+ if ($$lt+10 > $t && $self->{down} && !$self->{no_timeouts}) {
+ $code = CAMM::Commands::tool_down().$code;
+ }
+ $$lt = $t;
+ $out->($code);
+ }
+}
+
+sub flush {
+ my $self = shift;
+ my $output = $self->{output};
+ if (ref $output eq "SCALAR") {
+ my $s = $$output;
+ $$output = "";
+ return $s;
+ }
+ return;
+}
+
+sub content {
+ my $self = shift;
+ my $output = $self->{output};
+ if (ref $output eq "SCALAR") {
+ my $s = $$output;
+ return $s;
+ }
+ return;
+}
+
+# $paths = [$polyline,...]
+# $polyline = ["open"|"closed",[point,...]]
+# options:
+# boolean: header, footer, headerfooter, relative
+# float: epsilon, offset, shortline, smallangle
+
+sub from_polylines {
+ my $self = shift;
+ $self = $self->new unless ref $self;
+ #my $self = (@_ && ref $$_[0] eq __PACKAGE__) ? shift : $global_object;
+ my ($paths,%options) = @_;
+ @options{qw(header footer)} = (1,1) if $options{headerfooter};
+ $self->header() if $options{header};
+ my $eps = $options{epsilon}//0.00001;
+ for (@$paths) {
+ my $points = $$_[1];
+ $self->moveto(@{$$points[0]});
+ if ($options{offset}) { # if offset = 0, use the other code as well.
+ my $offs = $options{offset};
+ my $short_line = $options{shortline}//80; # 1.5mm is small.
+ my $small_angle = $options{smallangle}//10; # 10° is small.
+ my @p = @{$$points[0]};
+ my $first = 1;
+ for my $i (1..$#$points) {
+ my $pt = $$points[$i];
+ my @q = ($$pt[0]-$p[0],$$pt[1]-$p[1]);
+ my $l = sqrt($q[0]**2+$q[1]**2);
+ next unless $l > $eps;
+ my @r = @q;
+ if ($first) {
+ $_ *= 1+$offs/$l for @q;
+ }
+ $_ *= -$offs/$l for @r;
+ @p = @$pt;
+ $first = 0;
+
+ # sadly, we can't use relative coordinates here, because we don't
+ # know how arc end coordinates are rounded by the device.
+
+ #$res .= CAMM::lineto_relative(@q);
+ #$res .= CAMM::lineto($$pt[0],$$pt[1]);
+ #my @q_abs = map lround($$pt[$_]-$r[$_]), 0,1;
+ my @q_abs = map $$pt[$_]-$r[$_], 0,1;
+ $self->lineto(@q_abs);
+ for my $j ($i+1 .. $#$points) { # implicit $i < $#$points
+ my $pt2 = $$points[$j];
+ my @q2 = ($$pt2[0]-$$pt[0],$$pt2[1]-$$pt[1]);
+ my $l2 = sqrt($q2[0]**2+$q2[1]**2);
+ next unless $l2 > $eps;
+ # TODO: since arcs are rather slow, we might want to avoid real
+ # arcs here and use a polyline approximation instead.
+ # arg(q2/q1) = arg(q2*conj(q1))
+ my $angle = 180/pi*
+ atan2($q2[1]*$q[0]-$q2[0]*$q[1], $q2[0]*$q[0]+$q2[1]*$q[1]);
+ #$res .= arc_relative(@r,$angle);
+ # if the angle is small and the next line is short, we assume an
+ # interpolated curved line. No need to emphasize the corners.
+ if (abs($angle) > $small_angle || $l2 > $short_line) {
+ $self->arc(@p,$angle);
+ }
+ last;
+ }
+ }
+ } else {
+ my @coords;
+ if ($options{relative}) {
+ my @p = @{$$points[0]};
+ for (@$points[1..$#$points]) {
+ push @coords, $$_[0]-$p[0],$$_[1]-$p[1];
+ @p = @$_;
+ }
+ $self->polylineto_relative(@coords);
+ } else {
+ @coords = map @$_[0,1], @$points[1..$#$points];
+ #$res .= "# ".scalar(@coords)." points;\n";
+ $self->polylineto(@coords);
+ }
+ }
+ }
+ $self->footer() if $options{footer};
+ #return $self->flush();
+ return $self;
+}
+
+# parsing
+
+my $florex = qr/[-+]?(?:\d+(?:\.\d+)?|\.\d+)(?:[eE][-+]?\d+)?/;
+
+sub _take_token {
+ my ($camm,$esc) = @_;
+ $$camm =~ s/^\s+//;
+ my ($cmd,@args);
+ my $check_numeric = 0;
+ if ($$camm =~ s/^(?:\^[ \t]*)?([A-Z]{2})//) {
+ # mode 2
+ $cmd = $1;
+ if ($cmd eq "LB" || $cmd eq "WD") {
+ my $i = index($$camm,$esc);
+ die "unterminated \"$cmd\"." if $i == -1;
+ @args = (substr($$camm,0,$i-1));
+ substr($$camm,0,$i) = "";
+ # $$camm =~ s/^(.*)\Q$esc\E//s;...
+ } elsif ($cmd eq "DT") {
+ @args = (substr($$camm,0,1));
+ $$camm =~ s/^.[^;\n]*;//s;
+ } elsif ($$camm =~ s/^([^;]*);//) {
+ my $argstr = $1;
+ die "line break in argument to \"$cmd\"" if $argstr =~ /\n/;
+ @args = split /,/,$argstr;
+ $check_numeric = 1;
+ } else {
+ die "missing semicolon in command \"$cmd\".";
+ }
+ } elsif ($$camm =~ s/^(![A-Z]{2})(.*)//) { # implicit /$/
+ # mode 1 & 2 common
+ ($cmd,@args) = ($1,$2);
+ $check_numeric = 1;
+ } elsif ($$camm =~ s/^\e(\.[A-Z@])//) {
+ # device control instructions over RS-232
+ $cmd = $1;
+ if ($$camm =~ s/^([^:\n]*)://) {
+ @args = split /;/, $1;
+ }
+ $check_numeric = 1;
+ } elsif ($$camm =~ s/^([A-Z])//) {
+ # mode 1
+ $cmd = $1;
+ if ($$camm =~ s/^(.*)//) {
+ @args = $cmd eq "P" ? ($1) : split /,/,$1;
+ $check_numeric = 1 if $cmd ne "P";
+ }
+ } else {
+ return;
+ }
+ if ($check_numeric) {
+ for (@args) {
+ if (/^\s*($florex)\s*$/) {
+ $_ = $1;
+ } else {
+ die "non-numerical argument \"$_\" in command \"$cmd\".";
+ }
+ }
+ }
+ return ($cmd,\@args);
+}
+
+# FIXED: PA,PR,AA,AR don't up/down the pen!
+# FIXED: Idleness makes the machine up the pen and not down it again!
+# (therefore we use PU/PD whenever possible.)
+# FIXED: The machine can do floating point!
+# FIXED: PA/PR influence absoluteness/relativity of PU/PD!
+# Note: arcs turn positively leftways (as expected).
+
+our %camm2svg_commands;
+{
+ my $unimplemented = sub {
+ warn "command \"$_[1]\" is not implemented yet.";
+ };
+ %camm2svg_commands = (
+ # command => sub ($context,$command,@arguments)
+ # $context = { p => [$x,$y], d => "", escape_char => "\003" }
+ IN => sub {
+ @{$_[0]{p}} = (0,0);
+ $_[0]{escape_char} = "\003";
+ $_[0]{d} .= "M 0,0 ";
+ },
+ DT => sub { $_[0]{escape_char} = $_[2]; },
+ PA => sub {
+ $_ = 0+$_ for @_[2..$#_];
+ my ($ctx,$cmd,@xy) = @_;
+ my $i = {PA=>0,PR=>1,PU=>2,PD=>3}->{$cmd};
+ $ctx->{$i&2?"down":"relative"} = $i&1;
+ pop @xy if @xy%2 != 0;
+ return if !@xy;
+
+ my $p = $ctx->{p};
+ my $letter = $ctx->{down} ? "l" : "m";
+ if ($ctx->{relative}) {
+ for (0..$#xy) {
+ $$p[$_%2] += $xy[$_];
+ }
+ } else {
+ @$p = @xy[-2,-1];
+ $letter = uc($letter);
+ }
+ $_[0]{d} .= "$letter ".join(",",@xy)." ";
+ },
+# PU => sub {
+# $_[0]{down} = 0;
+# pop if @_%2 != 0;
+# return if @_ <= 2;
+# $_ = 0+$_ for @_[2..$#_];
+# @{$_[0]{p}} = @_[-2,-1];
+# $_[0]{d} .= "M ".join(",",@{$_[0]{p}})." ";
+# },
+# PD => sub {
+# $_[0]{down} = 1;
+# pop if @_%2 != 0;
+# return if @_ <= 2;
+# $_ = 0+$_ for @_[2..$#_];
+# @{$_[0]{p}} = @_[-2,-1];
+# $_[0]{d} .= "L ".join(",",@_[2..$#_])." ";
+# },
+# PR => sub {
+# $_[0]{relative} = 1;
+# pop if @_%2 != 0;
+# return if @_ <= 2;
+# $_ = 0+$_ for @_[2..$#_];
+# my $p = $_[0]{p};
+# for (2..$#_) {
+# $$p[$_%2] += $_[$_];
+# }
+# $_[0]{d} .= "l ".join(",",@_[2..$#_])." ";
+# },
+ #AA => alias for AR,
+ AR => sub {
+ $_ = 0+$_ for @_[2..$#_];
+ my ($ctx,$cmd,$x,$y,$ang) = @_;
+ my $p = $ctx->{p};
+ if ($cmd eq "AA") {
+ $x -= $$p[0];
+ $y -= $$p[1];
+ }
+ my $r = sqrt($x**2+$y**2);
+ my $a1 = atan2(-$y,-$x);
+ my $a2 = $a1+$ang/180*pi;
+ my $longarc = $ang > 180 ? 1 : 0;
+ my $rightways = $ang < 0 ? 0 : 1; # FIXME: svg coord system is left-handed, but we manually flip the y axis to make do....
+ my @dp = ($r*(cos($a2)-cos($a1)),$r*(sin($a2)-sin($a1)));
+ $$p[$_] += $dp[$_] for 0,1;
+ if ($ctx->{down}) {
+ $ctx->{d} .= "a $r,$r,0,$longarc,$rightways,".join(",",@dp)." ";
+ } else {
+ $ctx->{d} .= "m ".join(",",@dp)." ";
+ }
+ },
+ CI => sub {
+ return unless $_[0]{down};
+ $_ = 0+$_ for @_[2..$#_];
+ my $r = $_[2];
+ $_[0]{d} .= "m $r,0 a $r,$r,0,0,0,".(-2*$r).",0 ".
+ "a $r,$r,0,0,0,".(2*$r).",0 z m ".(-$r).",0 ";
+ },
+
+ IW => sub { # need to implement this because it is in our header.
+ my ($ctx,$cmd,@args) = @_;
+ $ctx->{input_window} = [@args];
+ #"IW0,0,47000,64000;";
+ },
+ map({$_ => 0} qw(
+ OA OC OE OF OH OI OO OP OS OW SS SP VS
+ !FS !NR !PG !ST
+ .B .M .N .H .I .@ .O .E .L .J .K .R) # actual no-ops
+ ),
+ map({$_ => $unimplemented} qw(
+ H D M I R L B X P S Q N C E A G K T
+ CA CP CS DF DI DR EA ER EW FT IM IP LB LT
+ PT RA RO RR SA SC SI SL SM SR TL UC WD WG XT YT
+ ) # unimplemented ops
+ ),
+ );
+ my @aliases = qw(
+ PD PA
+ PU PA
+ PR PA
+ AA AR
+ H IN
+ D PD
+ M PU
+ I PR
+ );
+ for (0..@aliases/2-1) {
+ $camm2svg_commands{$aliases[2*$_]} = $camm2svg_commands{$aliases[2*$_+1]};
+ }
+ # maybe: !PG
+}
+
+# <path d="%s" style='stroke:black; stroke-width: 40px; fill:#000000; fill-opacity:0.2;' />
+our $svg_template = <<'EOSVG';
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" width="%f" height="%f">
+<g transform='scale(0.025,-0.025) translate(%f,%f)'>
+%s
+</g>
+</svg>
+EOSVG
+
+our $svg_path_template = <<'EOSVG';
+ <path d="%s" style='stroke:%s; stroke-width: 40px; fill:#000000; fill-opacity:0.2;' />
+EOSVG
+
+sub to_svgpath {
+ my ($self,$camm,$splittable) = @_;
+ $self = $self->new unless ref $self;
+ my %defcontext = (
+ escape_char => "\003",
+ p => [0,0],
+ d => "",
+ );
+ $self->{$_} //= $defcontext{$_} for keys %defcontext;
+ while ($camm ne "") {
+ my ($cmd,$args) = _take_token(\$camm,$self->{escape_char});
+ if (!defined $cmd) {
+ $camm =~ s/^[^;\n]*[;\n]//;
+ next;
+ }
+ my $handler = $camm2svg_commands{$cmd};
+ if (defined $handler) {
+ $handler->($self,$cmd,@$args) if $handler != 0;
+ if ($splittable && !$self->{down}) {
+ $self->{d} .= " M ".join(",",@{$self->{p}})." ";
+ }
+ } else {
+ warn "ignoring unknown command \"$cmd\"";
+ }
+ }
+ my $d = $self->{d};
+ delete $self->{d};
+ return $d;
+}
+
+# TODO: convert mm to pixels
+sub to_svg {
+ my ($self,$camm,$split,$colored) = @_;
+ #$self = $self->new unless ref $self;
+ $self = $self->new(output => sub {});
+ my $d = $self->to_svgpath($camm,$split);
+ my $win = $self->{input_window};
+ my @origin = (0,0);
+ my @size = (100,100);
+ my $scale = 1/$units_per_mm;
+ if (defined $win) {
+ @origin = @$win[0,3];
+ $_ = -$_ for @origin;
+ @size = (($$win[2]-$$win[0])*$scale,($$win[3]-$$win[1])*$scale);
+ }
+ my $color = "black";
+ my @paths;
+ if ($split) {
+ @paths = split /(?=M )/, $d;
+ my $i = 0;
+ for (@paths) {
+ $color = sprintf "#%02x%02x%02x", map 127*(1+cos(($i/@paths*5/6-$_/3)*2*pi)),0..2
+ if $colored;
+ $_ = (sprintf $svg_path_template, $_, $color);
+ $i++;
+ }
+ } else {
+ @paths = (sprintf $svg_path_template, $d, $color);
+ }
+ return sprintf $svg_template, @size, @origin, join("",@paths);
+}
+
+1;
+
diff --git a/DXF.pm b/DXF.pm
new file mode 100644
index 0000000..2e4bd1a
--- /dev/null
+++ b/DXF.pm
@@ -0,0 +1,1469 @@
+#!/usr/bin/perl
+
+# Module to read/write DXF files
+
+## Copyright (c) 2018-2020 by Thomas Kremer
+## License: GPL ver. 2 or 3
+
+package DXF;
+
+use strict;
+use warnings;
+
+use List::Util qw(max);
+use Math::Trig qw(pi);
+use POSIX qw(floor);
+use IO::Handle;
+# xml related methods will require XML::DOM at runtime.
+#use XML::DOM;
+
+# --- internal stuff ---
+
+# 9: variable name identifier.
+our %dxf_node_ids = (0 => 1, 9 => 1);
+our %dxf_end_nodes = (
+ "ENDSEC" => "SECTION",
+ "ENDTAB" => "TABLE",
+ "EOF" => "dxf",
+ "ENDBLK" => "BLOCK", # ENDBLK has additional parameters?!
+ "SEQEND" => "POLYLINE",
+);
+
+our %dxf_attr_names = (
+ 1 => "text",
+ 2 => "name",
+ 3 => "text2",
+ 4 => "text3",
+ 5 => "handle", # entity handle, up to 16 hex digits.
+ 6 => "linetype",
+ 7 => "textstyle",
+ 8 => "layer",
+ # 10..18: x, 20..28: y, 30..37: z
+ 38 => "elevation",
+ 39 => "thickness",
+# 40 = radius of circle, but other floats in other contexts...
+# 40 => "r", # 40-48: floating point values...
+ 48 => "linetype_scale",
+ 49 => "values", # multiple values that make up a list, need a repeat count (7x) before!
+ # 50..58: angles in degrees
+ 60 => "invisibile", # 1 for invisible, 0 or undef for visible
+ 62 => "color",
+ 66 => "entities_follow", # flag. what for?
+ 67 => "space", # model- or paper-space
+ # 70-78: integer values, such as repeat counts, flag bits or modes
+ # 90-98: 32-bit integer values
+ 100 => "subclass", # required if object is derived from another concrete class
+ 102 => "control_string", # application defined stuff
+ 105 => "dimvar_handle",
+ 210 => "extrusion_direction_x",
+ 220 => "extrusion_direction_y",
+ 230 => "extrusion_direction_z",
+
+ 999 => "comment",
+
+ # we ignore anything above 999 here for now.
+
+ # TODO: object-specific attribute names. Sometimes sensible...
+
+# TABLE: 2, 70
+# LAYER: 2, 6, 62, 70
+# LTYPE: 3, 40, 70, 72, 73
+);
+
+my @dxf_groupcode_typeranges = (
+ [10,9,"x"],[20,9,"y"],[30,8,"z"],
+ [40,8,"float"],
+ [50,9,"angle"],
+ [70,9,"int"],[90,9,"int_32"],[280,10,"int_8"],
+ [290,10,"bool"],[300,10,"textstring"],[310,10,"blob"],
+ [320,10,"obj_handle"],[330,10,"softptr"],[340,10,"hardptr"],
+ [350,10,"softowner"],[360,10,"hardowner"],
+# ...
+ [370,10,"lineweight"],[380,10,"plotstyle"],[390,10,"plotstyle_handle"],
+ [400,10,"int_16"],
+ [410,10,"string"],
+);
+
+# Attributes that are preserved when substituting an entity by
+# one or more simpler entities in boil_down().
+our @general_attributes = qw(
+ linetype
+ textstyle
+ layer
+ elevation
+ thickness
+ linetype_scale
+ invisibile
+ color
+ space
+ comment
+);
+
+for my $range (@dxf_groupcode_typeranges) {
+ my $name = $$range[2];
+ my $start = $$range[3]//0;
+ my $sep = substr($name,-1) =~ /^\d$/ ? "_" : "";
+ for (0..$$range[1]) {
+ my $i = $start+$_;
+ $dxf_attr_names{$$range[0]+$_} = $name.($i == 0 ? "" : $sep.$i);
+ }
+}
+
+
+our %dxf_attr_ids = reverse %dxf_attr_names;
+our %dxf_node_ends = reverse(%dxf_end_nodes);#, "dxf" => "EOF");
+#$dxf_attr_ids{$dxf_attr_names{$_}} = $_ for keys %dxf_attr_names;
+
+sub dxf_node_id { $_[0] =~ /^\$/ ? 9 : 0 }
+
+
+# my %dxf_node_id0 = (
+# SECTION => 1,
+# TABLE => 1,
+# dxf => 1,
+# VPORT => 1,
+# LTYPE => 1,
+#
+# );
+
+# -- basic parsing and construction --
+
+sub parse_dxf {
+ my $fh = shift;
+ if (ref $fh eq "") {
+ my $s = $fh;
+ $fh = undef;
+ open ($fh, "<", \$s) or die "cannot open memory file";
+ }
+ my $root = {name => "dxf", attrs => {}, children => []};
+ my @contents = ($root);
+ while (my $id = <$fh>) {
+ my $param = <$fh>;
+ chomp $id;
+ if ($id =~ /^\s*(\d+)\s*$/) {
+ $id = $1;
+ } else {
+ die "id \"$id\" is not numeric as expected";
+ }
+ chomp $param;
+ $param =~ s/\r$//;
+ if ($dxf_node_ids{$id}) {
+# print STDERR "$param => $id\n" if $id == 9;
+ push @contents, {name => $param, attrs => {}, children => []};
+ } else {
+ my $attr = $dxf_attr_names{$id}//"i$id";
+ my $att = \$contents[-1]{attrs}{$attr};
+ if (!defined($$att)) {
+ $$att = $param;
+ } elsif (ref $$att eq "ARRAY") {
+ push @$$att, $param;
+ } else {
+ $$att = [$$att,$param];
+ }
+ #$contents[-1]{attrs}{$attr} = $param;
+ }
+ }
+ for (my $i = 0; $i <= $#contents; $i++) {
+ my $start = $dxf_end_nodes{$contents[$i]{name}};
+ if (defined $start) {
+ my $begin = undef;
+ for (my $j = $i-1; $j >= 0; $j--) {
+ if ($contents[$j]{name} eq $start && !$contents[$j]{_completed}) {
+ $begin = $j;
+ last;
+ }
+ }
+ if (!defined $begin) {
+ warn "end tag found without matching starting \"$start\"";
+ splice @contents,$i,1;
+ $i--;
+ next;
+ }
+ my $parent = $contents[$begin];
+ my @children = splice(@contents,$begin+1,$i-$begin);
+
+ # Preserve the endtag, as it may have individual attributes and
+ # we don't want to drop any information at this stage.
+ $parent->{endtag} = pop @children;
+ for (@children) {
+ delete $_->{_completed};
+ }
+ $parent->{children} = \@children;
+ $parent->{_completed} = 1;
+ $i = $begin;
+ }
+ }
+ for (@contents) {
+ delete $_->{_completed};
+ }
+# if ($contents[-1]{name} eq "EOF") {
+# pop @contents;
+# } else {
+# warn "EOF tag is missing.";
+# }
+ if (@contents != 1) {
+ die "EOF tag is missing (or worse).";
+ }
+ if ($contents[0] != $root) {
+ die "something went terribly wrong!";
+ }
+ #my $root = {name => "dxf", attrs => {}, children => \@contents};
+ return $root;
+}
+
+sub lol2xml {
+ my ($lol,$doc,$indent) = @_;
+ my $name = $lol->{name}//"dxf";
+ $name =~ s/\$/_/g;
+ my $node;
+
+ $indent = "" unless defined $indent;
+
+ if (!defined $doc) {
+ require XML::DOM;
+ $doc = XML::DOM::Parser->new->parse("<$name></$name>");
+ $node = $doc->getDocumentElement;
+ #$doc = XML::DOM::Document->new();
+ } else {
+ $node = $doc->createElement($name);
+ }
+
+ my @attrs = keys %{$lol->{attrs}};
+ @attrs = sort
+ {
+ ($a =~ /^i(\d+)$/ ? $1 : $dxf_attr_ids{$a}) <=>
+ ($b =~ /^i(\d+)$/ ? $1 : $dxf_attr_ids{$b})
+ } @attrs;
+ for (@attrs) {
+ my $attr = $_;
+ my $value = $lol->{attrs}{$attr};
+ # XML doesn't accept multiple attributes of the same name, but DXF does.
+ if (ref $value eq "ARRAY") {
+ $value = join(" ",@$value);
+ $attr .= "-array";
+ }
+ $node->setAttribute($attr,$value);
+# for (ref ($value) eq "ARRAY" ? @$value : $value) {
+# $node->setAttribute($attr,$_);
+# }
+ }
+ my @children = @{$lol->{children}};
+# my $i = 0;
+ for (@children) {
+ $node->appendChild($doc->createTextNode("\n ".$indent));
+ $node->appendChild(lol2xml($_,$doc,$indent." "));
+# $node->appendChild($doc->createTextNode("\n".($i == $#children ? "" : " ").$indent));
+# $i++;
+ }
+ my $emit_endtag = defined $lol->{endtag} && %{$lol->{endtag}{attrs}};
+ if ($emit_endtag) {
+ $node->appendChild($doc->createTextNode("\n ".$indent));
+ my $s = lol2xml($lol->{endtag},$doc,"")->toString;
+ $s =~ s/^</ /;
+ $s =~ s/\/>$/ /;
+ $node->appendChild($doc->createComment($s));
+ }
+ $node->appendChild($doc->createTextNode("\n".$indent)) if @children || $emit_endtag;
+ return $node;
+}
+
+sub lol {
+ my ($type,$attr,$content) = @_;
+ if (@_ == 2) {
+ if (ref $attr eq "HASH") {
+ $content = [];
+ } elsif (ref $attr eq "ARRAY") {
+ $content = $attr;
+ $attr = {};
+ } else {
+ die "invalid lol";
+ }
+ } elsif (@_ == 1) {
+ $attr = {};
+ $content = [];
+ } elsif (@_ == 3) {
+ die "invalid lol" unless ref $attr eq "HASH" && ref $content eq "ARRAY";
+ } else {
+ die "invalid lol";
+ }
+ return { name => $type, attrs => $attr, children => $content};
+}
+
+sub minimal_header_lol {
+ return
+ lol("SECTION",{name=>"HEADER"},[
+ lol("\$ACADVER",{text=>"AC1014"}), # "caption"? where did it come from?
+ lol("\$HANDSEED",{handle=>"FFFF"}), # i5
+ lol("\$MEASUREMENT",{int=>1}) # i70
+ ]);
+}
+
+sub drawing2dxflol {
+ my (%layers) = @_;
+ my @lines;
+ for my $layer (keys %layers) {
+ my $paths = $layers{$layer};
+ for (@$paths) {
+ if (ref($_) eq "ARRAY") {
+ next if @$_ < 2;
+# my $type = ...
+ my $p = $$_[0];
+ for my $i (1..$#$_) {
+ my $q = $$_[$i];
+ push @lines, lol(LINE => {x=>$$p[0], y=>$$p[1],x1=>$$q[0],y1=>$$q[1], layer => $layer});
+ $p = $q;
+ }
+ }
+ }
+ }
+
+ my $lol = lol("dxf",[
+ minimal_header_lol(),
+# lol("SECTION",{name=>"HEADER"},[
+# lol("\$ACADVER",{text=>"AC1014"}), # "caption"? where did it come from?
+# lol("\$HANDSEED",{handle=>"FFFF"}), # i5
+# lol("\$MEASUREMENT",{int=>1}) # i70
+# ]),
+ lol(SECTION => {name => "BLOCKS"}),
+ lol(SECTION => {name => "ENTITIES"},[
+ @lines
+ ]),
+ lol(SECTION => {name => "OBJECTS"},[lol("DICTIONARY")])
+ ]);
+ return $lol;
+}
+
+sub lol2dxf {
+ my ($lol,$pr) = @_;
+ my $res = "";
+ if (!defined $pr) {
+ $pr = sub { $res .= $_ for @_; };
+ }
+
+ my ($name,$attrs,$children,$endtag) = @$lol{qw(name attrs children endtag)};
+
+ my $node_id = dxf_node_id($name);
+ $pr->(sprintf "%3d\n%s\n", $node_id, $name) if $name ne "dxf";
+ my %seen;
+ for my $attr (sort
+# {
+# ($a =~ /^i(\d+)$/ ? $1 : $dxf_attr_ids{$a}) <=>
+# ($b =~ /^i(\d+)$/ ? $1 : $dxf_attr_ids{$b})
+# }
+ keys %$attrs) {
+ next if $seen{$attr};
+ my $param = $attrs->{$attr};
+ my $id = $attr =~ /^i(\d+)$/ ? $1 : $dxf_attr_ids{$attr};
+ die "invalid attribute \"$attr\" in node \"$name\" (value \"".($param//"undef")."\")" unless defined $id;
+
+ if ($id >= 10 && $id < 20 && ref($param) eq "ARRAY") {
+ # special case: coordinates of multiple points must be interleaved.
+ my $y = $dxf_attr_names{$id+10};
+ my $z = $dxf_attr_names{$id+20};
+ my @x = @$param;
+ my $_y = $attrs->{$y}//[];
+ my $_z = $attrs->{$z}//[];
+ my @y = ref $_y ? @$_y : ($_y);
+ my @z = ref $_z ? @$_z : ($_z);
+ my $max = max($#x,$#y,$#z);
+ my @p = map [$x[$_],$y[$_],$z[$_]], 0..$max;
+ for (@p) {
+ for my $i (0..2) {
+ $pr->(sprintf "%3d\n%s\n", $id+$i*10, $$_[$i]) if defined $$_[$i];
+ }
+ }
+ $seen{$y} = 1;
+ $seen{$z} = 1;
+ } else {
+ for (ref ($param) eq "ARRAY" ? @$param : $param) {
+ $pr->(sprintf "%3d\n%s\n", $id, $_);
+ }
+ }
+ }
+ for (@$children) {
+ lol2dxf($_,$pr);
+ }
+ if (defined $endtag) {
+ lol2dxf($endtag,$pr);
+ } else {
+ my $nodeend = $dxf_node_ends{$name};
+ $pr->(sprintf "%3d\n%s\n", $node_id, $nodeend) if defined $nodeend;
+ }
+ return $res;
+}
+
+sub drawing2dxf {
+ return lol2dxf(drawing2dxflol(@_));
+}
+
+sub xml2lol {
+ my $node = shift;
+ my $name = $node->getTagName;
+ $name =~ s/^_/\$/;
+ my @xmlattrs = $node->getAttributes->getValues;
+ my %attrs;
+ for (@xmlattrs) {
+ my $aname = $_->getName;
+ my $aval = $_->getValue;
+ if ($aname =~ s/-array$//) {
+ $aval = [split / /,$aval];
+ }
+ $attrs{$aname} = $aval;
+ }
+ my @children;
+ for ($node->getChildNodes) {
+ require XML::DOM;
+ if ($_->getNodeType == XML::DOM::ELEMENT_NODE()) {
+ push @children, xml2lol($_);
+ }
+ }
+ return lol($name => \%attrs,\@children);
+}
+
+# -- modification and filters --
+
+sub deep_copy {
+ my ($x,$prefilter,$postfilter) = @_;
+ my $ctx = {};
+ if (defined $prefilter) {
+ $x = $prefilter->($x,$ctx);
+ }
+ my $r = ref $x;
+ if (($r//"") eq "") {
+ $x = $postfilter->($x,$ctx) if defined $postfilter;
+ return $x;
+ }
+ if ($r eq "SCALAR") {
+ my $pv = deep_copy($$x);
+ my $v = \$pv;
+ $v = $postfilter->($v,$ctx) if defined $postfilter;
+ return $v;
+ } elsif ($r eq "ARRAY") {
+ my $v = [@$x];
+ $_ = deep_copy($_) for @$v;
+ $v = $postfilter->($v,$ctx) if defined $postfilter;
+ return $v;
+ } elsif ($r eq "HASH") {
+ my $v = {%$x};
+ $$v{$_} = deep_copy($$v{$_}) for keys %$v;
+ $v = $postfilter->($v,$ctx) if defined $postfilter;
+ return $v;
+ } else {
+ die "cannot deep_copy type \"$r\".";
+ }
+}
+
+# To make calling conventions more clear we let argument-modifying
+# procedural subs below explicitly return 1.
+
+#sub sample_prefilter {
+# my $arr = shift;
+# @$arr = (@$arr,@$arr);
+# $$arr[0]{attrs}{comment} = "Has been visited and duplicated";
+# return (1,1);
+#}
+
+# each filter gets an array, may modify that
+# array, but must return 1 iff it did.
+# Additionally it gets a hashref that can be used to transfer information
+# from prefilter to postfilter.
+# prefilter may return a second value, indicating whether to skip
+# the node's contents.
+# prefilter's array always contains exactly 1 element.
+# postfilter gets the array the way prefilter leaves it.
+sub tree_walk {
+ my ($node,$prefilter,$postfilter) = @_;
+ my $list = $node->{children};
+ return if !defined $list || !@$list;
+ for (my $i = 0; $i <= $#$list; $i++) {
+ my @x = ($$list[$i]);
+ my $ctx = {};
+ my ($mod,$skip) = (defined $prefilter && $prefilter->(\@x,$ctx));
+ if (!$skip) {
+ for (@x) {
+ tree_walk($_,$prefilter,$postfilter);
+ }
+ }
+ $mod ||= (defined $postfilter && $postfilter->(\@x,$ctx));
+ if ($mod) {
+ splice(@$list,$i,1,@x);
+ $i += @x-1;
+ }
+ }
+ return 1;
+}
+
+sub get_sections {
+ my ($dxf,$croak_on_duplicates) = @_;
+ my %sections;
+ for (@{$dxf->{children}}) {
+ if ($_->{name} eq "SECTION") {
+ my $n = $_->{attrs}{name};
+ if (defined $sections{$n}) {
+ die "duplicate section \"$n\"" if $croak_on_duplicates;
+ push @{$sections{$n}{children}}, @{$_->{children}};
+ } else {
+ $sections{$n} = $_;
+ }
+ }
+ }
+ return \%sections;
+}
+
+# - keep all objects.
+# - merge duplicate sections.
+# - remove stored end tags.
+# modifies the input.
+sub canonicalize {
+ my $dxf = shift;
+ my $sections = get_sections($dxf);
+ for (qw(CLASSES TABLES BLOCKS ENTITIES OBJECTS)) {
+ $$sections{$_} //= lol(SECTION => {name => $_});
+ }
+ $$sections{HEADER} //= minimal_header_lol();
+ $dxf->{children} = [@$sections{qw(HEADER CLASSES TABLES BLOCKS ENTITIES OBJECTS)}];
+
+ tree_walk($dxf,sub { delete $_[0][0]{endtag}; });
+ return 1;
+}
+
+# strip comments, tables, objects
+# modifies the input.
+sub strip {
+ my $dxf = shift;
+ my %delete_sections = ( TABLES => 1, CLASSES => 1);
+ my %clear_sections = ( BLOCKS => 1, OBJECTS => 1);
+
+ @{$dxf->{children}} = grep !$delete_sections{$_->{attrs}{name}//""}, @{$dxf->{children}};
+ for (@{$dxf->{children}}) {
+ if ($clear_sections{$_->{attrs}{name}//""}) {
+ $_->{children} = [];
+ }
+ }
+ delete $dxf->{attrs}{comment};
+ tree_walk($dxf,sub { delete $_[0][0]{attrs}{comment}; });
+ return 1;
+}
+
+# makes a copy and removes superfluous data.
+sub clean_dxf {
+ my $dxf = deep_copy(shift());
+ strip($dxf);
+ return $dxf;
+# my %delete_sections = ( TABLES => 1);
+# my %clear_sections = ( BLOCKS => 1, OBJECTS => 1);
+#
+# @{$dxf->{children}} = grep !$delete_sections{$_->{attrs}{name}//""}, @{$dxf->{children}};
+# for (@{$dxf->{children}}) {
+# if ($clear_sections{$_->{attrs}{name}//""}) {
+# $_->{children} = [];
+# }
+# }
+# return $dxf;
+}
+
+# gets an array of entities that belong to a given block and transforms them according to an <INCLUDE> node.
+# $blocks is a hashref of known blocks, $node is the INCLUDE node.
+sub get_block_replacement {
+ my ($blocks,$node) = @_;
+ my ($insblock,$x,$y,$z,$xscale,$yscale,$zscale,$rot,$cols,$rows,$colspace,$rowspace) = @{$node->{attrs}}{qw(name x y z float1 float2 float3 angle int int1 float4 float5)};
+ die "undefined block used" unless defined $blocks->{$insblock};
+ die "incomplete block usage" unless $blocks->{$insblock}{finished};
+ my @anchor = @{$blocks->{$insblock}{block}{attrs}}{qw(x y z)};
+ $_ //= 0 for @anchor,$colspace,$rowspace;
+ $_ //= 1 for $cols,$rows;
+ my @p = ($x//0,$y//0,$z//0);
+ my @s = ($xscale//1,$yscale//1,$zscale//1);
+ my ($c,$s) = (cos($rot/180*pi),sin($rot/180*pi));
+
+ my $inserted = $blocks->{$insblock}{objects};
+ my %supported = (
+ LINE => 1,
+ SPLINE => 1,
+ POINT => 1,
+ LWPOLYLINE => 1,
+ );
+ #print STDERR "inserting: ".Dumper($inserted);
+ my @res;
+ #die "not implemented" if ($cols//1) != 1 || ($rows//1) != 1;
+ for my $row (0..$rows-1) {
+ for my $col (0..$cols-1) {
+ my @pos = @p;
+ $pos[0] += $col*$colspace;
+ $pos[1] += $row*$rowspace;
+ for (@$inserted) {
+ die "not implemented" unless $supported{$_->{name}};
+ my %a = %{$_->{attrs}};
+ die "unexpected child" if @{$_->{children}};
+ for (0..9) {
+ my ($x,$y,$z) = @DXF::dxf_attr_names{10+$_,20+$_,30+$_};
+ if (defined $a{$x}) {
+ my @v = @a{$x,$y,$z};
+ if (!defined $v[2]) {
+ $v[2] = ref($v[0]) eq "ARRAY" ? [(0)x@{$v[0]}] : 0;
+ }
+ if (ref $v[0] eq "ARRAY") {
+ $_ = [@$_] for @v;
+ for my $i (0..$#{$v[0]}) {
+ $v[$_][$i] -= $anchor[$_] for 0..2;
+ $v[$_][$i] *= $s[$_] for 0..2;
+ ($v[0][$i],$v[1][$i]) = ($v[0][$i]*$c-$v[1][$i]*$s,
+ $v[0][$i]*$s+$v[1][$i]*$c);
+ $v[$_][$i] += $pos[$_] for 0..2;
+ }
+ @a{$x,$y,$z} = @v;
+ } else {
+ $v[$_] -= $anchor[$_] for 0..2;
+ $v[$_] *= $s[$_] for 0..2;
+ ($v[0],$v[1]) = ($v[0]*$c-$v[1]*$s,$v[0]*$s+$v[1]*$c);
+ $v[$_] += $pos[$_] for 0..2;
+ @a{$x,$y,$z} = @v;
+ }
+ #print STDERR "changed $x/$y/$z\n";
+ }
+ #print STDERR "tried $x/$y/$z\n";
+ }
+ push @res, DXF::lol($_->{name},\%a);
+ }
+ }
+ }
+ return \@res;
+}
+
+# TODO: safe recursion
+sub flatten {
+ my $dxf = shift;
+ my @blocksecs = grep $_->{attrs}{name} eq "BLOCKS", @{$dxf->{children}};
+ my %blocks;
+ for (@blocksecs) {
+ for (@{$_->{children}}) {
+ die "non-block in blocks section" unless $_->{name} eq "BLOCK";
+ my $n = $_->{attrs}{name};
+ die "duplicate block \"$n\"" if defined $blocks{$n};
+ $blocks{$n} = { finished => 1, block => $_, objects => $_->{children} };
+ }
+ }
+ tree_walk($dxf,sub {
+ my ($x,$ctx) = @_;
+ return (0,1)
+ if $$x[0]{name} eq "SECTION" &&
+ $$x[0]{attrs}{name} !~ /^BLOCKS$|^ENTITIES$|^OBJECTS$/;
+ if ($$x[0]{name} eq "BLOCK") {
+ my $n = $$x[0]{attrs}{name};
+ $blocks{$n}{finished} = 0;
+ $ctx->{name} = $n;
+ }
+ if ($$x[0]{name} eq "INSERT") {
+ my $n = $$x[0]{attrs}{name};
+ my $insobjects = get_block_replacement(\%blocks,$$x[0]);
+ @$x = @$insobjects;
+ # FIXME: recursion protection done right.
+ #$blocks{$n}{finished} = 0;
+ #$ctx->{name} = $n;
+ return 1;
+ }
+ },sub {
+ my ($x,$ctx) = @_;
+ if (defined $ctx->{name}) {
+ $blocks{$ctx->{name}}{finished} = 1;
+ }
+ #if (@$x && $$x[0]{name} eq "BLOCK") {
+ # $blocks{$$x[0]{attrs}{name}}{finished} = 1;
+ #}
+ });
+ return 1;
+}
+
+# removes superfluous garbage and flattens all INCLUDEs.
+sub flatten_dxf {
+ my $dxf = deep_copy(shift());
+ canonicalize($dxf);
+ flatten($dxf);
+ strip($dxf);
+ my $sections = get_sections($dxf);
+ $$sections{BLOCKS} = DXF::lol(SECTION => {name => "BLOCKS"});
+ # actually we may even drop the whole section.
+ $dxf = DXF::lol("dxf",[@$sections{qw(HEADER BLOCKS ENTITIES OBJECTS)}]);
+ return $dxf;
+}
+
+## removes superfluous garbage and flattens all INCLUDEs.
+#sub flatten_dxf {
+# my $dxf = deep_copy(shift());
+# delete $dxf->{attrs}{comment};
+# for (@{$dxf->{children}}) {
+# undef $_, next if $_->{name} ne "SECTION";
+# undef $_, next if !defined $_->{attrs}{name};
+# delete $_->{attrs}{comment};
+# delete $_->{attrs}{comment} for @{$_->{children}//[]};
+# }
+# my (%sections,%blocks,@entities,@objects);
+# @entities = ([],[]);
+# for (@{$dxf->{children}}) {
+# next unless defined;
+# if ($_->{attrs}{name} eq "BLOCKS") {
+# my $blocks = $_->{children};
+# my $current_block = "";
+# my $objects;
+# for my $i (0..$#$blocks) {
+# if ($blocks->[$i]{name} eq "BLOCK") {
+# $current_block = $blocks->[$i]{attrs}{name};
+# $objects = [];
+# $blocks{$current_block} =
+# { finished => 0, start => $i, objects => $objects};
+# } elsif ($blocks->[$i]{name} eq "ENDBLK") {
+# @{$blocks{$current_block}}{qw(finished end)} = (1,$i);
+# $objects = undef;
+## print STDERR "finished block $current_block:\n".Dumper(\%blocks);
+# $current_block = "";
+# } else {
+# if ($blocks->[$i]{name} eq "INSERT") {
+# my $insobjects = get_block_replacement(\%blocks,$blocks->[$i]);
+# push @$objects, @$insobjects;
+# } else {
+# push @$objects, $blocks->[$i];
+# }
+# }
+# }
+## undef $_;
+# } elsif ($_->{attrs}{name} eq "ENTITIES" ||
+# $_->{attrs}{name} eq "OBJECTS") {
+# my $type = $_->{attrs}{name} eq "ENTITIES" ? 0 : 1;
+# for (@{$_->{children}}) {
+# if ($_->{name} eq "INSERT") {
+# my $insobjects = get_block_replacement(\%blocks,$_);
+## print STDERR "inserted block ".$_->{attrs}{name}.":\n".Dumper($insobjects);
+# push @{$entities[$type]},@$insobjects;
+# } else {
+# push @{$entities[$type]},$_;
+# }
+# }
+## undef $_;
+# } else {
+# push @{$sections{$_->{attrs}{name}}}, $_;
+# }
+# }
+# for (keys %sections) {
+# my $all = $sections{$_};
+# my $first = shift @$all;
+# for (@$all) {
+# push @{$first->{children}}, @{$_->{children}};
+# }
+# $sections{$_} = $first;
+# }
+# #my @sections = grep defined, @{$dxf->{children}};
+# $sections{ENTITIES} = DXF::lol(SECTION => {name => "ENTITIES"},$entities[0]);
+# $sections{OBJECTS} = DXF::lol(SECTION => {name => "OBJECTS"},$entities[1]);
+# $sections{BLOCKS} = DXF::lol(SECTION => {name => "BLOCKS"}); # actually we may even drop the whole section.
+# if (!defined $sections{HEADER}) {
+# $sections{HEADER} = DXF::minimal_header_lol();
+# }
+# $dxf = DXF::lol("dxf",[@sections{qw(HEADER BLOCKS ENTITIES OBJECTS)}]);
+# return $dxf;
+#}
+
+sub merge_dxf {
+ my ($dxf1,$dxf2) = @_;
+
+ # ignore BLOCKS, we can't handle them anyway.
+ # delete TABLES. They will not be up-to-date.
+ # join ENTITIES. That's what we're interested in.
+ # what are OBJECTS anyway? Better delete them, there are some dictionaries there...
+ my ($e1) = grep(($_->{attrs}{name}//"") eq "ENTITIES", @{$dxf1->{children}});
+ my @e2 = grep(($_->{attrs}{name}//"") eq "ENTITIES", @{$dxf2->{children}});
+
+ push @{$e1->{children}}, map @{$_->{children}}, @e2;
+ return $dxf1;
+}
+
+sub colorize_dxf {
+ my ($dxf,$color) = @_;
+ #my @e = grep(($_->{attrs}{name}//"") eq "ENTITIES", @{$dxf->{children}});
+ for (@{$dxf->{children}}) {
+ if (($_->{attrs}{name}//"") eq "ENTITIES") {
+ for (@{$_->{children}}) {
+ $_->{attrs}{color} = $color;
+ }
+ }
+ }
+ return $dxf;
+}
+
+# POINT, LINE, SPLINE, POLYLINE, LWPOLYLINE, CIRCLE, ARC, ELLIPSE, TEXT, INCLUDE,
+
+# POLYLINE
+# SPLINE----------------------> v
+# \---> ARC -> ELLIPSE -> LWPOLYLINE <---> LINE
+# CIRCLE------^
+
+# spline -> circle:
+# p1 ---p2 p3
+# | |
+# | __-- p4
+# X--M2--
+# |
+# M1
+#
+# [p1,p2,p3,p4] -> arc(p1-M1-q,r=r1),arc(q-M2-p4,r=r2)
+# a := |p1,X|, x := |X,M1|; b := |p4,X|, y := |X,M2|; phi := deg(p1,X,p4)
+# r1 = a+x, r2 = b-y, l := a-b, r1-r2 = |M1,M2| = |[y*cos(phi)+x,y*sin(phi)]|
+# r1-r2 = a-b+x+y = |[y*cos(phi)+x,y*sin(phi)]|
+# = sqrt((y*c+x)^2+y^2*(1-c^2))
+# = sqrt(y^2*c^2+x^2 +2*y*c*x +y^2-y^2*c^2)
+# l+x+y = sqrt(x^2+y^2 +2*x*y*c)
+# l^2+x^2+y^2+2*x*y+2*l*(x+y) = x^2+y^2 +2*x*y*c
+# l^2 + 2*l*(x+y) + 2*x*y*(1-c) = 0
+# l^2 + 2*l*x + 2*l*y + 2*x*y*(1-c) = 0
+# y = -(l^2 + 2*l*x)/(2*l + 2*x*(1-c))
+# m := r1/r2 = (a+x)/(b-y)
+# y = b-(a+x)/m
+# b-(a+x)/m = -(l^2 + 2*l*x)/(2*l + 2*x*(1-c))
+# (b-(a+x)/m)*(2*l + 2*x*(1-c)) = -(l^2 + 2*l*x)
+# 2*(b*m-a-x)*(l + x*(1-c)) + l^2*m = -2*l*x*m
+# 2*(b*m-a)*l + 2*(b*m-a)*x*(1-c) - 2*x*l - 2*x^2*(1-c) + l^2*m + 2*l*x*m = 0
+# -2*(1-c)*x^2 + 2*((b*m-a)*(1-c) + l*(m-1))*x + (2*b*m+l*m-2*a)*l = 0
+
+# TODO: code 70: open and closed splines and polylines.
+# dest => source => sub{}
+my %replacers = (
+ LWPOLYLINE => {
+ SPLINE => sub {
+ my $node = shift;
+ my (@x,@y);
+ my @sx = @{$node->{attrs}{x}};
+ my @sy = @{$node->{attrs}{y}};
+ my $deg = $node->{attrs}{int1}//3;
+ my $flags = $node->{attrs}{int}//8;
+ my $closed = $flags & 1;
+ my $planar = $flags & 8;
+ die "degrees other than 3 are not implemented" unless $deg == 3;
+ warn "spline not marked as planar. Using it anyway" unless $planar;
+ die "invalid spline"
+ unless @sx == @sy && (@sx % 3) == 1;
+ push @x, $sx[0];
+ push @y, $sy[0];
+ my $fn = 20;
+ while (@sx >= 4) {
+ # TODO: subdivide by angle first. Estimate curvature.
+ for my $i (1..$fn) {
+ my $t = $i/$fn;
+ my $x1 = $sx[0]*(1-$t)**3 + 3*$sx[1]*(1-$t)**2*$t
+ + 3*$sx[2]*(1-$t)*$t**2 + $sx[3]*$t**3;
+ my $y1 = $sy[0]*(1-$t)**3 + 3*$sy[1]*(1-$t)**2*$t
+ + 3*$sy[2]*(1-$t)*$t**2 + $sy[3]*$t**3;
+ push @x, $x1;
+ push @y, $y1;
+ #push @coords,$x1,$y1;
+ #@p = ($x1,$y1);
+ }
+ splice @sx,0,3;
+ splice @sy,0,3;
+ }
+ return lol(LWPOLYLINE => {x => \@x, y => \@y, int => $closed?1:0});
+ },
+ POLYLINE => sub {
+ my $node = shift;
+ my (@x,@y);
+ my $flags = $node->{attrs}{int}//0;
+ my $closed = $flags & 1;
+ for (@{$node->{children}}) {
+ die "invalid POLYLINE" unless $_->{name} eq "VERTEX";
+ push @x, $_->{attrs}{x};
+ push @y, $_->{attrs}{y};
+ }
+ return lol(LWPOLYLINE => {x => \@x, y => \@y, int => $closed?1:0});
+ },
+ # DONE: Test whether my idea of an ellipse is the same as librecad's.
+ ELLIPSE => sub {
+ my $node = shift;
+ my (@x,@y);
+ my($x,$y,$x1,$y1,$min,$a1,$a2) = @{$node->{attrs}}{qw(x y x1 y1 float float1 float2)};
+ # f(t) = [$x,$y]+scale($min along [$y1,-$x1])turn($a1+($a2+$a1)*t)[$x1,$y1]
+ my $incl = atan2($y1,$x1);
+ my $r1 = sqrt($x1**2+$y1**2);
+ my $r2 = $min*$r1;
+ # f(t) = [$x,$y]+turn(incl)[cos(a)*$r1,sin(a)*$r2]
+
+ # Not sensible, but this is how it's interpreted by librecad:
+ while ($a2 < $a1) {
+ $a2 += 2*pi;
+ }# 0.000000005
+ while ($a2 > $a1+2*pi+0.000000005) { # constant estimated by experimentation.
+ $a2 -= 2*pi;
+ }
+ my $rounds = ($a2-$a1)/pi/2;
+ my $closed = $rounds-floor($rounds) < 0.001;
+
+ my $fn = floor(($a2-$a1)*$r1); # 1mm minimum
+ $fn = 20 if $fn < 20;
+
+ for my $i (0..$fn) {
+ my $t = $i/$fn;
+ my $angle = $a1+($a2-$a1)*$t;
+ my ($px,$py) = (cos($angle)*$r1,sin($angle)*$r2);
+ my ($qx,$qy) = ($x+$px*cos($incl)-$py*sin($incl),
+ $y+$px*sin($incl)+$py*cos($incl));
+ push @x, $qx;
+ push @y, $qy;
+ }
+ return lol(LWPOLYLINE => {x => \@x, y => \@y, int => $closed?1:0});
+ },
+ LINE => sub {
+ my $node = shift;
+ my (@x,@y);
+ @x = @{$node->{attrs}}{qw(x x1)};
+ @y = @{$node->{attrs}}{qw(y y1)};
+ return lol(LWPOLYLINE => {x => \@x, y => \@y, int => 0});
+ },
+ },
+ ELLIPSE => {
+ ARC => sub {
+ my $node = shift;
+ my($x,$y,$r,$a1,$a2) = @{$node->{attrs}}{qw(x y float angle angle1)};
+ return lol(ELLIPSE => {x => $x, y => $y, x1 => $r, y1 => 0,
+ float => 1, float1 => $a1*pi/180, float2 => $a2*pi/180});
+ }
+ },
+ ARC => {
+ CIRCLE => sub {
+ my $node = shift;
+ my($x,$y,$r) = @{$node->{attrs}}{qw(x y float)};
+ return lol(ARC => {x => $x, y => $y, float => $r,
+ angle => 0, angle1 => 360});
+ },
+# SPLINE => sub {
+# # TODO: approximate (reasonably small/simple) spline with two arcs,
+# # maintaining smoothness of the curve.
+# ...
+# }
+ },
+ LINE => {
+ LWPOLYLINE => sub {
+ my $node = shift;
+ my @x = @{$node->{attrs}{x}};
+ my @y = @{$node->{attrs}{y}};
+ die "invalid polyline"
+ unless @x == @y && @x >= 1;
+ my @lines = map lol(LINE => {x => $x[$_], y => $y[$_],
+ x1 => $x[$_+1], y1 => $y[$_+1] }), 0..$#x-1;
+ return @lines;
+ }
+ },
+);
+
+sub boil_down {
+ my ($dxf,$acceptable,$to_replace) = @_;
+ $to_replace //= [map keys %$_, values %replacers];
+ $acceptable //= ["POINT","LINE"];
+ my (%accept,%replace);
+ $accept{$_} = 1 for @$acceptable;
+ $replace{$_} = 1 for @$to_replace;
+ delete $replace{$_} for @$acceptable;
+ $to_replace = [keys %replace];
+ my %paths;
+ my $good = $acceptable;
+ $paths{$_} = [] for @$good;
+ # find shortest path for any object to replace.
+ OUTER: while (@$good) {
+ my @more = ();
+ for my $g (@$good) {
+ my $p = $paths{$g};
+ for (keys %{$replacers{$g}}) {
+ my $p2 = $paths{$_};
+ if (!defined $p2) { # || @$p2 > @$p)
+ $paths{$_} = [@$p,[$_,$replacers{$g}{$_}]];
+ push @more, $_;
+ delete $replace{$_};
+ last OUTER if !%replace;
+ }
+ }
+ }
+ $good = \@more;
+ }
+ if (%replace) {
+ die "unable to boil down these objects: ".join(",",sort keys %replace);
+ }
+
+ %replace = map {$_ => [reverse @{$paths{$_}}]} @$to_replace;
+
+# FIXED: particularly wrong.
+ #return deep_copy($dxf,sub{ my $x = shift; my $r = $replace{$x}; return $x unless defined $r; for (@$r) { $x = $r->($x); } return $x; });
+
+ tree_walk($dxf,sub {
+ my ($x,$ctx) = @_;
+ return (0,1)
+ if $$x[0]{name} eq "SECTION" &&
+ $$x[0]{attrs}{name} !~ /^BLOCKS$|^ENTITIES$|^OBJECTS$/;
+ my $r = $replace{$$x[0]{name}};
+ return 0 unless defined $r;
+ my %attrs = %{$$x[0]{attrs}};
+ for my $entry (@$r) {
+ my ($n,$sub) = @$entry;
+ @$x = map $_->{name} eq $n ? $sub->($_) : $_, @$x;
+ }
+ my @gen_attrs = grep defined $attrs{$_}, @general_attributes;
+ for my $entity (@$x) {
+ for (@gen_attrs) {
+ $entity->{attrs}{$_} //= $attrs{$_};
+ }
+ }
+ return 1;
+ });
+}
+
+sub parse_property_criteria {
+ my ($criteria,$name) = @_;
+ if (ref $criteria eq "") {
+ $criteria =~ /^([-+])?([^=]*=)?(.*)$/s or die "cannot happen";
+ my ($type,$nname,$ent) = ($1//"-",$2//$name,$3);
+ #$criteria = {_ => $type, $ent => 1};
+ $criteria = $type eq "+" ? sub { $_[1]{attrs}{$nname} eq $ent; }
+ : sub { $_[1]{attrs}{$nname} ne $ent; };
+ } elsif (ref $criteria eq "HASH") {
+ my ($type,$hash) = ($criteria->{_}//"-",$criteria);
+ $type =~ /^([-+])?(.*)$/s or die "cannot happen";
+ $name = $2 if $2 ne "";
+ $type = $1//"-";
+ $criteria = $type eq "+" ? sub { $$hash{$_[1]{attrs}{$name}}; }
+ : sub { !$$hash{$_[1]{attrs}{$name}}; };
+ }
+ die "criteria must be scalar, hash or coderef."
+ if (ref $criteria ne "CODE");
+ return $criteria;
+}
+
+# criteria can be an entity type, a hashtable of (entity type => 1) or a sub.
+# the type may be prefixed with "+" or "-", the hashtable may
+# contain _ => ("+"|"-"), to specify inclusion or exclusion filters.
+# default is exclusion.
+# the sub gets ($entity->{name},$entity) as parameters.
+sub filter {
+ my ($dxf,$criteria) = @_;
+ if (ref $criteria eq "") {
+ $criteria =~ /^([-+])?(.*)$/s or die "cannot happen";
+ my ($type,$ent) = ($1//"-",$2);
+ #$criteria = {_ => $type, $ent => 1};
+ $criteria = $type eq "+" ? sub { $_[0] eq $ent; } : sub { $_[0] ne $ent };
+ } elsif (ref $criteria eq "HASH") {
+ my ($type,$hash) = ($criteria->{_}//"-",$criteria);
+ $criteria = $type eq "+" ? sub { $$hash{$_[0]}; } : sub { !$$hash{$_[0]} };
+ }
+ die "criteria must be scalar, hash or coderef."
+ if (ref $criteria ne "CODE");
+
+ my $sections = get_sections($dxf,1);
+ my $blocks = ($sections->{BLOCKS}//{})->{children}//[];
+
+ my @base = (@$blocks,grep defined, @$sections{qw(ENTITIES OBJECTS)});
+ for (@base) {
+ tree_walk($_,sub {
+ my ($x,$ctx) = @_;
+ my $keep = $criteria->($$x[0]{name},$$x[0]);
+ return 0 if $keep;
+ @$x = ();
+ return 1;
+ });
+ }
+}
+
+sub filter_by_layer {
+ my ($dxf,$layers) = @_;
+ filter($dxf,parse_property_criteria($layers,"layer"));
+}
+
+sub filter_by_color {
+ my ($dxf,$colors) = @_;
+ filter($dxf,parse_property_criteria($colors,"color"));
+}
+
+sub filter_by {
+ my ($dxf,$crit) = @_;
+ filter($dxf,parse_property_criteria($crit,""));
+}
+
+sub deparse {
+ my ($dxf,$sub) = @_;
+ my @stack = ([]);
+ tree_walk($dxf,sub {
+ my ($x,$ctx) = @_;
+ my $prune = $sub->("prune",$$x[0]);
+ push @stack,[];
+ return (0,1) if $prune;
+ return 0;
+ },
+ sub {
+ my ($x,$ctx) = @_;
+ my $content = pop @stack;
+ my $res = $sub->("collect",$$x[0],$content);
+ push @{$stack[-1]},$res;
+ }
+ );
+ die "WTF" if @stack != 1;
+ my $content = pop @stack;
+ my $res = $sub->("collect",$dxf,$content);
+ return $res;
+}
+
+
+
+
+
+
+# --- interface ---
+# much TODO
+# DONE: flatten
+# TODO: bbox
+# DONE: filter by layer
+# DONE: filter by color
+
+package File::DXF;
+
+sub new {
+ my ($class,%args) = @_;
+ if (defined $args{copy}) {
+ return $args{copy}->copy;
+ }
+ my $self = bless {}, ref $class || $class;
+ if (defined $args{file}) {
+ $self->parsefile($args{file});
+ } elsif (defined $args{data}) {
+ $self->parse($args{data});
+ } elsif (defined $args{tree}) {
+ $self->load_tree($args{tree});
+ } elsif (defined $args{xml}) {
+ $self->from_xml($args{xml});
+ } else {
+ $self->load_tree(DXF::lol("dxf"));
+ }
+ return $self;
+}
+
+sub copy {
+ my $self = shift;
+ my %new = %$self;
+ for (keys %new) {
+ $new{$_} = DXF::deep_copy($new{$_});
+ }
+ return bless \%new, ref $self;
+}
+
+sub drop_caches {
+ my ($self) = @_;
+ delete @$self{qw(sections header tables blocks bboxes)};
+}
+
+sub get_sections {
+ my $self = shift;
+ my $res = $self->{sections};
+ if (!defined $res) {
+ $res = DXF::get_sections($self->{tree},1);
+ $self->{sections} = $res;
+ }
+ return $res;
+}
+
+sub _need_vars_and_types {
+ my ($self) = @_;
+ return if defined $self->{header};
+ my (%vars,%types,%nodes);
+ for (@{$self->get_sections->{HEADER}{children}}) {
+ my $name = $_->{name};
+ $name =~ s/^$//;
+ my @k = keys %{$_->{attrs}};
+
+ my ($type,$value) = @k == 1 ? ($k[0],$_->{attrs}{$k[0]}) :
+ @k == 0 ? () :
+ defined $_->{attrs}{x} ? ("point",[@{$_->{attrs}}{qw(x y z)}]) :
+ do { warn "variable \"$name\" has multiple values"; (); };
+ $vars{$name} = $value;
+ $types{$name} = $type;
+ $nodes{$name} = $_;
+ }
+ @$self{header} = {vars => \%vars, types => \%types, nodes => \%nodes};
+}
+
+sub tree {
+ my $self = shift;
+ return $self->{tree};
+}
+
+sub get_vars {
+ my $self = shift;
+ $self->_need_vars_and_types;
+ return $self->{header}{vars};
+}
+
+sub get_vartypes {
+ my $self = shift;
+ $self->_need_vars_and_types;
+ return $self->{header}{types};
+}
+
+sub get_blocks {
+ my $self = shift;
+ my $res = $self->{blocks};
+ if (!defined $res) {
+ $res = {};
+ for (@{$self->get_sections->{BLOCKS}{children}}) {
+ die "not a block: $$_{name}, but in BLOCKS."
+ unless $_->{name} eq "BLOCK";
+ $$res{$_->{attrs}{name}} = $_;
+ }
+ $self->{blocks} = $res;
+ }
+ return $res;
+}
+
+sub get_tables {
+ my $self = shift;
+ my $res = $self->{tables};
+ if (!defined $res) {
+ $res = {};
+ for (@{$self->get_sections->{TABLES}{children}}) {
+ die "not a table: $$_{name}, but in TABLES."
+ unless $_->{name} eq "BLOCK";
+ $$res{$_->{attrs}{name}} = $_;
+ }
+ $self->{tables} = $res;
+ }
+ return $res;
+}
+
+sub get_bboxes {
+ my $self = shift;
+ die "not implemented yet";
+ my $res = $self->{bboxes};
+ if (!defined $res) {
+ my $sections = $self->get_sections;
+ my $ent = $sections->{ENTITIES};
+ my @blocks = $sections->{BLOCKS}{children};
+
+ for (@blocks) {
+ }
+ $self->{bboxes} = $res;
+ }
+ return $res;
+}
+
+sub load_tree { # load a complete DXF::lol()-based tree
+ my ($self,$tree,$no_copy_needed) = @_;
+ # can be used as a constructor, too.
+ $self = $self->new if ref $self eq "";
+ $tree = DXF::deep_copy($tree) unless $no_copy_needed;
+ DXF::canonicalize($tree);
+ my $sections = DXF::get_sections($tree,1);
+ $self->drop_caches;
+ $self->{tree} = $tree;
+ return $self;
+}
+
+sub parse { # scalar data or file handle.
+ my ($self,$data) = @_;
+ # can be used as a constructor, too.
+ $self = $self->new if ref $self eq "";
+ my $tree = DXF::parse_dxf($data);
+ return $self->load_tree($tree,1);
+}
+
+*from_dxf = \&parse;
+
+sub parsefile { # filename
+ my ($self,$fname) = @_;
+ # can be used as a constructor, too.
+ $self = $self->new if ref $self eq "";
+ open(my $f,"<",$fname) or die "cannot open \"$fname\": $!";
+ my $tree = DXF::parse_dxf($f);
+ return $self->load_tree($tree,1);
+}
+
+sub get_var {
+ my ($self,$name) = @_;
+ return $self->get_vars->{$name};
+}
+
+sub get_var_type {
+ my ($self,$name) = @_;
+ return $self->get_vartypes->{$name};
+}
+
+sub set_var {
+ my ($self,$name,$type,$value) = @_;
+ die "set_var needs a type" if @_ < 4;
+ my $types = $self->get_vartypes;
+ my $vars = $self->get_vars;
+ my $nodes = $self->{header}{nodes};
+
+ my %dxfval = $type ne "point" ? ($type => $value)
+ : map (("x","y","z")[$_] => $$value[$_], 0..$#$value);
+ # (x => $$value[0], y => $$value[1], z => $$value[2])
+ if (exists $$types{$name}) {
+ if ($$types{$name} ne $type) {
+ warn "warning: changing header variable type.";
+ $types->{$name} = $type;
+ }
+ $nodes->{$name}{attrs} = \%dxfval;
+ $vars->{$name} = $value;
+ } else {
+ my $s = $self->get_sections;
+ my $node = DXF::lol("\$$name" => \%dxfval);
+ push @{$s->{HEADER}{children}}, $node;
+ $nodes->{$name} = $node;
+ $types->{$name} = $type;
+ $vars->{$name} = $value;
+ }
+ 1;
+}
+
+sub change_var {
+ my ($self,$name,$value) = @_;
+ my $types = $self->get_vartypes;
+ die "varaiable \"$name\" does not yet exist" unless exists $$types{$name};
+ $self->set_var($name,$$types{$name},$value);
+}
+
+sub fulfill_version_requirements {
+ my ($self,$ver_str) = @_;
+ die "cannot set version: specs not yet fully implemented";
+ my $s = $self->get_sections;
+ if ($ver_str =~ /^AC(\d+)$/) {
+ my $num = $1;
+ my @req_sections = $num <= 1009 ? ("ENTITIES") :
+ (qw(HEADER CLASSES TABLES ENTITIES OBJECTS));
+ if (!defined $s->{ENTITIES}) {
+ my $node = lol(SECTION => {name => "ENTITIES"});
+ $s->{ENTITIES} = $node;
+ push @{$s->{tree}{children}}, $node;
+ }
+ for (@req_sections) {
+ if (!defined $s->{$_}) {
+ my $node = lol(SECTION => {name => $_});
+ $s->{$_} = $node;
+ push @{$s->{tree}{children}}, $node;
+ }
+ }
+ if ($num > 1009) {
+ # >= R13
+ die "versions >= R13 are not yet implemented";
+ # see https://ezdxf.readthedocs.io/en/master/dxfinternals/filestructure.html for detailed requirements.
+ }
+ } else {
+ die "cannot fulfill version requirements for version \"$ver_str\"";
+ }
+}
+
+# AC1006 = R10
+# AC1009 = R11 and R12
+# AC1012 = R13
+# AC1014 = R14
+# AC1015 = AutoCAD 2000
+# AC1018 = AutoCAD 2004
+# AC1021 = AutoCAD 2007
+# AC1024 = AutoCAD 2010
+# AC1027 = AutoCAD 2013
+# AC1032 = AutoCAD 2018
+
+sub version {
+ my ($self,$newver) = @_;
+ if (@_ > 1) {
+ $self->set_var("ACADVER",text => $newver);
+ $self->fulfill_version_requirements($newver);
+ }
+ return $self->get_var("ACADVER");
+}
+
+sub add_entities {
+ my ($self,$entities) = @_;
+ push @{$self->get_sections->{ENTITIES}{children}}, @$entities;
+ delete $self->{bboxes};
+}
+
+sub boil_down {
+ my ($self,$acceptable,$to_replace) = @_;
+ DXF::boil_down($self->{tree},$acceptable,$to_replace);
+ $self->drop_caches;
+}
+
+sub flatten {
+ my ($self) = @_;
+ DXF::flatten($self->{tree});
+ $self->drop_caches;
+}
+
+sub strip {
+ my ($self) = @_;
+ DXF::strip($self->{tree});
+ $self->drop_caches;
+}
+
+sub filter {
+ my ($self,$criteria) = @_;
+ DXF::filter($self->{tree},$criteria);
+ $self->drop_caches;
+}
+
+sub filter_by_layer {
+ my ($self,$criteria) = @_;
+ DXF::filter_by_layer($self->{tree},$criteria);
+ $self->drop_caches;
+}
+
+sub filter_by_color {
+ my ($self,$criteria) = @_;
+ DXF::filter_by_color($self->{tree},$criteria);
+ $self->drop_caches;
+}
+
+sub filter_by {
+ my ($self,$criteria) = @_;
+ DXF::filter_by($self->{tree},$criteria);
+ $self->drop_caches;
+}
+
+sub to_dxf {
+ my ($self) = @_;
+ return DXF::lol2dxf($self->{tree});
+}
+
+sub to_xml_doc {
+ my ($self) = @_;
+ return DXF::lol2xml($self->{tree});
+}
+
+sub to_xml {
+ my ($self) = @_;
+ return DXF::lol2xml($self->{tree})->toString;
+}
+
+sub from_xml {
+ my ($self,$doc) = @_;
+ # can be used as a constructor, too.
+ $self = $self->new if ref $self eq "";
+ my $disp = 0;
+ if (!ref $doc) {
+ require XML::DOM;
+ $doc = XML::DOM::Parser->new->parse($doc);
+ $disp = 1;
+ }
+ my $lol = DXF::xml2lol($doc->getDocumentElement);
+ $doc->dispose if $disp;
+ $self->load_tree($lol,1);
+}
+
+
+#my $file = shift;
+#my $f;
+#if (defined $file) {
+# open($f, "<", $file) or die "cannot open file";
+#} else {
+# $f = \*STDIN;
+#}
+#my $lol = parse_dxf($f);
+#my $dxf2 = lol2dxf($lol);
+##print $dxf2;
+#use Data::Dumper;
+##print Dumper($lol);
+#my $xml = lol2xml($lol);
+#print $xml->toString;
+
+
+
diff --git a/GPL-2.txt b/GPL-2.txt
new file mode 100644
index 0000000..d159169
--- /dev/null
+++ b/GPL-2.txt
@@ -0,0 +1,339 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The licenses for most software are designed to take away your
+freedom to share and change it. By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users. This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it. (Some other Free Software Foundation software is covered by
+the GNU Lesser General Public License instead.) You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+ To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have. You must make sure that they, too, receive or can get the
+source code. And you must show them these terms so they know their
+rights.
+
+ We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+ Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software. If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+ Finally, any free program is threatened constantly by software
+patents. We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary. To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ GNU GENERAL PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ 0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License. The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language. (Hereinafter, translation is included without limitation in
+the term "modification".) Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope. The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+ 1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+ 2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+ a) You must cause the modified files to carry prominent notices
+ stating that you changed the files and the date of any change.
+
+ b) You must cause any work that you distribute or publish, that in
+ whole or in part contains or is derived from the Program or any
+ part thereof, to be licensed as a whole at no charge to all third
+ parties under the terms of this License.
+
+ c) If the modified program normally reads commands interactively
+ when run, you must cause it, when started running for such
+ interactive use in the most ordinary way, to print or display an
+ announcement including an appropriate copyright notice and a
+ notice that there is no warranty (or else, saying that you provide
+ a warranty) and that users may redistribute the program under
+ these conditions, and telling the user how to view a copy of this
+ License. (Exception: if the Program itself is interactive but
+ does not normally print such an announcement, your work based on
+ the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole. If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works. But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+ 3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+ a) Accompany it with the complete corresponding machine-readable
+ source code, which must be distributed under the terms of Sections
+ 1 and 2 above on a medium customarily used for software interchange; or,
+
+ b) Accompany it with a written offer, valid for at least three
+ years, to give any third party, for a charge no more than your
+ cost of physically performing source distribution, a complete
+ machine-readable copy of the corresponding source code, to be
+ distributed under the terms of Sections 1 and 2 above on a medium
+ customarily used for software interchange; or,
+
+ c) Accompany it with the information you received as to the offer
+ to distribute corresponding source code. (This alternative is
+ allowed only for noncommercial distribution and only if you
+ received the program in object code or executable form with such
+ an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it. For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable. However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+ 4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License. Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+ 5. You are not required to accept this License, since you have not
+signed it. However, nothing else grants you permission to modify or
+distribute the Program or its derivative works. These actions are
+prohibited by law if you do not accept this License. Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+ 6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions. You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+ 7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all. For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices. Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+ 8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded. In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+ 9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation. If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+ 10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission. For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this. Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+ NO WARRANTY
+
+ 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+ 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+ <one line to give the program's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along
+ with this program; if not, write to the Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+ Gnomovision version 69, Copyright (C) year name of author
+ Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary. Here is a sample; alter the names:
+
+ Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+ `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+ <signature of Ty Coon>, 1 April 1989
+ Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs. If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.
diff --git a/GPL-3.txt b/GPL-3.txt
new file mode 100644
index 0000000..94a9ed0
--- /dev/null
+++ b/GPL-3.txt
@@ -0,0 +1,674 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users. We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors. You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights. Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received. You must make sure that they, too, receive
+or can get the source code. And you must show them these terms so they
+know their rights.
+
+ Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+ For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software. For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+ Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so. This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software. The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable. Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products. If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+ Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary. To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Use with the GNU Affero General Public License.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+ <one line to give the program's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+ <program> Copyright (C) <year> <name of author>
+ This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+<http://www.gnu.org/licenses/>.
+
+ The GNU General Public License does not permit incorporating your program
+into proprietary programs. If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License. But first, please read
+<http://www.gnu.org/philosophy/why-not-lgpl.html>.
diff --git a/LICENSE.txt b/LICENSE.txt
new file mode 100644
index 0000000..3b45007
--- /dev/null
+++ b/LICENSE.txt
@@ -0,0 +1,11 @@
+This project is licensed under the terms of the GNU GPL ver. 2 or 3.
+Copyright (c) 2011-2017 by Thomas Kremer.
+
+The only exception is the font in consolefont_tiny.pgm and testfont.h,
+which seems to be the font
+ "-misc-fixed-medium-r-normal--8-80-75-75-c-50-iso8859-1"
+(actually just a screenshot of it).
+Author: Markus Kuhn, License: "Public domain font. Share and enjoy."
+Original source: https://www.x.org/releases/individual/font/font-misc-misc-1.0.0.tar.bz2
+
+
diff --git a/README.txt b/README.txt
new file mode 100644
index 0000000..bda0f2d
--- /dev/null
+++ b/README.txt
@@ -0,0 +1,12 @@
+DXF utilities and CAMM-GL III converters
+
+To convert a DXF file to CAMM-GL (which is apparently basically just HP-GL):
+
+$ perl -I. ./dxf2camm.pl --sort box,bottom,left camm-test.dxf > camm-test.camm
+
+To see, what is being plotted, the file can be converted to SVG (colors represent order, going from red to violet):
+
+$ perl -I. ./camm2svg.pl camm-test.camm > camm-test.svg
+
+For the XML tools you need the perl module XML::DOM (package "libxml-dom-perl" in Debian)
+
diff --git a/camm2svg.pl b/camm2svg.pl
new file mode 100755
index 0000000..a1c11d5
--- /dev/null
+++ b/camm2svg.pl
@@ -0,0 +1,23 @@
+#!/usr/bin/perl
+
+# convert a CAMM-GL III file to SVG to see what is being plotted
+# Line colors represent the order of plotting (red to violet)
+
+## Copyright (c) 2019-2020 by Thomas Kremer
+## License: GPL ver. 2 or 3
+
+# usage:
+# camm2svg.pl infile > outfile
+# camm2svg.pl < infile > outfile
+
+use strict;
+use warnings;
+use CAMM;
+
+local $/ = undef;
+
+my $camm = <>;
+
+my $svg = CAMM->to_svg($camm,1,1);
+
+print $svg;
diff --git a/dxf2camm.pl b/dxf2camm.pl
new file mode 100755
index 0000000..9ded0a4
--- /dev/null
+++ b/dxf2camm.pl
@@ -0,0 +1,505 @@
+#!/usr/bin/perl
+
+# Convert a DXF file to CAMM-GL III
+
+## Copyright (c) 2019-2020 by Thomas Kremer
+## License: GPL ver. 2 or 3
+
+# TODO: For pdf/ps/eps input:
+# gs -dBATCH -dSAFER -dNOPAUSE -sDEVICE=pdfwrite -dCompressPages=false -dNoOutputFonts -dCompressStreams=false -dUNROLLFORMS -sOutputFile=foo.pdf testseite.ps
+# (ps2write and eps2write are essentially just a pdf-interpreter plus the pdf)
+
+# perl -e 'use strict; use warnings; use CAM::PDF; { package CAM::PDF::Renderer::Dump; sub handler { my $name = shift; return eval q!sub { my $self = shift; my ($x,$y) = $self->userToDevice(@{$self->{last}}); print "$name($x,$y): ".join(",",map $_//"undef", @_)."\n"; $self->!."SUPER::$name".q!(@_); }!; } no strict "refs"; *$_ = handler($_) for qw(w d m l s c); } my $pdf = CAM::PDF->new($ARGV[0]); $pdf->getPageContentTree(1)->render("CAM::PDF::Renderer::Dump");' ~/foo.pdf
+
+use strict;
+use warnings;
+
+#use POSIX qw(lround);
+use Math::Trig qw(pi);
+use DXF;
+use CAMM;
+use Getopt::Long qw(:config bundling);
+
+sub dxf_extract_polylines {
+ my ($dxf) = @_;
+ my @res;
+ for my $e (@{$dxf->get_sections->{ENTITIES}{children}}) {
+ warn("ignoring entity: $e->{name}"),next unless $e->{name} eq "LWPOLYLINE";
+ my ($x,$y) = @{$e->{attrs}}{qw(x y)};
+ die "invalid number of coordinates in lwpolyline"
+ unless ref $x eq "ARRAY" && @$x == @$y && @$x >= 1;
+ my $closed = $e->{attrs}{int} & 1;
+ my @points = map [$$x[$_],$$y[$_]], 0..$#$x;
+ push @res, [($closed?"closed":"open"),\@points];
+ }
+ check_polylines(\@res,"extract");
+ return \@res;
+}
+
+sub check_polylines {
+ my ($l,$context) = @_;
+ my $pre = defined($context) ? $context.": " : "";
+ die $pre."not an array ref" unless ref $l eq "ARRAY";
+ for my $line (@$l) {
+ die $pre."not a pair" unless ref $line eq "ARRAY" && @$line == 2;
+ die $pre."not open|closed" if $$line[0] !~ /^(?:open|closed)$/;
+ my $points = $$line[1];
+ die $pre."points not an array" unless ref $points eq "ARRAY";
+ die $pre."points empty" unless @$points;
+ for my $point (@$points) {
+ die $pre."point not a pair" unless ref $point eq "ARRAY" && @$point == 2;
+ for (0,1) {
+ die $pre."coordinate $_ undef" unless defined $$point[$_];
+ }
+ }
+ }
+}
+
+# returns -1 if $x is within $y, 1 if $y is within $x, 0 otherwise or equal
+sub rect_containment_cmp {
+ my ($x,$y) = @_;
+ my @possible = (1,1,1); # (less-than, strictly, greater-than)
+ for (0..3) {
+ my $i = ($$x[$_] <=> $$y[$_])*($_ >= 2 ? 1 : -1);
+ $possible[$i+1] = 0;
+ }
+ #if ($possible[0] != $possible[2]) {
+ # print STDERR $possible[0]-$possible[2]," : [", join(",",map int($_),@$x),"] <=> [",join(",",map int($_),@$y),"]\n";
+ #}
+ # proves this function correct:
+ # my $res = $possible[0]-$possible[2];
+ # my $c = 0;
+ # if ($$x[0] >= $$y[0] && $$x[1] >= $$y[1] &&
+ # $$x[2] <= $$y[2] && $$x[3] <= $$y[3]) {
+ # $c = -1;
+ # }
+ # if ($$x[0] <= $$y[0] && $$x[1] <= $$y[1] &&
+ # $$x[2] >= $$y[2] && $$x[3] >= $$y[3]) {
+ # $c = 1;
+ # }
+ # if ($res != $c) {
+ # print STDERR "$res : [", join(",",map int($_),@$x),"] <$c> [",join(",",map int($_),@$y),"]\n" if $c ne "";
+ # }
+ #print STDERR ".";
+ return $possible[0]-$possible[2];
+}
+
+# sort by partial order, O(n^2), inplace
+sub partial_sort {
+ my ($sub,$array) = @_;
+ my @res;
+ local $b;
+ for $b (@$array) {
+ my $i = 0;
+ for (;$i<@res;$i++) {
+ local $a = $res[$i];
+ my $cmp = &$sub();
+ #my $cmp = $sub->($res[$i],$e);
+ last if $cmp > 0;
+ }
+ splice @res,$i,0,$b;
+ }
+ @$array = @res;
+}
+
+sub sort_polylines {
+ my ($lines,$order) = @_;
+ check_polylines($lines,"sort");
+
+ my @bboxes;
+
+ for (@$lines) {
+ my $bbox = [$$_[1][0],$$_[1][0]];
+ my @bbox = (undef)x4;
+ for my $p (@{$$_[1]}) {
+ for (0,1) {
+ $bbox[$_] = $$p[$_]
+ if !defined $bbox[$_] || $bbox[$_] > $$p[$_];
+ $bbox[$_+2] = $$p[$_]
+ if !defined $bbox[$_+2] || $bbox[$_+2] < $$p[$_];
+ }
+ }
+ push @bboxes, \@bbox;
+ }
+ # bboxes are calculated correctly:
+ #@$lines = map ["closed",[[@$_[0,1]],[@$_[2,1]],[@$_[2,3]],[@$_[0,3]],[@$_[0,1]]]], @bboxes;
+
+ use sort "stable";
+
+ my %h = qw(left 0 bottom 1 right 2 top 3);
+ my @criteria = ();
+ for (split /,/, $order) {
+ if (/^(left|bottom|right|top)(?:-(asc|desc))?$/) {
+ my ($i,$f) = ($h{$1}, ($2//"asc") eq "asc" ? 1 : -1);
+ #@perm = sort {($bboxes[$a][$i] <=> $bboxes[$b][$i])*$f} @perm;
+ push @criteria, [$i,$f];
+ } elsif (/^box$/) {
+ #@perm = sort {rect_containment_cmp($bboxes[$a],$bboxes[$b])} @perm;
+ push @criteria, \&rect_containment_cmp;
+ } else {
+ die "unknown sort order: \"$_\"";
+ }
+ }
+ return $lines if !@criteria;
+
+ # # FIXED: non-totalness of box sorting kills transitivity of combined sort.
+ # @perm = sort {
+ # my $res = 0;
+ # for (@criteria) {
+ # my $res;
+ # if (ref eq "CODE") {
+ # $res = $_->($bboxes[$a],$bboxes[$b]);
+ # } else {
+ # $res = ($bboxes[$a][$$_[0]] <=> $bboxes[$b][$$_[0]])*$$_[1];
+ # }
+ # return $res if $res != 0;
+ # }
+ # return 0;
+ # } 0..$#$lines;
+
+ my @perm = 0..$#$lines;
+ for my $crit (reverse @criteria) {
+ if (ref $crit eq "CODE") {
+ partial_sort(sub {$crit->($bboxes[$a],$bboxes[$b])},\@perm);
+ #@perm = sort {$crit->($bboxes[$a],$bboxes[$b])} @perm;
+ } else {
+ @perm = sort {($bboxes[$a][$$crit[0]] <=> $bboxes[$b][$$crit[0]])*$$crit[1]} @perm;
+ }
+ }
+ return [@$lines[@perm]];
+}
+
+sub coarsify_polylines {
+ my ($lines,$mindist) = @_;
+ check_polylines($lines,"coarsify");
+ my $min = $mindist**2;
+ my @res = @$lines;
+ for (@res) {
+ my $points = $$_[1];
+ undef $_, next if @$points < 2;
+ $_ = [@$_];
+ $points = [@$points];
+ $$_[1] = $points;
+ my $closed = $$_[0] eq "closed";
+ my $p = $$points[0];
+ #die "wtf: $p, @$p" if ref $p ne "ARRAY" || @$p != 2 || !defined $$p[0] || !defined $$p[1];
+ for my $q (@$points[1..$#$points-1]) {
+ my $d2 = 0;
+ #die "wtf2: $q, @$q" if ref $q ne "ARRAY" || @$q != 2 || !defined $$q[0] || !defined $$q[1];
+ $d2 += ($$p[$_]-$$q[$_])**2 for 0..1;
+ undef $q, next if $d2 < $min;
+ $p = $q;
+ }
+ @$points = grep defined, @$points;
+ }
+ @res = grep defined, @res;
+ return \@res;
+}
+
+# input is an array of ["closed"|"open",[p_1,...,p_n]], where p_i are [x,y]-points.
+sub combine_polylines {
+ my ($lines,$try_join_cycles,$try_reverse_paths) = @_;
+ check_polylines($lines,"combine");
+ my $first = undef;
+ my (%starts,%ends); # end|start => [[points,start,end],...]
+ my (@cycles,@noncycles); # [[points,start,end],...]
+ #for my $e (@{get_sections($dxf)->{ENTITIES}{children}}) {
+ for (@$lines) {
+ # @cycles contains all encountered cycles
+ # %starts contains all encountered non-cycles by start point
+ # %ends contains all encountered non-cycles by end point
+
+ # when adding a new segment, we check if it can continue a previous
+ # segment, if it can be continued by a previous segment or both or none.
+ my ($type,$_points) = @$_;
+ my @points = @$_points;
+ my $closed = $type eq "closed";
+
+ my ($start,$end) = map join(";",@$_), @points[0,-1];
+ if ($closed && $start ne $end) {
+ push @points, $points[0];
+ $end = $start;
+ }
+ my $elem = [\@points,$start,$end];
+ if ($start eq $end) {
+ push @cycles, $elem;
+ next;
+ }
+ my ($needstart,$needend) = (1,1);
+ if ($ends{$start} && @{$ends{$start}}) {
+ my $e2 = pop @{$ends{$start}};
+ push @{$$e2[0]},@{$$elem[0]};
+ $$e2[2] = $end;
+ $start = $$e2[1];
+ $elem = $e2;
+ $needstart = 0;
+ if ($start eq $end) {
+ @{$starts{$start}} = grep $_ != $elem, @{$starts{$start}};
+ push @cycles, $elem;
+ next;
+ }
+ }
+ if ($starts{$end} && @{$starts{$end}}) {
+ my $e2 = pop @{$starts{$end}};
+ if ($needstart) {
+ unshift @{$$e2[0]},@{$$elem[0]};
+ $$e2[1] = $start;
+ $end = $$e2[2];
+ $elem = $e2;
+ $needend = 0;
+ } else {
+ # we need to remove $e2 because $elem is already linked in %start
+ push @{$$elem[0]},@{$$e2[0]};
+ my $end2 = $end;
+ $end = $$e2[2];
+ @{$starts{$end2}} = grep $_ != $e2, @{$starts{$end2}};
+ @{$ends{$end}} = grep $_ != $e2, @{$ends{$end}};
+ }
+ if ($start eq $end) {
+ @{$starts{$start}} = grep $_ != $elem, @{$starts{$start}}
+ if !$needstart;
+ @{$ends{$end}} = grep $_ != $elem, @{$ends{$end}}
+ if !$needend;
+ push @cycles, $elem;
+ next;
+ }
+ }
+ push @{$starts{$start}}, $elem if $needstart;
+ push @{$ends{$end}}, $elem if $needend;
+ }
+ for (keys %starts) {
+ delete $starts{$_} if !@{$starts{$_}};
+ }
+ for (keys %ends) {
+ delete $ends{$_} if !@{$ends{$_}};
+ }
+
+ if ($try_reverse_paths) {
+ # join paths with same start or end by reversing one.
+ my %corners; # end|start => [[[points,start,end],is_end],...]
+ for (keys %starts) {
+ my $arr = $starts{$_};
+ next unless @$arr;
+ $corners{$_} = [ map [$_,0], @$arr ];
+ }
+ for (keys %ends) {
+ my $arr = $ends{$_};
+ next unless @$arr;
+ push @{$corners{$_}}, map [$_,1], @$arr;
+ }
+
+ for (values %corners) {
+ while (@$_ >= 2) {
+ my ($ee1,$ee2) = sort {$$b[1] <=> $$a[1]} splice @$_, 0,2;
+ # get start and end
+ my $p1 = $$ee1[0][0];
+ my $p2 = $$ee2[0][0];
+ my $ix1 = $$ee1[1];
+ my $ix2 = $$ee2[1];
+ my $start = $$ee1[0][2-$ix1];
+ my $end = $$ee2[0][2-$ix2];
+ my $s1 = $corners{$start};
+ my $s2 = $corners{$end};
+ # check for loop
+ my $is_loop = $start eq $end;
+ if (!$ix1) {
+ # reverse actual points
+ @$p1 = reverse @$p1;
+ # change startpoint entry to start if first is reversed
+ $$ee1[0][1] = $start;
+ for (@$s1) {
+ $$_[1] = 0 if $$_[0] == $$ee1[0];
+ }
+ }
+ if ($is_loop) {
+ # note: $s1 != $_, $s2 != $_, since we don't have cycles in the hash.
+ @$s1 = grep $$_[0] != $$ee1[0], @$s1;
+ @$s2 = grep $$_[0] != $$ee2[0], @$s2;
+ push @cycles, $$ee1[0];
+ } else {
+ # change endpoint entry from second to first
+ for (@$s2) {
+ @$_ = ($$ee1[0],1) if $$_[0] == $$ee2[0];
+ }
+ }
+ if ($ix2) {
+ # reverse added points
+ $p2 = [reverse @$p2];
+ }
+ # add points to first, dropping second.
+ $$ee1[0][2] = $end;
+ push @$p1,@$p2;
+ }
+ }
+ #%starts = ();
+ #%ends = ();
+ for (keys %corners) {
+ my $arr = $corners{$_};
+ next unless @$arr;
+ push @noncycles, map $$_[0], grep $$_[1] == 0, @$arr;
+ #$starts{$_} = [map $$_[0], grep $$_[1] == 0, @$arr];
+ #$ends{$_} = [map $$_[0], grep $$_[1] == 1, @$arr];
+ }
+ } else {
+ for (values %starts) {
+ push @noncycles, @$_;
+ }
+ }
+
+ if ($try_join_cycles) {
+ # embed cycles into other paths
+ my %cyclepoints; # pointstr => [i_th_cycle,k_th_pointincycle]
+ # every point gets marked with an unembedded cycle containing it.
+ # duplicate points are used to embed cycles immediately into other cycles.
+ for my $i (0..$#cycles) {
+ next unless defined;
+ my $c = $cycles[$i][0];
+ for (my $k = 0; $k < $#$c; $k++) {
+ my $p = $$c[$k];
+ my $ps = join(";",@$p);
+ if (defined $cyclepoints{$ps} && $cyclepoints{$ps}[0] != $i) {
+ my ($i2,$k2) = @{$cyclepoints{$ps}};
+ my @points = @{$cycles[$i2][0]};
+ @points = @points[$k2..$#points-1,0..$k2];
+ splice @$c, $k, 1, @points;
+ undef $cycles[$i2];
+ }
+ $cyclepoints{$ps} = [$i,$k];
+ }
+ }
+ # non-cycles are scanned for containing cycles.
+ for (@noncycles) {
+ my $c = $$_[0];
+ for (my $j = 0; $j < @$c; $j++) {
+ my $p = $$c[$j];
+ my $ps = join(";",@$p);
+ if (defined $cyclepoints{$ps}) {
+ my ($i,$k) = @{$cyclepoints{$ps}};
+ next unless defined $cycles[$i];
+ my @points = @{$cycles[$i][0]};
+ @points = @points[$k..$#points-1,0..$k];
+ splice @$c, $j, 1, @points;
+ $j += @points-1;
+ undef $cycles[$i];
+ }
+ }
+ }
+ @cycles = grep defined, @cycles;
+ }
+
+ my @paths = (map(["closed",$$_[0]], @cycles),
+ map(["open",$$_[0]], @noncycles));
+
+ return \@paths;
+}
+
+# option parsing.
+
+my (@opts,%opts,%opts_explained);
+
+sub usage {
+ my $ret = shift//0;
+ if ($ret != 0) {
+ print STDERR "wrong parameter. Left are: ",join(" ",@ARGV),"\n";
+ }
+ #print join("\n --",$0,@opts),"\n";
+ print STDERR "usage:\n $0\n";
+ for (@opts) {
+ my $name = $_ =~ s/[|!=:].*//r;
+ my $value = $opts{$name}//"undefined";
+ if (ref $value eq "SCALAR") {
+ $value = $$value;
+ } elsif (ref $value eq "CODE") {
+ $value = undef;
+ }
+ my $explanation = $opts_explained{$name};
+ print STDERR " --",$_,(defined $value ? " (value: $value)":""),"\n",
+ defined($explanation) ? " $explanation\n":"";
+
+ }
+ print STDERR " <dxffile>\n";
+ print STDERR " read DXF data from this file instead of stdin.\n";
+ exit($ret);
+}
+
+%opts = (
+ coarsify => 1/4,
+ combine => 1,
+ combine_cycles => 1,
+ combine_reverse => 1,
+ scale => 1,
+ help => sub { usage(0); },
+);
+
+%opts_explained = (
+ output => "Write CAMM data to this file instead of stdout.",
+ offset => "Set knive offset to this value (mm).",
+ raw => "Don't emit header/footer commands.",
+ relative => "Use relative commands when possible (better compression).",
+ epsilon => "jump over line segments of at most this length.",
+ shortline => "maximum length of a short line (mm); smoothen corners only for those lines.",
+ smallangle => "maximum angle (degrees) considered small; smoothen corners only for those angles.",
+ coarsify => "segments smaller than this length (mm) are combined to straight lines.",
+ combine => "draw polylines that touch each other in one go.",
+ combine_cycles => "Allow embedding cycles into other polylines to combine them.",
+ combine_reverse => "Allow reversing of polylines to combine more of them.",
+ translate => "Translate everything to this point (\"x,y\")",
+ scale => "Scale everything by this factor",
+ sort => "Sort order: /(left|bottom|right|top)(|-asc|-desc)|box/, comma-separated",
+ help => "Show this help screen.",
+);
+
+@opts = qw(output|o=s offset|off=f raw! relative! epsilon=f shortline=f smallangle=f coarsify=f combine! combine_cycles|cycles! combine_reverse|reverse! translate=s scale=f sort=s help|h|?);
+
+GetOptions(\%opts,@opts) or usage(2);
+
+usage(2) if @ARGV > 1;
+
+$opts{headerfooter} = !$opts{raw};
+$opts{offset} *= CAMM::units_per_mm if defined $opts{offset};
+$opts{shortline} *= CAMM::units_per_mm if defined $opts{shortline};
+$opts{translate} = [split /,/,$opts{translate}] if defined $opts{translate};
+
+
+my $dxffile = shift;
+## TODO: get paths from dxf in a good way.
+
+my $dxf = File::DXF->new(defined($dxffile)?(file=>$dxffile):(data=>\*STDIN));
+$dxf->boil_down(["POINT","LWPOLYLINE"]);
+$dxf->filter({_ => "+", INSERT => 1, LWPOLYLINE => 1});
+$dxf->flatten;
+
+my $paths = dxf_extract_polylines($dxf);
+
+for (@$paths) { # a path
+ for my $p (@{$$_[1]}) { # a point
+ if (defined $opts{translate}) {
+ $$p[$_] += $opts{translate}[$_] for 0,1;
+ }
+ for (@$p) { # a coordinate
+ $_ = $opts{scale}*$_*CAMM::units_per_mm;
+ #$_ = lround($opts{scale}*$_*CAMM::units_per_mm);
+ }
+ }
+}
+
+$paths = combine_polylines($paths,$opts{combine_cycles},$opts{combine_reverse})
+ if $opts{combine};
+$paths = coarsify_polylines($paths,$opts{coarsify}*CAMM::units_per_mm)
+ if $opts{coarsify};
+#$CAMM::units_per_mm/4);
+$paths = sort_polylines($paths,$opts{sort})
+ if defined $opts{sort};
+
+my $camm = CAMM->from_polylines($paths,%opts);
+#headerfooter=>1,offset=>10*$CAMM::units_per_mm);
+
+my $out;
+if (defined $opts{output}) {
+ open($out,">",$opts{output}) or die "cannot open $opts{output}: $!";
+} else {
+ $out = \*STDOUT;
+}
+
+print $out $camm;
+
diff --git a/dxf2xml.pl b/dxf2xml.pl
new file mode 100755
index 0000000..4ed31af
--- /dev/null
+++ b/dxf2xml.pl
@@ -0,0 +1,29 @@
+#!/usr/bin/perl
+
+# converts a DXF file to XML for viewing and editing.
+
+## Copyright (c) 2018-2020 by Thomas Kremer
+## License: GPL ver. 2 or 3
+
+# usage:
+# dxf2xml.pl infile.dxf > outfile.xml
+# dxf2xml.pl < infile.dxf > outfile.xml
+
+use strict;
+use warnings;
+
+use DXF;
+use XML::DOM;
+use IO::Handle;
+
+my $file = shift;
+my $f;
+if (defined $file) {
+ open($f, "<", $file) or die "cannot open file";
+} else {
+ $f = \*STDIN;
+}
+my $lol = DXF::parse_dxf($f);
+my $xml = DXF::lol2xml($lol);
+print $xml->toString;
+
diff --git a/dxf_simplify.pl b/dxf_simplify.pl
new file mode 100755
index 0000000..4502517
--- /dev/null
+++ b/dxf_simplify.pl
@@ -0,0 +1,43 @@
+#!/usr/bin/perl
+
+# simplifies a DXF by converting everything using a given set of DXF primitives.
+
+## Copyright (c) 2018-2020 by Thomas Kremer
+## License: GPL ver. 2 or 3
+
+# usage:
+# dxf_simplify.pl infile.dxf POINT,LINE > outfile.dxf
+# dxf_simplify.pl infile.dxf > outfile.dxf
+# dxf_simplify.pl < infile.dxf > outfile.dxf
+
+use strict;
+use warnings;
+
+use DXF;
+
+my ($dxffile,$set) = @ARGV;
+my $f;
+
+if (defined $dxffile) {
+ open($f,"<",$dxffile) or die "cannot open dxf \"$dxffile\": $!";
+} else {
+ $f = \*STDIN;
+}
+
+$set //= "POINT,LWPOLYLINE";
+$set = [split /,/, $set];
+
+my $dxf = DXF::parse_dxf($f);
+
+DXF::canonicalize($dxf);
+#DXF::boil_down($dxf,["POINT","LWPOLYLINE","CIRCLE"]);
+#DXF::filter($dxf,{_=>"+", INSERT => 1, LWPOLYLINE => 1, POINT => 1, CIRCLE => 1});
+#DXF::flatten_dxf($dxf);
+#my $copy = DXF::deep_copy($dxf);
+DXF::boil_down($dxf,$set);
+DXF::flatten($dxf);
+DXF::strip($dxf);
+
+print DXF::lol2dxf($dxf);
+#print DXF::lol2xml($dxf)->toString;
+
diff --git a/xml2dxf.pl b/xml2dxf.pl
new file mode 100755
index 0000000..19e9f4c
--- /dev/null
+++ b/xml2dxf.pl
@@ -0,0 +1,57 @@
+#!/usr/bin/perl
+
+# converts a DXF in XML format back to DXF.
+
+## Copyright (c) 2018-2020 by Thomas Kremer
+## License: GPL ver. 2 or 3
+
+# usage:
+# xml2dxf.pl infile.xml > outfile.dxf
+# xml2dxf.pl < infile.xml > outfile.dxf
+
+use strict;
+use warnings;
+
+use DXF;
+use XML::DOM;
+use IO::Handle;
+
+sub xml2lol {
+ my $node = shift;
+ my $name = $node->getTagName;
+ $name =~ s/^_/\$/;
+ my @xmlattrs = $node->getAttributes->getValues;
+ my %attrs;
+ for (@xmlattrs) {
+ my $aname = $_->getName;
+ my $aval = $_->getValue;
+ if ($aname =~ s/-array$//) {
+ $aval = [split / /,$aval];
+ }
+ $attrs{$aname} = $aval;
+ }
+ my @children;
+ for ($node->getChildNodes) {
+ if ($_->getNodeType == XML::DOM::ELEMENT_NODE) {
+ push @children, xml2lol($_);
+ }
+ }
+ return DXF::lol($name => \%attrs,\@children);
+}
+
+my $file = shift;
+my $f;
+if (defined $file) {
+ open($f, "<", $file) or die "cannot open file";
+} else {
+ $f = \*STDIN;
+}
+
+$/ = undef;
+my $content = <$f>;
+
+my $xmldoc = XML::DOM::Parser->new->parse($content);
+my $lol = xml2lol($xmldoc->getDocumentElement);
+$xmldoc->dispose;
+DXF::lol2dxf($lol,sub {print @_;});
+