summaryrefslogtreecommitdiffstats
path: root/share/extensions/SpSVG.pm
blob: b3f7ed09fb4063219b4830989d827490099624d5 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
#!/usr/bin/perl -w
#
# SpSVG
# 
# Perl module for sodipodi extensions
#
# This is a temporary hack that provides the following:
#   * Some standard getopts (help, i/o, ids)
#   * A way to exit that produces the error codes outlined in
#     the extension specs (SpSVG::error)
#   * A method that takes a function as its arguments and passes
#     each specified element ('--id=foo --id=bar', 'ids=fooz,baaz',
#     and so forth) as plain text to the function. The function is 
#     expected to return the processed version of this text.
#     
# TODO:
#
#   * Write POD
#   * Exit with a friendly message if XML::XQL isn't installed
#   * Decide how to implement the module interface
#   * Move from XML::XQL to SVG/SVG::Parser (see below)
#   * Make the process method more efficient (again, see below)
#
# Authors: Daniel Goude (goude@dtek.chalmers.se)
#

package SpSVG; # Think of a better name
use strict;
#use Carp;
use Exporter;
use Getopt::Long;
#use Data::Dumper; # For debugging

# From the SVG.pm documentation (actually 
# http://roasp.com/tutorial/tutorial6.shtml):
#
# > Currently, version 2.0 of SVG.pm does not internally support DOM
# > traversiong functionality such as getting the children,siblings,or
# > parent of an element, so the interaction capability between SVG::Parser
# > and SVG is limited to manipulations of a known image. The next version
# > of SVG will support all these and more key functions which will make
# > SVG::Parser extremely useful.
#
# I plan to replace the /XML::XQL(::DOM)?/ code as soon as this is
# fixed.

#use SVG;
#use SVG::Parser;

use XML::XQL;
use XML::XQL::DOM;

use vars qw(@ISA @EXPORT $VERSION);

$VERSION = 1.02; # fixme: use SpSVG 1.01 doesn't raise exception.
@ISA = qw(Exporter);

# Symbols 
@EXPORT = qw(

); 

sub new {
    my $self = {
        status   => make_status(),
        name     => '',      # Name of script
        usage    => '',      # Usage string
        opt_help => [],      # Used for --help
        
        ids     => [],       # Array of ids that will be iterated over 
                             # in process()
        svg     => '',       # SVG document object
        
    };
    bless $self;
}

sub parse {
    my $self = shift;
    
    my $infile = $self->{'opts'}->{'file'};

    my $xml;
    {
        local $/=undef;
        if ($infile) {
            open (IN, $infile) or 
                $self->error('IO_ERR', "Can't open $infile: $!\n");
            $xml = <IN>;
            close IN or 
                $self->error('IO_ERR', "Can't close $infile: $!\n");
        } else {
            $xml = <>;
        }
    }


    $self->{'parser'} = new XML::DOM::Parser;
    my $parser = $self->{'parser'};
    my $svg = $parser->parse($xml) ||
            $self->error('INPUT_ERR', "Couldn't parse input: $!.");
    $self->{'svg'} = $svg;
}

# Return SVG document as a string
sub get {
    my $self = shift;
    my $string =  $self->{'svg'}->toString;
    
}

# Print to $outfile|STDOUT
sub dump {
    my $self = shift;
    my $outfile = $self->{'opts'}->{'output'};
    if ($outfile) {
        open(OUT, ">$outfile") or 
            $self->error('IO_ERR', "Can't open $outfile for writing: $!\n");
        print OUT $self->get;
        close OUT or $self->error('IO_ERR', "Can't close $outfile: $!\n");
    } else {
        print $self->get;
    }
}

sub process_ids {
    my $self = shift;
    my $func = shift;

    my @ids = @{$self->{'ids'}};

    # Apply a user supplied function to each id
    foreach my $id (@ids) {
        my $svg = $self->{'svg'};
        #warn "ID: $id\n";
        my @nodes = $svg->xql("//*[\@id = '$id']") or
            $self->error('NOOP_ERR', "Couldn't find element $id.");
        my $node = shift @nodes; # Ids are unique
                                 # fixme: Add more checking.

        # Call the user function on the node identified by $id
        my $new_node = $func->($node->toString);
    
        # Replace the comment with user generated SVG
        my $parent = $node->getParentNode;
        my $comment = $svg->createComment('SpSVG');
        $parent->replaceChild($comment, $node);
        my $output =  $self->{'svg'}->toString;
        $output =~ s/<!--SpSVG-->/$new_node/;

        # Here the whole (new) document is parsed. Probably VERY inefficient,
        # but at least you get syntax checking for free..
        $self->{'svg'} = $self->{'parser'}->parse($output);
        #print $self->{'svg'}->toString;
    }

    
} 

# Exit status codes
sub make_status {
    my $self = shift;
    my %status = (
        0 => ["SUCCESS", "Extension exited gracefully"],
        1 => ["GEN_FAIL", "General failure"],
        2 => ["MEM_ERR", "Memory error"],
        3 => ["IO_ERR", "File I/O error"],
        4 => ["MATH_ERR", "Math error"],
        5 => ["INPUT_ERR", "Input not understood (not valid SVG)"],
        6 => ["NOOP_ERR", "Could not operate on any objects in this " . 
            "data stream"],
        7 => ["ARG_ERR", "Incorrect script arguments"]
    );

    # Generate error subs dynamically
    foreach my $exit_code (sort keys %status) {
        eval "sub $status{$exit_code}[0] { $exit_code; }";
        die $@ if $@;
    }
    return \%status;

}

# Create an option array suitable for Getopt::Long
sub make_opt_vals {
    my $self = shift;
    my @opt_desc = @_;
    my @opt_vals;
    my @opt_help = @{$self->{'opt_help'}};
    foreach (@opt_desc) {
        my %h = %$_;
        foreach my $key (keys %h) {
            #print "Key : $h{$key}\n";
            if ($key eq 'opt') {
                push @opt_vals, $h{'opt'};
            } elsif ($key eq 'desc') {
                my $option = $h{'opt'};
                $option =~ s/([^=]+)=.+/$1/;
                $option =~ s/([^|]+)/(length "$1" > 1 ? '--' : '-') . "$1"/eg;
                push @opt_help, [$option, $h{'desc'}];
            }
        }
    }
    $self->{'opt_help'} = \@opt_help;
    return @opt_vals;
}

# Parse command line options
sub get_opts {
    my $self = shift;
    my @user_opt_desc = @_;
   
    my @opt_desc = (
        {
            opt => 'help|h',
            desc => 'Display this help and exit.',
        },
        
        {
            opt => 'version|v',
            desc => 'Print version and exit.',
        },           
        
        {
            opt => 'file|F=s',
            desc => 'Input file (default: STDIN).',
        },            
        
        {
            opt => 'output|o=s',
            desc => 'Output file (default: STDOUT).',
        },
        
        {
            opt => 'id=s@',
            desc => 'svg id to operate on (can be multiple).',
        },           
        
        {   
            opt => 'ids=s',
            desc => 'Comma-separated list of svg ids to operate on.',
        },           
    );
 
    # Create option arrays for Getopt::Long
    my @opt_vals = $self->make_opt_vals(@opt_desc);
    my @user_opt_vals = $self->make_opt_vals(@user_opt_desc);
    
    # Append user options 
    foreach (@user_opt_vals) {
        push @opt_vals, $_;
    }
    
    # Where the parsed options are stored
    my %opts;

    #exit 0;

    # Parse all options
    GetOptions(\%opts, @opt_vals) or usage();    

    # Handle comma-separated 'ids=foo,bar'
    my @ids = @{$opts{'id'}} if $opts{'id'};
    if (exists $opts{'ids'} && $opts{'ids'} =~ /[\w\d_]+(,[\w\d_]+)*/) {
        push (@ids, split(/,/, $opts{'ids'}));
    }

    # Display usage etc. (and exit)
    exists $opts{'version'} && $self->version();
    exists $opts{'help'} && $self->usage(); 

    # Save id values for later processing 
    $self->{'ids'} = \@ids;
    
    # Save options
    $self->{'opts'} = \%opts;

    # Return the options to script
    return %opts;
}

# Exit with named exit status
sub error {
    my $self = shift;
    my $error_name = shift;
    my $script_error_msg = shift || '';
   
    my %status = %{$self->{'status'}};

    foreach (keys %status) {
        if ($status{$_}[0] eq $error_name) {
            $! = $_; # Set exit status

            # Commented out; let sodipodi handle the error code instead
            #my $msg =  ($status{$_}->[1] . ": $script_error_msg");
            
            my $msg =  "$script_error_msg";
            die $msg;
        }
    }
    
    # Will not be reached unless an improper error_name is given
    $! = 255; # Exit status 
    warn "Illegal error code '$error_name' called from script\n";
}

# Some accessor methods
sub set_usage {
    my $self = shift;
    my $usage = shift || die "No usage string supplied!\n";
    $self->{'usage'} = $usage;
}

sub set_name {
    my $self = shift;
    my $name = shift || die "No script name supplied!\n";
    $self->{'name'} = $name;
}

# Print usage and exit
sub usage {
    my $self = shift;
    print "Usage: $self->{'name'} OPTIONS FILE\n";
    print $self->{'usage'};
    
    my @opt_help = @{$self->{'opt_help'}};
    foreach (@opt_help) {
        print pad($_->[0]) . $_->[1] . "\n";
    }

    exit ARG_ERR(); 
}

sub pad {
    my $string = shift;
    my $width = '20';
    return $string . ' ' x ($width - length($string));
}

# Print version
sub version {
    print "Uses SpSVG version $VERSION\n";
    exit ARG_ERR();
}

# End of module; return something true
1;

__END__

DOCUMENTATION HERE