Jump to content

Reading and writing clipboard selection data


MJW
 Share

Recommended Posts

I wrote routines to parse the clipboard selection data. Perhaps others will find them useful.

 

ParseClipboardSelection takes a clipboard selection-data string and produces a jagged PointF array that represents the selection.

ToClipboardSelectionString takes a jagged PointF array and produces a string that can be written to the clipboard.

 

I'm sure it could be more compact if I'd used regular expressions or such. One might reasonably question my decision to write a number parsing routine (GetDouble) instead of just using the built-in Parse methods. My objective (reasonable or not) was to do the conversion in place without having to produce a bunch of substrings. I will say, it was a very simple routine until I realized I also had to handle number strings with the "E" exponent format.

 

I've tested the routines a fair amount. So far, so good. Please let me know of any problems.

 

Spoiler
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Text;

public static class ProcessClipboardSelection
{
    static ProcessClipboardSelection()
    {
    }

    static int GetDouble(string s, int end, int index, out double d)
    {
        char c;
        bool isNeg = false;
        bool haveDigit = false;
        bool havePoint = false;
        bool hasExponent = false;
        double pointScale = 1.0;

        d = 0.0;
        index = SkipWhiteSpace(s, end, index);
        if (index < 0)
            return -1;

        int i;
        for (i = index; i < end; i++)
        {
            c = s[i];
            if (char.IsDigit(c))
            {
                haveDigit = true;
                d = 10.0 * d + (c - '0');
                if (havePoint)
                    pointScale *= 10.0;
            }
            else if (c == '-')
            {
                if (i != index)	// Must be first character.
                    return -1;
                isNeg = true;
            }
            else if (c == '.')
            {
                if (havePoint || !haveDigit)
                    return -1;
                havePoint = true;
            }
            else if (c == 'E')
            {
                hasExponent = true;
                break;
            }
            else
            {
                break;
            }			
        }

        if (!haveDigit)
            return -1;

        if (isNeg)
            d = -d;

        // I could multiply pointScale by 0.1 in the loop, then multiply here instead
        // of divide, but this provides more accuracy, since 0.1 isn't exact in binary.
        if (havePoint)
            d /= pointScale;

        if (hasExponent)
        {
            bool isNegE = false;
            bool haveSignE = false;
            bool haveDigitE = false;
            int e = 0;
            for (i++ ; i < end; i++)
            {
                c = s[i];
                if (char.IsDigit(c))
                {
                    if (!haveSignE)	// Require a sign.
                        return -1;
                    haveDigitE = true;
                    e = 10 * e + (c - '0');
                }
                else if ((c == '+') || (c == '-'))
                {
                    if (haveDigitE || haveSignE)
                        return -1;
                    haveSignE = true;
                    if (c == '-')
                        isNegE = true;
                }
                else
                {
                    break;
                }
            }

            if (!haveDigitE)
                return -1;

            if (isNegE)
                e = -e;

            // I don't expect this to be needed often, so I'll do it the simple way.
            d *= Math.Pow(10, e);
        }

        return i;	// Index of terminating character.
    }
    static int FindChar(string s, char c, int end, int index)
    {
        for (int i = index; i < end; i++)
        {
            if (s[i] == c)
                return i;
        }

        return -1;
    }

    // Find the closing qutation marks.
    // Count the commas, and add one and sivide by two to get the point count.
    static int FindSpanEnd(string s, int end, int index, out int count)
    {
        count = 0;
        for (int i = index; i < end; i++)
        {
            if (s[i] == ',')
            {
                count++;
            }
            else if (s[i] == '"')
            {
                // The comma count must be odd and at least 5 for thee to be a polygon.
                if (((count & 1) == 0) || (count < 5))
                    return -1;

                count = (count + 1) / 2;   // return number of points.
                return i + 1;	// return index of next char.
            }
        }

        return -1;
    }


    static int SkipWhiteSpace(string s, int end, int index)
    {
        for (int i = index; i < end; i++)
        {
            if (!char.IsWhiteSpace(s[i]))
                return i;
        }

        return -1;
    }

