Sign in to follow this  
Zagna

OpenRaster Filetype

Recommended Posts

Updated the plugin with support for blend modes. I hope I did it right... the new attribute compositeop should be correct.

When saving a .ora file, it matches blend modes to composite ops from SVG 1.2 for all but reflect, glow and negation. I didn't know if SVG 1.2 actually had those blend modes so I didn't use them. While loading it tries to match all modes just in case something changes later so maybe.

Anyone can check the source link in the first post for comments

Edit: A newer version is there now... this time with composite-op attribute.... I'm not really sure which is correct, with or without '-'...

Edit2: Quick irc question and it is with '-' so it's best to update plugin install in case you dl'd the first version

Edited by Zagna

Share this post


Link to post
Share on other sites

Just a small hints.

 

The plugin is not working in PDN 4 because Paint.NET no longer installs ICSharpCode.SharpZipLib.dll. A temporary solution is to copy the dll from 3.5.11 to 4.0. You may implement this or use .NET 4.5 System,IO.Compression.ZipFile class.

Share this post


Link to post
Share on other sites

I downloaded a fresh version yesterday but maybe I called then an old version of the installer. I don't know. Anyway it's working for me now. Thanks.

Share this post


Link to post
Share on other sites

The installer in the original post still is the old 3.5 version.... I haven't updated it because 4.0 hasn't been released.

So I don't know what you did to get it to work :P

Share this post


Link to post
Share on other sites

I was confused because you pointed to a thread that tells that it's working now. So I thought that I missed the update. But the reason why it is working for me is still that I copied ICSharpCode.SharpZipLib.dll from PDN3.5.11 to PDN4.

 

That's why I don't like these exe solutions without any version number.

Share this post


Link to post
Share on other sites

Muahahah my lovely exe :P

So here's a new 4.0 dll then.

Hidden Content:
// Author of OraFormat.cs: 2010 Maia Kozheva

//

// Copyright (c) 2010 Maia Kozheva

//

// Permission is hereby granted, free of charge, to any person obtaining a copy

// of this software and associated documentation files (the "Software"), to deal

// in the Software without restriction, including without limitation the rights

// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell

// copies of the Software, and to permit persons to whom the Software is

// furnished to do so, subject to the following conditions:

//

// The above copyright notice and this permission notice shall be included in

// all copies or substantial portions of the Software.

//

// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR

// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,

// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE

// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER

// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,

// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN

// THE SOFTWARE.

//

// Paint.NET Plugin - (c) 2013 null54 & Zagna & shrike



using PaintDotNet;

using System;

using System.Drawing;

using System.Drawing.Imaging;

using System.Globalization;

using System.IO;

using System.Xml;

using System.IO.Compression;

using System.Collections.Generic;

using System.Linq;



namespace OpenRaster_Filetype

