Click here to Skip to main content
15,886,362 members
Please Sign up or sign in to vote.
5.00/5 (1 vote)
Hi,

I've searched the entire internet for two consecutive days, with little to no success on this issue:

To append images in the System.Windows.Forms.RichTextBox class without copying to clipboard or any other dirty tricks, I have stumbled upon the ExRichTextBox class by Khendys Gordon here on http://www.codeproject.com

This class can convert an Image type to the corresponding RTF string which is needed to display it in the RichTextBox.

The RTF's picture control MUST be .WMF data either in binary or hexadecimals (this is also what WordPad does when you insert an image). WordPad and the RichTextBox ignores every other control word (e.g. PNG, JPG etc.) for pictures in RTF. See page 148 in the RTF Specification

Now here is the problem: http://puu.sh/1XnKL.jpg (image was 19x25 so I zoomed a little bit)

- The left one is converted from PNG to WMF using ExRichTextBox

- The right one is directly added to WordPad (so WordPad did the conversion)

Now, in the ExRichTextBox class the conversion goes by the following:

1. Create EMF from the Bitmap/Image,
2. then convert to WMF by using unmanaged code functions (in GDI+ API)
3. finally convert the bytes to hexadecimals

Here is the code of the method which returns the WMF data when you pass an Image type. This method is in the ExRichTextBox class which inherits from System.Windows.Forms.RichTextBox:

C#
[DllImportAttribute("gdiplus.dll")]
private static extern uint GdipEmfToWmfBits (IntPtr _hEmf, uint _bufferSize,
    byte[] _buffer, int _mappingMode, EmfToWmfBitsFlags _flags);


private string GetRtfImage(Image _image) {

    StringBuilder _rtf = null;

    // Used to store the enhanced metafile
    MemoryStream _stream = null;

    // Used to create the metafile and draw the image
    Graphics _graphics = null;

    // The enhanced metafile
    Metafile _metaFile = null;

    // Handle to the device context used to create the metafile
    IntPtr _hdc;

    try {
        _rtf = new StringBuilder();
        _stream = new MemoryStream();

        // Get a graphics context from the RichTextBox
        using(_graphics = this.CreateGraphics()) {

            // Get the device context from the graphics context
            _hdc = _graphics.GetHdc();

            // Create a new Enhanced Metafile from the device context
            _metaFile = new Metafile(_stream, _hdc);

            // Release the device context
            _graphics.ReleaseHdc(_hdc);
        }

        // Get a graphics context from the Enhanced Metafile
        using(_graphics = Graphics.FromImage(_metaFile)) {

            // Draw the image on the Enhanced Metafile
            _graphics.DrawImage(_image, new Rectangle(0, 0, _image.Width,
                _image.Height));

        }

        // Get the handle of the Enhanced Metafile
        IntPtr _hEmf = _metaFile.GetHenhmetafile();

        // A call to EmfToWmfBits with a null buffer return the size of the
        // buffer need to store the WMF bits.  Use this to get the buffer
        // size.
        uint _bufferSize = GdipEmfToWmfBits(_hEmf, 0, null, MM_ANISOTROPIC,
            EmfToWmfBitsFlags.EmfToWmfBitsFlagsDefault);

        // Create an array to hold the bits
        byte[] _buffer = new byte[_bufferSize];

        // A call to EmfToWmfBits with a valid buffer copies the bits into the
        // buffer an returns the number of bits in the WMF.  
        uint _convertedSize = GdipEmfToWmfBits(_hEmf, _bufferSize, _buffer,
            MM_ANISOTROPIC, EmfToWmfBitsFlags.EmfToWmfBitsFlagsDefault);

        // Append the bits to the RTF string
        for(int i = 0; i < _buffer.Length; ++i) {
            _rtf.Append(String.Format("{0:X2}", _buffer[i]));
        }

        return _rtf.ToString();
    }
    finally {
        if(_graphics != null)
            _graphics.Dispose();
        if(_metaFile != null)
            _metaFile.Dispose();
        if(_stream != null)
            _stream.Close();
    }
}