    static int ConfirmNextNonWhiteChar(string s, char c, int end, int index)
    {
        int i = SkipWhiteSpace(s, end, index);
        if ((i < 0) || (s[i] != c))
            return -1;

        return i + 1; // return index of character following the one we were looking for.
    }

    public static PointF[][] ParseClipboardSelection(string selText)
    {
        int index;
        int selLength = selText.Length;

        // Confirm the basic format is correct.
        index = ConfirmNextNonWhiteChar(selText, '{', selLength, 0);
        if (index < 0)
            return null;

        index = SkipWhiteSpace(selText, selText.Length, index);
        if (index < 0)
            return null;

        if (string.Compare(selText, index, "\"polygonList\"", 0, 13) != 0)
            return null;

        index = ConfirmNextNonWhiteChar(selText, ':', selLength, index + 13);
        if (index < 0)
            return null;

        index = ConfirmNextNonWhiteChar(selText, '[', selLength, index);
        if (index < 0)
            return null;

        int selStart = index;

        index = FindChar(selText, ']', selLength, index);
        if (index < 0)
            return null;

        int selPolygonEnd = index - 1;

        index = ConfirmNextNonWhiteChar(selText, '}', selLength, index + 1);
        if (index < 0)
            return null;

        if (SkipWhiteSpace(selText, selLength, index) > 0)
            return null;

        // Find the individual polygons defined by the quotation marks.
        // Save the coordinate count for each polygon.
        // I scan through the polygons twice so I can allocate the right
        // sized arrays for the second time when I collect the coordinates.
        List<int> PolygonPointCount = new List<int>(256);
        index = selStart;
        while (true)
        {
            int count;

            // Find next starting quotaion mark.
            index = FindChar(selText, '"', selPolygonEnd, index);
            if (index < 0)
                return null;	// Either no poygons or bad syntax.
        
            // Find ending quotaion mark and get the number of x and y coordinates.
            index = FindSpanEnd(selText, selPolygonEnd, index + 1, out count);
            if (index < 0)
                return null;

            PolygonPointCount.Add(count);

            // See if next non-white char is a comma or if there is none.
            index = SkipWhiteSpace(selText, selPolygonEnd + 1, index);
            if (index < 0)
                break;	// Reached the end.

            if (selText[index] != ',')
                return null;
        }

        // Allocate the array of arrays to be returned.
        PointF[][] polygonArrays = new PointF[PolygonPointCount.Count][];
        for (int j = 0; j < PolygonPointCount.Count; j++)
        {
            polygonArrays[j] = new PointF[PolygonPointCount[j]];
        }

        index = selStart;
        int i = 0;
        while (true)
        {
            int pointCount = PolygonPointCount[i];
            PointF[] point = polygonArrays[i];

            // Find next starting quotaion mark.
            index = ConfirmNextNonWhiteChar(selText, '"', selPolygonEnd, index); ;
            if (index < 0)
                return null;

            // Get the x and y coordinates.
            int j = 0;
            while (true)
            {
                double d;
                index = GetDouble(selText, selPolygonEnd, index, out d);
                if (index < 0)
                    return null;
                point[j].X = (float)d;
                index = ConfirmNextNonWhiteChar(selText, ',', selPolygonEnd, index);
                if (index < 0)
                    return null;
                index = GetDouble(selText, selPolygonEnd, index, out d);
                if (index < 0)
                    return null;
                point[j].Y = (float)d;
                if (++j != pointCount)	// See if this is the last point.
                {
                    // Not the last point, so check for comma.
                    index = ConfirmNextNonWhiteChar(selText, ',', selPolygonEnd, index);
                    if (index < 0)
                        return null;
                }
                else
                {
                    // The last point. Check for closing quotation mark.
                    index = ConfirmNextNonWhiteChar(selText, '"', selPolygonEnd, index);
                    if (index < 0)
                        return null;

                    if (++i != PolygonPointCount.Count)	// See if this is the last polygon.
                    {
                        // Not the last polygon, so check for comma.
                        index = ConfirmNextNonWhiteChar(selText, ',', selPolygonEnd, index);
                        if (index < 0)
                            return null;
                        break;
                    }
                    else
                    {
                        // The last point of the last polygon, so check the end stuff.
                        // This is somewhat redundant, but it doesn't hurt to doublecheck the format.
                        index = ConfirmNextNonWhiteChar(selText, ']', selLength, index);
                        if (index < 0)
                            return null;
                        index = ConfirmNextNonWhiteChar(selText, '}', selLength, index);
                        if (index < 0)
                            return null;
                        if (SkipWhiteSpace(selText, selLength, index) > 0)
                            return null;

                        return polygonArrays;
                    }
                }
            }
        }		
    }

