Supermini_man Posted March 19, 2011 Share Posted March 19, 2011 Thank you very much for this plugin! I use MyPaint a lot for sketching so this is real useful for me. Quote Artist/Game Developer/Traveller DeviantARTGame Jolt Profile Link to comment Share on other sites More sharing options...
Zagna Posted June 3, 2011 Author Share Posted June 3, 2011 (edited) 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 June 3, 2011 by Zagna Quote Link to comment Share on other sites More sharing options...
midora Posted March 1, 2014 Share Posted March 1, 2014 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. Quote Link to comment Share on other sites More sharing options...
Zagna Posted March 2, 2014 Author Share Posted March 2, 2014 I noticed that back with the first build 5034 and fixed it back then with System.IO.Compression.ZipArchive. http://forums.getpaint.net/index.php?/topic/27227-paintnet-40-alpha-build-5034/?p=399034 Quote Link to comment Share on other sites More sharing options...
midora Posted March 2, 2014 Share Posted March 2, 2014 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. Quote Link to comment Share on other sites More sharing options...
Zagna Posted March 2, 2014 Author Share Posted March 2, 2014 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 Quote Link to comment Share on other sites More sharing options...
midora Posted March 2, 2014 Share Posted March 2, 2014 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. Quote Link to comment Share on other sites More sharing options...
Zagna Posted March 2, 2014 Author Share Posted March 2, 2014 (edited) Muahahah my lovely exe 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 February 21, 2015 by Ego Eram Reputo Wrapped source in code in tags Quote Link to comment Share on other sites More sharing options...
Ego Eram Reputo Posted March 2, 2014 Share Posted March 2, 2014 Please add this updated attachment to the first post. Members will never find it otherwise Quote ebook: Mastering Paint.NET | resources: Plugin Index | Stereogram Tut | proud supporter of Codelab plugins: EER's Plugin Pack | Planetoid | StickMan | WhichSymbol+ | Dr Scott's Markup Renderer | CSV Filetype | dwarf horde plugins: Plugin Browser | ShapeMaker Link to comment Share on other sites More sharing options...
midora Posted March 2, 2014 Share Posted March 2, 2014 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). 1 Quote Link to comment Share on other sites More sharing options...
midora Posted April 10, 2015 Share Posted April 10, 2015 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. Quote Link to comment Share on other sites More sharing options...
Zagna Posted April 10, 2015 Author Share Posted April 10, 2015 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...? Quote Link to comment Share on other sites More sharing options...
midora Posted April 10, 2015 Share Posted April 10, 2015 (edited) 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 April 10, 2015 by midora Quote Link to comment Share on other sites More sharing options...
null54 Posted April 10, 2015 Share Posted April 10, 2015 It appears to be a bug in the .NET 4.5 ZipArchive class, the entry is still compressed when NoCompression is specified. Quote Plugin Pack | PSFilterPdn | Content Aware Fill | G'MIC | Paint 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 More sharing options...
Zagna Posted April 10, 2015 Author Share Posted April 10, 2015 (edited) I might have fixed it I found this by googling through stackoverflow 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 June 30, 2015 by Zagna Quote Link to comment Share on other sites More sharing options...
midora Posted April 10, 2015 Share Posted April 10, 2015 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 ;-) Quote Link to comment Share on other sites More sharing options...
Zagna Posted June 30, 2015 Author Share Posted June 30, 2015 Added mimetype checking, should work, tested it with broken files and such... Quote Link to comment Share on other sites More sharing options...
Zagna Posted January 25, 2016 Author Share Posted January 25, 2016 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() }; } } } Quote Link to comment Share on other sites More sharing options...
Nai Posted April 26, 2016 Share Posted April 26, 2016 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. Quote Link to comment Share on other sites More sharing options...
Zagna Posted May 9, 2016 Author Share Posted May 9, 2016 (edited) 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 May 9, 2016 by Zagna 2 Quote Link to comment Share on other sites More sharing options...
Reptillian Posted February 9, 2019 Share Posted February 9, 2019 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. Quote 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 More sharing options...
Zagna Posted February 9, 2019 Author Share Posted February 9, 2019 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. Quote Link to comment Share on other sites More sharing options...
Reptillian Posted February 9, 2019 Share Posted February 9, 2019 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? Quote 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 More sharing options...
Zagna Posted February 9, 2019 Author Share Posted February 9, 2019 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. Quote Link to comment Share on other sites More sharing options...
Reptillian Posted February 9, 2019 Share Posted February 9, 2019 (edited) 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 February 9, 2019 by Reptillian Quote 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 More sharing options...
Recommended Posts
Join the conversation
You can post now and register later. If you have an account, sign in now to post with your account.