Question: WHY and WHERE does the image lose its quality?
Posted

When you convert from EMF to WMF, you are converting from a 32bit file to 16bit file - What is WMF?[^].
As a result, loss in quality is unavoidable.
 
Share this answer
 
Comments
QuantumHive 5-Feb-13 13:35pm    
If it is unavoidable, then how does Microsoft manage to do this in WordPad WITHOUT the quality loss? (As you can see clearly the difference in the image I provided)
This is the code I use to encode images into RTF.
It's not the nicest code, but it gets the job done.


C#
public enum FormatHexStyles
{
    LittleEndian2Bytes,
    LittleEndian4Bytes,
    BigEndian2Bytes,
    BigEndian4Bytes
}

public static class ImageUtil
{
    public const int    HMM_PER_INCH    = 2540;
    public const int    TWIPS_PER_INCH  = 1440;
    public const float  DEFAULT_DPIX    = 96.0F;
    public const float  DEFAULT_DPIY    = 96.0F;

    public static readonly string[] HEX_X2_TABLE =
    {
        "00","01","02","03","04","05","06","07","08","09","0a","0b","0c","0d","0e","0f",
        "10","11","12","13","14","15","16","17","18","19","1a","1b","1c","1d","1e","1f",
        "20","21","22","23","24","25","26","27","28","29","2a","2b","2c","2d","2e","2f",
        "30","31","32","33","34","35","36","37","38","39","3a","3b","3c","3d","3e","3f",
        "40","41","42","43","44","45","46","47","48","49","4a","4b","4c","4d","4e","4f",
        "50","51","52","53","54","55","56","57","58","59","5a","5b","5c","5d","5e","5f",
        "60","61","62","63","64","65","66","67","68","69","6a","6b","6c","6d","6e","6f",
        "70","71","72","73","74","75","76","77","78","79","7a","7b","7c","7d","7e","7f",
        "80","81","82","83","84","85","86","87","88","89","8a","8b","8c","8d","8e","8f",
        "90","91","92","93","94","95","96","97","98","99","9a","9b","9c","9d","9e","9f",
        "a0","a1","a2","a3","a4","a5","a6","a7","a8","a9","aa","ab","ac","ad","ae","af",
        "b0","b1","b2","b3","b4","b5","b6","b7","b8","b9","ba","bb","bc","bd","be","bf",
        "c0","c1","c2","c3","c4","c5","c6","c7","c8","c9","ca","cb","cc","cd","ce","cf",
        "d0","d1","d2","d3","d4","d5","d6","d7","d8","d9","da","db","dc","dd","de","df",
        "e0","e1","e2","e3","e4","e5","e6","e7","e8","e9","ea","eb","ec","ed","ee","ef",
        "f0","f1","f2","f3","f4","f5","f6","f7","f8","f9","fa","fb","fc","fd","fe","ff"
    };

    public static int RoundTo4ByteBoundary( int aValue )
    {
        // Check sign
        bool negative = false;
        if( aValue < 0 )
        {
            // Set negative flag and invert value
            negative = true;
            aValue = -aValue;
        }

        // Declare variables
        int real = aValue & ~0x3;
        int mod  = aValue & 0x3;

        // Check if we must increment real with boundary base
        if( mod != 0 )
            real += 0x4;

        // Check if we must apply sign
        if( negative )
            return -real;
        else
            return real;
    }

