Working with CorelDRAW Palettes part 1, reading .pal files
I recently picked up a copy of CorelDRAW! 3.0 from eBay which
came on two CD's with a different version on each. That gave me
two different surprises, the first in that 3.0A wasn't an
improved version of 3.0, and secondly instead of the .cpl
format I was expecting to find, there were two different .pal
formats, one text based (for CorelDRAW!) and one binary (for
PHOTO-PAINT! (very shouty this software!)). This first article
covers reading the text based palette format.
Format
The palette format itself is simple enough, from the example
colours below we can infer that each colour entry is in CMYK
format with the range 0-100
. Although it looks as if it is a
fixed width format, when looking at other palettes using this
format this isn't the case and columns can be of differing
widths.
I'm not sure what limits there are on the number of colours, but one of the palettes I looked at had over 2000 colours so clearly not as limited as some older formats.
"Black" 0 0 0 100
"90% Black" 0 0 0 90
"White" 0 0 0 0
"Blue" 100 100 0 0
"Green" 100 0 100 0
"Red" 0 100 100 0
In order to have a common base to compare to, I recreated DawnBringer's 32 colour palette using the CorelDRAW application.
From experimenting with CorelDRAW, I also found that it won't allow swatch names to be longer than 29 characters.
Reading the palette
Reading the palette is straight forward enough, although one of
the palettes I tested did have a old school wild card. As this
is a plain text format with each colour on a single line, I'm
just going to use a StreamReader
to pull out each line.
public void Load(Stream stream)
{
_palette.Clear();
using (TextReader reader = new StreamReader(stream, Encoding.ASCII))
{
string line;
while ((line = reader.ReadLine()) != null && (line.Length == 0 || line[0] != (char)26))
{
if (!string.IsNullOrEmpty(line))
{
// Parse the color
}
}
}
}
End of file
In the above while loop, instead of just checking that the line
is not null
as I usually would, I also have an extra check to
see if the first character is (char)26
and if so, to cancel
loading. But what is this character?
This is the SUB
or substitute character and was
originally designed to be used in place of a character that is
invalid. Historical DOS systems and its predecessors used this
as an end of file character, which is how it has been used in
one of the palettes that shipped with this version of CorelDRAW.
An interesting facet of computing history I wasn't expecting to find!
Reading the swatch name
As swatch names start with a quote and end with a quote, and are
the only quoted string in the line we can find the end of the
name by using LastIndexOf
.
string name;
lastQuote = line.LastIndexOf('"');
name = line.Substring(1, lastQuote - 1);
As the .NET Color
structure doesn't allow the Name
property
to be set it isn't used by the demonstration, but is still
preserved for use with custom structures.
While testing I discovered that although CorelDRAW lets you enter quotes into the palette editor, it silently discarded any swatches with quotes in their name.
Reading the colour values
Although I don't go to crazy lengths, string parsing is typically quite expensive in terms of memory and/or processing power and so I often try and optimise this as I go.
In this case, while I could do string.Split
, I'd rather avoid
the array allocation so I'm manually looking for the number
sequences. As we already have the end of the swatch name from
the previous section, we can walk the rest of the string,
discarding the white-space, finding the longest sequence of
digits and then parsing out the result.
private byte NextNumber(string line, ref int start)
{
int length;
int valueLength;
int maxLength;
byte result;
// skip any leading spaces
while (char.IsWhiteSpace(line[start]))
{
start++;
}
length = line.Length;
maxLength = Math.Min(3, length - start);
valueLength = 0;
for (int i = 0; i < maxLength; i++)
{
if (char.IsDigit(line[start + i]))
{
valueLength++;
}
else
{
break;
}
}
result = byte.Parse(line.Substring(start, valueLength));
start += valueLength;
return result;
}
I start by iterating the string from an given start position and keep moving forward as long as the current character is a space.
Once I've ran out of spaces, I check how much of the string is left to determine the maximum number of characters to read - normally I want to read 3 as this is the maximum number of digits which can be present, however if it is at the end of the line and the final result isn't a 3 digit number there won't be enough data to read.
With the maximum number of digits established, I then check through these characters - if I find a digit, I increment a counter telling me how long the final number is. Anything else and I break out.
With both the start of the numeric field and its length I can
then use byte.Parse
on the substring.
It would be interesting to know if I could use the new
Span<T>
functionality in .NET Core to skip any string processing at all. An experiment for a future day!
The final action I perform is to record the new start position, so the next call to the method will continue from this point.
With this method in place, I can quite easily read out the four colour components without doing any array allocation.
int numberPosition;
byte cyan;
byte magenta;
byte yellow;
byte black;
numberPosition = lastQuote + 1;
cyan = this.NextNumber(line, ref numberPosition);
magenta = this.NextNumber(line, ref numberPosition);
yellow = this.NextNumber(line, ref numberPosition);
black = this.NextNumber(line, ref numberPosition);
Does it really matter?
Reading palettes is hardly going to be a performance critical
operation, so maybe you don't need to write any exotic code. If
you don't care about the allocations, you could write the value
extraction a lot simpler by using string.Split
. Note that even
here, I still want to avoid "silent" allocations and use a
prepared array with the split characters.
private readonly char[] _whitespace = { ' ' };
string[] parts;
parts = line.Substring(lastQuote + 1).Split(_whitespace, StringSplitOptions.RemoveEmptyEntries);
cyan = byte.Parse(parts[0]);
magenta = byte.Parse(parts[1]);
yellow = byte.Parse(parts[2]);
black = byte.Parse(parts[3]);
Personally, I prefer the former approach however I have to admit I didn't profile either approach as the article is overdue.
Converting CMYK to RGB
In absolutely no co-incidence at all, my previous article described how to convert between CMYK and RGB so I don't need to have a digression in this article.
private Color ConvertCmykToRgb(int c, int m, int y, int k)
{
int r;
int g;
int b;
float multiplier;
multiplier = 1 - k / 100F;
r = Convert.ToInt32(255 * (1 - c / 100F) * multiplier);
g = Convert.ToInt32(255 * (1 - m / 100F) * multiplier);
b = Convert.ToInt32(255 * (1 - y / 100F) * multiplier);
return Color.FromArgb(r, g, b);
}
With this helper in place and regardless of which method you used to get the individual C, M, Y and K components, we can now fully read CorelDRAW 3.0 text based palettes into .NET.
_palette.Add(this.ConvertCmykToRgb(cyan, magenta, yellow, black));
Sample application
As usual, an example project demonstrating how to read CorelDRAW
.pal
files is available from the link below.
Update History
- 2018-07-21 - First published
- 2020-11-22 - Updated formatting
Related articles you may be interested in
Downloads
Filename | Description | Version | Release Date | |
---|---|---|---|---|
CorelDrawPalPaletteLoader.zip
|
Sample project for the reading CorelDRAW Palettes Part 1, .pal files blog post |
21/07/2018 | Download |
Leave a Comment
While we appreciate comments from our users, please follow our posting guidelines. Have you tried the Cyotek Forums for support from Cyotek and the community?