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