    public static string ConvertToHex( int aValue, FormatHexStyles aHexFormat )
    {
        // Check lower range
        if( aValue < 0 )
            throw new ArgumentOutOfRangeException( string.Format(
                "Invalid value '{0}' specified; negative values are not supported",
                aValue ));

        // Declare variables
        string hex;

        // Check required format
        switch( aHexFormat )
        {
            case FormatHexStyles.LittleEndian2Bytes:
                // Check upper range
                if( aValue > 0xFFFF )
                    throw new ArgumentOutOfRangeException( string.Format(
                        "Invalid value '{0}' specified; allowed range [0..{1}]",
                        aValue,
                        0xFFFF ));

                // Compute little endian 2 byte format
                hex =
                    HEX_X2_TABLE[aValue & 0xFF] +
                    HEX_X2_TABLE[(aValue & 0xFF00) >> 8];
                break;

            case FormatHexStyles.LittleEndian4Bytes:
                // Check upper range
                if( aValue > 0x7FFFFFFF )
                    throw new ArgumentOutOfRangeException( string.Format(
                        "Invalid value '{0}' specified; allowed range [0..{1}]",
                        aValue,
                        0x7FFFFFFF ));

                // Compute little endian 4 byte format
                hex =
                    HEX_X2_TABLE[aValue & 0xFF] +
                    HEX_X2_TABLE[(aValue & 0xFF00) >> 8] +
                    HEX_X2_TABLE[(aValue & 0xFF0000) >> 16] +
                    HEX_X2_TABLE[(aValue & 0x7F000000) >> 24];
                break;

            case FormatHexStyles.BigEndian2Bytes:
                // Check upper range
                if( aValue > 0xFFFF )
                    throw new ArgumentOutOfRangeException( string.Format(
                        "Invalid value '{0}' specified; allowed range [0..{1}]",
                        aValue,
                        0xFFFF ));

                // Compute big endian 2 byte format
                hex =
                    HEX_X2_TABLE[(aValue & 0xFF00) >> 8] +
                    HEX_X2_TABLE[aValue & 0xFF];
                break;

            case FormatHexStyles.BigEndian4Bytes:
                // Check upper range
                if( aValue > 0x7FFFFFFF )
                    throw new ArgumentOutOfRangeException( string.Format(
                        "Invalid value '{0}' specified; allowed range [0..{1}]",
                        aValue,
                        0x7FFFFFFF ));

                // Compute big endian 4 byte format
                hex =
                    HEX_X2_TABLE[(aValue & 0x7F000000) >> 24] +
                    HEX_X2_TABLE[(aValue & 0xFF0000) >> 16] +
                    HEX_X2_TABLE[(aValue & 0xFF00) >> 8] +
                    HEX_X2_TABLE[aValue & 0xFF];
                break;

            default:
                throw new InvalidOperationException( string.Format(
                    "Invalid FormatHexStyles encountered; {0} not supported",
                    aHexFormat ));
        }

        // Return converted hex
        return hex;
    }

    public static Size CalcSize( Image anImage, Size aNewSize )
    {
        // Check input
        if( anImage == null )
            return Size.Empty;

        // Declar variables
        Size size;

        // Determine resize with or without aspect
        if( aNewSize.Width <= 0 &&
            aNewSize.Height <= 0 )
        {
            // Keep original size
            size = new Size( anImage.Width, anImage.Height );
        }
        else if(
            aNewSize.Width > 0 &&
            aNewSize.Height > 0 )
        {
            // Resize without aspect
            size = aNewSize;
        }
        else if( aNewSize.Height <= 0 )
        {
            // Compute new height with aspect
            float f = (float)aNewSize.Width / (float)anImage.Width;
            int   h = (int)((float)anImage.Height * f);
            size = new Size(  aNewSize.Width, h );
        }
        else
        {
            // Compute new with with aspect
            float f = (float)aNewSize.Height / (float)anImage.Height;
            int   w = (int)((float)anImage.Width * f);
            size = new Size( w, aNewSize.Height );
        }

        // Return computed size
        return size;
    }

    public static string ConvertToRtf( Bitmap anImage, Color anImageBackcolor )
    {
        return ConvertToRtf(
            anImage,
            anImageBackcolor,
            anImage.Width,
            anImage.Height,
            DEFAULT_DPIX,
            DEFAULT_DPIY );
    }