{

public class OraFileType : FileType

{

private const int ThumbMaxSize = 256;



private static Dictionary BlendDict = new Dictionary()

{

{ LayerBlendMode.Normal, "svg:src-over" },

{ LayerBlendMode.Multiply, "svg:multiply" },

{ LayerBlendMode.Additive, "svg:plus" },

{ LayerBlendMode.ColorBurn, "svg:color-burn" },

{ LayerBlendMode.ColorDodge, "svg:color-dodge" },

{ LayerBlendMode.Reflect, "pdn-reflect" },

{ LayerBlendMode.Glow, "pdn-glow" },

{ LayerBlendMode.Overlay, "svg:overlay" },

{ LayerBlendMode.Difference, "svg:difference" },

{ LayerBlendMode.Negation, "pdn-negation" },

{ LayerBlendMode.Lighten, "svg:lighten" },

{ LayerBlendMode.Darken, "svg:darken" },

{ LayerBlendMode.Screen, "svg:screen" },

{ LayerBlendMode.Xor, "svg:xor" }

};



private static Dictionary SVGDict = BlendDict.ToDictionary(x => x.Value, x => x.Key);



public OraFileType()

: base("OpenRaster", FileTypeFlags.SupportsLoading | FileTypeFlags.SupportsSaving | FileTypeFlags.SupportsLayers, new String[] { ".ora" })

{

strokeMapVersions = new string[2] { "mypaint_strokemap", "mypaint_strokemap_v2" };

SVGDict.Add("pinta-reflect", LayerBlendMode.Reflect);

SVGDict.Add("pinta-glow", LayerBlendMode.Glow);

SVGDict.Add("pinta-negation", LayerBlendMode.Negation);

}



///

/// Gets the bitmap from the ora layer.

///

///The x offset of the layer image.

///The y offset of the layer image.

///The input stream containing the layer image.

///The width of the base document.

///The height of the base document.

private unsafe Bitmap GetBitmapFromOraLayer(int xofs, int yofs, Stream inStream, int baseWidth, int baseHeight)

{

Bitmap image = null;



using (Bitmap layer = new Bitmap(baseWidth, baseHeight))

{

using (Bitmap bmp = new Bitmap(inStream))

{

BitmapData layerData = layer.LockBits(new Rectangle(xofs, yofs, bmp.Width, bmp.Height), ImageLockMode.WriteOnly, PixelFormat.Format32bppArgb);

BitmapData bmpData = bmp.LockBits(new Rectangle(0, 0, bmp.Width, bmp.Height), ImageLockMode.ReadOnly, bmp.PixelFormat);



int bpp = Bitmap.GetPixelFormatSize(bmp.PixelFormat) / 8;



for (int y = 0; y < bmp.Height; y++)

{

for (int x = 0; x < bmp.Width; x++)

{

byte* dst = (byte*)layerData.Scan0.ToPointer() + (y * layerData.Stride) + (x * 4);

byte* src = (byte*)bmpData.Scan0.ToPointer() + (y * bmpData.Stride) + (x * bpp);



dst[0] = src[0]; // B

dst[1] = src[1]; // G

dst[2] = src[2]; // R



if (bpp == 4)

{

dst[3] = src[3]; // A

}

else

{

dst[3] = 255;

}

}

}

bmp.UnlockBits(bmpData);

layer.UnlockBits(layerData);

}

image = (Bitmap)layer.Clone();

}

return image;

}



///

/// The formats for MyPaint's stroke map, version 1 and version 2.

///

private readonly string[] strokeMapVersions;



protected override Document OnLoad(Stream input)

{

using (ZipArchive file = new ZipArchive(input, ZipArchiveMode.Read))

{

XmlDocument stackXml = new XmlDocument();

stackXml.Load(file.GetEntry("stack.xml").Open());



XmlElement imageElement = stackXml.DocumentElement;

int width = int.Parse(imageElement.GetAttribute("w"), CultureInfo.InvariantCulture);

int height = int.Parse(imageElement.GetAttribute("h"), CultureInfo.InvariantCulture);



Document doc = new Document(width, height);



XmlElement stackElement = (XmlElement)stackXml.GetElementsByTagName("stack")[0];

XmlNodeList layerElements = stackElement.GetElementsByTagName("layer");



if (layerElements.Count == 0)

throw new FormatException("No layers found in OpenRaster file");

int layerCount = layerElements.Count - 1;



for (int i = layerCount; i >= 0; i--) // The last layer in the list is the background so load in reverse

{

XmlElement layerElement = (XmlElement)layerElements[i];

int x = int.Parse(GetAttribute(layerElement, "x", "0"), CultureInfo.InvariantCulture); // the x offset within the layer

int y = int.Parse(GetAttribute(layerElement, "y", "0"), CultureInfo.InvariantCulture); // the y offset within the layer



int layerNum = layerCount - i;



string name = GetAttribute(layerElement, "name", string.Format("Layer {0}", layerNum));



ZipArchiveEntry zf = file.GetEntry(layerElement.GetAttribute("src"));



using (Stream s = zf.Open())

{

using (Bitmap bmp = GetBitmapFromOraLayer(x, y, s, width, height))

{

BitmapLayer myLayer = null;

if (i == layerCount) // load the background layer first

{

myLayer = Layer.CreateBackgroundLayer(width, height);

}

else

{

myLayer = new BitmapLayer(width, height);

}



myLayer.Name = name;

myLayer.Opacity = ((byte)(255.0 * double.Parse(GetAttribute(layerElement, "opacity", "1"), CultureInfo.InvariantCulture)));

myLayer.Visible = (GetAttribute(layerElement, "visibility", "visible") == "visible"); // newer ora files have this

try

{

myLayer.BlendMode = SVGDict[GetAttribute(layerElement, "composite-op", "svg:src-over")];

}

catch (KeyNotFoundException e)

{

myLayer.BlendMode = LayerBlendMode.Normal;

}



myLayer.Surface.CopyFromGdipBitmap(bmp, false); // does this make sense?



string backTile = GetAttribute(layerElement, "background_tile", string.Empty);

if (!string.IsNullOrEmpty(backTile))

{

ZipArchiveEntry tileZf = file.GetEntry(backTile);

byte[] tileBytes = null;

using (Stream tileStream = tileZf.Open())

{

tileBytes = new byte[(int)tileStream.Length];



int numBytesToRead = (int)tileStream.Length;

int numBytesRead = 0;

while (numBytesToRead > 0)

{

// Read may return anything from 0 to numBytesToRead.

int n = tileStream.Read(tileBytes, numBytesRead, numBytesToRead);

// The end of the file is reached.

if (n == 0)

{

break;

}

numBytesRead += n;

numBytesToRead -= n;

}

}



string tileData = Convert.ToBase64String(tileBytes);

// convert the tile image to a Base64String and then save it in the layer's MetaData.

myLayer.Metadata.SetUserValue("OraBackgroundTile", tileData);

}



foreach (string version in strokeMapVersions)

{

string strokeMap = GetAttribute(layerElement, version, string.Empty);

if (!string.IsNullOrEmpty(strokeMap))

{

ZipArchiveEntry strokeZf = file.GetEntry(strokeMap);

byte[] strokeBytes = null;

using (Stream strokeStream = strokeZf.Open())

{

strokeBytes = new byte[(int)strokeStream.Length];



int numBytesToRead = (int)strokeStream.Length;

int numBytesRead = 0;

while (numBytesToRead > 0)

{

// Read may return anything from 0 to numBytesToRead.

int n = strokeStream.Read(strokeBytes, numBytesRead, numBytesToRead);

// The end of the file is reached.

if (n == 0)

{

break;

}

numBytesRead += n;

numBytesToRead -= n;

}

}

string strokeData = Convert.ToBase64String(strokeBytes);

// convert the stroke map to a Base64String and then save it in the layer's MetaData.



myLayer.Metadata.SetUserValue("OraMyPaintStrokeMapData", strokeData);



// Save the version of the stroke map in the MetaData

myLayer.Metadata.SetUserValue("OraMyPaintStrokeMapVersion", version);

}

}

doc.Layers.Insert(layerNum, myLayer);

}

}

}

return doc;

}

}



// A struct to store the new x,y offsets

private struct LayerInfo

{

public int x;

public int y;



public LayerInfo(int x, int y)

{

this.x = x;

this.y = y;

}

}



protected override void OnSave(Document input, Stream output, SaveConfigToken token, Surface scratchSurface, ProgressEventHandler callback)

{

using (ZipArchive archive = new ZipArchive(output, ZipArchiveMode.Create, true))

{

ZipArchiveEntry mimetype = archive.CreateEntry("mimetype", CompressionLevel.NoCompression);

byte[] databytes = System.Text.Encoding.ASCII.GetBytes("image/openraster");



using (Stream streamy = mimetype.Open())

{

streamy.Write(databytes, 0, databytes.Length);

}



//mimetype.Open().Write(databytes, 0, databytes.Length);



LayerInfo[] layerInfo = new LayerInfo[input.Layers.Count];

for (int i = 0; i < input.Layers.Count; i++)

{

BitmapLayer layer = (BitmapLayer)input.Layers[i];

Rectangle bounds = layer.Surface.Bounds;



int left = layer.Width;

int top = layer.Height;

int right = 0;

int bottom = 0;

unsafe

{

for (int y = 0; y < layer.Height; y++)

{

ColorBgra* row = layer.Surface.GetRowAddress(y);

ColorBgra* pixel = row;



for (int x = 0; x < layer.Width; x++)

{

if (pixel->A > 0)

{

if (x < left)

{

left = x;

}

if (x > right)

{

right = x;

}

if (y < top)

{

top = y;

}

if (y > bottom)

{

bottom = y;

}

}

pixel++;

}

}

}



if (left < layer.Width && top < layer.Height) // is the layer not empty

{

bounds = new Rectangle(left, top, (right - left) + 1, (bottom - top) + 1); // clip it to the visible rectangle

layerInfo[i] = new LayerInfo(left, top);

}

else

{

layerInfo[i] = new LayerInfo(0, 0);

}



string tileData = layer.Metadata.GetUserValue("OraBackgroundTile");

if (!string.IsNullOrEmpty(tileData)) // save the background_tile png if it exists

{

ZipArchiveEntry bgtile = archive.CreateEntry("data/background_tile.png");



using (Stream streamy = bgtile.Open())

{

byte[] tileBytes = Convert.FromBase64String(tileData);

streamy.Write(tileBytes, 0, tileBytes.Length);

}

}



string strokeData = layer.Metadata.GetUserValue("OraMyPaintStrokeMapData");

if (!string.IsNullOrEmpty(strokeData)) // save MyPaint's stroke data if it exists

{

ZipArchiveEntry strokemap = archive.CreateEntry("data/layer" + i.ToString(CultureInfo.InvariantCulture) + "_strokemap.dat");



using (Stream streamy = strokemap.Open())

{

byte[] tileBytes = Convert.FromBase64String(strokeData);

streamy.Write(tileBytes, 0, tileBytes.Length);

}

}



byte[] buf = null;

using (MemoryStream ms = new MemoryStream())

{

layer.Surface.CreateAliasedBitmap(bounds, true).Save(ms, ImageFormat.Png);

buf = ms.ToArray();

}

ZipArchiveEntry layerpng = archive.CreateEntry("data/layer" + i.ToString(CultureInfo.InvariantCulture) + ".png");



using (Stream streamy = layerpng.Open())

{

streamy.Write(buf, 0, buf.Length);

}

}



ZipArchiveEntry stackxml = archive.CreateEntry("stack.xml");



using (Stream streamy = stackxml.Open())

{

databytes = GetLayerXmlData(input.Layers, layerInfo);

streamy.Write(databytes, 0, databytes.Length);

}



using (Surface flat = new Surface(input.Width, input.Height))

{

input.Flatten(flat);

Size thumbSize = GetThumbDimensions(input.Width, input.Height);



Surface scale = new Surface(thumbSize);

scale.FitSurface(ResamplingAlgorithm.SuperSampling, flat);



using (MemoryStream ms = new MemoryStream())

{

scale.CreateAliasedBitmap().Save(ms, ImageFormat.Png);

databytes = ms.ToArray();

}

scale.Dispose();

}

ZipArchiveEntry thumbsy = archive.CreateEntry("Thumbnails/thumbnail.png");



using (Stream streamy = thumbsy.Open())

{

streamy.Write(databytes, 0, databytes.Length);

}

}

System.Diagnostics.Debug.WriteLine("All done here");

}



private byte[] GetLayerXmlData(LayerList layers, LayerInfo[] info) // OraFormat.cs - some changes

{

byte[] buf = null;



using (MemoryStream ms = new MemoryStream())

{

XmlTextWriter writer = new XmlTextWriter(ms, System.Text.Encoding.UTF8);

writer.Formatting = Formatting.Indented;

writer.WriteStartDocument();



writer.WriteStartElement("image");

writer.WriteAttributeString("w", layers.GetAt(0).Width.ToString(CultureInfo.InvariantCulture));

writer.WriteAttributeString("h", layers.GetAt(0).Height.ToString(CultureInfo.InvariantCulture));



writer.WriteStartElement("stack");



// ORA stores layers top to bottom

for (int i = layers.Count - 1; i >= 0; i--)

{

BitmapLayer layer = (BitmapLayer)layers[i];



writer.WriteStartElement("layer");



string backTile = layer.Metadata.GetUserValue("OraBackgroundTile");

if (!string.IsNullOrEmpty(backTile))

{

writer.WriteAttributeString("background_tile", "data/background_tile.png");

}

string strokeMapVersion = layer.Metadata.GetUserValue("OraMyPaintStrokeMapVersion");

if (!string.IsNullOrEmpty(strokeMapVersion))

{

writer.WriteAttributeString(strokeMapVersion, "data/layer" + i.ToString(CultureInfo.InvariantCulture) + "_strokemap.dat");

}

if (string.IsNullOrEmpty(strokeMapVersion)) // the stroke map layer does not have a name

{

writer.WriteAttributeString("name", layer.Name);

}



writer.WriteAttributeString("opacity", ((double)(layer.Opacity / 255.0)).Clamp(0.0, 1.0).ToString("N2", CultureInfo.InvariantCulture)); // this is even more bizarre 



writer.WriteAttributeString("src", "data/layer" + i.ToString(CultureInfo.InvariantCulture) + ".png");

writer.WriteAttributeString("visible", layer.Visible ? "visible" : "hidden");



writer.WriteAttributeString("x", info[i].x.ToString(CultureInfo.InvariantCulture));

writer.WriteAttributeString("y", info[i].y.ToString(CultureInfo.InvariantCulture));

writer.WriteAttributeString("composite-op", BlendDict[layer.BlendMode]);



writer.WriteEndElement();

}

writer.WriteEndElement(); // stack

writer.WriteEndElement(); // image

writer.WriteEndDocument();



writer.Close();



buf = ms.ToArray();

}

return buf;

}



private Size GetThumbDimensions(int width, int height) // OraFormat.cs

{

if (width <= ThumbMaxSize && height <= ThumbMaxSize)

return new Size(width, height);



if (width > height)

return new Size(ThumbMaxSize, (int)((double)height / width * ThumbMaxSize));

else

return new Size((int)((double)width / height * ThumbMaxSize), ThumbMaxSize);

}



private static string GetAttribute(XmlElement element, string attribute, string defValue) // OraFormat.cs

{

string ret = element.GetAttribute(attribute);

return string.IsNullOrEmpty(ret) ? defValue : ret;

}

}



public class MyFileTypeFactory : IFileTypeFactory

{

public FileType[] GetFileTypeInstances()

{

return new FileType[] { new OraFileType() };

}

}

}

 

