///////////////////////////////////////////////////////////////////////////////// // Paint.NET // // Copyright (C) Rick Brewster, Tom Jackson, and past contributors. // // Portions Copyright (C) Microsoft Corporation. All Rights Reserved. // // See src/Resources/Files/License.txt for full licensing and attribution // // details. // // . // ///////////////////////////////////////////////////////////////////////////////// using PaintDotNet.Data.Quantize; using PaintDotNet.SystemLayer; using System; using System.Drawing; using System.Drawing.Imaging; using System.IO; using System.Reflection; using System.Runtime; using System.Runtime.Serialization; using System.Runtime.Serialization.Formatters.Soap; using System.Text; namespace PaintDotNet { /// /// Represents one type of file that PaintDotNet can load or save. /// public abstract class FileType { private string[] extensions; private string name; private bool supportsLayers; private bool supportsCustomHeaders; private bool supportsSaving; private bool supportsLoading; private bool savesWithProgress; // should be of the format ".ext" ... like ".bmp" or ".jpg" // The first extension in this list is the default extension (".jpg" for JPEG, // for instance, as ".jfif" etc. are not seen very often) public string[] Extensions { get { return (string[])this.extensions.Clone(); } } /// /// Gets the default extension for the FileType. /// /// /// This is always the first extension that is supported /// public string DefaultExtension { get { return this.extensions[0]; } } /// /// Returns the friendly name of the file type, such as "Bitmap" or "JPEG". /// public string Name { get { return this.name; } } /// /// Gets a flag indicating whether this FileType supports layers. /// /// /// If a FileType is asked to save a Document that has more than one layer, /// it will flatten it before it saves it. /// public bool SupportsLayers { get { return this.supportsLayers; } } /// /// Gets a flag indicating whether this FileType supports custom headers. /// /// /// If this returns false, then the Document's CustomHeaders will be discarded /// on saving. /// public bool SupportsCustomHeaders { get { return this.supportsCustomHeaders; } } /// /// Gets a flag indicating whether this FileType supports the Save() method. /// /// /// If this property returns false, calling Save() will throw a NotSupportedException. /// public bool SupportsSaving { get { return this.supportsSaving; } } /// /// Gets a flag indicating whether this FileType supports the Load() method. /// /// /// If this property returns false, calling Load() will throw a NotSupportedException. /// public bool SupportsLoading { get { return this.supportsLoading; } } /// /// Gets a flag indicating whether this FileType reports progress while saving. /// /// /// If false, then the callback delegate passed to Save() will be ignored. /// public bool SavesWithProgress { get { return this.savesWithProgress; } } public FileType(string name, bool supportsLayers, bool supportsCustomHeaders, string[] extensions) : this(name, supportsLayers, supportsCustomHeaders, true, true, false, extensions) { } public FileType(string name, bool supportsLayers, bool supportsCustomHeaders, bool supportsSaving, bool supportsLoading, bool savesWithProgress, string[] extensions) { this.name = name; this.supportsLayers = supportsLayers; this.supportsCustomHeaders = supportsCustomHeaders; this.supportsLoading = supportsLoading; this.supportsSaving = supportsSaving; this.savesWithProgress = savesWithProgress; this.extensions = extensions; } public bool SupportsExtension(string ext) { foreach (string ext2 in extensions) { if (0 == string.Compare(ext2, ext, StringComparison.InvariantCultureIgnoreCase)) { return true; } } return false; } /// /// Takes a Surface and quantizes it down to an 8-bit bitmap. /// /// The Surface to quantize. /// How strong should dithering be applied. 0 for no dithering, 8 for full dithering. /// The maximum number of colors to use. This may range from 2 to 255. /// The progress callback delegate. /// An 8-bit Bitmap that is the same size as quantizeMe. protected Bitmap Quantize(Surface quantizeMe, int ditherAmount, int maxColors, ProgressEventHandler progressCallback) { if (ditherAmount < 0 || ditherAmount > 8) { throw new ArgumentOutOfRangeException( "ditherAmount", ditherAmount, "Out of bounds. Must be in the range [0, 8]"); } if (maxColors < 2 || maxColors > 255) { throw new ArgumentOutOfRangeException( "maxColors", maxColors, "Out of bounds. Must be in the range [2, 255]"); } using (Bitmap bitmap = quantizeMe.CreateAliasedBitmap(quantizeMe.Bounds, true)) { OctreeQuantizer quantizer = new OctreeQuantizer(maxColors, 8); quantizer.DitherLevel = ditherAmount; Bitmap quantized = quantizer.Quantize(bitmap, progressCallback); return quantized; } } [Obsolete("Use the other Save() overload instead", true)] public void Save(Document input, Stream output, SaveConfigToken token, ProgressEventHandler callback, bool rememberToken) { using (Surface scratch = new Surface(input.Width, input.Height)) { Save(input, output, token, callback, rememberToken); } } public void Save( Document input, Stream output, SaveConfigToken token, Surface scratchSurface, ProgressEventHandler callback, bool rememberToken) { if (!this.SupportsSaving) { throw new NotImplementedException("Saving is not supported by this FileType"); } else { Surface disposeMe = null; if (scratchSurface == null) { disposeMe = new Surface(input.Size); scratchSurface = disposeMe; } else if (scratchSurface.Size != input.Size) { throw new ArgumentException("scratchSurface.Size must equal input.Size"); } if (rememberToken) { Type ourType = this.GetType(); string savedTokenName = ourType.Namespace + "." + ourType.Name; SoapFormatter soap = new SoapFormatter(); MemoryStream ms = new MemoryStream(); soap.Serialize(ms, token); byte[] bytes = ms.GetBuffer(); string utf8 = Encoding.UTF8.GetString(bytes); Settings.CurrentUser.SetString(savedTokenName, utf8); } if (!this.SavesWithProgress) { try { OnSave(input, output, token, scratchSurface, null); } catch (OnSaveNotImplementedException) { OldOnSaveTrampoline(input, output, token, null); } } else { try { OnSave(input, output, token, scratchSurface, callback); } catch (OnSaveNotImplementedException) { OldOnSaveTrampoline(input, output, token, callback); } } if (disposeMe != null) { disposeMe.Dispose(); disposeMe = null; } } } private sealed class OnSaveNotImplementedException : Exception { public OnSaveNotImplementedException(string message) : base(message) { } } /// /// Because the old OnSave() method is obsolete, we must use reflection to call it. /// This is important for legacy FileType plugins. It allows us to ensure that no /// new plugins can be compiled using the old OnSave() overload. /// private void OldOnSaveTrampoline(Document input, Stream output, SaveConfigToken token, ProgressEventHandler callback) { MethodInfo onSave = GetType().GetMethod( "OnSave", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.FlattenHierarchy, Type.DefaultBinder, new Type[] { typeof(Document), typeof(Stream), typeof(SaveConfigToken), typeof(ProgressEventHandler) }, null); onSave.Invoke( this, new object[] { input, output, token, callback }); } [Obsolete("Use the other OnSave() overload. It provides a scratch rendering surface that may enable your plugin to conserve memory usage.")] protected virtual void OnSave(Document input, Stream output, SaveConfigToken token, ProgressEventHandler callback) { } protected virtual void OnSave(Document input, Stream output, SaveConfigToken token, Surface scratchSurface, ProgressEventHandler callback) { throw new OnSaveNotImplementedException("Derived classes must implement this method. It is virtual instead of abstract in order to maintain compatibility with legacy plugins."); } /// /// Determines if saving with a given SaveConfigToken would alter the image /// in any way. Put another way, if the document is saved with these settings /// and then immediately loaded, would it have exactly the same pixel values? /// Any lossy codec should return 'false'. /// This value is used to optimizing preview rendering memory usage, and as such /// flattening should not be taken in to consideration. For example, the codec /// for PNG returns true, even though it flattens the image. /// /// The SaveConfigToken to determine reflexiveness for. /// true if the save would be reflexive, false if not /// If the SaveConfigToken is for another FileType, the result is undefined. public virtual bool IsReflexive(SaveConfigToken token) { return false; } public virtual SaveConfigWidget CreateSaveConfigWidget() { return new NoSaveConfigWidget(); } [Serializable] private sealed class NoSaveConfigToken : SaveConfigToken { } /// /// Gets a flag indicating whether or not the file type supports configuration /// via a SaveConfigToken and SaveConfigWidget. /// /// /// Implementers of FileType derived classes don't need to do anything special /// for this property to be accurate. If your FileType implements /// CreateDefaultSaveConfigToken, this will correctly return true. /// public bool SupportsConfiguration { get { SaveConfigToken token = CreateDefaultSaveConfigToken(); return !(token is NoSaveConfigToken); } } public SaveConfigToken GetLastSaveConfigToken() { Type ourType = this.GetType(); string savedTokenName = ourType.Namespace + "." + ourType.Name; string savedToken = Settings.CurrentUser.GetString(savedTokenName, null); SaveConfigToken saveConfigToken = null; if (savedToken != null) { try { SoapFormatter soap = new SoapFormatter(); byte[] bytes = Encoding.UTF8.GetBytes(savedToken); MemoryStream ms = new MemoryStream(bytes); SerializationFallbackBinder sfb = new SerializationFallbackBinder(); sfb.AddAssembly(this.GetType().Assembly); sfb.AddAssembly(typeof(FileType).Assembly); soap.Binder = sfb; object obj = soap.Deserialize(ms); ms.Close(); SaveConfigToken sct = new SaveConfigToken(); saveConfigToken = (SaveConfigToken)obj; } catch { // Ignore erros and revert to default saveConfigToken = null; } } if (saveConfigToken == null) { saveConfigToken = CreateDefaultSaveConfigToken(); } return saveConfigToken; } public SaveConfigToken CreateDefaultSaveConfigToken() { return OnCreateDefaultSaveConfigToken(); } /// /// Creates a SaveConfigToken for this FileType with the default values. /// protected virtual SaveConfigToken OnCreateDefaultSaveConfigToken() { return new NoSaveConfigToken(); } public Document Load(Stream input) { if (!this.SupportsLoading) { throw new NotSupportedException("Loading not supported for this FileType"); } else { return OnLoad(input); } } protected abstract Document OnLoad(Stream input); public override bool Equals(object obj) { if (obj == null || !(obj is FileType)) { return false; } return this.name.Equals(((FileType)obj).Name); } public override int GetHashCode() { return this.name.GetHashCode(); } /// /// Returns a string that can be used for populating a *FileDialog common dialog. /// public override string ToString() { StringBuilder sb = new StringBuilder(name); sb.Append(" ("); for (int i = 0; i < extensions.Length; ++i) { sb.Append("*"); sb.Append(extensions[i]); if (i != extensions.Length - 1) { sb.Append("; "); } else { sb.Append(")"); } } sb.Append("|"); for (int i = 0; i < extensions.Length; ++i) { sb.Append("*"); sb.Append(extensions[i]); if (i != extensions.Length - 1) { sb.Append(";"); } } return sb.ToString(); } } }