Kaydet (Commit) dd547e4a authored tarafından Stephan Bergmann's avatar Stephan Bergmann

Fix headless mode glyph cache memory handling

...the original code was riddled with errors.  It leaked memory, which if it
didn't it would have deleted multiple times.

Change-Id: Ic70b425fac02ef894e35b3dc15039d217f8870f5
üst 333b7fb3
...@@ -650,7 +650,6 @@ ImplFontEntry* ImplFTSFontData::CreateFontInstance( FontSelectPattern& rFSD ) co ...@@ -650,7 +650,6 @@ ImplFontEntry* ImplFTSFontData::CreateFontInstance( FontSelectPattern& rFSD ) co
ServerFont::ServerFont( const FontSelectPattern& rFSD, FtFontInfo* pFI ) ServerFont::ServerFont( const FontSelectPattern& rFSD, FtFontInfo* pFI )
: maGlyphList( 0), : maGlyphList( 0),
maFontSelData(rFSD), maFontSelData(rFSD),
mnExtInfo(0),
mnRefCount(1), mnRefCount(1),
mnBytesUsed( sizeof(ServerFont) ), mnBytesUsed( sizeof(ServerFont) ),
mpPrevGCFont( NULL ), mpPrevGCFont( NULL ),
...@@ -1420,20 +1419,19 @@ bool ServerFont::GetGlyphBitmap1( int nGlyphIndex, RawBitmap& rRawBitmap ) const ...@@ -1420,20 +1419,19 @@ bool ServerFont::GetGlyphBitmap1( int nGlyphIndex, RawBitmap& rRawBitmap ) const
if( rRawBitmap.mnAllocated < nNeededSize ) if( rRawBitmap.mnAllocated < nNeededSize )
{ {
delete[] rRawBitmap.mpBits;
rRawBitmap.mnAllocated = 2*nNeededSize; rRawBitmap.mnAllocated = 2*nNeededSize;
rRawBitmap.mpBits = new unsigned char[ rRawBitmap.mnAllocated ]; rRawBitmap.mpBits.reset(new unsigned char[ rRawBitmap.mnAllocated ]);
} }
if( !mbArtBold || pFTEmbolden ) if( !mbArtBold || pFTEmbolden )
{ {
memcpy( rRawBitmap.mpBits, rBitmapFT.buffer, nNeededSize ); memcpy( rRawBitmap.mpBits.get(), rBitmapFT.buffer, nNeededSize );
} }
else else
{ {
memset( rRawBitmap.mpBits, 0, nNeededSize ); memset( rRawBitmap.mpBits.get(), 0, nNeededSize );
const unsigned char* pSrcLine = rBitmapFT.buffer; const unsigned char* pSrcLine = rBitmapFT.buffer;
unsigned char* pDstLine = rRawBitmap.mpBits; unsigned char* pDstLine = rRawBitmap.mpBits.get();
for( int h = rRawBitmap.mnHeight; --h >= 0; ) for( int h = rRawBitmap.mnHeight; --h >= 0; )
{ {
memcpy( pDstLine, pSrcLine, rBitmapFT.pitch ); memcpy( pDstLine, pSrcLine, rBitmapFT.pitch );
...@@ -1441,7 +1439,7 @@ bool ServerFont::GetGlyphBitmap1( int nGlyphIndex, RawBitmap& rRawBitmap ) const ...@@ -1441,7 +1439,7 @@ bool ServerFont::GetGlyphBitmap1( int nGlyphIndex, RawBitmap& rRawBitmap ) const
pSrcLine += rBitmapFT.pitch; pSrcLine += rBitmapFT.pitch;
} }
unsigned char* p = rRawBitmap.mpBits; unsigned char* p = rRawBitmap.mpBits.get();
for( sal_uLong y=0; y < rRawBitmap.mnHeight; y++ ) for( sal_uLong y=0; y < rRawBitmap.mnHeight; y++ )
{ {
unsigned char nLastByte = 0; unsigned char nLastByte = 0;
...@@ -1549,13 +1547,12 @@ bool ServerFont::GetGlyphBitmap8( int nGlyphIndex, RawBitmap& rRawBitmap ) const ...@@ -1549,13 +1547,12 @@ bool ServerFont::GetGlyphBitmap8( int nGlyphIndex, RawBitmap& rRawBitmap ) const
const sal_uLong nNeededSize = rRawBitmap.mnScanlineSize * rRawBitmap.mnHeight; const sal_uLong nNeededSize = rRawBitmap.mnScanlineSize * rRawBitmap.mnHeight;
if( rRawBitmap.mnAllocated < nNeededSize ) if( rRawBitmap.mnAllocated < nNeededSize )
{ {
delete[] rRawBitmap.mpBits;
rRawBitmap.mnAllocated = 2*nNeededSize; rRawBitmap.mnAllocated = 2*nNeededSize;
rRawBitmap.mpBits = new unsigned char[ rRawBitmap.mnAllocated ]; rRawBitmap.mpBits.reset(new unsigned char[ rRawBitmap.mnAllocated ]);
} }
const unsigned char* pSrc = rBitmapFT.buffer; const unsigned char* pSrc = rBitmapFT.buffer;
unsigned char* pDest = rRawBitmap.mpBits; unsigned char* pDest = rRawBitmap.mpBits.get();
if( !bEmbedded ) if( !bEmbedded )
{ {
for( int y = rRawBitmap.mnHeight, x; --y >= 0 ; ) for( int y = rRawBitmap.mnHeight, x; --y >= 0 ; )
...@@ -1585,7 +1582,7 @@ bool ServerFont::GetGlyphBitmap8( int nGlyphIndex, RawBitmap& rRawBitmap ) const ...@@ -1585,7 +1582,7 @@ bool ServerFont::GetGlyphBitmap8( int nGlyphIndex, RawBitmap& rRawBitmap ) const
if( mbArtBold && !pFTEmbolden ) if( mbArtBold && !pFTEmbolden )
{ {
// overlay with glyph image shifted by one left pixel // overlay with glyph image shifted by one left pixel
unsigned char* p = rRawBitmap.mpBits; unsigned char* p = rRawBitmap.mpBits.get();
for( sal_uLong y=0; y < rRawBitmap.mnHeight; y++ ) for( sal_uLong y=0; y < rRawBitmap.mnHeight; y++ )
{ {
unsigned char nLastByte = 0; unsigned char nLastByte = 0;
...@@ -1601,7 +1598,7 @@ bool ServerFont::GetGlyphBitmap8( int nGlyphIndex, RawBitmap& rRawBitmap ) const ...@@ -1601,7 +1598,7 @@ bool ServerFont::GetGlyphBitmap8( int nGlyphIndex, RawBitmap& rRawBitmap ) const
if( !bEmbedded && mbUseGamma ) if( !bEmbedded && mbUseGamma )
{ {
unsigned char* p = rRawBitmap.mpBits; unsigned char* p = rRawBitmap.mpBits.get();
for( sal_uLong y=0; y < rRawBitmap.mnHeight; y++ ) for( sal_uLong y=0; y < rRawBitmap.mnHeight; y++ )
{ {
for( sal_uLong x=0; x < rRawBitmap.mnWidth; x++ ) for( sal_uLong x=0; x < rRawBitmap.mnWidth; x++ )
......
...@@ -23,16 +23,12 @@ ...@@ -23,16 +23,12 @@
RawBitmap::RawBitmap() RawBitmap::RawBitmap()
: mpBits(0), mnAllocated(0) : mnAllocated(0)
{} {}
RawBitmap::~RawBitmap() RawBitmap::~RawBitmap()
{ {}
delete[] mpBits;
mpBits = 0;
mnAllocated = 0;
}
// used by 90 and 270 degree rotations on 8 bit deep bitmaps // used by 90 and 270 degree rotations on 8 bit deep bitmaps
...@@ -171,7 +167,7 @@ bool RawBitmap::Rotate( int nAngle ) ...@@ -171,7 +167,7 @@ bool RawBitmap::Rotate( int nAngle )
mnYOffset = -(mnYOffset + mnHeight); mnYOffset = -(mnYOffset + mnHeight);
if( mnBitCount == 8 ) if( mnBitCount == 8 )
{ {
ImplRotate8_180( mpBits, mnWidth, mnHeight, mnScanlineSize-mnWidth ); ImplRotate8_180( mpBits.get(), mnWidth, mnHeight, mnScanlineSize-mnWidth );
return true; return true;
} }
nNewWidth = mnWidth; nNewWidth = mnWidth;
...@@ -203,7 +199,7 @@ bool RawBitmap::Rotate( int nAngle ) ...@@ -203,7 +199,7 @@ bool RawBitmap::Rotate( int nAngle )
{ {
case 1800: // rotate by 180 degrees case 1800: // rotate by 180 degrees
// we know we only need to deal with 1 bit depth // we know we only need to deal with 1 bit depth
ImplRotate1_180( pBuf, mpBits + mnHeight * mnScanlineSize, ImplRotate1_180( pBuf, mpBits.get() + mnHeight * mnScanlineSize,
mnWidth, mnHeight, mnScanlineSize - (mnWidth + 7) / 8 ); mnWidth, mnHeight, mnScanlineSize - (mnWidth + 7) / 8 );
break; break;
case +900: // rotate left by 90 degrees case +900: // rotate left by 90 degrees
...@@ -211,11 +207,11 @@ bool RawBitmap::Rotate( int nAngle ) ...@@ -211,11 +207,11 @@ bool RawBitmap::Rotate( int nAngle )
mnXOffset = mnYOffset; mnXOffset = mnYOffset;
mnYOffset = -nNewHeight - i; mnYOffset = -nNewHeight - i;
if( mnBitCount == 8 ) if( mnBitCount == 8 )
ImplRotate8_90( pBuf, mpBits + mnWidth - 1, ImplRotate8_90( pBuf, mpBits.get() + mnWidth - 1,
nNewWidth, nNewHeight, +mnScanlineSize, -1-mnHeight*mnScanlineSize, nNewWidth, nNewHeight, +mnScanlineSize, -1-mnHeight*mnScanlineSize,
nNewScanlineSize - nNewWidth ); nNewScanlineSize - nNewWidth );
else else
ImplRotate1_90( pBuf, mpBits + (mnWidth - 1) / 8, ImplRotate1_90( pBuf, mpBits.get() + (mnWidth - 1) / 8,
nNewWidth, nNewHeight, +mnScanlineSize, nNewWidth, nNewHeight, +mnScanlineSize,
(-mnWidth & 7), +1, nNewScanlineSize - (nNewWidth + 7) / 8 ); (-mnWidth & 7), +1, nNewScanlineSize - (nNewWidth + 7) / 8 );
break; break;
...@@ -225,11 +221,11 @@ bool RawBitmap::Rotate( int nAngle ) ...@@ -225,11 +221,11 @@ bool RawBitmap::Rotate( int nAngle )
mnXOffset = -(nNewWidth + mnYOffset); mnXOffset = -(nNewWidth + mnYOffset);
mnYOffset = i; mnYOffset = i;
if( mnBitCount == 8 ) if( mnBitCount == 8 )
ImplRotate8_90( pBuf, mpBits + mnScanlineSize * (mnHeight-1), ImplRotate8_90( pBuf, mpBits.get() + mnScanlineSize * (mnHeight-1),
nNewWidth, nNewHeight, -mnScanlineSize, +1+mnHeight*mnScanlineSize, nNewWidth, nNewHeight, -mnScanlineSize, +1+mnHeight*mnScanlineSize,
nNewScanlineSize - nNewWidth ); nNewScanlineSize - nNewWidth );
else else
ImplRotate1_90( pBuf, mpBits + mnScanlineSize * (mnHeight-1), ImplRotate1_90( pBuf, mpBits.get() + mnScanlineSize * (mnHeight-1),
nNewWidth, nNewHeight, -mnScanlineSize, nNewWidth, nNewHeight, -mnScanlineSize,
+7, -1, nNewScanlineSize - (nNewWidth + 7) / 8 ); +7, -1, nNewScanlineSize - (nNewWidth + 7) / 8 );
break; break;
...@@ -241,13 +237,12 @@ bool RawBitmap::Rotate( int nAngle ) ...@@ -241,13 +237,12 @@ bool RawBitmap::Rotate( int nAngle )
if( nBufSize < mnAllocated ) if( nBufSize < mnAllocated )
{ {
memcpy( mpBits, pBuf, nBufSize ); memcpy( mpBits.get(), pBuf, nBufSize );
delete[] pBuf; delete[] pBuf;
} }
else else
{ {
delete[] mpBits; mpBits.reset(pBuf);
mpBits = pBuf;
mnAllocated = nBufSize; mnAllocated = nBufSize;
} }
......
...@@ -315,9 +315,9 @@ void GlyphCache::GrowNotify() ...@@ -315,9 +315,9 @@ void GlyphCache::GrowNotify()
} }
inline void GlyphCache::RemovingGlyph( ServerFont& rSF, GlyphData& rGD, int nGlyphIndex ) inline void GlyphCache::RemovingGlyph( GlyphData& rGD )
{ {
mrPeer.RemovingGlyph( rSF, rGD, nGlyphIndex ); mrPeer.RemovingGlyph( rGD );
mnBytesUsed -= sizeof( GlyphData ); mnBytesUsed -= sizeof( GlyphData );
--mnGlyphCount; --mnGlyphCount;
} }
...@@ -372,7 +372,7 @@ void ServerFont::GarbageCollect( long nMinLruIndex ) ...@@ -372,7 +372,7 @@ void ServerFont::GarbageCollect( long nMinLruIndex )
{ {
OSL_ASSERT( mnBytesUsed >= sizeof(GlyphData) ); OSL_ASSERT( mnBytesUsed >= sizeof(GlyphData) );
mnBytesUsed -= sizeof( GlyphData ); mnBytesUsed -= sizeof( GlyphData );
GlyphCache::GetInstance().RemovingGlyph( *this, rGD, it->first ); GlyphCache::GetInstance().RemovingGlyph( rGD );
maGlyphList.erase( it ); maGlyphList.erase( it );
it_next = maGlyphList.begin(); it_next = maGlyphList.begin();
} }
......
...@@ -17,6 +17,10 @@ ...@@ -17,6 +17,10 @@
* the License at http://www.apache.org/licenses/LICENSE-2.0 . * the License at http://www.apache.org/licenses/LICENSE-2.0 .
*/ */
#include "sal/config.h"
#include <cassert>
#include <basegfx/range/b2drange.hxx> #include <basegfx/range/b2drange.hxx>
#include <basegfx/range/b2ibox.hxx> #include <basegfx/range/b2ibox.hxx>
#include <basegfx/polygon/b2dpolypolygon.hxx> #include <basegfx/polygon/b2dpolypolygon.hxx>
...@@ -50,7 +54,7 @@ public: ...@@ -50,7 +54,7 @@ public:
protected: protected:
virtual void RemovingFont( ServerFont& ); virtual void RemovingFont( ServerFont& );
virtual void RemovingGlyph( ServerFont&, GlyphData&, int nGlyphIndex ); virtual void RemovingGlyph( GlyphData& );
class SvpGcpHelper class SvpGcpHelper
{ {
...@@ -113,14 +117,14 @@ BitmapDeviceSharedPtr SvpGlyphPeer::GetGlyphBmp( ServerFont& rServerFont, ...@@ -113,14 +117,14 @@ BitmapDeviceSharedPtr SvpGlyphPeer::GetGlyphBmp( ServerFont& rServerFont,
int nGlyphIndex, basebmp::Format nBmpFormat, B2IPoint& rTargetPos ) int nGlyphIndex, basebmp::Format nBmpFormat, B2IPoint& rTargetPos )
{ {
GlyphData& rGlyphData = rServerFont.GetGlyphData( nGlyphIndex ); GlyphData& rGlyphData = rServerFont.GetGlyphData( nGlyphIndex );
SvpGcpHelper* pGcpHelper = (SvpGcpHelper*)rGlyphData.ExtDataRef().mpData;
// nothing to do if the GlyphPeer hasn't allocated resources for the glyph
if( rGlyphData.ExtDataRef().meInfo != nBmpFormat ) if( rGlyphData.ExtDataRef().meInfo != nBmpFormat )
{ {
if( rGlyphData.ExtDataRef().meInfo == FORMAT_NONE ) SvpGcpHelper* pGcpHelper = static_cast<SvpGcpHelper*>(
rGlyphData.ExtDataRef().mpData);
bool bNew = pGcpHelper == 0;
if( bNew )
pGcpHelper = new SvpGcpHelper; pGcpHelper = new SvpGcpHelper;
RawBitmap& rRawBitmap = pGcpHelper->maRawBitmap;
// get glyph bitmap in matching format // get glyph bitmap in matching format
bool bFound = false; bool bFound = false;
...@@ -143,22 +147,28 @@ BitmapDeviceSharedPtr SvpGlyphPeer::GetGlyphBmp( ServerFont& rServerFont, ...@@ -143,22 +147,28 @@ BitmapDeviceSharedPtr SvpGlyphPeer::GetGlyphBmp( ServerFont& rServerFont,
// return .notdef glyph if needed // return .notdef glyph if needed
if( !bFound && (nGlyphIndex != 0) ) if( !bFound && (nGlyphIndex != 0) )
{ {
delete pGcpHelper; if( bNew )
delete pGcpHelper;
return GetGlyphBmp( rServerFont, 0, nBmpFormat, rTargetPos ); return GetGlyphBmp( rServerFont, 0, nBmpFormat, rTargetPos );
} }
// construct alpha mask from raw bitmap // construct alpha mask from raw bitmap
const B2IVector aSize( rRawBitmap.mnScanlineSize, rRawBitmap.mnHeight ); const B2IVector aSize(
pGcpHelper->maRawBitmap.mnScanlineSize,
pGcpHelper->maRawBitmap.mnHeight );
if( aSize.getX() && aSize.getY() ) if( aSize.getX() && aSize.getY() )
{ {
static PaletteMemorySharedVector aDummyPAL; static PaletteMemorySharedVector aDummyPAL;
RawMemorySharedArray aRawPtr( rRawBitmap.mpBits ); pGcpHelper->maBitmapDev = createBitmapDevice( aSize, true, nBmpFormat, pGcpHelper->maRawBitmap.mpBits, aDummyPAL );
pGcpHelper->maBitmapDev = createBitmapDevice( aSize, true, nBmpFormat, aRawPtr, aDummyPAL );
} }
rServerFont.SetExtended( nBmpFormat, (void*)pGcpHelper ); rGlyphData.ExtDataRef().meInfo = nBmpFormat;
rGlyphData.ExtDataRef().mpData = pGcpHelper;
} }
SvpGcpHelper* pGcpHelper = static_cast<SvpGcpHelper*>(
rGlyphData.ExtDataRef().mpData);
assert(pGcpHelper != 0);
rTargetPos += B2IPoint( pGcpHelper->maRawBitmap.mnXOffset, pGcpHelper->maRawBitmap.mnYOffset ); rTargetPos += B2IPoint( pGcpHelper->maRawBitmap.mnXOffset, pGcpHelper->maRawBitmap.mnYOffset );
return pGcpHelper->maBitmapDev; return pGcpHelper->maBitmapDev;
} }
...@@ -170,16 +180,13 @@ void SvpGlyphPeer::RemovingFont( ServerFont& ) ...@@ -170,16 +180,13 @@ void SvpGlyphPeer::RemovingFont( ServerFont& )
} }
void SvpGlyphPeer::RemovingGlyph( ServerFont&, GlyphData& rGlyphData, int /*nGlyphIndex*/ ) void SvpGlyphPeer::RemovingGlyph( GlyphData& rGlyphData )
{ {
if( rGlyphData.ExtDataRef().mpData != 0 ) SvpGcpHelper* pGcpHelper = static_cast<SvpGcpHelper*>(
{ rGlyphData.ExtDataRef().mpData);
// release the glyph related resources rGlyphData.ExtDataRef().meInfo = basebmp::FORMAT_NONE;
DBG_ASSERT( (rGlyphData.ExtDataRef().meInfo <= FORMAT_MAX), "SVP::RG() invalid alpha format" ); rGlyphData.ExtDataRef().mpData = 0;
SvpGcpHelper* pGcpHelper = (SvpGcpHelper*)rGlyphData.ExtDataRef().mpData; delete pGcpHelper;
delete[] pGcpHelper->maRawBitmap.mpBits;
delete pGcpHelper;
}
} }
......
...@@ -34,6 +34,7 @@ struct ImplKernPairData; ...@@ -34,6 +34,7 @@ struct ImplKernPairData;
class ImplFontOptions; class ImplFontOptions;
#include <tools/gen.hxx> #include <tools/gen.hxx>
#include <basebmp/bitmapdevice.hxx>
#include <boost/unordered_map.hpp> #include <boost/unordered_map.hpp>
#include <boost/unordered_set.hpp> #include <boost/unordered_set.hpp>
#include <boost/shared_ptr.hpp> #include <boost/shared_ptr.hpp>
...@@ -90,7 +91,7 @@ private: ...@@ -90,7 +91,7 @@ private:
friend class ServerFont; friend class ServerFont;
// used by ServerFont class only // used by ServerFont class only
void AddedGlyph( ServerFont&, GlyphData& ); void AddedGlyph( ServerFont&, GlyphData& );
void RemovingGlyph( ServerFont&, GlyphData&, int nGlyphIndex ); void RemovingGlyph( GlyphData& );
void UsingGlyph( ServerFont&, GlyphData& ); void UsingGlyph( ServerFont&, GlyphData& );
void GrowNotify(); void GrowNotify();
...@@ -139,7 +140,9 @@ private: ...@@ -139,7 +140,9 @@ private:
// ----------------------------------------------------------------------- // -----------------------------------------------------------------------
// the glyph specific data needed by a GlyphCachePeer is usually trivial, // the glyph specific data needed by a GlyphCachePeer is usually trivial,
// not attaching it to the corresponding GlyphData would be overkill // not attaching it to the corresponding GlyphData would be overkill;
// this is currently only used by the headless (aka svp) plugin, where meInfo is
// basebmp::Format and mpData is SvpGcpHelper*
struct ExtGlyphData struct ExtGlyphData
{ {
int meInfo; int meInfo;
...@@ -219,10 +222,6 @@ public: ...@@ -219,10 +222,6 @@ public:
bool GetGlyphBitmap1( int nGlyphIndex, RawBitmap& ) const; bool GetGlyphBitmap1( int nGlyphIndex, RawBitmap& ) const;
bool GetGlyphBitmap8( int nGlyphIndex, RawBitmap& ) const; bool GetGlyphBitmap8( int nGlyphIndex, RawBitmap& ) const;
void SetExtended( int nInfo, void* ppVoid );
int GetExtInfo() { return mnExtInfo; }
void* GetExtPointer() { return mpExtData; }
private: private:
friend class GlyphCache; friend class GlyphCache;
friend class ServerFontLayout; friend class ServerFontLayout;
...@@ -248,10 +247,6 @@ private: ...@@ -248,10 +247,6 @@ private:
const FontSelectPattern maFontSelData; const FontSelectPattern maFontSelData;
// info for GlyphcachePeer
int mnExtInfo;
void* mpExtData;
// used by GlyphCache for cache LRU algorithm // used by GlyphCache for cache LRU algorithm
mutable long mnRefCount; mutable long mnRefCount;
mutable sal_uLong mnBytesUsed; mutable sal_uLong mnBytesUsed;
...@@ -351,7 +346,7 @@ protected: ...@@ -351,7 +346,7 @@ protected:
public: public:
sal_Int32 GetByteCount() const { return mnBytesUsed; } sal_Int32 GetByteCount() const { return mnBytesUsed; }
virtual void RemovingFont( ServerFont& ) {} virtual void RemovingFont( ServerFont& ) {}
virtual void RemovingGlyph( ServerFont&, GlyphData&, int ) {} virtual void RemovingGlyph( GlyphData& ) {}
protected: protected:
sal_Int32 mnBytesUsed; sal_Int32 mnBytesUsed;
...@@ -367,7 +362,7 @@ public: ...@@ -367,7 +362,7 @@ public:
bool Rotate( int nAngle ); bool Rotate( int nAngle );
public: public:
unsigned char* mpBits; basebmp::RawMemorySharedArray mpBits;
sal_uLong mnAllocated; sal_uLong mnAllocated;
sal_uLong mnWidth; sal_uLong mnWidth;
...@@ -382,14 +377,6 @@ public: ...@@ -382,14 +377,6 @@ public:
// ======================================================================= // =======================================================================
inline void ServerFont::SetExtended( int nInfo, void* pVoid )
{
mnExtInfo = nInfo;
mpExtData = pVoid;
}
// =======================================================================
// ExtraKernInfo allows an on-demand query of extra kerning info #i29881# // ExtraKernInfo allows an on-demand query of extra kerning info #i29881#
// The kerning values have to be scaled to match the font size before use // The kerning values have to be scaled to match the font size before use
class VCL_DLLPUBLIC ExtraKernInfo class VCL_DLLPUBLIC ExtraKernInfo
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment