Jump to content
How to Install Plugins ×

OpenRaster Filetype


Zagna

Recommended Posts

  • 2 months later...

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

sig2024.jpg.4c3dd6a1ed919373afa78c73a19ed629.jpg

Link to comment
Share on other sites

  • 2 years later...

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.

midoras signature.gif

Link to comment
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.

midoras signature.gif

Link to comment
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

sig2024.jpg.4c3dd6a1ed919373afa78c73a19ed629.jpg

Link to comment
Share on other sites

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

Link to comment
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

midoras signature.gif

Link to comment
Share on other sites

  • 1 year later...

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.

midoras signature.gif

Link to comment
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...?

sig2024.jpg.4c3dd6a1ed919373afa78c73a19ed629.jpg

Link to comment
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

midoras signature.gif

Link to comment
Share on other sites

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

PdnSig.png

Plugin Pack | PSFilterPdn | Content Aware Fill | G'MICPaint Shop Pro Filetype | RAW Filetype | WebP Filetype

The small increase in performance you get coding in C++ over C# is hardly enough to offset the headache of coding in the C++ language. ~BoltBait

 

Link to comment
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

sig2024.jpg.4c3dd6a1ed919373afa78c73a19ed629.jpg

Link to comment
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 ;-)

midoras signature.gif

Link to comment
Share on other sites

  • 2 months later...
  • 6 months later...

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() };
			}
		}
	} 

 

sig2024.jpg.4c3dd6a1ed919373afa78c73a19ed629.jpg

Link to comment
Share on other sites

  • 3 months later...

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.

 

 

 

ba2ec8c.png

Link to comment
Share on other sites

  • 2 weeks later...

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

sig2024.jpg.4c3dd6a1ed919373afa78c73a19ed629.jpg

Link to comment
Share on other sites

  • 1 year later...

Is there a way to add a interface for changing blend modes compatibility? Right now, I'm interested in adding a checkmark tool to allow compatibility for Krita. The reason? It is the first notable program to support every PDN blend modes as of today and gmic existence.

G'MIC Filter Developer

 

I am away from this forum for undetermined amount of time: If you really need anything related to my PDN plugin or my G'MIC filter within G'MIC plugin, then you can contact me via Paint.NET discord, and mention me.

Link to comment
Share on other sites

33 minutes ago, Reptillian said:

Is there a way to add a interface for changing blend modes compatibility? Right now, I'm interested in adding a checkmark tool to allow compatibility for Krita. The reason? It is the first notable program to support every PDN blend modes as of today and gmic existence.

I'm looking at Krita's source but I can't find blend modes for Negation, Glow or Reflect? For other blend modes it should support the SVG definitions of them.

sig2024.jpg.4c3dd6a1ed919373afa78c73a19ed629.jpg

Link to comment
Share on other sites

1 minute ago, Zagna said:

I'm looking at Krita's source but I can't find blend modes for Negation, Glow or Reflect? For other blend modes it should support the SVG definitions of them.

That because it been commited yesterday and can be tested via Krita NEXT - https://phabricator.kde.org/D15584

 

So, should I export ora using Krita NEXT?

 

 

G'MIC Filter Developer

 

I am away from this forum for undetermined amount of time: If you really need anything related to my PDN plugin or my G'MIC filter within G'MIC plugin, then you can contact me via Paint.NET discord, and mention me.

Link to comment
Share on other sites

2 minutes ago, Reptillian said:

That because it been commited yesterday and can be tested via Krita NEXT - https://phabricator.kde.org/D15584

 

So, should I export ora using Krita NEXT?

Even in that patch, they haven't added the blend mode import/export to the .ora plugin?

SVG standard lacks filters for Negation/Blend/Glow so I've been using pdn-* definitions.

It all depends when and what names they choose to use for saving the blend mode names.

sig2024.jpg.4c3dd6a1ed919373afa78c73a19ed629.jpg

Link to comment
Share on other sites

Here's the name info inside stack.xml in .ora, and it's not they made the patch. I made the patch actually.

 

<image w="256" version="0.0.1" h="256" xres="300" yres="300">
 <stack visibility="visible" isolation="isolate" opacity="1" name="root" composite-op="svg:src-over">
  <layer src="data/layer4.png" visibility="visible" opacity="1" selected="true" name="Layer 4" composite-op="krita:xor"/>
  <layer src="data/layer3.png" visibility="visible" opacity="1" name="Layer 3" composite-op="krita:negation"/>
  <layer src="data/layer2.png" visibility="visible" opacity="1" name="Copy of Layer 2" composite-op="krita:reflect"/>
  <layer src="data/layer1.png" visibility="visible" opacity="1" name="Layer 2" composite-op="krita:glow"/>
  <layer src="data/layer0.png" visibility="visible" opacity="1" name="Layer 1" composite-op="svg:src-over"/>
 </stack>
</image>
Edited by Reptillian

G'MIC Filter Developer

 

I am away from this forum for undetermined amount of time: If you really need anything related to my PDN plugin or my G'MIC filter within G'MIC plugin, then you can contact me via Paint.NET discord, and mention 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.

×
×
  • Create New...