Attachment 'inp2vtu_gz.pl'
Download 1 #!/usr/bin/perl
2 use strict;
3 use MIME::Base64;
4 use Compress::Raw::Zlib;
5
6 # Copyright (C) 2008 Stefan Tibus
7 #
8 # inp2vtu_gz.pl is free software; you can redistribute it and/or modify
9 # it under the terms of the GNU General Public License as published by
10 # the Free Software Foundation; either version 2 of the License, or
11 # (at your option) any later version.
12 #
13 # inp2vtu_gz.pl is distributed in the hope that it will be useful,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 # GNU General Public License for more details.
17 #
18 # You should have received a copy of the GNU General Public License
19 # along with magpar; if not, write to the Free Software
20 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
21
22 # 2007-11-20 STi: inp2vtu_gz.pl
23 # Supports node data and writes compressed binary data Vtk-XML files.
24 # Nodes and cells have to be ordered and continuous.
25 #
26 # 2008-02-01 STi: inp2vtu_gz.pl
27 # Added support for cell data.
28
29 # Note: Additional information on writing binary vtk files taken from
30 # http://news.tiker.net/node/430
31 # "What they don't tell you about VTK XML binary formats"
32
33 # Check Number of Arguments
34 # =========================
35 if($#ARGV!=0) {
36 die("Usage: inp2vtu_gz.pl <problem>\n");
37 }
38
39 # Get Arguments
40 # =============
41 my $problem=$ARGV[0];
42
43 # Get List of .inp Files
44 # ======================
45 my @inpfiles=`ls -1 \"$problem.\"????\".inp\"`;
46 # Strip leading/trailing spaces/tabs/newlines
47 foreach my $inpfile (@inpfiles) {
48 $inpfile=~s/^\s*(.*)\s*\n?/$1/;
49 }
50 # Skip last file if it has the number 9999
51 if(@inpfiles[-1]=~/^$problem\.9999\.inp/) {
52 pop(@inpfiles);
53 }
54
55 #DBG#foreach my $inpfile (@inpfiles) {
56 #DBG# print(stderr $inpfile."\n");
57 #DBG#}
58
59 # =================
60 # Create .vtu Files
61 # =================
62 my @vtufiles;
63
64 foreach my $inpfile (@inpfiles) {
65 my $vtufile=$inpfile;
66 # Change and store filename
67 $vtufile=~s/(.*)\.(\d\d\d\d)\.inp/$1_$2.vtu/;
68 push(@vtufiles,$vtufile);
69
70 # skip conversion if VTU file is newer!
71 if ( (stat($vtufile))[9] > (stat($inpfile))[9] ) {
72 print "Skipping $inpfile because $vtufile is newer!\n\n";
73 next;
74 }
75
76 # Read From .inp File
77 # ===================
78 open(INPFILE,"<",$inpfile) || die("Cannot open file \"$inpfile\" for read: $!\n");
79
80 # Read Header
81 # -----------
82 # nnodes, ncells, nnodedata, ncelldata, nmodeldata
83 my $line=<INPFILE>;
84 $line=~s/^\s*(.*)\s*$/$1/;
85 my($nnodes,$ncells,$nnodedata,$ncelldata,$nmodeldata)=split(/\s+/,$line,5);
86 if(($nmodeldata!=0)) {
87 die("Don't know how to handle model data in file \"$inpfile\"!\n");
88 }
89
90 # Read Nodes
91 # ----------
92 print(STDERR $nnodes," nodes\n");
93 my @nodes;
94 for(my $i=0;$i<$nnodes;$i++) {
95 $line=<INPFILE>;
96 $line=~s/^\s*(.*)\s*$/$1/;
97 my($n,$x,$y,$z)=split(/\s+/,$line,4);
98 if($n!=($i+1)) {
99 die("Don't know how to handle unordered nodes in file \"$inpfile\"!\n");
100 }
101 push(@nodes,[$x,$y,$z]);
102 }
103
104 # Read Cells
105 # ----------
106 print(STDERR $ncells," cells\n");
107 my @cells;
108 for(my $i=0;$i<$ncells;$i++) {
109 $line=<INPFILE>;
110 $line=~s/^\s*(.*)\s*$/$1/;
111 my($n,$mat,$type,$remainder)=split(/\s+/,$line,4);
112 if($type!='tet') {
113 die("Don't know how to handle cells other than tetrahedra in file \"$inpfile\"!\n");
114 }
115 if($n!=($i+1)) {
116 die("Don't know how to handle unordered cells in file \"$inpfile\"!\n");
117 }
118 my($n0,$n1,$n2,$n3)=split(/\s+/,$remainder,4);
119 push(@cells,[4,$n0,$n1,$n2,$n3,$mat]);
120 }
121
122 # Read Node Data
123 # --------------
124 my @nodelabels;
125 my @nodedimensions;
126 my @nodefirstelems;
127 my @nodelabeldatatypes;
128 my $nnodelabels;
129 my @nodevectors;
130 if($nnodedata>0) {
131 print(STDERR $nnodedata," nodedata\n");
132 $line=<INPFILE>;
133 $line=~s/^\s*(.*)\s*$/$1/;
134 my($nnodevalues,$remainder)=split(/\s+/,$line,2);
135 my(@snodevalues)=split(/\s+/,$remainder,$nnodevalues);
136 my $totalsize;
137 for(my $i=0;$i<$nnodevalues;$i++) {
138 $totalsize+=$snodevalues[$i];
139 }
140 if($totalsize!=$nnodedata) {
141 die("Node data size mismatch in file \"$inpfile\"!\n");
142 }
143 # Read data labels
144 my $label;
145 my $dimension;
146 my $firstelem=0;
147 my $labeldatatype;
148 for(my $i=0;$i<$nnodevalues;$i++) {
149 $line=<INPFILE>;
150 $line=~s/^\s*(.*)\s*$/$1/;
151 $line=~s/^(.*),.*$/$1/;
152 if($line=~/_?[XxYyZz]$/) {
153 # expect x,y,z
154 $line=~s/^(.*?)_?[XxYyZz]$/$1/;
155 if($line eq $label) {
156 # increase dimensionality of former label
157 $nodedimensions[-1]++;
158 # increase index of first element of next label
159 $firstelem++;
160 next;
161 } else {
162 $label=$line;
163 $dimension=1;
164 }
165 } else {
166 $label=$line;
167 $dimension=1;
168 }
169 push(@nodelabels,$label);
170 push(@nodedimensions,$dimension);
171 push(@nodefirstelems,$firstelem);
172 if($label eq "id") {
173 $labeldatatype="UInt32";
174 } elsif($label eq "proc") {
175 $labeldatatype="UInt32";
176 } elsif($label eq "vert_pid") {
177 $labeldatatype="UInt32";
178 } elsif($label eq "prop") {
179 $labeldatatype="UInt32";
180 } else {
181 $labeldatatype="Float32";
182 }
183 push(@nodelabeldatatypes,$labeldatatype);
184 # increase index of first element of next label
185 $firstelem++;
186 }
187 $nnodelabels=scalar(@nodelabels);
188 # Read data values
189 for(my $i=0;$i<$nnodes;$i++) {
190 $line=<INPFILE>;
191 $line=~s/^\s*(.*)\s*$/$1/;
192 my($n,@vector)=split(/\s+/,$line,($nnodevalues+1));
193 if($n!=($i+1)) {
194 die("Don't know how to handle unordered node data in file \"$inpfile\"!\n");
195 }
196 push(@nodevectors,[@vector]);
197 }
198 }
199
200 # Read Cell Data
201 # --------------
202 my @celllabels;
203 my @celldimensions;
204 my @cellfirstelems;
205 my @celllabeldatatypes;
206 my $ncelllabels;
207 my @cellvectors;
208 if($ncelldata>0) {
209 print(STDERR $ncelldata," celldata\n");
210 $line=<INPFILE>;
211 $line=~s/^\s*(.*)\s*$/$1/;
212 my($ncellvalues,$remainder)=split(/\s+/,$line,2);
213 my(@scellvalues)=split(/\s+/,$remainder,$ncellvalues);
214 my $totalsize;
215 for(my $i=0;$i<$ncellvalues;$i++) {
216 $totalsize+=$scellvalues[$i];
217 }
218 if($totalsize!=$ncelldata) {
219 die("Cell data size mismatch in file \"$inpfile\"!\n");
220 }
221 # Read data labels
222 my $label;
223 my $dimension;
224 my $firstelem=0;
225 my $labeldatatype;
226 for(my $i=0;$i<$ncellvalues;$i++) {
227 $line=<INPFILE>;
228 $line=~s/^\s*(.*)\s*$/$1/;
229 $line=~s/^(.*),.*$/$1/;
230 if($line=~/_?[XxYyZz]$/) {
231 # expect x,y,z
232 $line=~s/^(.*?)_?[XxYyZz]$/$1/;
233 if($line eq $label) {
234 # increase dimensionality of former label
235 $celldimensions[-1]++;
236 # increase index of first element of next label
237 $firstelem++;
238 next;
239 } else {
240 $label=$line;
241 $dimension=1;
242 }
243 } else {
244 $label=$line;
245 $dimension=1;
246 }
247 push(@celllabels,$label);
248 push(@celldimensions,$dimension);
249 push(@cellfirstelems,$firstelem);
250 if($label eq "id") {
251 $labeldatatype="UInt32";
252 } elsif($label eq "proc") {
253 $labeldatatype="UInt32";
254 } elsif($label eq "vert_pid") {
255 $labeldatatype="UInt32";
256 } elsif($label eq "prop") {
257 $labeldatatype="UInt32";
258 } else {
259 $labeldatatype="Float32";
260 }
261 push(@celllabeldatatypes,$labeldatatype);
262 # increase index of first element of next label
263 $firstelem++;
264 }
265 $ncelllabels=scalar(@celllabels);
266 # Read data values
267 for(my $i=0;$i<$ncells;$i++) {
268 $line=<INPFILE>;
269 $line=~s/^\s*(.*)\s*$/$1/;
270 my($n,@vector)=split(/\s+/,$line,($ncellvalues+1));
271 if($n!=($i+1)) {
272 die("Don't know how to handle unordered cell data in file \"$inpfile\"!\n");
273 }
274 push(@cellvectors,[@vector]);
275 }
276 }
277
278 # Close .inp File
279 # ---------------
280 close(INPFILE);
281
282 # Helper Functions
283 # ================
284
285 my %packformats=( "UInt8" => "C", "UInt16" => "S", "UInt32" => "L", "Float32" => "f" );
286 my %binsizes=( "UInt8" => 1, "UInt16" => 2, "UInt32" => 4, "Float32" => 4 );
287
288 # Encode Binary Data
289 # ------------------
290 # Prepend appropriate header and base64-encode
291 sub encodebin {
292 my $bindata=shift(@_);
293
294 my $binsize=length($bindata);
295 my $base64header=encode_base64(pack("L",$binsize));
296 my $base64data=encode_base64($bindata);
297 my $base64record=$base64header.$base64data;
298 $base64record=~s/\n//g;
299 $base64record.="\n";
300
301 return($base64record);
302 }
303
304 # Deflate Using Zlib
305 # ------------------
306 sub deflatezlib {
307 my $indata=shift(@_);
308 my $status;
309 my $outpart;
310 my $outdata;
311
312 my $deflator=new Compress::Raw::Zlib::Deflate or die("Cannot create a deflation stream!\n");
313 $status=$deflator->deflate($indata,$outpart);
314 if($status!=Z_OK) {
315 die("Deflation failed!\n");
316 }
317 $outdata.=$outpart;
318 $status=$deflator->flush($outpart);
319 if($status!=Z_OK) {
320 die("Deflation failed!\n");
321 }
322 $outdata.=$outpart;
323
324 return($outdata);
325 }
326
327 # Inflate Using Zlib
328 # ------------------
329 sub inflatezlib {
330 my $indata=shift(@_);
331 my $status;
332 my $outdata;
333
334 my $inflator=new Compress::Raw::Zlib::Inflate or die("Cannot create an inflation stream!\n");
335 $status=$inflator->inflate($indata,$outdata);
336 if($status!=Z_STREAM_END) {
337 die("Inflation failed!\n");
338 }
339
340 return($outdata);
341 }
342
343 sub encodebinzlib {
344 my $bindata=shift(@_);
345
346 my $binsize=length($bindata);
347 #DBG#print($binsize);
348 my $zippeddata=deflatezlib($bindata);
349 my $zippedsize=length($zippeddata);
350 #DBG#print(" -> ".$zippedsize);
351 my $base64header=encode_base64(pack("L4",1,$binsize,$binsize,$zippedsize));
352 my $base64data=encode_base64($zippeddata);
353 #DBG#print(" -> ".length($base64data));
354 #DBG#my $unbase64data=decode_base64($base64data);
355 #DBG#my $unzippeddata=inflatezlib($unbase64data);
356 #DBG#$base64header=encode_base64(pack("L",$binsize));
357 #DBG#$base64data=encode_base64($unzippeddata);
358 #DBG#print(" -> ".length($unzippeddata)."\n");
359 my $base64record=$base64header.$base64data;
360 $base64record=~s/\n//g;
361 $base64record.="\n";
362
363 return($base64record);
364 }
365
366 # Verify Data Type Sizes
367 # ======================
368 if(length(pack("c",1))!=1) {
369 die("char is not 8 bits long!\n");
370 }
371 if(length(pack("C",1))!=1) {
372 die("unsigned char is not 8 bits long!\n");
373 }
374 if(length(pack("s",1))!=2) {
375 die("short is not 16 bits long!\n");
376 }
377 if(length(pack("S",1))!=2) {
378 die("unsigned short is not 16 bits long!\n");
379 }
380 if(length(pack("l",1))!=4) {
381 die("long is not 32 bits long!\n");
382 }
383 if(length(pack("L",1))!=4) {
384 die("unsigned long is not 32 bits long!\n");
385 }
386 if(length(pack("f",1.0))!=4) {
387 die("float is not 32 bits long!\n");
388 }
389
390 # Write to .vtu File
391 # ==================
392 open(VTUFILE,">",$vtufile) || die("Cannot open or create file \"$vtufile\" for write: $!\n");
393 binmode(VTUFILE,':raw');
394 # Write Header
395 # ------------
396 print(VTUFILE "<?xml version=\"1.0\"?>\n");
397 print(VTUFILE "<VTKFile type=\"UnstructuredGrid\" byte_order=\"LittleEndian\" compressor=\"vtkZLibDataCompressor\">\n");
398 # Note: Intel 80x86 is little endian, Sun SPARC and IBM PowerPC are big endian platforms.
399 # See: http://www.netrino.com/Publications/Glossary/Endianness.php
400 # See: http://www.intel.com/design/intarch/papers/endian.pdf
401 print(VTUFILE " <UnstructuredGrid>\n");
402 print(VTUFILE " <Piece NumberOfPoints=\"$nnodes\" NumberOfCells=\"$ncells\">\n");
403
404 # Write Point (Node) Data
405 # -----------------------
406 # User data
407 # Set $nnodelabels=1 if you want first data set only (data for mumag / M for magpar)
408 #$nnodelabels=1;
409 if($nnodelabels>0) {
410 if($nodedimensions[0]>1) {
411 print(VTUFILE " <PointData Vectors=\"$nodelabels[0]\">\n");
412 } else {
413 print(VTUFILE " <PointData>\n");
414 }
415 for(my $i=0;$i<$nnodelabels;$i++) {
416 my $label=$nodelabels[$i];
417 my $dimension=$nodedimensions[$i];
418 my $firstelem=$nodefirstelems[$i];
419 my $labeldatatype=$nodelabeldatatypes[$i];
420 my $labelpackformat=$packformats{$labeldatatype};
421 my $bindata;
422 if($dimension==1) {
423 print(VTUFILE " <DataArray Name=\"$label\" NumberOfComponents=\"$dimension\" type=\"$labeldatatype\" format=\"binary\">\n");
424 for(my $j=0;$j<$nnodes;$j++) {
425 $bindata.=pack( $labelpackformat, $nodevectors[$j][$firstelem]);
426 }
427 } elsif($dimension==2) {
428 print(VTUFILE " <DataArray Name=\"$label\" NumberOfComponents=\"$dimension\" type=\"$labeldatatype\" format=\"binary\">\n");
429 for(my $j=0;$j<$nnodes;$j++) {
430 $bindata.=pack( $labelpackformat."2", $nodevectors[$j][$firstelem],$nodevectors[$j][$firstelem+1]);
431 }
432 } elsif($dimension==3) {
433 print(VTUFILE " <DataArray Name=\"$label\" NumberOfComponents=\"$dimension\" type=\"$labeldatatype\" format=\"binary\">\n");
434 for(my $j=0;$j<$nnodes;$j++) {
435 $bindata.=pack( $labelpackformat."3", $nodevectors[$j][$firstelem],$nodevectors[$j][$firstelem+1],$nodevectors[$j][$firstelem+2]);
436 }
437 } else {
438 die("Don't know how to handle vectors of dimension > 3 in file \"$inpfile\"!\n");
439 }
440 my $binsize=length($bindata);
441 if($binsize!=($dimension*$nnodes*$binsizes{$labeldatatype})) {
442 die("Size of binary data for \"$label\" is wrong!\n");
443 }
444 print(VTUFILE encodebinzlib($bindata));
445 print(VTUFILE " </DataArray>\n");
446 }
447 print(VTUFILE " </PointData>\n");
448 }
449
450 # Write Cell Data
451 # ---------------
452 # Material
453 print(VTUFILE " <CellData Scalars=\"Material\">\n");
454 print(VTUFILE " <DataArray Name=\"Material\" NumberOfComponents=\"1\" type=\"UInt32\" format=\"binary\">\n");
455 my $bindata;
456 for (my $i=0;$i<$ncells;$i++) {
457 $bindata.=pack( $packformats{"UInt32"}, $cells[$i][5]);
458 }
459 my $binsize=length($bindata);
460 if($binsize!=$ncells*$binsizes{"UInt32"}) {
461 die("Size of binary data for \"Material\" is wrong!\n");
462 }
463 print(VTUFILE encodebinzlib($bindata));
464 print(VTUFILE " </DataArray>\n");
465 # User data
466 # Set $ncelllabels=0 if you want to suppress cell data
467 #$ncelllabels=0;
468 if($ncelllabels>0) {
469 for(my $i=0;$i<$ncelllabels;$i++) {
470 my $label=$celllabels[$i];
471 my $dimension=$celldimensions[$i];
472 my $firstelem=$cellfirstelems[$i];
473 my $labeldatatype=$celllabeldatatypes[$i];
474 my $labelpackformat=$packformats{$labeldatatype};
475 my $bindata;
476 if($dimension==1) {
477 print(VTUFILE " <DataArray Name=\"$label\" NumberOfComponents=\"$dimension\" type=\"$labeldatatype\" format=\"binary\">\n");
478 for(my $j=0;$j<$ncells;$j++) {
479 $bindata.=pack( $labelpackformat, $cellvectors[$j][$firstelem]);
480 }
481 } elsif($dimension==2) {
482 print(VTUFILE " <DataArray Name=\"$label\" NumberOfComponents=\"$dimension\" type=\"$labeldatatype\" format=\"binary\">\n");
483 for(my $j=0;$j<$ncells;$j++) {
484 $bindata.=pack( $labelpackformat."2", $cellvectors[$j][$firstelem],$cellvectors[$j][$firstelem+1]);
485 }
486 } elsif($dimension==3) {
487 print(VTUFILE " <DataArray Name=\"$label\" NumberOfComponents=\"$dimension\" type=\"$labeldatatype\" format=\"binary\">\n");
488 for(my $j=0;$j<$ncells;$j++) {
489 $bindata.=pack( $labelpackformat."3", $cellvectors[$j][$firstelem],$cellvectors[$j][$firstelem+1],$cellvectors[$j][$firstelem+2]);
490 }
491 } else {
492 die("Don't know how to handle vectors of dimension > 3 in file \"$inpfile\"!\n");
493 }
494 my $binsize=length($bindata);
495 if($binsize!=($dimension*$ncells*$binsizes{$labeldatatype})) {
496 die("Size of binary data for \"$label\" is wrong!\n");
497 }
498 print(VTUFILE encodebinzlib($bindata));
499 print(VTUFILE " </DataArray>\n");
500 }
501 }
502 print(VTUFILE " </CellData>\n");
503
504 # Write Nodes
505 # -----------
506 # Point coordinates
507 print(VTUFILE " <Points>\n");
508 print(VTUFILE " <DataArray NumberOfComponents=\"3\" type=\"Float32\" format=\"binary\">\n");
509 my $bindata;
510 for(my $i=0;$i<$nnodes;$i++) {
511 $bindata.=pack( "f3", $nodes[$i][0],$nodes[$i][1],$nodes[$i][2]);
512 }
513 my $binsize=length($bindata);
514 if($binsize!=$nnodes*3*4) {
515 die("Size of binary data for points is wrong!\n");
516 }
517 print(VTUFILE encodebinzlib($bindata));
518 print(VTUFILE " </DataArray>\n");
519 print(VTUFILE " </Points>\n");
520
521 # Write Cells
522 # -----------
523 # Cell connectivity (tetrahedrons only)
524 print(VTUFILE " <Cells>\n");
525 print(VTUFILE " <DataArray Name=\"connectivity\" type=\"UInt32\" format=\"binary\">\n");
526 my $bindata;
527 for(my $i=0;$i<$ncells;$i++) {
528 $bindata.=pack( "L4", $cells[$i][1]-1,$cells[$i][2]-1,$cells[$i][3]-1,$cells[$i][4]-1);
529 }
530 my $binsize=length($bindata);
531 if($binsize!=$ncells*4*4) {
532 die("Size of binary data for \"connectivity\" is wrong!\n");
533 }
534 print(VTUFILE encodebinzlib($bindata));
535 print(VTUFILE " </DataArray>\n");
536 # Cell connectivity offsets (for tetrahedrons)
537 print(VTUFILE " <DataArray Name=\"offsets\" type=\"UInt32\" format=\"binary\">\n");
538 my $bindata;
539 for(my $i=0;$i<$ncells;$i++) {
540 $bindata.=pack( "L", ($i+1)*4);
541 }
542 my $binsize=length($bindata);
543 if($binsize!=$ncells*4) {
544 die("Size of binary data for \"offsets\" is wrong!\n");
545 }
546 print(VTUFILE encodebinzlib($bindata));
547 print(VTUFILE " </DataArray>\n");
548 # Cell types (tetrahedrons only)
549 # Tetrahedron = 10
550 print(VTUFILE " <DataArray Name=\"types\" type=\"UInt8\" format=\"binary\">\n");
551 my $bindata;
552 for(my $i=0;$i<$ncells;$i++) {
553 $bindata.=pack( "C", 10);
554 }
555 my $binsize=length($bindata);
556 if($binsize!=$ncells*1) {
557 die("Size of binary data for \"types\" is wrong!\n");
558 }
559 print(VTUFILE encodebinzlib($bindata));
560 print(VTUFILE " </DataArray>\n");
561 print(VTUFILE " </Cells>\n");
562
563 # Close .vtu File
564 # ---------------
565 print(VTUFILE " </Piece>\n");
566 print(VTUFILE " </UnstructuredGrid>\n");
567 print(VTUFILE "</VTKFile>\n");
568 close(VTUFILE);
569 print($vtufile."\n");
570 }
571
572 # ================
573 # Create .pvd File
574 # ================
575
576 if(scalar(@vtufiles)>1) {
577 my $pvdfile=$problem.".pvd";
578
579 open(PVDFILE,">",$pvdfile) || die("Cannot open or create file \"$pvdfile\" for write: $!\n");
580
581 # Write Header
582 # ============
583 print(PVDFILE "<?xml version=\"1.0\"?>\n");
584 print(PVDFILE "<VTKFile type=\"Collection\">\n");
585 print(PVDFILE " <Collection>\n");
586
587 # Get List of .vtu Files
588 # ======================
589 my @vtufiles=`ls -1 \"${problem}_\"????\".vtu\"`;
590 # Strip leading/trailing spaces/tabs/newlines
591 foreach my $vtufile (@vtufiles) {
592 $vtufile=~s/^\s*(.*)\s*\n?/$1/;
593 }
594
595 # Write List of .vtu Files
596 # ========================
597 my $i=0;
598 foreach my $vtufile (@vtufiles) {
599 printf(PVDFILE " <DataSet timestep=\"%04d\" file=\"%s\"/>\n",$i,$vtufile);
600 $i++;
601 }
602
603 # Close .pvd File
604 # ===============
605 print(PVDFILE " </Collection>\n");
606 print(PVDFILE "</VTKFile>\n");
607 close(PVDFILE);
608 print($pvdfile."\n");
609 }
610
611 # vim:set ft=perl sts=4 sw=4:
Attached Files
To refer to attachments on a page, use attachment:filename, as shown below in the list of files. Do NOT use the URL of the [get] link, since this is subject to change and can break easily.You are not allowed to attach a file to this page.