Edited by Ego Eram Reputo
Wrapped source in code in tags

Share this post


Link to post
Share on other sites

Please add this updated attachment to the first post. Members will never find it otherwise ;)

 

If you are doing this then please keep the old one and mark it with 3.5.11. Because the new one is Paint.NET 4 only! (It requires .NET >= 4.0).

  • Upvote 1

Share this post


Link to post
Share on other sites

Hi Zagna,

 

http://freedesktop.org/wiki/Specifications/OpenRaster/Draft/FileLayout/

 

tells

 

The first file in the archive must be called "mimetype", without a file name extension. It must be STORED without compression. This file must contain the string "image/openraster", with no whitespace or trailing newline.

 

but the plugin stores this file with 'deflated' compression.

 

I detected this while testing the 'What's It?' plugin which detects well-formed ORAs.

In the moment the plugin identifies the ORAs created with this plugin just as ZIP-Container.

Share this post


Link to post
Share on other sites

Hmm... since I changed to .NET ZipArchive....

ZipArchiveEntry mimetype = archive.CreateEntry("mimetype", CompressionLevel.NoCompression);

So that's a bit different than what null54 has here earlier.... back then there was CompressionMethod.

Hmm... remove ZipArchive and...?

Share this post


Link to post
Share on other sites

At first glance this code line looks ok. But the generated zips are using deflate.

I'm using Ionic.Zip.

Maybe null54 has an idea.

 

I got ORAs from other sources where mimetype is not the first file (there are folder files in front).

The issue is that a typical loader will just ignore these restrictions in the specification because the loader will just ignore them.

 

I guess the reason for the restriction is to identify the file content fast using a forensic signature without unpacking the zip.

 

I will add these kind of .ORAs as ORA-NWF (not well-formed) for now to the WhatIsIt database. Better than just to tell that the file is a ZIP-Container.

Edited by midora

Share this post


Link to post
Share on other sites

I might have fixed it :D I found this by googling through stackoverflow :D

 

I made a .zip containing only the mimetype uncompressed. Fudged it in the source code via base64, opened the zip and skipped the mimetype bit entirely. It only adds layers and xml stuff and so on.

 

midora, rename that file to .ora and check if it is ok?

 

 

Edited by Zagna

Share this post


Link to post
Share on other sites

midora, rename that file to .ora and check if it is ok?

 

I do not have to rename it ;-) WhatIsIt tells me that this zip is an ORA and proposes to change the extension to .ora.

So this trick is fine to solve the issue. Thanks.

 

BTW, I had a quick look to the code you attached to the last mail.

I do not see a check in OnLoad that the file "mimetype" exists and has the right content.

I would propose to add this check and throw an exception in the error case.

I guess it is not necessary to check that it is the first file.

 

 

It appears to be a bug in the .NET 4.5 ZipArchive class, the entry is still compressed when NoCompression is specified.

 

That's bad. Especially in this case bacause packing increases the file size by 5 bytes ;-)

Share this post


Link to post
Share on other sites

Added mimetype checking, should work, tested it with broken files and such...

Hidden Content:
// Author of OraFormat.cs: 2010 Maia Kozheva <sikon@ubuntu.com>
//
// Copyright (c) 2010 Maia Kozheva <sikon@ubuntu.com>
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
// Paint.NET Plugin - (c) 2015 null54 & Zagna

using PaintDotNet;
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Imaging;
using System.Globalization;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Xml;