    public static string ToClipboardSelectionString(PointF[][] polygonArrays, int places = -1)
    {
        StringBuilder sb = new StringBuilder(1024);

        sb.Append("{\r\n   \"polygonList\": ["); 
        for (int i = 0; i < polygonArrays.Length; i++)
        {
            sb.Append("\r\n\"");
            PointF[] point = polygonArrays[i];
            int pairCount = point.Length;
            if (pairCount < 3)
                return null;
            for (int j = 0; j < pairCount; j++)
            {
                float cX = point[j].X, cY = point[j].Y;
                if (places >= 0)
                {
                    cX = (float)Math.Round(cX, places);
                    cY = (float)Math.Round(cY, places);
                }
                sb.Append(cX.ToString() + "," + cY.ToString());
                if (j != pairCount - 1)
                    sb.Append(",");
            }

            sb.Append("\"");
            if (i != polygonArrays.Length - 1)
                sb.Append(",");
        }
        sb.Append("\r\n   ]\r\n}\r\n");

        return sb.ToString();
    }
}

 

 

  • Like 1
Link to comment
Share on other sites

Wow -- you really shouldn't be parsing this yourself! Just use a JSON library :) And definitely don't parse numbers on your own, that's what double.Parse()/TryParse() is for.

 

This is from the PDN code, which uses Newtonsoft.Json. You should be able to convert it to System.Text.Json pretty easily. I may also be convinced to make this available for plugins starting in v4.4, as I'd really rather not have plugins doing this kind of thing on their own. Way too easy to get it wrong, or for the format to change, and cause crashes (plugins are also notoriously bad with error handling).

 

private const string polygonListID = "polygonList";

/**
    * The JSON format is as follows:
    * 
    *   {
    *     polygonList: [
    *       "...", // polygonList[0]
    *       "..."  // polygonList[1]
    *              // etc.
    *     ]
    *   }
    *   
    **/

private sealed class JsonSelectionReader
    : SelectionClipboardReaderImpl
{
    private IClipboardReader<string> textReader;

    public JsonSelectionReader(IClipboardTransaction transaction)
        : base(transaction)
    {
        this.textReader = transaction.CreateTextReader();
    }

    protected override bool OnIsDataMaybeAvailable()
    {
        if (this.textReader.IsDataMaybeAvailable())
        {
            string? text = this.textReader.MaybeGetData();
            if (text != null &&
                text.IndexOf(polygonListID, StringComparison.InvariantCulture) != -1)
            {
                return true;
            }
        }

        return false;
    }

    protected override IList<Point2Double[]>? OnMaybeGetDataT()
    {
        string? text = this.textReader.MaybeGetData();
        if (text != null)
        {
            return DeserializePolygonListFromJson(text);
        }
        else
        {
            return null;
        }
    }

    private static IList<Point2Double[]> DeserializePolygonListFromJson(string jsonText)
    {
        if (jsonText.IndexOf(polygonListID, StringComparison.InvariantCulture) == -1)
        {
            throw new FormatException($"'{polygonListID}' property was not found");
        }

        JObject jsonObject = JObject.Parse(jsonText);
        JArray polygonListArray = (JArray?)jsonObject[polygonListID]!;

        List<Point2Double[]> polygonList = new List<Point2Double[]>(polygonListArray.Count);
        for (int i = 0, count = polygonListArray.Count; i < count; ++i)
        {
            string elementText = (string)polygonListArray[i]!;
            Point2Double[] polygon = DeserializePolygonFromString(elementText);
            polygonList.Add(polygon);
        }

        return polygonList;
    }

    private static Point2Double[] DeserializePolygonFromString(string text)
    {
        string[] elementStrings = text.Split(',');
        if (elementStrings.Length % 2 != 0)
        {
            throw new FormatException(@"expected an even number of elements");
        }

        double[] elements = elementStrings
            .Select(e => double.Parse(e, CultureInfo.InvariantCulture))
            .ToArrayEx();

        Point2Double[] polygon = new Point2Double[elements.Length / 2];
        for (int i = 0; i < polygon.Length; ++i)
        {
            polygon[i] = new Point2Double(elements[i * 2], elements[(i * 2) + 1]);
        }

        return polygon;
    }
}