    public static string ConvertToRtf(
        Bitmap anImage,
        Color aBackgroundColor,
        int aWidth,
        int aHeight,
        float aDpiX,
        float aDpiY )
    {
        // Check input
        if( anImage == null )
            return "<No image available>";

        // Declare variables
        int   intWidth  = anImage.Width;
        int   intHeight = anImage.Height;
        float w         = intWidth;
        float h         = intHeight;
        float hmm       = HMM_PER_INCH;
        float twip      = TWIPS_PER_INCH;

        // Calculate effective image dimensions (in 0.01mm)
        int picw        = (int)Math.Round((w/aDpiX)*hmm);
        int pich        = (int)Math.Round((h/aDpiY)*hmm);

        // Compute new desired size of image
        SizeF size      = CalcSize(anImage, new Size(aWidth,aHeight));

        // Calculate requested image dimensions (in twips)
        int picwgoal    = (int)Math.Round((size.Width/aDpiX)*twip);
        int pichgoal    = (int)Math.Round((size.Height/aDpiY)*twip);

        // Calculate bytesize (DWORD aligned)
        int sizeA       = ((RoundTo4ByteBoundary(intWidth*3)*intHeight)/2)+56;
        int sizeB       = sizeA - 0x16;
        int sizeC       = RoundTo4ByteBoundary(intWidth*3)*intHeight;

        // Declare RTF masks
        string maskRtfPicHeader = @"\pict\wmetafile8\picw{0}\pich{1}\picwgoal{2}\pichgoal{3} ";
        string maskRtfWmfHeader = @"010009000003{0}0000{1}0000050000000b0200000000050000000c02{2}{3}{1}430f2000cc000000{4}{5}00000000{2}{3}00000000";
        string maskRtfBmpHeader = @"28000000{0}{1}0100180000000000{2}c40e0000c40e00000000000000000000";
        string maskRtf          = @"{0}{1}{2}{3}{4}030000000000{5}";

        // Create headers
        string rtfPicHeader     = string.Format(
            maskRtfPicHeader,
            picw,
            pich,
            picwgoal,
            pichgoal );
        string rtfWmfHeader     = string.Format(
            maskRtfWmfHeader,
            ConvertToHex( sizeA, FormatHexStyles.LittleEndian4Bytes ),
            ConvertToHex( sizeB, FormatHexStyles.LittleEndian4Bytes ),
            ConvertToHex( pich, FormatHexStyles.LittleEndian2Bytes ),
            ConvertToHex( picw, FormatHexStyles.LittleEndian2Bytes ),
            ConvertToHex( intHeight, FormatHexStyles.LittleEndian2Bytes ),
            ConvertToHex( intWidth, FormatHexStyles.LittleEndian2Bytes ));
        string rtfBmpHeader     = string.Format(
            maskRtfBmpHeader,
            ConvertToHex( intWidth, FormatHexStyles.LittleEndian4Bytes ),
            ConvertToHex( intHeight, FormatHexStyles.LittleEndian4Bytes ),
            ConvertToHex( sizeC, FormatHexStyles.LittleEndian4Bytes ));

        // Obtain bitmap data
        BitmapData bmpData = anImage.LockBits(
            new Rectangle(0, 0, intWidth, intHeight),
            ImageLockMode.ReadOnly,
            anImage.PixelFormat );

        // Declare an array to hold the pixels of the bitmap.
        uint[] pixelsARGB   = new uint[intWidth*intHeight];

        unsafe
        {
            // Read bitmap pixels
            switch( anImage.PixelFormat )
            {
                case PixelFormat.Format32bppArgb:
                case PixelFormat.Format32bppPArgb:
                case PixelFormat.Format32bppRgb:
                    // Simply copy ARGB bitmap data into buffer
                    uint* ptrARGB = (uint*)bmpData.Scan0;
                    for( int y = 0; y < intHeight; y++ )
                    {
                        int yOffset = y*intWidth;
                        for( int x = 0; x < intWidth; x++ )
                            pixelsARGB[yOffset+x] = ptrARGB[yOffset+x];
                    }

                    // Release lock
                    anImage.UnlockBits( bmpData );
                    break;

                case PixelFormat.Format8bppIndexed:
                    // Use palette to map ARGB values
                    byte*   ptrPalette      = (byte*)bmpData.Scan0;
                    uint[]  colorsPalette   = new uint[anImage.Palette.Entries.Length];
                    for( int col=0; col < anImage.Palette.Entries.Length; col++ )
                        colorsPalette[col] = (uint)anImage.Palette.Entries[col].ToArgb();

                    // Use color palette as source to create ARGB array
                    for( int y = 0; y < intHeight; y++ )
                    {
                        int yOffset = y*intWidth;
                        for( int x = 0; x < intWidth; x++ )
                            pixelsARGB[yOffset+x] = colorsPalette[ptrPalette[yOffset+x]];
                    }

                    // Release lock
                    anImage.UnlockBits( bmpData );
                    break;

                default:
                    // Release lock first
                    anImage.UnlockBits( bmpData );

                    // In this case we default to the slow GetPixel method.
                    for( int y = 0; y < intHeight; y++ )
                    {
                        int yOffset = y*intWidth;
                        for( int x = 0; x < intWidth; x++ )
                            pixelsARGB[yOffset+x] = (uint)anImage.GetPixel(x,y).ToArgb();
                    }
                    break;
            }
        }

        // Determine if we need to blend colors
        bool    doBlend = aBackgroundColor != Color.Empty;
        float   backR   = aBackgroundColor.R;
        float   backG   = aBackgroundColor.G;
        float   backB   = aBackgroundColor.B;

        // Compute stride (i.e. 4byte alignment)
        int alignRGB    = intWidth*3;
        int alignDWORD  = RoundTo4ByteBoundary(alignRGB);
        int alignStride = alignDWORD-alignRGB;

        // Check if stride is required
        bool    hasStride   = alignStride > 0;
        string  stride      = null;
        if( hasStride )
        {
            // Align on DWORD boundary
            StringBuilder sbStride = new StringBuilder();
            for( int i = 0; i < alignStride; i++ )
                sbStride.Append( HEX_X2_TABLE[0] );
            stride = sbStride.ToString();
        }

        // Create data buffer
        StringBuilder sbData = new StringBuilder();
        for( int y = intHeight-1; y >= 0; y-- )
        {
            // Iterate image from last row up to first row
            int yOffset = y*intWidth;

            // Iterate row from left pixel to right pixel
            for( int x = 0; x < intWidth; x++ )
            {
                // Get ARGB pixel values
                uint    pix = pixelsARGB[yOffset+x];
                float   dr  = (pix & 0x00FF0000) >> 16;
                float   dg  = (pix & 0x0000FF00) >> 8;
                float   db  = (pix & 0x000000FF);

                // Blend pixel with background?
                if( doBlend )
                {
                    // Get A value and normaize (i.e. compute range [0..1])
                    float t = ((float)((pix & 0xFF000000) >> 24))/255F;

                    // Convert RGB into delta's
                    dr  -= backR;
                    dg  -= backG;
                    db  -= backB;

                    // Mix backcolor with forecolor
                    dr  = (dr*t) + backR;
                    dg  = (dg*t) + backG;
                    db  = (db*t) + backB;
                }

                // Write hex RGB to buffer
                sbData.Append( HEX_X2_TABLE[(int)db] );
                sbData.Append( HEX_X2_TABLE[(int)dg] );
                sbData.Append( HEX_X2_TABLE[(int)dr] );
            }

            // Align on DWORD boundary
            if( hasStride )
                sbData.Append( stride );
        }

        // Flush stringbuilder
        string rtfBmpData = sbData.ToString();

        // Compose entire chunk
        string rtfChunk = string.Format(
            maskRtf,
            '{',
            rtfPicHeader,
            rtfWmfHeader,
            rtfBmpHeader,
            rtfBmpData,
            '}' );

        // Return result
        return rtfChunk;
    }
}
 
Share this answer
 
v2
Comments
jonney3099 14-Nov-19 6:54am    
Peter's code works seamlessly. thanks.

This content, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)



CodeProject, 20 Bay Street, 11th Floor Toronto, Ontario, Canada M5J 2N8 +1 (416) 849-8900