namespace OpenRaster_Filetype
	{
	public class OraFileType : FileType
		{
		private const int ThumbMaxSize = 256;

		private string mimetype_zip = "UEsDBBQAAAAAAAAAIQDHmvCMEAAAABAAAAAIAAAAbWltZXR5cGVpbWFnZS9vcGVucmFzdGVyUEsBAhQDFAAAAAAAAAAhAMea8IwQAAAAEAAAAAgAAAAAAAAAAAAAAKSBAAAAAG1pbWV0eXBlUEsFBgAAAAABAAEANgAAADYAAAAAAA==";

		private static Dictionary<LayerBlendMode, string> BlendDict = new Dictionary<LayerBlendMode, string>()
			{
			{ LayerBlendMode.Normal, "svg:src-over" },
			{ LayerBlendMode.Multiply, "svg:multiply" },
			{ LayerBlendMode.Additive, "svg:plus" },
			{ LayerBlendMode.ColorBurn, "svg:color-burn" },
			{ LayerBlendMode.ColorDodge, "svg:color-dodge" },
			{ LayerBlendMode.Reflect, "pdn-reflect" },
			{ LayerBlendMode.Glow, "pdn-glow" },
			{ LayerBlendMode.Overlay, "svg:overlay" },
			{ LayerBlendMode.Difference, "svg:difference" },
			{ LayerBlendMode.Negation, "pdn-negation" },
			{ LayerBlendMode.Lighten, "svg:lighten" },
			{ LayerBlendMode.Darken, "svg:darken" },
			{ LayerBlendMode.Screen, "svg:screen" },
			{ LayerBlendMode.Xor, "svg:xor" }
			};

		private static Dictionary<string, LayerBlendMode> SVGDict = BlendDict.ToDictionary(x => x.Value, x => x.Key);

		public OraFileType()
			: base("OpenRaster", FileTypeFlags.SupportsLoading | FileTypeFlags.SupportsSaving | FileTypeFlags.SupportsLayers, new String[] { ".ora" })
			{
			strokeMapVersions = new string[2] { "mypaint_strokemap", "mypaint_strokemap_v2" };
			}

		/// <summary>
		/// Gets the bitmap from the ora layer.
		/// </summary>
		/// <param name="xofs">The x offset of the layer image.</param>
		/// <param name="yofs">The y offset of the layer image.</param>
		/// <param name="inStream">The input stream containing the layer image.</param>
		/// <param name="baseWidth">The width of the base document.</param>
		/// <param name="baseHeight">The height of the base document.</param>
		private unsafe Bitmap GetBitmapFromOraLayer(int xofs, int yofs, Stream inStream, int baseWidth, int baseHeight)
			{
			Bitmap image = null;

			using (Bitmap layer = new Bitmap(baseWidth, baseHeight))
				{
				using (Bitmap bmp = new Bitmap(inStream))
					{
					BitmapData layerData = layer.LockBits(new Rectangle(xofs, yofs, bmp.Width, bmp.Height), ImageLockMode.WriteOnly, PixelFormat.Format32bppArgb);
					BitmapData bmpData = bmp.LockBits(new Rectangle(0, 0, bmp.Width, bmp.Height), ImageLockMode.ReadOnly, bmp.PixelFormat);

					int bpp = Bitmap.GetPixelFormatSize(bmp.PixelFormat) / 8;

					for (int y = 0; y < bmp.Height; y++)
						{
						for (int x = 0; x < bmp.Width; x++)
							{
							byte* dst = (byte*)layerData.Scan0.ToPointer() + (y * layerData.Stride) + (x * 4);
							byte* src = (byte*)bmpData.Scan0.ToPointer() + (y * bmpData.Stride) + (x * bpp);

							dst[0] = src[0]; // B
							dst[1] = src[1]; // G
							dst[2] = src[2]; // R

							if (bpp == 4)
								{
								dst[3] = src[3]; // A
								}
							else
								{
								dst[3] = 255;
								}
							}
						}
					bmp.UnlockBits(bmpData);
					layer.UnlockBits(layerData);
					}
				image = (Bitmap)layer.Clone();
				}
			return image;
			}

		/// <summary>
		/// The formats for MyPaint's stroke map, version 1 and version 2.
		/// </summary>
		private readonly string[] strokeMapVersions;

		protected override Document OnLoad(Stream input)
			{
			using (ZipArchive file = new ZipArchive(input, ZipArchiveMode.Read))
				{
				try
					{
					string mimetype;

					ZipArchiveEntry mime_entry = file.GetEntry("mimetype");
					using (StreamReader reader = new StreamReader(mime_entry.Open()))
						{
						mimetype = reader.ReadToEnd();
						}
					
					if (!mimetype.Equals("image/openraster", StringComparison.Ordinal))
						throw new FormatException("Incorrect mimetype: " + mimetype);
					}
				catch (NullReferenceException)
					{
					throw new FormatException("No mimetype found in OpenRaster file");
					}

				XmlDocument stackXml = new XmlDocument();
				stackXml.Load(file.GetEntry("stack.xml").Open());

				XmlElement imageElement = stackXml.DocumentElement;
				int width = int.Parse(imageElement.GetAttribute("w"), CultureInfo.InvariantCulture);
				int height = int.Parse(imageElement.GetAttribute("h"), CultureInfo.InvariantCulture);

				Document doc = new Document(width, height);

				XmlElement stackElement = (XmlElement)stackXml.GetElementsByTagName("stack")[0];
				XmlNodeList layerElements = stackElement.GetElementsByTagName("layer");

				if (layerElements.Count == 0)
					throw new FormatException("No layers found in OpenRaster file");
				int layerCount = layerElements.Count - 1;

				for (int i = layerCount; i >= 0; i--) // The last layer in the list is the background so load in reverse
					{
					XmlElement layerElement = (XmlElement)layerElements[i];
					int x = int.Parse(GetAttribute(layerElement, "x", "0"), CultureInfo.InvariantCulture); // the x offset within the layer
					int y = int.Parse(GetAttribute(layerElement, "y", "0"), CultureInfo.InvariantCulture); // the y offset within the layer

					int layerNum = layerCount - i;

					string name = GetAttribute(layerElement, "name", string.Format("Layer {0}", layerNum, CultureInfo.InvariantCulture));

					ZipArchiveEntry zf = file.GetEntry(layerElement.GetAttribute("src"));

					using (Stream s = zf.Open())
						{
						using (Bitmap bmp = GetBitmapFromOraLayer(x, y, s, width, height))
							{
							BitmapLayer myLayer = null;

							if (i == layerCount) // load the background layer first
								{
								myLayer = Layer.CreateBackgroundLayer(width, height);
								}
							else
								{
								myLayer = new BitmapLayer(width, height);
								}

							myLayer.Name = name;
							myLayer.Opacity = ((byte)(255.0 * double.Parse(GetAttribute(layerElement, "opacity", "1"), CultureInfo.InvariantCulture)));
							myLayer.Visible = (GetAttribute(layerElement, "visibility", "visible") == "visible"); // newer ora files have this

							try
								{
								myLayer.BlendMode = SVGDict[GetAttribute(layerElement, "composite-op", "svg:src-over")];
								}
							catch (KeyNotFoundException)
								{
								myLayer.BlendMode = LayerBlendMode.Normal;
								}

							myLayer.Surface.CopyFromGdipBitmap(bmp, false); // does this make sense?

							string backTile = GetAttribute(layerElement, "background_tile", string.Empty);

							if (!string.IsNullOrEmpty(backTile))
								{
								ZipArchiveEntry tileZf = file.GetEntry(backTile);
								byte[] tileBytes = null;
								using (Stream tileStream = tileZf.Open())
									{
									tileBytes = new byte[(int)tileStream.Length];

									int numBytesToRead = (int)tileStream.Length;
									int numBytesRead = 0;
									while (numBytesToRead > 0)
										{
										// Read may return anything from 0 to numBytesToRead.
										int n = tileStream.Read(tileBytes, numBytesRead, numBytesToRead);
										// The end of the file is reached.
										if (n == 0)
											{
											break;
											}
										numBytesRead += n;
										numBytesToRead -= n;
										}
									}

								string tileData = Convert.ToBase64String(tileBytes);
								// convert the tile image to a Base64String and then save it in the layer's MetaData.
								myLayer.Metadata.SetUserValue("OraBackgroundTile", tileData);
								}

							foreach (string version in strokeMapVersions)
								{
								string strokeMap = GetAttribute(layerElement, version, string.Empty);

								if (!string.IsNullOrEmpty(strokeMap))
									{
									ZipArchiveEntry strokeZf = file.GetEntry(strokeMap);
									byte[] strokeBytes = null;
									using (Stream strokeStream = strokeZf.Open())
										{
										strokeBytes = new byte[(int)strokeStream.Length];

										int numBytesToRead = (int)strokeStream.Length;
										int numBytesRead = 0;
										while (numBytesToRead > 0)
											{
											// Read may return anything from 0 to numBytesToRead.
											int n = strokeStream.Read(strokeBytes, numBytesRead, numBytesToRead);
											// The end of the file is reached.
											if (n == 0)
												{
												break;
												}
											numBytesRead += n;
											numBytesToRead -= n;
											}
										}
									string strokeData = Convert.ToBase64String(strokeBytes);
									// convert the stroke map to a Base64String and then save it in the layer's MetaData.

									myLayer.Metadata.SetUserValue("OraMyPaintStrokeMapData", strokeData);

									// Save the version of the stroke map in the MetaData
									myLayer.Metadata.SetUserValue("OraMyPaintStrokeMapVersion", version);
									}
								}
							doc.Layers.Insert(layerNum, myLayer);
							}
						}
					}
				return doc;
				}
			}

		// A struct to store the new x,y offsets
		private struct LayerInfo
			{
			public int x;
			public int y;

			public LayerInfo(int x, int y)
				{
				this.x = x;
				this.y = y;
				}
			}

		protected override void OnSave(Document input, Stream output, SaveConfigToken token, Surface scratchSurface, ProgressEventHandler callback)
			{
			byte[] databytes = Convert.FromBase64String(mimetype_zip);
			output.Write(databytes, 0, databytes.Length);

			using (ZipArchive archive = new ZipArchive(output, ZipArchiveMode.Update, true))
				{
				LayerInfo[] layerInfo = new LayerInfo[input.Layers.Count];

				for (int i = 0; i < input.Layers.Count; i++)
					{
					BitmapLayer layer = (BitmapLayer)input.Layers[i];
					Rectangle bounds = layer.Surface.Bounds;

					int left = layer.Width;
					int top = layer.Height;
					int right = 0;
					int bottom = 0;
					unsafe
						{
						for (int y = 0; y < layer.Height; y++)
							{
							ColorBgra* row = layer.Surface.GetRowAddress(y);
							ColorBgra* pixel = row;

							for (int x = 0; x < layer.Width; x++)
								{
								if (pixel->A > 0)
									{
									if (x < left)
										{
										left = x;
										}
									if (x > right)
										{
										right = x;
										}
									if (y < top)
										{
										top = y;
										}
									if (y > bottom)
										{
										bottom = y;
										}
									}
								pixel++;
								}
							}
						}

					if (left < layer.Width && top < layer.Height) // is the layer not empty
						{
						bounds = new Rectangle(left, top, (right - left) + 1, (bottom - top) + 1); // clip it to the visible rectangle
						layerInfo[i] = new LayerInfo(left, top);
						}
					else
						{
						layerInfo[i] = new LayerInfo(0, 0);
						}

					string tileData = layer.Metadata.GetUserValue("OraBackgroundTile");

					if (!string.IsNullOrEmpty(tileData)) // save the background_tile png if it exists
						{
						ZipArchiveEntry bgtile = archive.CreateEntry("data/background_tile.png");

						using (Stream streamy = bgtile.Open())
							{
							byte[] tileBytes = Convert.FromBase64String(tileData);
							streamy.Write(tileBytes, 0, tileBytes.Length);
							}
						}

					string strokeData = layer.Metadata.GetUserValue("OraMyPaintStrokeMapData");

					if (!string.IsNullOrEmpty(strokeData)) // save MyPaint's stroke data if it exists
						{
						ZipArchiveEntry strokemap = archive.CreateEntry("data/layer" + i.ToString(CultureInfo.InvariantCulture) + "_strokemap.dat");

						using (Stream streamy = strokemap.Open())
							{
							byte[] tileBytes = Convert.FromBase64String(strokeData);
							streamy.Write(tileBytes, 0, tileBytes.Length);
							}
						}

					byte[] buf = null;

					using (MemoryStream ms = new MemoryStream())
						{
						layer.Surface.CreateAliasedBitmap(bounds, true).Save(ms, ImageFormat.Png);
						buf = ms.ToArray();
						}

					ZipArchiveEntry layerpng = archive.CreateEntry("data/layer" + i.ToString(CultureInfo.InvariantCulture) + ".png");

					using (Stream streamy = layerpng.Open())
						{
						streamy.Write(buf, 0, buf.Length);
						}
					}

				ZipArchiveEntry stackxml = archive.CreateEntry("stack.xml");

				using (Stream streamy = stackxml.Open())
					{
					databytes = GetLayerXmlData(input.Layers, layerInfo);
					streamy.Write(databytes, 0, databytes.Length);
					}

				using (Surface flat = new Surface(input.Width, input.Height))
					{
					input.Flatten(flat);

					using (MemoryStream ms = new MemoryStream())
						{
						flat.CreateAliasedBitmap().Save(ms, ImageFormat.Png);
						databytes = ms.ToArray();
						}

					ZipArchiveEntry mergy = archive.CreateEntry("mergedimage.png");

					using (Stream streamy = mergy.Open())
						{
						streamy.Write(databytes, 0, databytes.Length);
						}

					Size thumbSize = GetThumbDimensions(input.Width, input.Height);

					Surface scale = new Surface(thumbSize);
					scale.FitSurface(ResamplingAlgorithm.SuperSampling, flat);

					using (MemoryStream ms = new MemoryStream())
						{
						scale.CreateAliasedBitmap().Save(ms, ImageFormat.Png);
						databytes = ms.ToArray();
						}
					scale.Dispose();

					ZipArchiveEntry thumbsy = archive.CreateEntry("Thumbnails/thumbnail.png");

					using (Stream streamy = thumbsy.Open())
						{
						streamy.Write(databytes, 0, databytes.Length);
						}
					}
				}

			System.Diagnostics.Debug.WriteLine("All done here");
			}

		private byte[] GetLayerXmlData(LayerList layers, LayerInfo[] info) // OraFormat.cs - some changes
			{
			byte[] buf = null;

			using (MemoryStream ms = new MemoryStream())
				{
				XmlTextWriter writer = new XmlTextWriter(ms, System.Text.Encoding.UTF8);
				writer.Formatting = Formatting.Indented;
				writer.WriteStartDocument();

				writer.WriteStartElement("image");
				writer.WriteAttributeString("w", layers.GetAt(0).Width.ToString(CultureInfo.InvariantCulture));
				writer.WriteAttributeString("h", layers.GetAt(0).Height.ToString(CultureInfo.InvariantCulture));

				writer.WriteStartElement("stack");

				// ORA stores layers top to bottom
				for (int i = layers.Count - 1; i >= 0; i--)
					{
					BitmapLayer layer = (BitmapLayer)layers[i];

					writer.WriteStartElement("layer");

					string backTile = layer.Metadata.GetUserValue("OraBackgroundTile");

					if (!string.IsNullOrEmpty(backTile))
						{
						writer.WriteAttributeString("background_tile", "data/background_tile.png");
						}
					string strokeMapVersion = layer.Metadata.GetUserValue("OraMyPaintStrokeMapVersion");

					if (!string.IsNullOrEmpty(strokeMapVersion))
						{
						writer.WriteAttributeString(strokeMapVersion, "data/layer" + i.ToString(CultureInfo.InvariantCulture) + "_strokemap.dat");
						}
					if (string.IsNullOrEmpty(strokeMapVersion)) // the stroke map layer does not have a name
						{
						writer.WriteAttributeString("name", layer.Name);
						}

					writer.WriteAttributeString("opacity", ((double)(layer.Opacity / 255.0)).Clamp(0.0, 1.0).ToString("N2", CultureInfo.InvariantCulture)); // this is even more bizarre 

					writer.WriteAttributeString("src", "data/layer" + i.ToString(CultureInfo.InvariantCulture) + ".png");
					writer.WriteAttributeString("visible", layer.Visible ? "visible" : "hidden");

					writer.WriteAttributeString("x", info[i].x.ToString(CultureInfo.InvariantCulture));
					writer.WriteAttributeString("y", info[i].y.ToString(CultureInfo.InvariantCulture));
					writer.WriteAttributeString("composite-op", BlendDict[layer.BlendMode]);

					writer.WriteEndElement();
					}

				writer.WriteEndElement(); // stack
				writer.WriteEndElement(); // image
				writer.WriteEndDocument();

				writer.Close();

				buf = ms.ToArray();
				}
			return buf;
			}

		private Size GetThumbDimensions(int width, int height) // OraFormat.cs
			{
			if (width <= ThumbMaxSize && height <= ThumbMaxSize)
				return new Size(width, height);

			if (width > height)
				return new Size(ThumbMaxSize, (int)((double)height / width * ThumbMaxSize));
			else
				return new Size((int)((double)width / height * ThumbMaxSize), ThumbMaxSize);
			}

		private static string GetAttribute(XmlElement element, string attribute, string defValue) // OraFormat.cs
			{
			string ret = element.GetAttribute(attribute);
			return string.IsNullOrEmpty(ret) ? defValue : ret;
			}
		}

	public class MyFileTypeFactory : IFileTypeFactory
		{
		public FileType[] GetFileTypeInstances()
			{
			return new FileType[] { new OraFileType() };
			}
		}
	} 