private sealed class JsonSelectionWriter
    : ClipboardWriterImpl<IList<Point2Double[]>>
{
    private IClipboardWriter<string> textWriter;

    public JsonSelectionWriter(IClipboardTransaction transaction)
        : base(transaction)
    {
        this.textWriter = transaction.CreateTextWriter();
    }

    protected override bool OnCanAddData(IList<Point2Double[]> data)
    {
        return true;
    }

    protected override void OnAddData(IList<Point2Double[]> data)
    {
        string polygonJson = SerializePolygonListToJson(data);
        this.textWriter.AddData(polygonJson);
    }

    private static string SerializePolygonListToJson(IList<Point2Double[]> polygonList)
    {
        IEnumerable<string> polygonStrings = polygonList.Select(poly => SerializePolygonToString(poly));

        JArray jsonArray = new JArray(polygonStrings.Select(ps => new JValue(ps)));
        JObject jsonObject = new JObject(new JProperty(polygonListID, jsonArray));

        return jsonObject.ToString();
    }

    private static string SerializePolygonToString(Point2Double[] polygon)
    {
        StringBuilder builder = new StringBuilder();

        bool isFirst = true;
        foreach (Point2Double point in polygon)
        {
            if (!isFirst)
            {
                builder.Append(",");
            }

            // re: "G17", https://docs.microsoft.com/en-us/dotnet/standard/base-types/standard-numeric-format-strings
            // "Note: Recommended for the BigInteger type only. For Double types, use "G17"; for Single types, use "G9"."
            string xString = point.X.ToString("G17", CultureInfo.InvariantCulture);
            string yString = point.Y.ToString("G17", CultureInfo.InvariantCulture);
            builder.Append(xString);
            builder.Append(",");
            builder.Append(yString);

            isFirst = false;
        }

        return builder.ToString();
    }
}

 

The Paint.NET Blog: https://blog.getpaint.net/

Donations are always appreciated! https://www.getpaint.net/donate.html

forumSig_bmwE60.jpg

Link to comment
Share on other sites

2 hours ago, Rick Brewster said:

Wow -- you really shouldn't be parsing this yourself! Just use a JSON library :) And definitely don't parse numbers on your own, that's what double.Parse()/TryParse() is for.

 

Except for my number-conversion routine, I don't see your code being that much simpler than mine. I like the fact that mine doesn't require any fancy-shmancy external parcing routine like JSON, though I can't say exactly why.  I explained why I chose to parse the numbers myself: I wanted to convert in place instead of first splitting the selection string into scads of substrings. You may consider that to be pointless (as it may well be), but I did mention my reason. In any event, foolish or not, it was a choice, not ignorance of the existence of the Parse methods, which I referred to. (I need to use foreach more often -- it does make things more compact and intuitive.  Most of my former high-level programming was in C, and I tend to fall back on old ways.)

 

Thanks to your posting the JSON version, users now have two choices. I'll continue to use mine, though I expect most everyone else will (no doubt wisely) choose yours.

 

I agree it would be very good for PDN to have built-in routines for converting the clipboard selection data. The possibility of a future change in the format did concern me.

Link to comment
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

 Share

×
×
  • Create New...