File Coverage

blib/lib/SVG/Metadata.pm
Criterion Covered Total %
statement 10 12 83.3
branch n/a
condition n/a
subroutine 4 4 100.0
pod n/a
total 14 16 87.5


line stmt bran cond sub pod time code
1             =head1 NAME
2              
3             SVG::Metadata - Perl module to capture metadata info about an SVG file
4              
5             =head1 SYNOPSIS
6              
7             use SVG::Metadata;
8              
9             my $svgmeta = new SVG::Metadata;
10              
11             $svgmeta->parse($filename)
12             or die "Could not parse $filename: " . $svgmeta->errormsg();
13             $svgmeta2->parse($filename2)
14             or die "Could not parse $filename: " . $svgmeta->errormsg();
15              
16             # Do the files have the same metadata (author, title, license)?
17             if (! $svgmeta->compare($svgmeta2) ) {
18             print "$filename is different than $filename2\n";
19             }
20              
21             if ($svgmeta->title() eq '') {
22             $svgmeta->title('Unknown');
23             }
24              
25             if ($svgmeta->author() eq '') {
26             $svgmeta->author('Unknown');
27             }
28              
29             if ($svgmeta->license() eq '') {
30             $svgmeta->license('Unknown');
31             }
32              
33             if (! $svgmeta->keywords()) {
34             $svgmeta->addKeyword('unsorted');
35             } elsif ($svgmeta->hasKeyword('unsorted') && $svgmeta->keywords()>1) {
36             $svgmeta->removeKeyword('unsorted');
37             }
38              
39             print $svgmeta->to_text();
40              
41             =head1 DESCRIPTION
42              
43             This module provides a way of extracting, browsing and using RDF
44             metadata embedded in an SVG file.
45              
46             The SVG spec itself does not provide any particular mechanisms for
47             handling metadata, but instead relies on embedded, namespaced RDF
48             sections, as per XML philosophy. Unfortunately, many SVG tools don't
49             support the concept of RDF metadata; indeed many don't support the idea
50             of embedded XML "islands" at all. Some will even ignore and drop the
51             rdf data entirely when encountered.
52              
53             The motivation for this module is twofold. First, it provides a
54             mechanism for accessing this metadata from the SVG files. Second, it
55             provides a means of validating SVG files to detect if they have the
56             metadata.
57              
58             The motivation for this script is primarily for the Open Clip Art
59             Library (http://www.openclipart.org), as a way of filtering out
60             submissions that lack metadata from being included in the official
61             distributions. A secondary motivation is to serve as a testing tool for
62             SVG editors like Inkscape (http://www.inkscape.org).
63              
64             =head1 FUNCTIONS
65              
66             =cut
67              
68             package SVG::Metadata;
69              
70 2     2   67001 use 5.006;
  2         8  
  2         85  
71 2     2   12 use strict;
  2         4  
  2         70  
72 2     2   10 use warnings;
  2         9  
  2         88  
73 2     2   2221 use XML::Twig;
  0            
  0            
74             use HTML::Entities;
75              
76             # use Data::Dumper; # DEBUG
77              
78             require Exporter;
79             our @ISA = qw(Exporter);
80             our @EXPORT_OK = ();
81              
82             our $VERSION = '0.28';
83              
84              
85             use fields qw(
86             _title
87             _description
88             _subject
89             _publisher
90             _publisher_url
91             _creator
92             _creator_url
93             _owner
94             _owner_url
95             _license
96             _license_date
97             _keywords
98             _language
99             _about_url
100             _date
101             _retain_xml
102             _strict_validation
103             _try_harder
104             _ERRORMSG
105             _RETAINED_XML
106             _RETAINED_DECLARATION
107             );
108             use vars qw( %FIELDS $AUTOLOAD );
109              
110              
111             =head2 new()
112              
113             Creates a new SVG::Metadata object. Optionally, can pass in arguments
114             'title', 'author', 'license', etc..
115              
116             my $svgmeta = new SVG::Metadata;
117             my $svgmeta = new SVG::Metadata(title=>'My title', author=>'Me', license=>'Public Domain');
118              
119             =cut
120              
121             sub new {
122             my $class = shift;
123             my %args = @_;
124              
125             my $self = bless [\%FIELDS], $class;
126              
127             while (my ($field, $value) = each %args) {
128             $self->{"_$field"} = $value
129             if (exists $FIELDS{"_$field"});
130             }
131             $self->{_creator} ||= $args{author} || '';
132             $self->{_language} ||= 'en';
133             $self->{_ERRORMSG} = '';
134             $self->{_strict_validation} = 0;
135              
136             return $self;
137             }
138              
139             # This automatically generates all the accessor functions for %FIELDS
140             sub AUTOLOAD {
141             my $self = shift;
142             my $attr = $AUTOLOAD;
143             $attr =~ s/.*:://;
144             return unless $attr =~ /[^A-Z]/; # skip DESTROY and all-cap methods
145             die "Invalid attribute method: ->$attr()\n" unless exists $FIELDS{"_$attr"};
146             $self->{"_$attr"} = shift if @_;
147             return $self->{"_$attr"};
148             }
149              
150             =head2 author()
151              
152             Alias for creator()
153              
154             =cut
155             sub author {
156             my $self = shift;
157             return $self->creator(@_);
158             }
159              
160             =head2 keywords_to_rdf()
161              
162             Generates an rdf:Bag based on the data structure of keywords.
163             This can then be used to populate the subject section of the metadata.
164             I.e.:
165              
166             $svgobj->subject($svg->keywords_to_rdf());
167              
168             See:
169             http://www.w3.org/TR/rdf-schema/#ch_bag
170             http://www.w3.org/TR/rdf-syntax-grammar/#section-Syntax-list-element
171             http://dublincore.org/documents/2002/05/15/dcq-rdf-xml/#sec2
172              
173             =cut
174             sub keywords_to_rdf {
175             my $self = shift;
176              
177             my $text = '';
178             foreach my $keyword ($self->keywords()) {
179             $keyword = $self->esc_ents($keyword);
180             $text .= qq( $keyword\n);
181             }
182              
183             if ($text ne '') {
184             return qq( \n$text );
185             } else {
186             return '';
187             }
188             }
189              
190              
191             =head2 errormsg()
192              
193             Returns the last encountered error message. Most of the error messages
194             are encountered during file parsing.
195              
196             print $svgmeta->errormsg();
197              
198             =cut
199              
200             sub errormsg {
201             my $self = shift;
202             return $self->{_ERRORMSG} || '';
203             }
204              
205              
206             =head2 parse($filename)
207              
208             Extracts RDF metadata out of an existing SVG file.
209              
210             $svgmeta->parse($filename) || die "Error: " . $svgmeta->errormsg();
211              
212             This routine looks for a field in the rdf:RDF section of the document
213             named 'ns:Work' and then attempts to load the following keys from it:
214             'dc:title', 'dc:rights'->'ns:Agent', and 'ns:license'. If any are
215             missing, it
216              
217             The $filename parameter can be a filename, or a text string containing
218             the XML to parse, or an open 'IO::Handle', or a URL.
219              
220             Returns false if there was a problem parsing the file, and sets an
221             error message appropriately. The conditions under which it will return
222             false are as follows:
223              
224             * No 'filename' parameter given.
225             * Filename does not exist.
226             * Document is not parseable XML.
227             * No rdf:RDF element was found in the document, and the try harder
228             option was not set.
229             * The rdf:RDF element did not have a ns:Work sub-element, and the
230             try_harder option was not set.
231             * Strict validation mode was turned on, and the document didn't
232             strictly comply with one or more of its extra criteria.
233              
234             =cut
235              
236             sub parse {
237             my ($self, $filename, %optn) = @_;
238             my $retaindecl;
239              
240             # For backward-compatibility, support retain_xml as an option here:
241             if ($optn{retain_xml}) { $self->retain_xml($optn{retain_xml}); }
242              
243             if (! defined($filename)) {
244             $self->{_ERRORMSG} = "No filename or text argument defined for parsing";
245             return;
246             }
247              
248             my $twig = XML::Twig->new( map_xmlns => {
249             'http://www.w3.org/2000/svg' => "svg", # W3C's SVG namespace
250             'http://www.w3.org/1999/02/22-rdf-syntax-ns#' => "rdf", # W3C's metadata namespace
251             'http://purl.org/dc/elements/1.1/' => "dc", # Dublin Core metadata namespace
252             'http://web.resource.org/cc/' => "cc", # a license description namespace
253             },
254             pretty_print => 'indented',
255             comments => 'keep',
256             pi => 'keep',
257             keep_original_prefix => 1, # prevents superfluous svg:element prefixing.
258             );
259              
260             if ($filename =~ m/\n.*\n/ || (ref $filename eq 'IO::Handle')) {
261             # Hmm, if it has newlines, it is likely to be a string instead of a filename
262             eval { $twig->parse($filename); };
263             if ($@) { $self->{_ERRORMSG} = "XML::Twig died; this may mean invalid XML."; return; }
264             if ($self->{_retain_xml}) {
265             ($retaindecl) = $filename =~ /(.*?)(
266             }
267             } elsif ($filename =~ /^http/ or $filename =~ /^ftp/) {
268             eval { $twig->parseurl($filename); };
269             if ($@) { $self->{_ERRORMSG} = "XML::Twig died; this may mean invalid XML."; return; }
270             if ($self->{_retain_xml}) {
271             open XML, '<', $filename; local $/ = '
272             my $content = ; close XML;
273             ($retaindecl) = $content =~ /(.*?)(
274             }
275             } elsif (! -e $filename) {
276             $self->{_ERRORMSG} = "Filename '$filename' does not exist"; return;
277             } else {
278             eval { $twig->parsefile($filename); };
279             if ($@) { $self->{_ERRORMSG} = "XML::Twig died; this may mean invalid XML."; return; }
280             if ($self->{_retain_xml}) {
281             open SVGIN, '<', $filename;
282             local $/ = '; close SVGIN;
283             ($retaindecl) = $raw =~ /(.*?)(
284             }
285             }
286              
287             if ($@) {
288             $self->{_ERRORMSG} = "Error parsing file: $@";
289             return;
290             }
291              
292             if (not ref $twig) {
293             $self->{_ERRORMSG} = "XML::Twig did not return a valid XML object";
294             return;
295             }
296             # If we get this far, we should return a valid object if try_harder is set.
297              
298             my $rdf;
299             my $metadata = $twig->root()->first_descendant('metadata') # preferred
300             || $twig->root()->first_descendant('svg:metadata'); # deprecated
301             if (ref $metadata) {
302             # This is the preferred way, as the rfd SHOULD be within a metadata element.
303             $rdf = $metadata->first_descendant('rdf:RDF') || # preferred
304             $metadata->first_descendant('RDF') || # mildly deprecated
305             $metadata->first_descendant('rdf'); # mildly deprecated
306             } else {
307             # But in non-strict mode we try a little harder:
308             $rdf = $twig->root()->first_descendant('rdf:RDF') || # deprecated
309             $twig->root()->first_descendant('RDF') || # very deprecated
310             $twig->root()->first_descendant('rdf'); # very deprecated
311             }
312             if (not ref $rdf) {
313             $self->{_ERRORMSG} = "No 'RDF' element found in " .
314             ((defined $metadata) ? "metadata element" : "document") . ".";
315             return unless $self->{_try_harder};
316             $rdf = $twig->root();
317             } elsif ($self->{_strict_validation} and not ref $metadata) {
318             $self->{_ERRORMSG} = "'RDF' element not contained in a block";
319             return unless $self->{_try_harder}; # undefined behavior, may change
320             }
321              
322             my $work = $rdf->first_descendant('cc:Work') || # preferred
323             $rdf->first_descendant('Work'); # also okay, I think
324             if (! defined($work)) {
325             $self->{_ERRORMSG} = "No 'Work' element found in the 'RDF' element";
326             return unless $self->{_try_harder};
327             $work = $rdf;
328             }
329              
330             my $getagent = sub {
331             my ($elt) = shift; return unless ref $elt;
332             return $elt->first_descendant('cc:Agent') # preferred
333             || $elt->first_descendant('Agent') # also okay, I think
334             || $elt; # and we treat the Agent wrapper as optional
335             };
336             my $getthingandurl = sub {
337             my ($thing, $elt, $thingdefault, $urldefault) = @_;
338             $thingdefault ||= ''; $urldefault ||= '';
339             $self->{'_'.$thing} = $thingdefault;
340             $self->{'_'.$thing.'_url'} = $urldefault;
341              
342             if (ref $elt) {
343             my $agent = $getagent->($elt);
344             my $title = $agent->first_descendant('dc:title') # preferred
345             || $agent->first_descendant('title'); # also okay, I think
346             my $about = $agent->att('rdf:about') # preferred
347             || $agent->att('about'); # deprecated
348             $self->{'_'.$thing} = (ref $title) ? $title->text() : $thingdefault;
349             $self->{'_'.$thing.'_url'} = ($about) ? $about : $urldefault;
350             }
351             };
352              
353             $getthingandurl->('publisher', $work->first_descendant('dc:publisher'),
354             # With defaults:
355             'Open Clip Art Library', 'http://www.openclipart.org/');
356             $getthingandurl->('creator', $work->first_descendant('dc:creator'));
357             $getthingandurl->('owner', $work->first_descendant('dc:rights'));
358              
359             $self->{_title} = _get_content($work->first_descendant('dc:title')) || '';
360             $self->{_description} = _get_content($work->first_descendant('dc:description')) || '';
361             my $license = $work->first_descendant('cc:license');
362             if (ref $license) {
363             $self->{_license} = _get_content($license->first_descendant('rdf:resource'))
364             || $license->att('rdf:resource') || '';
365             $self->{_license_date} = _get_content($license->first_descendant('dc:date')) || '';
366             }
367             $self->{_language} = _get_content($work->first_descendant('dc:language')) || 'en';
368             $self->{_about_url} = $work->att('rdf:about') || '';
369             $self->{_date} = _get_content($work->first_descendant('dc:date')) || '';
370              
371             # If only one of creator or owner is defined, default the other to match:
372             $self->{_creator} ||= $self->{_owner};
373             $self->{_creator_url} ||= $self->{_owner_url};
374             $self->{_owner} ||= $self->{_creator};
375             $self->{_owner_url} ||= $self->{_creator_url};
376              
377             if ($self->{_retain_xml}) {
378             $self->{_RETAINED_XML} = \$twig; # Keep the actual SVG around. (to_svg is worthless without this.)
379             $self->{_RETAINED_DECLARATION} = $retaindecl || ''; # and the XML declaration (and possibly also the doctype)
380             }
381              
382             my $subject = $work->first_descendant('dc:subject');
383             if (ref $subject) {
384             my @keyword = $subject->descendants('rdf:li');
385             # rdf:li elements are strongly preferred, and they should be wrapped in rdf:Bag
386             # But if that returns nothing, we try harder:
387             if (not @keyword) {
388             push @keyword, grep { $_ } # (Throw out empty keywords.)
389             split /(?:(?![-])\W)*/, # (Split on non-word chars *except* hyphen)
390             $subject->text(); # But this is a last resort, very deprecated.
391             }
392             my @keywordtext = map { $_->text() } @keyword;
393             $self->{_subject} = +{ map { $_ => 1 } @keywordtext }; # We *could* also map a split here...
394             }
395             if (not keys %{$self->{_subject}}) {
396             $self->{_subject} = { unsorted => 1 };
397             } elsif (keys %{$self->{_subject}} > 1 and exists $self->{_subject}->{unsorted}) {
398             delete ($self->{_subject}->{unsorted});
399             }
400             $self->{_keywords} = $self->{_subject}; # to_rdf() rebuilds _subject from _keywords
401             undef $self->{_subject}; # The POD for subject() says we do this.
402              
403             return $self; # references are always true in boolean context
404             }
405              
406             # XML::Twig::simplify has a bug where it only accepts "forcecontent", but
407             # the option to do that function is actually recognized as "force_content".
408             # As a result, we have to test to see if we're at a HASH node or a scalar.
409             sub _get_content {
410             my ($content)=@_;
411              
412             if (UNIVERSAL::isa($content,"HASH")
413             && exists($content->{'content'})) {
414             return $content->{'content'};
415             } elsif (ref $content) {
416             return $content->text();
417             } else {
418             return $content;
419             }
420             }
421              
422             =head2 title()
423              
424             Gets or sets the title.
425              
426             $svgmeta->title('My Title');
427             print $svgmeta->title();
428              
429             =head2 description()
430              
431             Gets or sets the description
432              
433             =head2 subject()
434              
435             Gets or sets the subject. Note that the parse() routine pulls the
436             keywords out of the subject and places them in the keywords
437             collection, so subject() will normally return undef. If you assign to
438             subject() it will override the internal keywords() mechanism, but this
439             may later be discarded again in favor of the keywords, if to_rdf() is
440             called, either directly or indirectly via to_svg().
441              
442             =head2 publisher()
443              
444             Gets or sets the publisher name. E.g., 'Open Clip Art Library'
445              
446             =head2 publisher_url()
447              
448             Gets or sets the web URL for the publisher. E.g., 'http://www.openclipart.org'
449              
450             =head2 creator()
451              
452             Gets or sets the creator.
453              
454             $svgmeta->creator('Bris Geek');
455             print $svgmeta->creator();
456              
457             =head2 creator_url()
458              
459             Gets or sets the URL for the creator.
460              
461             =head2 author()
462              
463             Alias for creator() - does the same thing
464              
465             $svgmeta->author('Bris Geek');
466             print $svgmeta->author();
467              
468             =head2 owner()
469              
470             Gets or sets the owner.
471              
472             $svgmeta->owner('Bris Geek');
473             print $svgmeta->owner();
474              
475             =head2 owner_url()
476              
477             Gets or sets the owner URL for the item
478              
479             =head2 license()
480              
481             Gets or sets the license.
482              
483             $svgmeta->license('Public Domain');
484             print $svgmeta->license();
485              
486             =head2 license_date()
487              
488             Gets or sets the date that the item was licensed
489              
490             =head2 language()
491              
492             Gets or sets the language for the metadata. This should be in the
493             two-letter lettercodes, such as 'en', etc.
494              
495             =head2 retain_xml()
496              
497             Gets or sets the XML retention option, which (if true) will cause any
498             subsequent call to parse() to retain the XML. You have to turn this
499             on if you want to_svg() to work later.
500              
501             =head2 strict_validation()
502              
503             Gets or sets the strict validation option, which (if true) will cause
504             subsequent calls to parse() to be pickier about how things are
505             structured and possibly set an error and return undef when it
506             otherwise would succeed.
507              
508             =head2 try_harder()
509              
510             Gets or sets the try harder option option, which causes subsequent
511             calls to parse() to try to return a valid Metadata object even if it
512             can't find any metadata at all. The resulting object may contain
513             mostly empty fields.
514              
515             Parse will still fail and return undef if the input file does not
516             exist or cannot be parsed as XML, but otherwise it will attempt to
517             return an object.
518              
519             If you set both this option and the strict validation option at the
520             same time, the Undefined Behavior Fairy will come and zap you with a
521             frap ray blaster and take away your cookie.
522              
523             =head2 keywords()
524              
525             Gets or sets an array of keywords. Keywords are a categorization
526             mechanism, and can be used, for example, to sort the files topically.
527              
528             =cut
529              
530             sub keywords {
531             my $self = shift;
532             if (@_) {
533             $self->addKeyword(@_);
534             }
535             return undef unless defined($self->{_keywords});
536              
537             # warn Dumper(+{ _keywords => $self->{_keywords}}); # DEBUG
538              
539             return keys %{$self->{_keywords}};
540             }
541              
542              
543             =head2 addKeyword($kw1 [, $kw2 ...])
544              
545             Adds one or more a new keywords. Note that the keywords are stored
546             internally as a set, so only one copy of a given keyword will be stored.
547              
548             $svgmeta->addKeyword('Fruits and Vegetables');
549             $svgmeta->addKeyword('Fruit','Vegetable','Animal','Mineral');
550              
551             =cut
552              
553             sub addKeyword {
554             my $self = shift;
555             foreach my $new_keyword (@_) {
556             $self->{_keywords}->{$new_keyword} = 1;
557             }
558             }
559              
560              
561             =head2 removeKeyword($kw)
562              
563             Removes a given keyword
564              
565             $svgmeta->removeKeyword('Fruits and Vegetables');
566              
567             Return value: The keyword removed.
568              
569             =cut
570              
571             sub removeKeyword {
572             my $self = shift;
573             my $keyword = shift || return;
574              
575             return delete $self->{_keywords}->{$keyword};
576             }
577              
578              
579             =head2 hasKeyword($kw)
580              
581             Returns true if the metadata includes the given keyword
582              
583             =cut
584              
585             sub hasKeyword {
586             my $self = shift;
587             my $keyword = shift || return 0;
588              
589             return 0 unless defined($self->{_keywords});
590              
591             return (defined($self->{_keywords}->{$keyword}));
592             }
593              
594             =head2 compare($meta2)
595              
596             Compares this metadata to another metadata for equality.
597              
598             Two SVG file metadata objects are considered equivalent if they
599             have exactly the same author, title, and license. Keywords can
600             vary, as can the SVG file itself.
601              
602             =cut
603              
604             sub compare {
605             my $self = shift;
606             my $meta = shift;
607              
608             return ( $meta->author() eq $self->author() &&
609             $meta->title() eq $self->title() &&
610             $meta->license() eq $self->license()
611             );
612             }
613              
614              
615             =head2 to_text()
616              
617             Creates a plain text representation of the metadata, suitable for
618             debuggery, emails, etc. Example output:
619              
620             Title: SVG Road Signs
621             Author: John Cliff
622             License: http://web.resource.org/cc/PublicDomain
623             Keywords: unsorted
624              
625             Return value is a string containing the title, author, license, and
626             keywords, each value on a separate line. The text always ends with
627             a newline character.
628              
629             =cut
630              
631             sub to_text {
632             my $self = shift;
633              
634             my $text = '';
635             $text .= 'Title: ' . ($self->title()||'') . "\n";
636             $text .= 'Author: ' . ($self->author()||'') . "\n";
637             $text .= 'License: ' . ($self->license()||'') . "\n";
638             $text .= 'Keywords: ';
639             $text .= join("\n ", $self->keywords());
640             $text .= "\n";
641              
642             return $text;
643             }
644              
645             =head2 esc_ents($text)
646              
647             Escapes '<', '>', and '&' and single and double quote
648             characters to avoid causing rdf to become invalid.
649              
650             =cut
651              
652             sub esc_ents {
653             my $self = shift;
654             my $text = shift;
655             return $text unless $text;
656              
657             return encode_entities($text, qq(<>&"'));
658             }
659              
660             =head2 to_rdf()
661              
662             Generates an RDF snippet to describe the item. This includes the
663             author, title, license, etc. The text always ends with a newline
664             character.
665              
666             =cut
667              
668             sub to_rdf {
669             my $self = shift;
670              
671             my $about_url = $self->esc_ents($self->about_url()) || '';
672             my $title = $self->esc_ents($self->title()) || '';
673             my $creator = $self->esc_ents($self->creator()) || '';
674             my $creator_url = $self->esc_ents($self->creator_url()) || '';
675             my $owner = $self->esc_ents($self->owner()) || '';
676             my $owner_url = $self->esc_ents($self->owner_url()) || '';
677             my $date = $self->esc_ents($self->date()) || '';
678             my $license = $self->esc_ents($self->license()) || '';
679             my $license_date = $self->esc_ents($self->license_date()) || '';
680             my $description = $self->esc_ents($self->description()) || '';
681             my $subject = $self->keywords_to_rdf() || '';
682             my $publisher = $self->esc_ents($self->publisher()) || '';
683             my $publisher_url = $self->esc_ents($self->publisher_url()) || '';
684             my $language = $self->esc_ents($self->language()) || 'en';
685              
686             my $license_rdf = '';
687             if ($license eq 'Public Domain'
688             or $license eq 'http://web.resource.org/cc/PublicDomain') {
689             $license = "http://web.resource.org/cc/PublicDomain";
690             $license_rdf = qq(
691            
692            
693            
694            
695            
696             );
697             } elsif ($license eq 'http://creativecommons.org/licenses/by-nc-nd/2.0/') {
698             $license_rdf = qq(
699            
700            
701            
702            
703            
704            
705            
706             );
707             } elsif ($license eq 'http://creativecommons.org/licenses/by/2.0/') {
708             $license_rdf = qq(
709            
710            
711            
712            
713            
714            
715            
716             );
717             } elsif ($license eq 'http://creativecommons.org/licenses/by-nc/2.0/') {
718             $license_rdf = qq(
719            
720            
721            
722            
723            
724            
725            
726            
727             );
728             } elsif ($license eq 'http://creativecommons.org/licenses/by-nd/2.0/') {
729             $license_rdf = qq(
730            
731            
732            
733            
734            
735            
736             );
737             } elsif ($license eq 'http://creativecommons.org/licenses/by-nc-nd/2.0/') {
738             $license_rdf = qq(
739            
740            
741            
742            
743            
744            
745            
746             );
747             } elsif ($license eq 'http://creativecommons.org/licenses/by-nc-sa/2.0/') {
748             $license_rdf = qq(
749            
750            
751            
752            
753            
754            
755            
756            
757            
758             );
759             } elsif ($license eq 'http://creativecommons.org/licenses/by-sa/2.0/') {
760             $license_rdf = qq(
761            
762            
763            
764            
765            
766            
767            
768            
769             );
770             }
771              
772             my $pub_data = ($publisher_url ? ' rdf:about="'.$publisher_url.'"' : '');
773             my $creator_data = ($creator_url ? ' rdf:about="'.$creator_url.'"' : '');
774             my $owner_data = ($owner_url ? ' rdf:about="'.$owner_url.'"' : '');
775             return qq(
776            
777            
778             xmlns="http://web.resource.org/cc/"
779             xmlns:dc="http://purl.org/dc/elements/1.1/"
780             xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
781            
782             $title
783             $description
784            
785             $subject
786            
787            
788            
789             $publisher
790            
791            
792            
793            
794             $creator
795            
796            
797            
798            
799             $owner
800            
801            
802             $date
803             image/svg+xml
804            
805            
806             $language
807            
808             $license_rdf
809            
810            
811             );
812              
813             }
814              
815             =head2 to_svg()
816              
817             Returns the SVG with the updated metadata embedded. This can only be
818             done if parse() was called with the retain_xml option. Note that the
819             code's layout can change a little, especially in terms of whitespace,
820             but the semantics SHOULD be the same, except for the updated metadata.
821              
822             =cut
823              
824             sub to_svg {
825             my ($self) = shift;
826             if (not $self->{_RETAINED_XML}) {
827             $self->{_ERRORMSG} = "Cannot do to_svg because the XML was not retained. Pass a true value for the retain_xml option to parse to retain the XML, and check the return value of parse to make sure it succeeded.";
828             return undef;
829             }
830              
831             my $xml = ${$self->{_RETAINED_XML}};
832             my $metadata = XML::Twig->new(
833             map_xmlns => {
834             'http://web.resource.org/cc/' => "cc",
835             'http://www.w3.org/1999/02/22-rdf-syntax-ns#' => "rdf",
836             'http://purl.org/dc/elements/1.1/' => "dc",
837             },
838             pretty_print => 'indented',
839             );
840             $metadata->parse($self->to_rdf());
841             for ($xml->descendants(qr'metadata'),
842             $xml->descendants(qr'svg:metadata'),
843             # $xml->descendants(qr'rdf:RDF'), # These too? I'm not sure. Leaving them for now.
844             ) {
845             # Out with the old...
846             $_->delete() if defined $_;
847             }
848             # In with the new...
849             $metadata->root()->copy();
850             $metadata->root()->paste( first_child => $xml->root());
851             return $self->{_RETAINED_DECLARATION} . $xml->root()->sprint();
852             }
853              
854             1;
855             __END__