OraFileType.txt

Share this post


Link to post
Share on other sites

I found some changes in Pinta's plugin and did my best to bring them over. DPI should now be saved.

// Author of OraFormat.cs: 2010 Maia Kozheva <sikon@ubuntu.com>
//
// Copyright (c) 2010 Maia Kozheva <sikon@ubuntu.com>
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
// paint.net Plugin - (c) 2016 null54 & Zagna

using PaintDotNet;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Drawing.Imaging;
using System.Globalization;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Xml;

namespace OpenRasterFileType
	{
	public class OraFileType : FileType
		{
		private const int ThumbMaxSize = 256;

		private string MimeTypeZip = "UEsDBBQAAAAAAAAAIQDHmvCMEAAAABAAAAAIAAAAbWltZXR5cGVpbWFnZS9vcGVucmFzdGVyUEsBAhQDFAAAAAAAAAAhAMea8IwQAAAAEAAAAAgAAAAAAAAAAAAAAKSBAAAAAG1pbWV0eXBlUEsFBgAAAAABAAEANgAAADYAAAAAAA==";

		private static Dictionary<LayerBlendMode, string> BlendDict = new Dictionary<LayerBlendMode, string>()
			{
			{ LayerBlendMode.Normal, "svg:src-over" },
			{ LayerBlendMode.Multiply, "svg:multiply" },
			{ LayerBlendMode.Additive, "svg:plus" },
			{ LayerBlendMode.ColorBurn, "svg:color-burn" },
			{ LayerBlendMode.ColorDodge, "svg:color-dodge" },
			{ LayerBlendMode.Reflect, "pdn-reflect" },
			{ LayerBlendMode.Glow, "pdn-glow" },
			{ LayerBlendMode.Overlay, "svg:overlay" },
			{ LayerBlendMode.Difference, "svg:difference" },
			{ LayerBlendMode.Negation, "pdn-negation" },
			{ LayerBlendMode.Lighten, "svg:lighten" },
			{ LayerBlendMode.Darken, "svg:darken" },
			{ LayerBlendMode.Screen, "svg:screen" },
			{ LayerBlendMode.Xor, "svg:xor" }
			};

		private static Dictionary<string, LayerBlendMode> SVGDict = BlendDict.ToDictionary(x => x.Value, x => x.Key);

		public OraFileType()
			: base("OpenRaster", FileTypeFlags.SupportsLoading | FileTypeFlags.SupportsSaving | FileTypeFlags.SupportsLayers, new String[] { ".ora" })
			{
			StrokeMapVersions = new string[2] { "mypaint_strokemap", "mypaint_strokemap_v2" };
			}

		/// <summary>
		/// Gets the bitmap from the ora layer.
		/// </summary>
		/// <param name="xofs">The x offset of the layer image.</param>
		/// <param name="yofs">The y offset of the layer image.</param>
		/// <param name="inStream">The input stream containing the layer image.</param>
		/// <param name="baseWidth">The width of the base document.</param>
		/// <param name="baseHeight">The height of the base document.</param>
		private unsafe Bitmap GetBitmapFromOraLayer(int xofs, int yofs, Stream inStream, int baseWidth, int baseHeight)
			{
			Bitmap Image = null;

			using (Bitmap Layer = new Bitmap(baseWidth, baseHeight))
				{
				using (Bitmap BMP = new Bitmap(inStream))
					{
					BitmapData LayerData = Layer.LockBits(new Rectangle(xofs, yofs, BMP.Width, BMP.Height), ImageLockMode.WriteOnly, PixelFormat.Format32bppArgb);
					BitmapData BMPData = BMP.LockBits(new Rectangle(0, 0, BMP.Width, BMP.Height), ImageLockMode.ReadOnly, BMP.PixelFormat);

					int bpp = Bitmap.GetPixelFormatSize(BMP.PixelFormat) / 8;

					for (int y = 0; y < BMP.Height; y++)
						{
						for (int x = 0; x < BMP.Width; x++)
							{
							byte* dst = (byte*)LayerData.Scan0.ToPointer() + (y * LayerData.Stride) + (x * 4);
							byte* src = (byte*)BMPData.Scan0.ToPointer() + (y * BMPData.Stride) + (x * bpp);

							dst[0] = src[0]; // B
							dst[1] = src[1]; // G
							dst[2] = src[2]; // R

							if (bpp == 4)
								{
								dst[3] = src[3]; // A
								}
							else
								{
								dst[3] = 255;
								}
							}
						}
					BMP.UnlockBits(BMPData);
					Layer.UnlockBits(LayerData);
					}
				Image = (Bitmap)Layer.Clone();
				}
			return Image;
			}

		/// <summary>
		/// The formats for MyPaint's stroke map, version 1 and version 2.
		/// </summary>
		private readonly string[] StrokeMapVersions;

		protected override Document OnLoad(Stream input)
			{
			using (ZipArchive File = new ZipArchive(input, ZipArchiveMode.Read))
				{
				try
					{
					string MimeType;

					ZipArchiveEntry MimeEntry = File.GetEntry("mimetype");
					using (StreamReader Reader = new StreamReader(MimeEntry.Open()))
						{
						MimeType = Reader.ReadToEnd();
						}
					
					if (!MimeType.Equals("image/openraster", StringComparison.Ordinal))
						throw new FormatException("Incorrect mimetype: " + MimeType);
					}
				catch (NullReferenceException)
					{
					throw new FormatException("No mimetype found in OpenRaster file");
					}

				XmlDocument stackXml = new XmlDocument();
				stackXml.Load(File.GetEntry("stack.xml").Open());

				XmlElement ImageElement = stackXml.DocumentElement;
				int Width = int.Parse(ImageElement.GetAttribute("w"), CultureInfo.InvariantCulture);
				int Height = int.Parse(ImageElement.GetAttribute("h"), CultureInfo.InvariantCulture);

				Document doc = new Document(Width, Height);
				doc.DpuUnit = MeasurementUnit.Inch;
				doc.DpuX = double.Parse(GetAttribute(ImageElement, "xres", "72"), CultureInfo.InvariantCulture);
				doc.DpuY = double.Parse(GetAttribute(ImageElement, "yres", "72"), CultureInfo.InvariantCulture);

				XmlElement stackElement = (XmlElement)stackXml.GetElementsByTagName("stack")[0];
				XmlNodeList LayerElements = stackElement.GetElementsByTagName("layer");

				if (LayerElements.Count == 0)
					throw new FormatException("No layers found in OpenRaster file");
				int LayerCount = LayerElements.Count - 1;

				for (int i = LayerCount; i >= 0; i--) // The last layer in the list is the background so load in reverse
					{
					XmlElement LayerElement = (XmlElement)LayerElements[i];
					int x = int.Parse(GetAttribute(LayerElement, "x", "0"), CultureInfo.InvariantCulture); // the x offset within the layer
					int y = int.Parse(GetAttribute(LayerElement, "y", "0"), CultureInfo.InvariantCulture); // the y offset within the layer

					int LayerNum = LayerCount - i;

					string name = GetAttribute(LayerElement, "name", string.Format("Layer {0}", LayerNum, CultureInfo.InvariantCulture));

					ZipArchiveEntry zf = File.GetEntry(LayerElement.GetAttribute("src"));

					using (Stream s = zf.Open())
						{
						using (Bitmap BMP = GetBitmapFromOraLayer(x, y, s, Width, Height))
							{
							BitmapLayer myLayer = null;

							if (i == LayerCount) // load the background layer first
								{
								myLayer = Layer.CreateBackgroundLayer(Width, Height);
								}
							else
								{
								myLayer = new BitmapLayer(Width, Height);
								}

							myLayer.Name = name;
							myLayer.Opacity = ((byte)(255.0 * double.Parse(GetAttribute(LayerElement, "opacity", "1"), CultureInfo.InvariantCulture)));
							myLayer.Visible = (GetAttribute(LayerElement, "visibility", "visible") == "visible"); // newer ora files have this

							try
								{
								myLayer.BlendMode = SVGDict[GetAttribute(LayerElement, "composite-op", "svg:src-over")];
								}
							catch (KeyNotFoundException)
								{
								myLayer.BlendMode = LayerBlendMode.Normal;
								}

							myLayer.Surface.CopyFromGdipBitmap(BMP, false); // does this make sense?

							string backTile = GetAttribute(LayerElement, "background_tile", string.Empty);

							if (!string.IsNullOrEmpty(backTile))
								{
								ZipArchiveEntry tileZf = File.GetEntry(backTile);
								byte[] tileBytes = null;
								using (Stream tileStream = tileZf.Open())
									{
									tileBytes = new byte[(int)tileStream.Length];

									int numBytesToRead = (int)tileStream.Length;
									int numBytesRead = 0;
									while (numBytesToRead > 0)
										{
										// Read may return anything from 0 to numBytesToRead.
										int n = tileStream.Read(tileBytes, numBytesRead, numBytesToRead);
										// The end of the file is reached.
										if (n == 0)
											{
											break;
											}
										numBytesRead += n;
										numBytesToRead -= n;
										}
									}

								string tileData = Convert.ToBase64String(tileBytes);
								// convert the tile image to a Base64String and then save it in the layer's MetaData.
								myLayer.Metadata.SetUserValue("OraBackgroundTile", tileData);
								}

							foreach (string version in StrokeMapVersions)
								{
								string strokeMap = GetAttribute(LayerElement, version, string.Empty);

								if (!string.IsNullOrEmpty(strokeMap))
									{
									ZipArchiveEntry strokeZf = File.GetEntry(strokeMap);
									byte[] strokeBytes = null;
									using (Stream strokeStream = strokeZf.Open())
										{
										strokeBytes = new byte[(int)strokeStream.Length];

										int numBytesToRead = (int)strokeStream.Length;
										int numBytesRead = 0;
										while (numBytesToRead > 0)
											{
											// Read may return anything from 0 to numBytesToRead.
											int n = strokeStream.Read(strokeBytes, numBytesRead, numBytesToRead);
											// The end of the file is reached.
											if (n == 0)
												{
												break;
												}
											numBytesRead += n;
											numBytesToRead -= n;
											}
										}
									string strokeData = Convert.ToBase64String(strokeBytes);
									// convert the stroke map to a Base64String and then save it in the layer's MetaData.

									myLayer.Metadata.SetUserValue("OraMyPaintStrokeMapData", strokeData);

									// Save the version of the stroke map in the MetaData
									myLayer.Metadata.SetUserValue("OraMyPaintStrokeMapVersion", version);
									}
								}
							doc.Layers.Insert(LayerNum, myLayer);
							}
						}
					}
				return doc;
				}
			}

		// A struct to store the new x,y offsets
		private struct LayerInfo
			{
			public int x;
			public int y;

			public LayerInfo(int x, int y)
				{
				this.x = x;
				this.y = y;
				}
			}

		protected override void OnSave(Document input, Stream output, SaveConfigToken token, Surface scratchSurface, ProgressEventHandler callback)
			{
			byte[] DataBytes = Convert.FromBase64String(MimeTypeZip);
			output.Write(DataBytes, 0, DataBytes.Length);

			using (ZipArchive Archive = new ZipArchive(output, ZipArchiveMode.Update, true))
				{
				
				LayerInfo[] LayerInfo = new LayerInfo[input.Layers.Count];

				for (int i = 0; i < input.Layers.Count; i++)
					{
					BitmapLayer Layer = (BitmapLayer)input.Layers[i];
					Rectangle Bounds = Layer.Surface.Bounds;

					int Left = Layer.Width;
					int Top = Layer.Height;
					int Right = 0;
					int Bottom = 0;
					unsafe
						{
						for (int y = 0; y < Layer.Height; y++)
							{
							ColorBgra* row = Layer.Surface.GetRowAddress(y);
							ColorBgra* pixel = row;

							for (int x = 0; x < Layer.Width; x++)
								{
								if (pixel->A > 0)
									{
									if (x < Left)
										{
										Left = x;
										}
									if (x > Right)
										{
										Right = x;
										}
									if (y < Top)
										{
										Top = y;
										}
									if (y > Bottom)
										{
										Bottom = y;
										}
									}
								pixel++;
								}
							}
						}

					if (Left < Layer.Width && Top < Layer.Height) // is the layer not empty
						{
						Bounds = new Rectangle(Left, Top, (Right - Left) + 1, (Bottom - Top) + 1); // clip it to the visible rectangle
						LayerInfo[i] = new LayerInfo(Left, Top);
						}
					else
						{
						LayerInfo[i] = new LayerInfo(0, 0);
						}

					string tileData = Layer.Metadata.GetUserValue("OraBackgroundTile");

					if (!string.IsNullOrEmpty(tileData)) // save the background_tile png if it exists
						{
						ZipArchiveEntry bgTile = Archive.CreateEntry("data/background_tile.png");

						using (Stream Streamy = bgTile.Open())
							{
							byte[] tileBytes = Convert.FromBase64String(tileData);
							Streamy.Write(tileBytes, 0, tileBytes.Length);
							}
						}

					string strokeData = Layer.Metadata.GetUserValue("OraMyPaintStrokeMapData");

					if (!string.IsNullOrEmpty(strokeData)) // save MyPaint's stroke data if it exists
						{
						ZipArchiveEntry strokeMap = Archive.CreateEntry("data/layer" + i.ToString(CultureInfo.InvariantCulture) + "_strokemap.dat");

						using (Stream Streamy = strokeMap.Open())
							{
							byte[] tileBytes = Convert.FromBase64String(strokeData);
							Streamy.Write(tileBytes, 0, tileBytes.Length);
							}
						}

					byte[] buf = null;

					using (MemoryStream ms = new MemoryStream())
						{
						Layer.Surface.CreateAliasedBitmap(Bounds, true).Save(ms, ImageFormat.Png);
						buf = ms.ToArray();
						}

					ZipArchiveEntry layerpng = Archive.CreateEntry("data/layer" + i.ToString(CultureInfo.InvariantCulture) + ".png");

					using (Stream Streamy = layerpng.Open())
						{
						Streamy.Write(buf, 0, buf.Length);
						}
					}

				ZipArchiveEntry stackxml = Archive.CreateEntry("stack.xml");

				using (Stream Streamy = stackxml.Open())
					{
					double dpiX;
					double dpiY;

					switch (input.DpuUnit)
						{
						case MeasurementUnit.Centimeter:
							dpiX = Document.DotsPerCmToDotsPerInch(input.DpuX);
							dpiY = Document.DotsPerCmToDotsPerInch(input.DpuY);
							break;

						case MeasurementUnit.Inch:
							dpiX = input.DpuX;
							dpiY = input.DpuY;
							break;

						case MeasurementUnit.Pixel:
							dpiX = Document.GetDefaultDpu(MeasurementUnit.Inch);
							dpiY = Document.GetDefaultDpu(MeasurementUnit.Inch);
							break;

						default:
							throw new InvalidEnumArgumentException();
						}

					DataBytes = GetLayerXmlData(input.Layers, LayerInfo, dpiX, dpiY);
					Streamy.Write(DataBytes, 0, DataBytes.Length);
					}

				using (Surface Flat = new Surface(input.Width, input.Height))
					{
					input.Flatten(Flat);

					using (MemoryStream ms = new MemoryStream())
						{
						Flat.CreateAliasedBitmap().Save(ms, ImageFormat.Png);
						DataBytes = ms.ToArray();
						}

					ZipArchiveEntry Mergy = Archive.CreateEntry("mergedimage.png");

					using (Stream Streamy = Mergy.Open())
						{
						Streamy.Write(DataBytes, 0, DataBytes.Length);
						}

					Size thumbSize = GetThumbDimensions(input.Width, input.Height);

					Surface Scale = new Surface(thumbSize);
					Scale.FitSurface(ResamplingAlgorithm.SuperSampling, Flat);

					using (MemoryStream ms = new MemoryStream())
						{
						Scale.CreateAliasedBitmap().Save(ms, ImageFormat.Png);
						DataBytes = ms.ToArray();
						}
					Scale.Dispose();

					ZipArchiveEntry Thumbsy = Archive.CreateEntry("Thumbnails/thumbnail.png");

					using (Stream Streamy = Thumbsy.Open())
						{
						Streamy.Write(DataBytes, 0, DataBytes.Length);
						}
					}
				}

			System.Diagnostics.Debug.WriteLine("All done here");
			}

		private byte[] GetLayerXmlData(LayerList layers, LayerInfo[] info, double dpiX, double dpiY) // OraFormat.cs - some changes
			{
			byte[] buf = null;

			using (MemoryStream ms = new MemoryStream())
				{
				XmlWriterSettings Settings = new XmlWriterSettings();
				Settings.Indent = true;
				Settings.OmitXmlDeclaration = false;
				Settings.ConformanceLevel = ConformanceLevel.Document;
				Settings.CloseOutput = false;
				XmlWriter Writer = XmlWriter.Create(ms, Settings);
				
				Writer.WriteStartDocument();

				Writer.WriteStartElement("image");
				Writer.WriteAttributeString("w", layers.GetAt(0).Width.ToString(CultureInfo.InvariantCulture));
				Writer.WriteAttributeString("h", layers.GetAt(0).Height.ToString(CultureInfo.InvariantCulture));
				Writer.WriteAttributeString("version", "0.0.3"); // mandatory

				Writer.WriteAttributeString("xres", dpiX.ToString(CultureInfo.InvariantCulture));
				Writer.WriteAttributeString("yres", dpiY.ToString(CultureInfo.InvariantCulture));

				Writer.WriteStartElement("stack");
				Writer.WriteAttributeString("name", "root");

				// ORA stores layers top to bottom
				for (int i = layers.Count - 1; i >= 0; i--)
					{
					BitmapLayer layer = (BitmapLayer)layers[i];

					Writer.WriteStartElement("layer");

					string backTile = layer.Metadata.GetUserValue("OraBackgroundTile");

					if (!string.IsNullOrEmpty(backTile))
						{
						Writer.WriteAttributeString("background_tile", "data/background_tile.png");
						}
					string strokeMapVersion = layer.Metadata.GetUserValue("OraMyPaintStrokeMapVersion");

					if (!string.IsNullOrEmpty(strokeMapVersion))
						{
						Writer.WriteAttributeString(strokeMapVersion, "data/layer" + i.ToString(CultureInfo.InvariantCulture) + "_strokemap.dat");
						}
					if (string.IsNullOrEmpty(strokeMapVersion)) // the stroke map layer does not have a name
						{
						Writer.WriteAttributeString("name", layer.Name);
						}

					Writer.WriteAttributeString("opacity", (layer.Opacity / 255.0).Clamp(0.0, 1.0).ToString("N2", CultureInfo.InvariantCulture)); // this is even more bizarre 

					Writer.WriteAttributeString("src", "data/layer" + i.ToString(CultureInfo.InvariantCulture) + ".png");
					Writer.WriteAttributeString("visibility", layer.Visible ? "visible" : "hidden");

					Writer.WriteAttributeString("x", info[i].x.ToString(CultureInfo.InvariantCulture));
					Writer.WriteAttributeString("y", info[i].y.ToString(CultureInfo.InvariantCulture));
					try
						{
						Writer.WriteAttributeString("composite-op", BlendDict[layer.BlendMode]);
						}
					catch (KeyNotFoundException)
						{
						Writer.WriteAttributeString("composite-op", "svg:src-over");
						}

					Writer.WriteEndElement();
					}

				Writer.WriteEndElement(); // stack
				Writer.WriteEndElement(); // image
				Writer.WriteEndDocument();

				Writer.Close();

				buf = ms.ToArray();
				}
			return buf;
			}

		private Size GetThumbDimensions(int width, int height) // OraFormat.cs
			{
			if (width <= ThumbMaxSize && height <= ThumbMaxSize)
				return new Size(width, height);

			if (width > height)
				return new Size(ThumbMaxSize, (int)((double)height / width * ThumbMaxSize));
			else
				return new Size((int)((double)width / height * ThumbMaxSize), ThumbMaxSize);
			}

		private static string GetAttribute(XmlElement element, string attribute, string defValue) // OraFormat.cs
			{
			string ret = element.GetAttribute(attribute);
			return string.IsNullOrEmpty(ret) ? defValue : ret;
			}
		}

	public class MyFileTypeFactory : IFileTypeFactory
		{
		public FileType[] GetFileTypeInstances()
			{
			return new FileType[] { new OraFileType() };
			}
		}
	} 

OraFileType.txt

Edited by Zagna

Share this post


Link to post
Share on other sites

So far... Perfect !

 

My .ora line file that I made with Paint.NET is successfully open in both Krita and MyPaint :

 

The screenshot in Paint.NET :

 

https://i.imgsafe.org/19c78de.png

 

The screenshot in Krita :

 

https://i.imgsafe.org/2c1637b.png

 

The screenshot in MyPaint :

 

https://i.imgsafe.org/2b6dec7.png

 

 

All of it are too big for the displaying in the forum.

 

 

 

Share this post


Link to post
Share on other sites

Itsy bitsy tiny change.  :|  :|  :|

 

Apparently all this time.... visibility has been loaded just fine but not saved with the correct xml tag (was it like that earlier? 6 years ago?). Visible instead of visibility....

 

It's just been there because nobody complained and I didn't realise to check the specs close enough...

 

So new version with just the tiniest change of visible to visibility.

 

I found out about this because this one github project had a "paint.net workaround"...

Edited by Zagna
  • Upvote 2

Share this post


Link to post
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
Sign in to follow this