kicad/common/common_plotDXF_functions.cpp

721 lines
21 KiB
C++
Raw Normal View History

/**
* @file common_plotDXF_functions.cpp
* @brief KiCad: Common plot DXF Routines.
*/
2009-06-30 10:43:20 +00:00
#include <fctsys.h>
#include <gr_basic.h>
#include <trigo.h>
#include <wxstruct.h>
#include <base_struct.h>
#include <plot_common.h>
#include <macros.h>
#include <kicad_string.h>
2009-06-30 10:43:20 +00:00
/**
* Oblique angle for DXF native text
* (I don't remember if 15 degrees is the ISO value... it looks nice anyway)
*/
static const double DXF_OBLIQUE_ANGLE = 15;
2009-06-30 10:43:20 +00:00
/**
* Set the scale/position for the DXF plot
* The DXF engine doesn't support line widths and mirroring. The output
* coordinate system is in the first quadrant (in mm)
2009-06-30 10:43:20 +00:00
*/
void DXF_PLOTTER::SetViewport( const wxPoint& aOffset, double aIusPerDecimil,
double aScale, bool aMirror )
2009-06-30 10:43:20 +00:00
{
wxASSERT( !outputFile );
plotOffset = aOffset;
plotScale = aScale;
2012-08-29 16:59:50 +00:00
/* DXF paper is 'virtual' so there is no need of a paper size.
Also this way we can handle the aux origin which can be useful
(for example when aligning to a mechanical drawing) */
paperSize.x = 0;
paperSize.y = 0;
/* Like paper size DXF units are abstract too. Anyway there is a
* system variable (MEASUREMENT) which will be set to 1 to indicate
* metric units */
m_IUsPerDecimil = aIusPerDecimil;
iuPerDeviceUnit = 1.0 / aIusPerDecimil; // Gives a DXF in decimils
2012-08-29 16:59:50 +00:00
iuPerDeviceUnit *= 0.00254; // ... now in mm
SetDefaultLineWidth( 0 ); // No line width on DXF
m_plotMirror = false; // No mirroring on DXF
m_currentColor = BLACK;
2009-06-30 10:43:20 +00:00
}
/**
* Opens the DXF plot with a skeleton header
*/
bool DXF_PLOTTER::StartPlot()
2009-06-30 10:43:20 +00:00
{
wxASSERT( outputFile );
// DXF HEADER - Boilerplate
// Defines the minimum for drawing i.e. the angle system and the
// continuous linetype
fputs( " 0\n"
"SECTION\n"
" 2\n"
"HEADER\n"
" 9\n"
"$ANGBASE\n"
" 50\n"
"0.0\n"
" 9\n"
"$ANGDIR\n"
" 70\n"
2012-08-29 16:59:50 +00:00
" 1\n"
" 9\n"
"$MEASUREMENT\n"
" 70\n"
"0\n"
2012-08-29 16:59:50 +00:00
" 0\n" // This means 'metric units'
"ENDSEC\n"
" 0\n"
"SECTION\n"
" 2\n"
"TABLES\n"
" 0\n"
"TABLE\n"
" 2\n"
"LTYPE\n"
" 70\n"
"1\n"
" 0\n"
"LTYPE\n"
" 2\n"
"CONTINUOUS\n"
" 70\n"
"0\n"
" 3\n"
"Solid line\n"
" 72\n"
"65\n"
" 73\n"
"0\n"
" 40\n"
"0.0\n"
" 0\n"
"ENDTAB\n",
outputFile );
// Text styles table
// Defines 4 text styles, one for each bold/italic combination
fputs( " 0\n"
"TABLE\n"
" 2\n"
"STYLE\n"
" 70\n"
"4\n", outputFile );
static const char *style_name[4] = {"KICAD", "KICADB", "KICADI", "KICADBI"};
for(int i = 0; i < 4; i++ )
{
fprintf( outputFile,
" 0\n"
"STYLE\n"
" 2\n"
"%s\n" // Style name
" 70\n"
"0\n" // Standard flags
" 40\n"
"0\n" // Non-fixed height text
" 41\n"
"1\n" // Width factor (base)
" 42\n"
"1\n" // Last height (mandatory)
" 50\n"
"%g\n" // Oblique angle
" 71\n"
"0\n" // Generation flags (default)
" 3\n"
// The standard ISO font (when kicad is build with it
// the dxf text in acad matches *perfectly*)
"isocp.shx\n", // Font name (when not bigfont)
// Apply a 15 degree angle to italic text
style_name[i], i < 2 ? 0 : DXF_OBLIQUE_ANGLE );
}
// Layer table - one layer per color
fprintf( outputFile,
" 0\n"
"ENDTAB\n"
" 0\n"
"TABLE\n"
" 2\n"
"LAYER\n"
" 70\n"
"%d\n", NBCOLORS );
/* The layer/colors palette. The acad/DXF palette is divided in 3 zones:
- The primary colors (1 - 9)
- An HSV zone (10-250, 5 values x 2 saturations x 10 hues
- Greys (251 - 255)
There is *no* black... the white does it on paper, usually, and
anyway it depends on the plotter configuration, since DXF colors
are meant to be logical only (they represent *both* line color and
width); later version with plot styles only complicate the matter!
As usual, brown and magenta/purple are difficult to place since
they are actually variations of other colors.
*/
static const struct {
const char *name;
int color;
} dxf_layer[NBCOLORS] = {
2013-04-05 17:23:56 +00:00
{ "BLACK", 7 }, // In DXF, color 7 is *both* white and black!
{ "GRAY1", 251 },
{ "GRAY2", 8 },
{ "GRAY3", 9 },
{ "WHITE", 7 },
{ "LYELLOW", 51 },
{ "BLUE1", 178 },
{ "GREEN1", 98 },
{ "CYAN1", 138 },
{ "RED1", 18 },
{ "MAGENTA1", 228 },
{ "BROWN1", 58 },
{ "BLUE2", 5 },
{ "GREEN2", 3 },
{ "CYAN2", 4 },
{ "RED2", 1 },
{ "MAGENTA2", 6 },
{ "BROWN2", 54 },
{ "BLUE3", 171 },
{ "GREEN3", 91 },
{ "CYAN3", 131 },
{ "RED3", 11 },
{ "MAGENTA3", 221 },
{ "YELLOW3", 2 },
{ "BLUE4", 5 },
{ "GREEN4", 3 },
{ "CYAN4", 4 },
{ "RED4", 1 },
{ "MAGENTA4", 6 },
{ "YELLOW4", 2 }
};
for( EDA_COLOR_T i = BLACK; i < NBCOLORS; i = NextColor(i) )
2009-06-30 10:43:20 +00:00
{
fprintf( outputFile,
" 0\n"
"LAYER\n"
" 2\n"
"%s\n" // Layer name
" 70\n"
"0\n" // Standard flags
" 62\n"
"%d\n" // Color number
" 6\n"
"CONTINUOUS\n",// Linetype name
dxf_layer[i].name, dxf_layer[i].color );
2009-06-30 10:43:20 +00:00
}
// End of layer table, begin entities
fputs( " 0\n"
"ENDTAB\n"
" 0\n"
"ENDSEC\n"
" 0\n"
"SECTION\n"
" 2\n"
"ENTITIES\n", outputFile );
return true;
2009-06-30 10:43:20 +00:00
}
bool DXF_PLOTTER::EndPlot()
2009-06-30 10:43:20 +00:00
{
wxASSERT( outputFile );
// DXF FOOTER
fputs( " 0\n"
"ENDSEC\n"
" 0\n"
"EOF\n", outputFile );
fclose( outputFile );
outputFile = NULL;
return true;
2009-06-30 10:43:20 +00:00
}
/**
* The DXF exporter handles 'colors' as layers...
2009-06-30 10:43:20 +00:00
*/
void DXF_PLOTTER::SetColor( EDA_COLOR_T color )
2009-06-30 10:43:20 +00:00
{
wxASSERT( outputFile );
if( ( color >= 0 && colorMode )
|| ( color == BLACK )
|| ( color == WHITE ) )
2009-06-30 10:43:20 +00:00
{
m_currentColor = color;
2009-06-30 10:43:20 +00:00
}
else
m_currentColor = BLACK;
2009-06-30 10:43:20 +00:00
}
/**
* DXF rectangle: fill not supported
*/
void DXF_PLOTTER::Rect( const wxPoint& p1, const wxPoint& p2, FILL_T fill, int width )
2009-06-30 10:43:20 +00:00
{
wxASSERT( outputFile );
MoveTo( p1 );
LineTo( wxPoint( p1.x, p2.y ) );
LineTo( wxPoint( p2.x, p2.y ) );
LineTo( wxPoint( p2.x, p1.y ) );
FinishTo( wxPoint( p1.x, p1.y ) );
2009-06-30 10:43:20 +00:00
}
/**
* DXF circle: full functionality; it even does 'fills' drawing a
* circle with a dual-arc polyline wide as the radius.
*
* I could use this trick to do other filled primitives
*/
void DXF_PLOTTER::Circle( const wxPoint& centre, int diameter, FILL_T fill, int width )
2009-06-30 10:43:20 +00:00
{
wxASSERT( outputFile );
double radius = userToDeviceSize( diameter / 2 );
DPOINT centre_dev = userToDeviceCoordinates( centre );
if( radius > 0 )
2009-06-30 10:43:20 +00:00
{
wxString cname( ColorGetName( m_currentColor ) );
if (!fill)
{
fprintf( outputFile, "0\nCIRCLE\n8\n%s\n10\n%g\n20\n%g\n40\n%g\n",
TO_UTF8( cname ),
centre_dev.x, centre_dev.y, radius );
}
if (fill == FILLED_SHAPE)
{
double r = radius*0.5;
fprintf( outputFile, "0\nPOLYLINE\n");
fprintf( outputFile, "8\n%s\n66\n1\n70\n1\n", TO_UTF8( cname ));
fprintf( outputFile, "40\n%g\n41\n%g\n", radius, radius);
fprintf( outputFile, "0\nVERTEX\n8\n%s\n", TO_UTF8( cname ));
fprintf( outputFile, "10\n%g\n 20\n%g\n42\n1.0\n",
centre_dev.x-r, centre_dev.y );
fprintf( outputFile, "0\nVERTEX\n8\n%s\n", TO_UTF8( cname ));
fprintf( outputFile, "10\n%g\n 20\n%g\n42\n1.0\n",
centre_dev.x+r, centre_dev.y );
fprintf( outputFile, "0\nSEQEND\n");
}
}
2009-06-30 10:43:20 +00:00
}
/**
* DXF polygon: doesn't fill it but at least it close the filled ones
2009-06-30 10:43:20 +00:00
*/
void DXF_PLOTTER::PlotPoly( const std::vector< wxPoint >& aCornerList,
FILL_T aFill, int aWidth)
2009-06-30 10:43:20 +00:00
{
if( aCornerList.size() <= 1 )
2009-06-30 10:43:20 +00:00
return;
MoveTo( aCornerList[0] );
for( unsigned ii = 1; ii < aCornerList.size(); ii++ )
LineTo( aCornerList[ii] );
2009-06-30 10:43:20 +00:00
// Close polygon if 'fill' requested
if( aFill )
2009-06-30 10:43:20 +00:00
{
unsigned ii = aCornerList.size() - 1;
if( aCornerList[ii] != aCornerList[0] )
LineTo( aCornerList[0] );
2009-06-30 10:43:20 +00:00
}
PenFinish();
2009-06-30 10:43:20 +00:00
}
void DXF_PLOTTER::PenTo( const wxPoint& pos, char plume )
2009-06-30 10:43:20 +00:00
{
wxASSERT( outputFile );
2009-06-30 10:43:20 +00:00
if( plume == 'Z' )
{
return;
}
DPOINT pos_dev = userToDeviceCoordinates( pos );
DPOINT pen_lastpos_dev = userToDeviceCoordinates( penLastpos );
2009-06-30 10:43:20 +00:00
if( penLastpos != pos && plume == 'D' )
2009-06-30 10:43:20 +00:00
{
// DXF LINE
wxString cname( ColorGetName( m_currentColor ) );
fprintf( outputFile, "0\nLINE\n8\n%s\n10\n%g\n20\n%g\n11\n%g\n21\n%g\n",
TO_UTF8( cname ),
pen_lastpos_dev.x, pen_lastpos_dev.y, pos_dev.x, pos_dev.y );
2009-06-30 10:43:20 +00:00
}
penLastpos = pos;
2009-06-30 10:43:20 +00:00
}
/**
* Dashed lines are not (yet) supported by DXF_PLOTTER
*/
void DXF_PLOTTER::SetDash( bool dashed )
2009-06-30 10:43:20 +00:00
{
// NOP for now
2009-06-30 10:43:20 +00:00
}
void DXF_PLOTTER::ThickSegment( const wxPoint& aStart, const wxPoint& aEnd, int aWidth,
EDA_DRAW_MODE_T aPlotMode )
2009-06-30 10:43:20 +00:00
{
if( aPlotMode == LINE ) // In line mode, just a line is OK
2009-06-30 10:43:20 +00:00
{
MoveTo( aStart );
FinishTo( aEnd );
2009-06-30 10:43:20 +00:00
}
else
{
segmentAsOval( aStart, aEnd, aWidth, aPlotMode );
}
2009-06-30 10:43:20 +00:00
}
/* Plot an arc in DXF format
* Filling is not supported
2009-06-30 10:43:20 +00:00
*/
void DXF_PLOTTER::Arc( const wxPoint& centre, double StAngle, double EndAngle, int radius,
FILL_T fill, int width )
2009-06-30 10:43:20 +00:00
{
wxASSERT( outputFile );
2009-06-30 10:43:20 +00:00
if( radius <= 0 )
2009-06-30 10:43:20 +00:00
return;
// In DXF, arcs are drawn CCW.
// In Kicad, arcs are CW or CCW
// If StAngle > EndAngle, it is CW. So transform it to CCW
if( StAngle > EndAngle )
{
EXCHG( StAngle, EndAngle );
}
DPOINT centre_dev = userToDeviceCoordinates( centre );
double radius_dev = userToDeviceSize( radius );
2009-06-30 10:43:20 +00:00
// Emit a DXF ARC entity
wxString cname( ColorGetName( m_currentColor ) );
fprintf( outputFile,
"0\nARC\n8\n%s\n10\n%g\n20\n%g\n40\n%g\n50\n%g\n51\n%g\n",
TO_UTF8( cname ),
centre_dev.x, centre_dev.y, radius_dev,
StAngle / 10.0, EndAngle / 10.0 );
2009-06-30 10:43:20 +00:00
}
/**
* DXF oval pad: always done in sketch mode
*/
void DXF_PLOTTER::FlashPadOval( const wxPoint& pos, const wxSize& aSize, double orient,
EDA_DRAW_MODE_T trace_mode )
2009-06-30 10:43:20 +00:00
{
wxASSERT( outputFile );
wxSize size( aSize );
2009-06-30 10:43:20 +00:00
/* The chip is reduced to an oval tablet with size.y > size.x
* (Oval vertical orientation 0) */
2009-06-30 10:43:20 +00:00
if( size.x > size.y )
{
EXCHG( size.x, size.y );
orient = AddAngles( orient, 900 );
2009-06-30 10:43:20 +00:00
}
sketchOval( pos, size, orient, -1 );
2009-06-30 10:43:20 +00:00
}
/**
* DXF round pad: always done in sketch mode; it could be filled but it isn't
* pretty if other kinds of pad aren't...
*/
void DXF_PLOTTER::FlashPadCircle( const wxPoint& pos, int diametre,
EDA_DRAW_MODE_T trace_mode )
2009-06-30 10:43:20 +00:00
{
wxASSERT( outputFile );
Circle( pos, diametre, NO_FILL );
2009-06-30 10:43:20 +00:00
}
/**
* DXF rectangular pad: alwayd done in sketch mode
2009-06-30 10:43:20 +00:00
*/
void DXF_PLOTTER::FlashPadRect( const wxPoint& pos, const wxSize& padsize,
double orient, EDA_DRAW_MODE_T trace_mode )
2009-06-30 10:43:20 +00:00
{
wxASSERT( outputFile );
2009-06-30 10:43:20 +00:00
wxSize size;
int ox, oy, fx, fy;
size.x = padsize.x / 2;
size.y = padsize.y / 2;
2009-06-30 10:43:20 +00:00
if( size.x < 0 )
size.x = 0;
if( size.y < 0 )
size.y = 0;
// If a dimension is zero, the trace is reduced to 1 line
2009-06-30 10:43:20 +00:00
if( size.x == 0 )
{
ox = pos.x;
oy = pos.y - size.y;
2009-06-30 10:43:20 +00:00
RotatePoint( &ox, &oy, pos.x, pos.y, orient );
fx = pos.x;
fy = pos.y + size.y;
2009-06-30 10:43:20 +00:00
RotatePoint( &fx, &fy, pos.x, pos.y, orient );
MoveTo( wxPoint( ox, oy ) );
FinishTo( wxPoint( fx, fy ) );
2009-06-30 10:43:20 +00:00
return;
}
if( size.y == 0 )
{
ox = pos.x - size.x;
oy = pos.y;
2009-06-30 10:43:20 +00:00
RotatePoint( &ox, &oy, pos.x, pos.y, orient );
fx = pos.x + size.x;
fy = pos.y;
2009-06-30 10:43:20 +00:00
RotatePoint( &fx, &fy, pos.x, pos.y, orient );
MoveTo( wxPoint( ox, oy ) );
FinishTo( wxPoint( fx, fy ) );
2009-06-30 10:43:20 +00:00
return;
}
ox = pos.x - size.x;
oy = pos.y - size.y;
2009-06-30 10:43:20 +00:00
RotatePoint( &ox, &oy, pos.x, pos.y, orient );
MoveTo( wxPoint( ox, oy ) );
2009-06-30 10:43:20 +00:00
fx = pos.x - size.x;
fy = pos.y + size.y;
2009-06-30 10:43:20 +00:00
RotatePoint( &fx, &fy, pos.x, pos.y, orient );
LineTo( wxPoint( fx, fy ) );
2009-06-30 10:43:20 +00:00
fx = pos.x + size.x;
fy = pos.y + size.y;
2009-06-30 10:43:20 +00:00
RotatePoint( &fx, &fy, pos.x, pos.y, orient );
LineTo( wxPoint( fx, fy ) );
2009-06-30 10:43:20 +00:00
fx = pos.x + size.x;
fy = pos.y - size.y;
2009-06-30 10:43:20 +00:00
RotatePoint( &fx, &fy, pos.x, pos.y, orient );
LineTo( wxPoint( fx, fy ) );
2009-06-30 10:43:20 +00:00
FinishTo( wxPoint( ox, oy ) );
2009-06-30 10:43:20 +00:00
}
/**
* DXF trapezoidal pad: only sketch mode is supported
2009-06-30 10:43:20 +00:00
*/
void DXF_PLOTTER::FlashPadTrapez( const wxPoint& aPadPos, const wxPoint *aCorners,
double aPadOrient, EDA_DRAW_MODE_T aTrace_Mode )
2009-06-30 10:43:20 +00:00
{
wxASSERT( outputFile );
wxPoint coord[4]; /* coord actual corners of a trapezoidal trace */
2009-06-30 10:43:20 +00:00
for( int ii = 0; ii < 4; ii++ )
{
coord[ii] = aCorners[ii];
RotatePoint( &coord[ii], aPadOrient );
coord[ii] += aPadPos;
2009-06-30 10:43:20 +00:00
}
// Plot edge:
MoveTo( coord[0] );
LineTo( coord[1] );
LineTo( coord[2] );
LineTo( coord[3] );
FinishTo( coord[0] );
}
/**
* Checks if a given string contains non-ASCII characters.
* FIXME: the performance of this code is really poor, but in this case it can be
* acceptable because the plot operation is not called very often.
* @param string String to check
* @return true if it contains some non-ASCII character, false if all characters are
* inside ASCII range (<=255).
*/
bool containsNonAsciiChars( const wxString& string )
{
for( unsigned i = 0; i < string.length(); i++ )
{
wchar_t ch = string[i];
if( ch > 255 )
return true;
}
return false;
}
void DXF_PLOTTER::Text( const wxPoint& aPos,
enum EDA_COLOR_T aColor,
const wxString& aText,
double aOrient,
const wxSize& aSize,
enum EDA_TEXT_HJUSTIFY_T aH_justify,
enum EDA_TEXT_VJUSTIFY_T aV_justify,
int aWidth,
bool aItalic,
bool aBold,
bool aMultilineAllowed )
{
// Fix me: see how to use DXF text mode for multiline texts
if( aMultilineAllowed && !aText.Contains( wxT( "\n" ) ) )
aMultilineAllowed = false; // the text has only one line.
if( textAsLines || containsNonAsciiChars( aText ) || aMultilineAllowed )
{
// output text as graphics.
// Perhaps miltiline texts could be handled as DXF text entity
// but I do not want spend time about this (JPC)
PLOTTER::Text( aPos, aColor, aText, aOrient, aSize, aH_justify, aV_justify,
aWidth, aItalic, aBold, aMultilineAllowed );
}
else
{
/* Emit text as a text entity. This loses formatting and shape but it's
more useful as a CAD object */
DPOINT origin_dev = userToDeviceCoordinates( aPos );
SetColor( aColor );
wxString cname( ColorGetName( m_currentColor ) );
DPOINT size_dev = userToDeviceSize( aSize );
int h_code = 0, v_code = 0;
switch( aH_justify )
{
case GR_TEXT_HJUSTIFY_LEFT:
h_code = 0;
break;
case GR_TEXT_HJUSTIFY_CENTER:
h_code = 1;
break;
case GR_TEXT_HJUSTIFY_RIGHT:
h_code = 2;
break;
}
switch( aV_justify )
{
case GR_TEXT_VJUSTIFY_TOP:
v_code = 3;
break;
case GR_TEXT_VJUSTIFY_CENTER:
v_code = 2;
break;
case GR_TEXT_VJUSTIFY_BOTTOM:
v_code = 1;
break;
}
// Position, size, rotation and alignment
// The two alignment point usages is somewhat idiot (see the DXF ref)
// Anyway since we don't use the fit/aligned options, they're the same
fprintf( outputFile,
" 0\n"
"TEXT\n"
" 7\n"
"%s\n" // Text style
" 8\n"
"%s\n" // Layer name
" 10\n"
"%g\n" // First point X
" 11\n"
"%g\n" // Second point X
" 20\n"
"%g\n" // First point Y
" 21\n"
"%g\n" // Second point Y
" 40\n"
"%g\n" // Text height
" 41\n"
"%g\n" // Width factor
" 50\n"
"%g\n" // Rotation
" 51\n"
"%g\n" // Oblique angle
" 71\n"
"%d\n" // Mirror flags
" 72\n"
"%d\n" // H alignment
" 73\n"
"%d\n", // V alignment
aBold ? (aItalic ? "KICADBI" : "KICADB")
: (aItalic ? "KICADI" : "KICAD"),
TO_UTF8( cname ),
origin_dev.x, origin_dev.x,
origin_dev.y, origin_dev.y,
size_dev.y, fabs( size_dev.x / size_dev.y ),
aOrient / 10.0,
aItalic ? DXF_OBLIQUE_ANGLE : 0,
size_dev.x < 0 ? 2 : 0, // X mirror flag
h_code, v_code );
/* There are two issue in emitting the text:
- Our overline character (~) must be converted to the appropriate
control sequence %%O or %%o
- Text encoding in DXF is more or less unspecified since depends on
the DXF declared version, the acad version reading it *and* some
system variables to be put in the header handled only by newer acads
Also before R15 unicode simply is not supported (you need to use
bigfonts which are a massive PITA). Common denominator solution:
use Latin1 (and however someone could choke on it, anyway). Sorry
for the extended latin people. If somewant want to try fixing this
recent version seems to use UTF-8 (and not UCS2 like the rest of
Windows)
XXX Actually there is a *third* issue: older DXF formats are limited
to 255 bytes records (it was later raised to 2048); since I'm lazy
and text so long is not probable I just don't implement this rule.
If someone is interested in fixing this, you have to emit the first
partial lines with group code 3 (max 250 bytes each) and then finish
with a group code 1 (less than 250 bytes). The DXF refs explains it
in no more details...
*/
bool overlining = false;
fputs( " 1\n", outputFile );
for( unsigned i = 0; i < aText.length(); i++ )
{
/* Here I do a bad thing: writing the output one byte at a time!
but today I'm lazy and I have no idea on how to coerce a Unicode
wxString to spit out latin1 encoded text ...
Atleast stdio is *supposed* to do output buffering, so there is
hope is not too slow */
wchar_t ch = aText[i];
if( ch > 255 )
{
// I can't encode this...
putc( '?', outputFile );
}
else
{
if( ch == '~' )
{
// Handle the overline toggle
fputs( overlining ? "%%o" : "%%O", outputFile );
overlining = !overlining;
}
else
{
putc( ch, outputFile );
}
}
}
putc( '\n', outputFile );
}
2009-06-30 10:43:20 +00:00
}