diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 1dd9a8c..fd2827a 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -6,7 +6,7 @@ on: [ push, pull_request ] jobs: analyze: name: Analyze - runs-on: windows-2019 + runs-on: windows-2025 permissions: actions: read contents: read @@ -36,6 +36,6 @@ jobs: with: configuration: Debug build_options: '/p:UseSharedCompilation=false' - + - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v3 diff --git a/.github/workflows/dotnet-release.yml b/.github/workflows/dotnet-release.yml index 9088ff0..55bc7c6 100644 --- a/.github/workflows/dotnet-release.yml +++ b/.github/workflows/dotnet-release.yml @@ -9,7 +9,7 @@ on: jobs: portable-build: - runs-on: windows-2019 + runs-on: windows-2025 steps: - uses: actions/checkout@v4 - uses: ./.github/build @@ -19,7 +19,7 @@ jobs: artifact: release_artifact_portable portable-publish: - runs-on: windows-2019 + runs-on: windows-2025 needs: portable-build steps: - name: Collect artifact @@ -41,7 +41,7 @@ jobs: asset_content_type: application/zip installer: - runs-on: windows-2019 + runs-on: windows-2025 steps: - name: Checkout uses: actions/checkout@v4 diff --git a/.github/workflows/dotnet-testbuild.yml b/.github/workflows/dotnet-testbuild.yml index aa995b0..5c01ad3 100644 --- a/.github/workflows/dotnet-testbuild.yml +++ b/.github/workflows/dotnet-testbuild.yml @@ -6,7 +6,7 @@ on: [ push, pull_request ] jobs: build: - runs-on: windows-2019 + runs-on: windows-2025 strategy: matrix: flavor: [Installer, Portable] @@ -19,7 +19,7 @@ jobs: artifact: ${{ matrix.flavor == 'Portable' && 'PasteIntoFile_debug_portable' || '' }} installer: - runs-on: windows-2019 + runs-on: windows-2025 steps: - name: Checkout uses: actions/checkout@v4 @@ -39,10 +39,10 @@ jobs: candle releaseFiles.wxs candle PasteIntoFile.wxs light -b ../${{steps.build.outputs.path}} releaseFiles.wixobj PasteIntoFile.wixobj -ext WixNetFxExtension -out Installer.msi - + # test: -# runs-on: windows-2019 # For a list of available runner types, refer to https://help.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobsjob_idruns-on +# runs-on: windows-2025 # For a list of available runner types, refer to https://help.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobsjob_idruns-on # # steps: # - name: Checkout diff --git a/PasteIntoFile.sln b/PasteIntoFile.sln index 07216d8..0f8dc74 100644 --- a/PasteIntoFile.sln +++ b/PasteIntoFile.sln @@ -5,6 +5,8 @@ VisualStudioVersion = 16.0.28729.10 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PasteIntoFile", "PasteIntoFile\PasteIntoFile.csproj", "{F6F4215C-6CD7-4279-9C4C-C2DA9A823D0C}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WebP", "WebP\WebP.csproj", "{D0BAB316-8DF0-4955-8B5A-E4909402A906}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -15,6 +17,10 @@ Global {F6F4215C-6CD7-4279-9C4C-C2DA9A823D0C}.Debug|Any CPU.Build.0 = Debug|Any CPU {F6F4215C-6CD7-4279-9C4C-C2DA9A823D0C}.Release|Any CPU.ActiveCfg = Release|Any CPU {F6F4215C-6CD7-4279-9C4C-C2DA9A823D0C}.Release|Any CPU.Build.0 = Release|Any CPU + {D0BAB316-8DF0-4955-8B5A-E4909402A906}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D0BAB316-8DF0-4955-8B5A-E4909402A906}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D0BAB316-8DF0-4955-8B5A-E4909402A906}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D0BAB316-8DF0-4955-8B5A-E4909402A906}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/PasteIntoFile/ClipboardContents.cs b/PasteIntoFile/ClipboardContents.cs index 7d3b52a..a9763b3 100644 --- a/PasteIntoFile/ClipboardContents.cs +++ b/PasteIntoFile/ClipboardContents.cs @@ -8,6 +8,7 @@ using System.Linq; using System.Net; using System.Runtime.InteropServices; +using System.Runtime.Serialization; using System.Text; using System.Text.RegularExpressions; using System.Windows.Forms; @@ -15,6 +16,7 @@ using PasteIntoFile.Properties; using PdfSharp.Drawing; using PdfSharp.Pdf; +using WebP; namespace PasteIntoFile { @@ -49,6 +51,57 @@ public TKey KeyOf(TValue value) { } } + /// + /// Class holding the preview of the clipboard contents, and the preview type as an enum + /// + public class PreviewHolder { + public Image Image = null; + public string Text = null; + public string Html = null; + public string Rtf = null; + public string[] List = null; + + /// + /// A friendly description of the contents + /// + public string Description = Resources.str_preview; + + private PreviewHolder(string description) { + Description = description ?? Resources.str_preview; + } + public static PreviewHolder ForImage(Image image, string description = null) { + if (description == null) + description = string.Format(Resources.str_preview_image, image.Width, image.Height); + var p = new PreviewHolder(description); + p.Image = image; + return p; + } + public static PreviewHolder ForText(string text, string description = null) { + if (description == null) + description = string.Format(Resources.str_preview_text, text.Length, text.Split('\n').Length); + var p = new PreviewHolder(description); + p.Text = text; + return p; + } + public static PreviewHolder ForHtml(string html, string description) { + var p = new PreviewHolder(description); + p.Html = html; + return p; + } + public static PreviewHolder ForRtf(string rtf, string description = null) { + if (description == null) + description = Resources.str_preview_rtf; + var p = new PreviewHolder(description); + p.Rtf = rtf; + return p; + } + public static PreviewHolder ForList(string[] list, string description) { + var p = new PreviewHolder(description); + p.List = list; + return p; + } + } + /// /// This is the base class to hold clipboard contents, metadata, and perform actions with it @@ -62,9 +115,11 @@ public abstract class BaseContent { public abstract string[] Extensions { get; } /// - /// A friendly description of the contents + /// The preview of the contents /// - public abstract string Description { get; } + /// File extension determining the format + /// + public abstract PreviewHolder Preview(string extension); /// /// The actual data content @@ -113,32 +168,30 @@ public static string NormalizeExtension(string extension) { /// Holds image contents /// public abstract class ImageLikeContent : BaseContent { - /// - /// Convert the image to the format used for saving it, so it can be used for a preview - /// - /// File extension determining the format - /// Image in target format or null if no suitable format is found - public abstract Image ImagePreview(string extension); } public class ImageContent : ImageLikeContent { - public static readonly string[] EXTENSIONS = { "png", "bmp", "gif", "jpg", "pdf", "tif", "ico" }; + public static readonly string[] EXTENSIONS = { "png", "webp", "jpg", "bmp", "gif", "pdf", "tif", "ico" }; public ImageContent(Image image) { Data = image; } public Image Image => Data as Image; public override string[] Extensions => EXTENSIONS; - public override string Description => string.Format(Resources.str_preview_image, Image.Width, Image.Height); public override void SaveAs(string path, string extension, bool append = false) { if (append) throw new AppendNotSupportedException(); - Image image = ImagePreview(extension); + var image = Preview(extension).Image; if (image == null) throw new FormatException(string.Format(Resources.str_error_cliboard_format_missmatch, extension)); + switch (NormalizeExtension(extension)) { + case "webp": + var bytes = new WebPObject(image).GetWebPLossless(); + File.WriteAllBytes(path, bytes); + return; case "pdf": // convert image to ximage var stream = new MemoryStream(); @@ -174,19 +227,26 @@ public override void SaveAs(string path, string extension, bool append = false) /// /// File extension determining the format /// Image in target format or null if no suitable format is found - public override Image ImagePreview(string extension) { + public override PreviewHolder Preview(string extension) { extension = NormalizeExtension(extension); // Special formats with intermediate conversion types switch (extension) { - case "pdf": extension = "png"; break; - case "ico": return ImageAsIcon.ToBitmap(); + case "pdf": + // Use png as intermediate format + extension = "png"; + break; + case "webp": + // Lossless as-is + return PreviewHolder.ForImage(Image); + case "ico": + return PreviewHolder.ForImage(ImageAsIcon.ToBitmap()); } // Find suitable codec and convert image foreach (var encoder in ImageCodecInfo.GetImageEncoders()) { if (encoder.FilenameExtension.ToLower().Contains(extension)) { var stream = new MemoryStream(); Image.Save(stream, encoder, null); - return Image.FromStream(stream); + return PreviewHolder.ForImage(Image.FromStream(stream)); } } // TODO: Support conversion to EMF, WMF @@ -230,7 +290,7 @@ public Icon ImageAsIcon { /// Like ImageContent, but only for formats which support alpha channel /// public class TransparentImageContent : ImageContent { - public static new readonly string[] EXTENSIONS = { "png", "gif", "pdf", "tif", "ico" }; + public static new readonly string[] EXTENSIONS = { "png", "webp", "gif", "pdf", "tif", "ico" }; public TransparentImageContent(Image image) : base(image) { } public override string[] Extensions => EXTENSIONS; // Note: gif has only alpha 100% or 0% } @@ -239,6 +299,7 @@ public TransparentImageContent(Image image) : base(image) { } /// Like ImageContent, but only for formats which support animated frames /// public class AnimatedImageContent : ImageContent { + // TODO: in principle "webp" can also support animated frames, but the library we use doesn't support it public static new readonly string[] EXTENSIONS = { "gif" }; public AnimatedImageContent(Image image) : base(image) { } public override string[] Extensions => EXTENSIONS; @@ -255,7 +316,6 @@ public VectorImageContent(Metafile metafile) { } public Metafile Metafile => Data as Metafile; public override string[] Extensions => EXTENSIONS; - public override string Description => string.Format(Resources.str_preview_image_vector, Metafile.Width, Metafile.Height, Math.Round(Metafile.HorizontalResolution / 2 + Metafile.VerticalResolution / 2)); public override void SaveAs(string path, string extension, bool append = false) { if (append) @@ -283,13 +343,14 @@ public override void SaveAs(string path, string extension, bool append = false) /// /// File extension determining the format /// Image in target format or null if no suitable format is found - public override Image ImagePreview(string extension) { + public override PreviewHolder Preview(string extension) { switch (NormalizeExtension(extension)) { case "emf": - return Metafile; + var description = string.Format(Resources.str_preview_image_vector, Metafile.Width, Metafile.Height, Math.Round(Metafile.HorizontalResolution / 2 + Metafile.VerticalResolution / 2)); + return PreviewHolder.ForImage(Metafile, description); default: // fallback to save as raster image - return new ImageContent(Metafile).ImagePreview(extension); + return new ImageContent(Metafile).Preview(extension); } } public override void AddTo(IDataObject data) { @@ -318,7 +379,6 @@ public string Xml { } public override string[] Extensions => EXTENSIONS; - public override string Description => Resources.str_preview_svg; public override void SaveAs(string path, string extension, bool append = false) { if (append) @@ -334,14 +394,15 @@ public override void SaveAs(string path, string extension, bool append = false) public override void AddTo(IDataObject data) { data.SetData("image/svg+xml", Stream); } - public override string TextPreview(string extension) { - return Xml; + public override PreviewHolder Preview(string extension) { + return PreviewHolder.ForHtml(Xml, Resources.str_preview_svg); } } public abstract class TextLikeContent : BaseContent { + public static new readonly string[] CLIP_FORMATS = { DataFormats.UnicodeText, DataFormats.Text }; public TextLikeContent(string text) { Data = text; } @@ -360,25 +421,18 @@ protected static void Save(string path, string text, bool append = false) { public static string EnsureNewline(string text) { return text.TrimEnd('\n') + '\n'; } - - /// - /// Return a string used for preview - /// - /// - public abstract string TextPreview(string extension); } public class TextContent : TextLikeContent { public TextContent(string text) : base(text) { } public override string[] Extensions => new[] { "txt", "md", "log", "bat", "ps1", "java", "js", "cpp", "cs", "py", "css", "html", "php", "json", "csv" }; - public override string Description => string.Format(Resources.str_preview_text, Text.Length, Text.Split('\n').Length); public override void AddTo(IDataObject data) { data.SetData(DataFormats.Text, Text); data.SetData(DataFormats.UnicodeText, Text); } - public override string TextPreview(string extension) { - return Text; + public override PreviewHolder Preview(string extension) { + return PreviewHolder.ForText(Text); } } @@ -386,7 +440,6 @@ public override string TextPreview(string extension) { public class HtmlContent : TextLikeContent { public HtmlContent(string text) : base(text) { } public override string[] Extensions => new[] { "html", "htm", "xhtml" }; - public override string Description => Resources.str_preview_html; public override void SaveAs(string path, string extension, bool append = false) { var html = Text; if (!append && !html.StartsWith("")) @@ -409,8 +462,8 @@ public override void AddTo(IDataObject data) { data.SetData(DataFormats.Html, header + Text); } - public override string TextPreview(string extension) { - return Text; + public override PreviewHolder Preview(string extension) { + return PreviewHolder.ForHtml(Text, Resources.str_preview_html); } } @@ -418,7 +471,6 @@ public override string TextPreview(string extension) { public class CsvContent : TextLikeContent { public CsvContent(string text) : base(text) { } public override string[] Extensions => new[] { "csv", "tsv", "tab", "md" }; - public override string Description => Resources.str_preview_csv; public override void AddTo(IDataObject data) { data.SetData(DataFormats.CommaSeparatedValue, Text); } @@ -472,17 +524,65 @@ private string AsMarkdown() { return header + markdown; } - public override string TextPreview(string extension) { + public override PreviewHolder Preview(string extension) { switch (NormalizeExtension(extension)) { case "md": - return AsMarkdown(); + return PreviewHolder.ForText(AsMarkdown(), Resources.str_preview_csv); default: - return Text; + return PreviewHolder.ForText(Text, Resources.str_preview_csv); } } public override void SaveAs(string path, string extension, bool append = false) { - Save(path, TextPreview(extension), append); + Save(path, Preview(extension).Text, append); + } + } + + + public class CalendarContent : TextLikeContent { + public CalendarContent(string text) : base(text) { } + public static new readonly string[] CLIP_FORMATS = TextLikeContent.CLIP_FORMATS.Concat(new[] { "text/calendar", "application/ics" }).ToArray(); + public static new readonly string[] FILE_EXTENSIONS = { "ics" }; + public override string[] Extensions => FILE_EXTENSIONS; + public static bool IsValidCalendar(string text) { + return text.StartsWith("BEGIN:VCALENDAR"); + } + public Ical.Net.Calendar Calendar => Ical.Net.Calendar.Load(Text); + public override void AddTo(IDataObject data) { + foreach (var f in CLIP_FORMATS) { + data.SetData(f, Text); + } + } + + public override PreviewHolder Preview(string extension) { + switch (extension) { + case "ics": + try { + return PreviewHolder.ForHtml( + "\n\n\n\n\n\n" + + string.Join("\n", Calendar.Events.Select( + e => string.Format("

{0}
{1}

", e.Start, e.Summary) + )) + + "\n\n\n", + Resources.str_preview_calendar + ); + } catch (SerializationException e) { + return PreviewHolder.ForText(Text, Resources.str_preview_calendar); + } + default: + return PreviewHolder.ForText(Text, Resources.str_preview_calendar); + } + } + public override void SaveAs(string path, string extension, bool append = false) { + if (append) + throw new AppendNotSupportedException(); + Save(path, Text); + + } } @@ -494,12 +594,19 @@ public GenericTextContent(string format, string extension, string text) : base(t Extensions = new[] { extension }; } public override string[] Extensions { get; } - public override string Description => Resources.str_preview; public override void AddTo(IDataObject data) { data.SetData(_format, Text); } - public override string TextPreview(string extension) { - return Text; + public override PreviewHolder Preview(string extension) { + switch (extension) { + case "rtf": + return PreviewHolder.ForRtf(Text); + case "dif": + return PreviewHolder.ForText(Text, Resources.str_preview_dif); + default: + return PreviewHolder.ForText(Text); + } + } } @@ -508,7 +615,6 @@ public class UrlContent : TextLikeContent { public static readonly string[] EXTENSIONS = { "url" }; public UrlContent(string text) : base(text) { } public override string[] Extensions => EXTENSIONS; - public override string Description => Resources.str_preview_url; public override void SaveAs(string path, string extension, bool append = false) { if (append) throw new AppendNotSupportedException(); @@ -517,8 +623,8 @@ public override void SaveAs(string path, string extension, bool append = false) public override void AddTo(IDataObject data) { data.SetData(DataFormats.Text, Text); } - public override string TextPreview(string extension) { - return Text; + public override PreviewHolder Preview(string extension) { + return PreviewHolder.ForText(Text, Resources.str_preview_url); } } @@ -538,7 +644,6 @@ public List FileList { public string FileListString => string.Join("\n", FileList); public override string[] Extensions => new[] { "zip", "m3u", "files", "txt" }; - public override string Description => string.Format(Resources.str_preview_files, Files.Count); public override void SaveAs(string path, string extension, bool append = false) { switch (NormalizeExtension(extension)) { case "zip": @@ -567,12 +672,13 @@ public override void SaveAs(string path, string extension, bool append = false) /// File extension determining the format /// Preview as text string /// - public string TextPreview(string extension) { + public override PreviewHolder Preview(string extension) { + var description = string.Format(Resources.str_preview_files, Files.Count); switch (NormalizeExtension(extension)) { case "zip": - return null; + return PreviewHolder.ForList(FileList.ToArray(), description); default: - return FileListString; + return PreviewHolder.ForText(FileListString, description); } } @@ -657,93 +763,107 @@ public static ClipboardContents FromClipboard() { // // Note: if multiple clipboard contents support to same extension, the first in Contents is used +#if DEBUG + Console.WriteLine(">>> Clipboard contents as of " + container.Timestamp + " <<<"); + var table = new List(); + foreach (var format in Clipboard.GetDataObject().GetFormats(false)) { + var df = DataFormats.GetFormat(format); + table.Add( + df.Id.ToString().PadLeft(6) + " " + + (Enum.IsDefined(typeof(CF), (uint)df.Id) ? "CF_" + (CF)(uint)df.Id : "").PadRight(15) + " " + + format.PadRight(30) + ); + } + table.Sort(); + foreach (var row in table) + Console.WriteLine(row); + Console.WriteLine(""); +#endif + // Images // ====== - var images = new Dict(); - var extensions = new HashSet(new[] { - ImageContent.EXTENSIONS, - TransparentImageContent.EXTENSIONS, - AnimatedImageContent.EXTENSIONS, - VectorImageContent.EXTENSIONS, - }.SelectMany(i => i)); - - // Native clipboard bitmap image - if (Clipboard.GetData(DataFormats.Dib) is Image dib) // device independent bitmap - images.Add("bmp", dib); - else if (Clipboard.GetData(DataFormats.Bitmap) is Image bmp) // device specific bitmap - images.Add("bmp", bmp); - else if (Clipboard.GetImage() is Image converted) // anything converted to device specific bitmap - images.Add("bmp", converted); - - // Native clipboard tiff image - if (Clipboard.GetData(DataFormats.Tiff) is Image tif) - images.Add("tif", tif); + // Collect images (preferred first) + var images = new Dict(); - // Native clipboard metafile (emf or wmf) - if (ReadClipboardMetafile() is Metafile emf) - images.Add("emf", emf); + // Generic image from file + if (Clipboard.ContainsFileDropList() && Clipboard.GetFileDropList() is { Count: 1 } files) { + var ext = BaseContent.NormalizeExtension(Path.GetExtension(files[0]).Trim('.')); + if (ImageContentFromBytes(ext, File.ReadAllBytes(files[0])) is { } imageContent) + images.Add(ext, imageContent); + } // Mime and file extension formats - var formats = extensions.SelectMany(ext => MimeForExtension(ext).Concat(new[] { ext })); - foreach (var format in formats) { // case insensitive - if (Clipboard.ContainsData(format) && Clipboard.GetData(format) is MemoryStream stream) - if (Image.FromStream(stream) is Image img) - images.Add(format, img); + foreach (var types in IMAGE_MIME_TYPES) { + var ext = BaseContent.NormalizeExtension(types.Key); + if (images.ContainsKey(ext)) + continue; + foreach (var format in types.Value.Concat([ext])) { + if (!Clipboard.ContainsData(format) || Clipboard.GetData(format) is not MemoryStream stream) + continue; + if (ImageContentFromBytes(ext, stream.ToArray()) is { } imageContent) + images.Add(ext, imageContent); + } } - // Generic image from encoded data uri - if (Clipboard.ContainsText() && ImageFromDataUri(Clipboard.GetText()) is Image uriImage) - images.Add(uriImage.RawFormat.ToString().ToLower(), uriImage); + // Image from encoded data uri + if (Clipboard.ContainsText()) { + var (mime_ext, bytes) = BytesFromDataUri(Clipboard.GetText()); + if (bytes != null && !images.ContainsKey(mime_ext)) + if (ImageContentFromBytes(mime_ext, bytes) is { } imageContent) + images.Add(mime_ext, imageContent); + } - // Generic image from file - if (Clipboard.ContainsFileDropList() && Clipboard.GetFileDropList() is StringCollection files && files.Count == 1) { - try { - images.Add(Path.GetExtension(files[0]).Trim('.').ToLower(), Image.FromFile(files[0])); - } catch { /* format not supported */ } + // Native clipboard bitmap image + if (!images.ContainsKey("bmp")) { + if (Clipboard.GetData(DataFormats.Dib) is Image dib) // device independent bitmap + images.Add("bmp", new ImageContent(dib)); + else if (Clipboard.GetData(DataFormats.Bitmap) is Image bmp) // device specific bitmap + images.Add("bmp", new ImageContent(bmp)); + else if (Clipboard.GetImage() is Image converted) // anything converted to device specific bitmap + images.Add("bmp", new ImageContent(converted)); } + // Native clipboard tiff image + if (!images.ContainsKey("tif") && Clipboard.GetData(DataFormats.Tiff) is Image tif) + images.Add("tif", new ImageContent(tif)); + + // Native clipboard metafile (emf or wmf) + if (!images.ContainsKey("emf") && ReadClipboardMetafile() is Metafile emf) + images.Add("emf", new VectorImageContent(emf)); + + // Since images can have features (transparency, animations) which are not supported by all file format, // we handel images with such features separately (in order of priority): - var remainingExtensions = new HashSet(extensions); + var remainingExtensions = new HashSet(new[] { + ImageContent.EXTENSIONS, + TransparentImageContent.EXTENSIONS, + AnimatedImageContent.EXTENSIONS, + VectorImageContent.EXTENSIONS, + }.SelectMany(i => i)); ; // 0. Vector image (if any) - foreach (var (ext, img) in images.Items) { - if (img is Metafile mf) { - container.Contents.Add(new VectorImageContent(mf)); - remainingExtensions.ExceptWith(VectorImageContent.EXTENSIONS); - break; - } + if (images.Values.FirstOrDefault(content => content is VectorImageContent) is { } vectorContent) { + container.Contents.Add(vectorContent); + remainingExtensions.ExceptWith(vectorContent.Extensions); } // 1. Animated image (if any) - if (images.GetAll(AnimatedImageContent.EXTENSIONS).FirstOrDefault() is Image animated) { - container.Contents.Add(new AnimatedImageContent(animated)); - remainingExtensions.ExceptWith(AnimatedImageContent.EXTENSIONS); - } else { - // no direct match, search for anything that looks like it's animated - foreach (var (ext, img) in images.Items) { - try { - if (img.GetFrameCount(FrameDimension.Time) > 1) { - container.Contents.Add(new AnimatedImageContent(img)); - remainingExtensions.ExceptWith(AnimatedImageContent.EXTENSIONS); - break; - } - } catch { /* format does not support frames */ - } - } + if (images.Values.FirstOrDefault(content => content is AnimatedImageContent) is { } animatedContent) { + container.Contents.Add(animatedContent); + remainingExtensions.ExceptWith(animatedContent.Extensions); } // 2. Transparent image (if any) - if (images.GetAll(TransparentImageContent.EXTENSIONS).FirstOrDefault() is Image transparent) { - container.Contents.Add(new TransparentImageContent(transparent)); - remainingExtensions.ExceptWith(TransparentImageContent.EXTENSIONS); + if (images.Values.FirstOrDefault(content => content is TransparentImageContent) is { } transparentContent) { + container.Contents.Add(transparentContent); + remainingExtensions.ExceptWith(transparentContent.Extensions); } else { - // no direct match, search for anything that looks like it's transparent - foreach (var (ext, img) in images.Items) { - if (((ImageFlags)img.Flags).HasFlag(ImageFlags.HasAlpha)) { - container.Contents.Add(new TransparentImageContent(img)); + // no direct match, search for anything that looks like it's transparent (e.g. transparent animated or vector image) + foreach (var cnt in images.Values) { + if (cnt is ImageContent imgCnt && ((ImageFlags)imgCnt.Image.Flags).HasFlag(ImageFlags.HasAlpha)) { + container.Contents.Add(new TransparentImageContent(imgCnt.Image)); remainingExtensions.ExceptWith(TransparentImageContent.EXTENSIONS); break; } @@ -751,11 +871,11 @@ public static ClipboardContents FromClipboard() { } // 3. Remaining image with no special features (if any) - if (images.GetAll(remainingExtensions).FirstOrDefault() is Image image) { - container.Contents.Add(new ImageContent(image)); - } else if (images.Values.FirstOrDefault() is Image anything) { + if (images.GetAll(remainingExtensions).FirstOrDefault() is ImageContent imgContent) { + container.Contents.Add(new ImageContent(imgContent.Image)); // as generic ImageContent + } else if (images.Values.FirstOrDefault() is ImageContent anything) { // no unique match, so accept anything (even if already used as special format) - container.Contents.Add(new ImageContent(anything)); + container.Contents.Add(new ImageContent(anything.Image)); // as generic ImageContent } @@ -785,6 +905,10 @@ public static ClipboardContents FromClipboard() { if (Clipboard.ContainsText() && Uri.IsWellFormedUriString(Clipboard.GetText().Trim(), UriKind.Absolute)) container.Contents.Add(new UrlContent(Clipboard.GetText().Trim())); + if (ReadClipboardString(CalendarContent.CLIP_FORMATS)?.Trim() is string cal) + if (CalendarContent.IsValidCalendar(cal)) + container.Contents.Add(new CalendarContent(cal)); + // make sure text content comes last, so it does not overwrite extensions used by previous special formats... if (ReadClipboardString(DataFormats.UnicodeText, DataFormats.Text, "text/plain") is string text) container.Contents.Add(new TextContent(text)); @@ -793,21 +917,33 @@ public static ClipboardContents FromClipboard() { if (Clipboard.ContainsFileDropList()) container.Contents.Add(new FilesContent(Clipboard.GetFileDropList())); +#if DEBUG + // print a list of all contens in the container to the console + foreach (var content in container.Contents) { + Console.WriteLine("> " + content.GetType()); + if (content.Preview(content.DefaultExtension).Text is string preview) { + preview = preview.Replace('\r', ' ').Replace('\n', ' ').Trim(); + Console.WriteLine(" " + preview.Substring(0, preview.Length > 100 ? 100 : preview.Length)); + } + } + Console.WriteLine(); +#endif + return container; } - private static IEnumerable MimeForExtension(string extension) { - switch (BaseContent.NormalizeExtension(extension)) { - case "jpg": return new[] { "image/jpeg" }; - case "bmp": return new[] { "image/bmp", "image/x-bmp", "image/x-ms-bmp" }; - case "tif": return new[] { "image/tiff", "image/tiff-fx" }; - case "ico": return new[] { "image/x-ico", "image/vnd.microsoft.icon" }; - case "emf": return new[] { "image/emf", "image/x-emf" }; - case "wmf": return new[] { "image/wmf", "image/x-wmf" }; - default: return new[] { "image/" + extension.ToLower() }; - } - } + private static Dict IMAGE_MIME_TYPES = new() { + { "bmp", new[] { "image/bmp", "image/x-bmp", "image/x-ms-bmp" } }, + { "emf", new[] { "image/emf", "image/x-emf" } }, + { "gif", new[] { "image/gif" } }, + { "ico", new[] { "image/x-ico", "image/vnd.microsoft.icon" } }, + { "jpg", new[] { "image/jpeg" } }, + { "png", new[] { "image/png" } }, + { "tif", new[] { "image/tiff", "image/tiff-fx" } }, + { "webp", new[] { "image/webp" } }, + { "wmf", new[] { "image/wmf", "image/x-wmf" } }, + }; private static string ReadClipboardHtml() { if (Clipboard.ContainsData(DataFormats.Html)) { @@ -865,16 +1001,9 @@ public static ClipboardContents FromFile(string path) { // add the file itself container.Contents.Add(new FilesContent(new StringCollection { path })); - // if it's an image (try&catch instead of maintaining a list of supported extensions) - try { - var img = Image.FromFile(path); - img = RotateFlipImageFromExif(img); - if (img is Metafile mf) { - container.Contents.Add(new VectorImageContent(mf)); - } else { - container.Contents.Add(new ImageContent(img)); - } - } catch { /* it's not */ } + // if it's an image + if (ImageContentFromBytes(ext, File.ReadAllBytes(path)) is BaseContent content) + container.Contents.Add(content); // if it's text like (check for absence of zero byte) @@ -899,10 +1028,14 @@ public static ClipboardContents FromFile(string path) { container.Contents.Add(new CsvContent(contents)); if (ext == "dif") container.Contents.Add(new GenericTextContent(DataFormats.Dif, ext, contents)); + if (CalendarContent.FILE_EXTENSIONS.Contains(ext)) + container.Contents.Add(new CalendarContent(contents)); if (ext == "rtf") container.Contents.Add(new GenericTextContent(DataFormats.Rtf, ext, contents)); if (ext == "syk") container.Contents.Add(new GenericTextContent(DataFormats.SymbolicLink, ext, contents)); + if (ext == "url") + container.Contents.Add(new UrlContent(contents)); } else { container.Contents.Add(new TextContent(path)); @@ -933,22 +1066,55 @@ private static bool LooksLikeBinaryFile(string filepath) { /// /// The data URI, typically starting with data:image/ /// The image or null if the uri is not an image or conversion failed - private static Image ImageFromDataUri(string uri) { + private static (string, byte[]) BytesFromDataUri(string uri) { try { - var match = Regex.Match(uri, @"^data:image/\w+(?;base64)?,(?.+)$"); + var match = Regex.Match(uri, @"^data:image/(?;\w+)(?;base64)?,(?.+)$"); if (match.Success) { + var ext = BaseContent.NormalizeExtension(match.Groups["ext"].Value); + byte[] bytes; if (match.Groups["base64"].Success) { // Base64 encoded - var bytes = Convert.FromBase64String(match.Groups["data"].Value); - return Image.FromStream(new MemoryStream(bytes)); + bytes = Convert.FromBase64String(match.Groups["data"].Value); } else { // URL encoded - var bytes = Encoding.Default.GetBytes(match.Groups["data"].Value); + bytes = Encoding.Default.GetBytes(match.Groups["data"].Value); bytes = WebUtility.UrlDecodeToBytes(bytes, 0, bytes.Length); - return Image.FromStream(new MemoryStream(bytes)); } + return (ext, bytes); } } catch { /* data uri malformed or not supported */ } + return (null, null); + } + + private static ImageLikeContent ImageContentFromBytes(string ext, byte[] bytes) { + try { + if (ext == "webp") { + var webp = new WebPObject(bytes); + var img = new Bitmap(webp.GetImage()); // create copy + if (webp.GetInfo().IsAnimated) + return new AnimatedImageContent(img); + if (webp.GetInfo().HasAlpha) + return new TransparentImageContent(img); + return new ImageContent(img); + } + } catch (Exception e) { /* not a webp, or an animated webp which we don't support yet */ + Console.WriteLine(e); + } + try { + var img = Image.FromStream(new MemoryStream(bytes)); + img = RotateFlipImageFromExif(img); + if (img is Metafile mf) + return new VectorImageContent(mf); + try { + if (img.GetFrameCount(FrameDimension.Time) > 1) + return new AnimatedImageContent(img); + } catch { /* not an animated image */ } + if (((ImageFlags)img.Flags).HasFlag(ImageFlags.HasAlpha)) + return new TransparentImageContent(img); + return new ImageContent(img); + } catch (Exception e) { /* not an image? */ + Console.WriteLine(e); + } return null; } diff --git a/PasteIntoFile/Dialog.cs b/PasteIntoFile/Dialog.cs index 2eb7698..603a4ab 100644 --- a/PasteIntoFile/Dialog.cs +++ b/PasteIntoFile/Dialog.cs @@ -213,9 +213,13 @@ public void updateFilename(string filenameTemplate = null) { /// Extension public static string determineExtension(ClipboardContents clipData) { // Determines primary data in clipboard according to a custom prioritisation order - var content = clipData.ForContentType(typeof(ImageLikeContent)) ?? - clipData.ForContentType(typeof(TextContent)) ?? - clipData.ForContentType(typeof(BaseContent)); + var content = + clipData.ForContentType(typeof(CsvContent)) ?? + clipData.ForContentType(typeof(ImageLikeContent)) ?? + clipData.ForContentType(typeof(SvgContent)) ?? + clipData.ForContentType(typeof(CalendarContent)) ?? + clipData.ForContentType(typeof(TextContent)) ?? + clipData.ForContentType(typeof(BaseContent)); // chose file extension based on user preference if available switch (content) { @@ -318,63 +322,51 @@ private void updateContentPreview() { return; } - box.Text = content.Description; - - if (content is ImageLikeContent imageContent) { - var img = imageContent.ImagePreview(ext); - if (img != null) { - imagePreview.Image = img; - - // Checkerboard background in case image is transparent - Bitmap bg = new Bitmap(img.Width, img.Height, PixelFormat.Format32bppArgb); - Graphics g = Graphics.FromImage(bg); - Brush brush = new SolidBrush(Color.LightGray); - float d = Math.Max(10, Math.Max(bg.Width, bg.Height) / 50f); - for (int x = 0; x < bg.Width / d; x++) { - for (int y = 0; y < bg.Height / d; y += 2) { - g.FillRectangle(brush, x * d, d * (y + x % 2), d, d); - } - } - imagePreview.BackgroundImage = bg; + var preview = content.Preview(ext); + box.Text = preview.Description; - imagePreview.Show(); - } else { - // conversion failed - box.Text = String.Format(Resources.str_error_cliboard_format_missmatch, comExt.Text); + if (preview.Image is Image img) { + imagePreview.Image = img; + + // Checkerboard background in case image is transparent + Bitmap bg = new Bitmap(img.Width, img.Height, PixelFormat.Format32bppArgb); + Graphics g = Graphics.FromImage(bg); + Brush brush = new SolidBrush(Color.LightGray); + float d = Math.Max(10, Math.Max(bg.Width, bg.Height) / 50f); + for (int x = 0; x < bg.Width / d; x++) { + for (int y = 0; y < bg.Height / d; y += 2) { + g.FillRectangle(brush, x * d, d * (y + x % 2), d, d); + } } + imagePreview.BackgroundImage = bg; + imagePreview.Show(); - } else if (content is HtmlContent htmlContent) { - htmlPreview.DocumentText = htmlContent.Text; - htmlPreview.Show(); + } else if (preview.Text is string text) { + textPreview.Text = text; + textPreview.Show(); - } else if (content is SvgContent svgContent) { - htmlPreview.DocumentText = svgContent.Xml; + } else if (preview.Html is string html) { + htmlPreview.DocumentText = html; htmlPreview.Show(); - } else if (content is TextLikeContent textLikeContent) { - if (content.Extensions.FirstOrDefault() == "rtf") - textPreview.Rtf = textLikeContent.TextPreview(ext); - else - textPreview.Text = textLikeContent.TextPreview(ext); + } else if (preview.Rtf is string rtf) { + textPreview.Rtf = rtf; textPreview.Show(); - } else if (content is FilesContent filesContent) { - if (filesContent.TextPreview(ext) is string preview) { - textPreview.Text = preview; - textPreview.Show(); - } else { - treePreview.BeginUpdate(); - treePreview.Nodes.Clear(); - foreach (var file in filesContent.FileList) { - treePreview.Nodes.Add(file); - } - treePreview.EndUpdate(); - treePreview.Show(); + } else if (preview.List is string[] list) { + treePreview.BeginUpdate(); + treePreview.Nodes.Clear(); + foreach (var entry in list) { + treePreview.Nodes.Add(entry); } + treePreview.EndUpdate(); + treePreview.Show(); + } else { + // conversion failed + box.Text = String.Format(Resources.str_error_cliboard_format_missmatch, comExt.Text); } - } diff --git a/PasteIntoFile/PasteIntoFile.csproj b/PasteIntoFile/PasteIntoFile.csproj index 7e269eb..3e16780 100644 --- a/PasteIntoFile/PasteIntoFile.csproj +++ b/PasteIntoFile/PasteIntoFile.csproj @@ -12,6 +12,7 @@ PasteIntoFile.Program app.manifest Resources/icon.ico + 12 @@ -48,11 +49,18 @@ + + + + {20e1114b-c211-46e5-a2e0-10a598ff4a44} + WebP + + diff --git a/PasteIntoFile/Properties/Resources.Designer.cs b/PasteIntoFile/Properties/Resources.Designer.cs index 80070e2..9257208 100644 --- a/PasteIntoFile/Properties/Resources.Designer.cs +++ b/PasteIntoFile/Properties/Resources.Designer.cs @@ -425,6 +425,15 @@ internal static string str_preview { } } + /// + /// Looks up a localized string similar to Calendar event preview. + /// + internal static string str_preview_calendar { + get { + return ResourceManager.GetString("str_preview_calendar", resourceCulture); + } + } + /// /// Looks up a localized string similar to CSV preview. /// diff --git a/PasteIntoFile/Properties/Resources.de.resx b/PasteIntoFile/Properties/Resources.de.resx index c8a1c32..f39c015 100644 --- a/PasteIntoFile/Properties/Resources.de.resx +++ b/PasteIntoFile/Properties/Resources.de.resx @@ -309,4 +309,7 @@ Allows to keep application window always on top (in foreground of other windows) Unterordner-Vorlage + + Termin Vorschau + diff --git a/PasteIntoFile/Properties/Resources.resx b/PasteIntoFile/Properties/Resources.resx index 06b73d7..93dba8a 100644 --- a/PasteIntoFile/Properties/Resources.resx +++ b/PasteIntoFile/Properties/Resources.resx @@ -321,4 +321,7 @@ Allows to keep application window always on top (in foreground of other windows) Subfolder template + + Calendar event preview + diff --git a/WebP/Helpers/ThrowHelper.cs b/WebP/Helpers/ThrowHelper.cs new file mode 100644 index 0000000..99ade4e --- /dev/null +++ b/WebP/Helpers/ThrowHelper.cs @@ -0,0 +1,46 @@ +using System; +using System.Data; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; + +namespace WebP.Helpers; + +internal static class ThrowHelper { + [Obsolete] + public static Exception Create( + Exception inner, + [CallerMemberName] string caller = "Unknown") { + return new Exception($"{inner.Message}\nIn {caller}", inner); + } + + public static Exception UnknownPlatform() { + return new PlatformNotSupportedException("Unknown platform detected. Platform must be x86 or x64"); + } + + [Obsolete] + public static Exception ContainsNoData([CallerMemberName] string caller = "Unknown") { + return Create(new DataException("Bitmap contains no data"), caller); + } + + [Obsolete] + public static Exception SizeTooBig([CallerMemberName] string caller = "Unknown") { + return + Create(new DataException($"Dimension of bitmap is too large. Max is {WebPObject.WebpMaxDimension}x{WebPObject.WebpMaxDimension} pixels"), + caller); + } + + [Obsolete] + public static Exception CannotEncodeByUnknown([CallerMemberName] string caller = "Unknown") { + return Create(new Exception("Cannot encode by unknown cause"), caller); + } + + [Obsolete] + public static Exception NullReferenced(string var, [CallerMemberName] string caller = "Unknown") { + return Create(new NullReferenceException($"{var} is null"), caller); + } + + [Obsolete] + public static Exception QualityOutOfRange([CallerMemberName] string caller = "Unknown") { + return Create(new NullReferenceException("Quality must be between"), caller); + } +} diff --git a/WebP/LICENSE b/WebP/LICENSE new file mode 100644 index 0000000..cede0dc --- /dev/null +++ b/WebP/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Sharp0802 + +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. diff --git a/WebP/Natives/Enums/Vp8StatusCode.cs b/WebP/Natives/Enums/Vp8StatusCode.cs new file mode 100644 index 0000000..faa0fe4 --- /dev/null +++ b/WebP/Natives/Enums/Vp8StatusCode.cs @@ -0,0 +1,12 @@ +namespace WebP.Natives.Enums; + +public enum Vp8StatusCode { + Ok, + OutOfMemory, + InvalidParam, + BitstreamError, + UnsupportedFeature, + Suspended, + UserAbort, + NotEnoughData +} diff --git a/WebP/Natives/Enums/WebPCspMode.cs b/WebP/Natives/Enums/WebPCspMode.cs new file mode 100644 index 0000000..778ae1a --- /dev/null +++ b/WebP/Natives/Enums/WebPCspMode.cs @@ -0,0 +1,22 @@ +namespace WebP.Natives.Enums; + +public enum WebpCspMode { + ModeRgb, + ModeRgba, + ModeBgr, + ModeBgra, + + ModeARGB, + ModeRGBA4444, + ModeRGB565, + + ModeRgbA, + ModeBgrA, + + ModeArgb, + ModeRgbA4444, + ModeYuv, + + ModeYuva, + ModeLast +} diff --git a/WebP/Natives/Enums/WebPImageHint.cs b/WebP/Natives/Enums/WebPImageHint.cs new file mode 100644 index 0000000..77e9abc --- /dev/null +++ b/WebP/Natives/Enums/WebPImageHint.cs @@ -0,0 +1,9 @@ +namespace WebP.Natives.Enums; + +public enum WebPImageHint { + Default, + Picture, + Photo, + Graph, + Last +} diff --git a/WebP/Natives/Enums/WebPPreset.cs b/WebP/Natives/Enums/WebPPreset.cs new file mode 100644 index 0000000..e873e43 --- /dev/null +++ b/WebP/Natives/Enums/WebPPreset.cs @@ -0,0 +1,10 @@ +namespace WebP.Natives.Enums; + +public enum WebPPreset { + Default, + Picture, + Photo, + Drawing, + Icon, + Text +} diff --git a/WebP/Natives/Native.cs b/WebP/Natives/Native.cs new file mode 100644 index 0000000..d70d632 --- /dev/null +++ b/WebP/Natives/Native.cs @@ -0,0 +1,216 @@ +using System; +using System.Security; +using WebP.Helpers; +using WebP.Natives.Enums; +using WebP.Natives.Structs; + +namespace WebP.Natives; + +using static Native86; +using static Native64; + +[SuppressUnmanagedCodeSecurity] +public static class Native { + private const int WebpDecoderAbiVersion = 0x0208; + + public static int WebPConfigInit(ref WebPConfig config, WebPPreset preset, float quality) { + return IntPtr.Size switch { + 4 => WebPConfigInitInternal_x86(ref config, preset, quality, WebpDecoderAbiVersion), + 8 => WebPConfigInitInternal_x64(ref config, preset, quality, WebpDecoderAbiVersion), + _ => throw ThrowHelper.UnknownPlatform() + }; + } + + public static Vp8StatusCode WebPGetFeatures(IntPtr rawWebP, int dataSize, ref WebPBitstreamFeatures features) { + return IntPtr.Size switch { + 4 => WebPGetFeaturesInternal_x86(rawWebP, (UIntPtr)dataSize, ref features, WebpDecoderAbiVersion), + 8 => WebPGetFeaturesInternal_x64(rawWebP, (UIntPtr)dataSize, ref features, WebpDecoderAbiVersion), + _ => throw ThrowHelper.UnknownPlatform() + }; + } + + public static int WebPConfigLosslessPreset(ref WebPConfig config, int level) { + return IntPtr.Size switch { + 4 => WebPConfigLosslessPreset_x86(ref config, level), + 8 => WebPConfigLosslessPreset_x64(ref config, level), + _ => throw ThrowHelper.UnknownPlatform() + }; + } + + public static int WebPValidateConfig(ref WebPConfig config) { + return IntPtr.Size switch { + 4 => WebPValidateConfig_x86(ref config), + 8 => WebPValidateConfig_x64(ref config), + _ => throw ThrowHelper.UnknownPlatform() + }; + } + + public static int WebPPictureInitInternal(ref WebPPicture pic) { + return IntPtr.Size switch { + 4 => WebPPictureInitInternal_x86(ref pic, WebpDecoderAbiVersion), + 8 => WebPPictureInitInternal_x64(ref pic, WebpDecoderAbiVersion), + _ => throw ThrowHelper.UnknownPlatform() + }; + } + + public static int WebPPictureImportBgr(ref WebPPicture pic, IntPtr bgr, int stride) { + return IntPtr.Size switch { + 4 => WebPPictureImportBGR_x86(ref pic, bgr, stride), + 8 => WebPPictureImportBGR_x64(ref pic, bgr, stride), + _ => throw ThrowHelper.UnknownPlatform() + }; + } + + public static int WebPPictureImportBgra(ref WebPPicture pic, IntPtr bgra, int stride) { + return IntPtr.Size switch { + 4 => WebPPictureImportBGRA_x86(ref pic, bgra, stride), + 8 => WebPPictureImportBGRA_x64(ref pic, bgra, stride), + _ => throw ThrowHelper.UnknownPlatform() + }; + } + + public static int WebPPictureImportBgrx(ref WebPPicture pic, IntPtr bgr, int stride) { + return IntPtr.Size switch { + 4 => WebPPictureImportBGRX_x86(ref pic, bgr, stride), + 8 => WebPPictureImportBGRX_x64(ref pic, bgr, stride), + _ => throw ThrowHelper.UnknownPlatform() + }; + } + + public static int WebPEncode(ref WebPConfig config, ref WebPPicture picture) { + return IntPtr.Size switch { + 4 => WebPEncode_x86(ref config, ref picture), + 8 => WebPEncode_x64(ref config, ref picture), + _ => throw ThrowHelper.UnknownPlatform() + }; + } + + public static void WebPPictureFree(ref WebPPicture picture) { + switch (IntPtr.Size) { + case 4: + WebPPictureFree_x86(ref picture); + break; + case 8: + WebPPictureFree_x64(ref picture); + break; + default: throw ThrowHelper.UnknownPlatform(); + } + } + + public static int WebPGetInfo(IntPtr data, int dataSize, out int width, out int height) { + return IntPtr.Size switch { + 4 => WebPGetInfo_x86(data, (UIntPtr)dataSize, out width, out height), + 8 => WebPGetInfo_x64(data, (UIntPtr)dataSize, out width, out height), + _ => throw ThrowHelper.UnknownPlatform() + }; + } + + public static int WebPDecodeBgrInto(IntPtr data, int dataSize, IntPtr outputBuffer, int outputBufferSize, + int outputStride) { + return IntPtr.Size switch { + 4 => WebPDecodeBGRInto_x86(data, (UIntPtr)dataSize, outputBuffer, outputBufferSize, outputStride), + 8 => WebPDecodeBGRInto_x64(data, (UIntPtr)dataSize, outputBuffer, outputBufferSize, outputStride), + _ => throw ThrowHelper.UnknownPlatform() + }; + } + + public static int WebPDecodeBgraInto(IntPtr data, int dataSize, IntPtr outputBuffer, int outputBufferSize, + int outputStride) { + return IntPtr.Size switch { + 4 => WebPDecodeBGRAInto_x86(data, (UIntPtr)dataSize, outputBuffer, outputBufferSize, outputStride), + 8 => WebPDecodeBGRAInto_x64(data, (UIntPtr)dataSize, outputBuffer, outputBufferSize, outputStride), + _ => throw ThrowHelper.UnknownPlatform() + }; + } + + public static int WebPInitDecoderConfig(ref WebPDecoderConfig webPDecoderConfig) { + return IntPtr.Size switch { + 4 => WebPInitDecoderConfigInternal_x86(ref webPDecoderConfig, WebpDecoderAbiVersion), + 8 => WebPInitDecoderConfigInternal_x64(ref webPDecoderConfig, WebpDecoderAbiVersion), + _ => throw ThrowHelper.UnknownPlatform() + }; + } + + public static Vp8StatusCode WebPDecode(IntPtr data, int dataSize, ref WebPDecoderConfig webPDecoderConfig) { + return IntPtr.Size switch { + 4 => WebPDecode_x86(data, (UIntPtr)dataSize, ref webPDecoderConfig), + 8 => WebPDecode_x64(data, (UIntPtr)dataSize, ref webPDecoderConfig), + _ => throw ThrowHelper.UnknownPlatform() + }; + } + + public static void WebPFreeDecBuffer(ref WebPDecBuffer buffer) { + switch (IntPtr.Size) { + case 4: + WebPFreeDecBuffer_x86(ref buffer); + break; + case 8: + WebPFreeDecBuffer_x64(ref buffer); + break; + default: throw ThrowHelper.UnknownPlatform(); + } + } + + public static int WebPEncodeBgr(IntPtr bgr, int width, int height, int stride, float qualityFactor, + out IntPtr output) { + return IntPtr.Size switch { + 4 => WebPEncodeBGR_x86(bgr, width, height, stride, qualityFactor, out output), + 8 => WebPEncodeBGR_x64(bgr, width, height, stride, qualityFactor, out output), + _ => throw ThrowHelper.UnknownPlatform() + }; + } + + public static int WebPEncodeBgra(IntPtr bgra, int width, int height, int stride, float qualityFactor, + out IntPtr output) { + return IntPtr.Size switch { + 4 => WebPEncodeBGRA_x86(bgra, width, height, stride, qualityFactor, out output), + 8 => WebPEncodeBGRA_x64(bgra, width, height, stride, qualityFactor, out output), + _ => throw ThrowHelper.UnknownPlatform() + }; + } + + public static int WebPEncodeLosslessBgr(IntPtr bgr, int width, int height, int stride, out IntPtr output) { + return IntPtr.Size switch { + 4 => WebPEncodeLosslessBGR_x86(bgr, width, height, stride, out output), + 8 => WebPEncodeLosslessBGR_x64(bgr, width, height, stride, out output), + _ => throw ThrowHelper.UnknownPlatform() + }; + } + + public static int WebPEncodeLosslessBgra(IntPtr bgra, int width, int height, int stride, out IntPtr output) { + return IntPtr.Size switch { + 4 => WebPEncodeLosslessBGRA_x86(bgra, width, height, stride, out output), + 8 => WebPEncodeLosslessBGRA_x64(bgra, width, height, stride, out output), + _ => throw ThrowHelper.UnknownPlatform() + }; + } + + public static void WebPFree(IntPtr p) { + switch (IntPtr.Size) { + case 4: + WebPFree_x86(p); + break; + case 8: + WebPFree_x64(p); + break; + default: throw ThrowHelper.UnknownPlatform(); + } + } + + public static int WebPGetDecoderVersion() { + return IntPtr.Size switch { + 4 => WebPGetDecoderVersion_x86(), + 8 => WebPGetDecoderVersion_x64(), + _ => throw ThrowHelper.UnknownPlatform() + }; + } + + public static int WebPPictureDistortion(ref WebPPicture srcPicture, ref WebPPicture refPicture, int metricType, + IntPtr pResult) { + return IntPtr.Size switch { + 4 => WebPPictureDistortion_x86(ref srcPicture, ref refPicture, metricType, pResult), + 8 => WebPPictureDistortion_x64(ref srcPicture, ref refPicture, metricType, pResult), + _ => throw ThrowHelper.UnknownPlatform() + }; + } +} diff --git a/WebP/Natives/Native64.cs b/WebP/Natives/Native64.cs new file mode 100644 index 0000000..a33eba9 --- /dev/null +++ b/WebP/Natives/Native64.cs @@ -0,0 +1,92 @@ +using System; +using System.Runtime.InteropServices; +using System.Security; +using WebP.Natives.Enums; +using WebP.Natives.Structs; + +namespace WebP.Natives; + +[SuppressUnmanagedCodeSecurity] +public static class Native64 { + private const string DllPath = "libwebp.x64.dll"; + + [DllImport(DllPath, CallingConvention = CallingConvention.Cdecl, EntryPoint = "WebPConfigInitInternal")] + public static extern int WebPConfigInitInternal_x64(ref WebPConfig config, WebPPreset preset, float quality, + int webpDecoderAbiVersion); + + [DllImport(DllPath, CallingConvention = CallingConvention.Cdecl, EntryPoint = "WebPGetFeaturesInternal")] + public static extern Vp8StatusCode WebPGetFeaturesInternal_x64([In] IntPtr rawWebP, UIntPtr dataSize, + ref WebPBitstreamFeatures features, + int webpDecoderAbiVersion); + + [DllImport(DllPath, CallingConvention = CallingConvention.Cdecl, EntryPoint = "WebPConfigLosslessPreset")] + public static extern int WebPConfigLosslessPreset_x64(ref WebPConfig config, int level); + + [DllImport(DllPath, CallingConvention = CallingConvention.Cdecl, EntryPoint = "WebPValidateConfig")] + public static extern int WebPValidateConfig_x64(ref WebPConfig config); + + [DllImport(DllPath, CallingConvention = CallingConvention.Cdecl, EntryPoint = "WebPPictureInitInternal")] + public static extern int WebPPictureInitInternal_x64(ref WebPPicture pic, int webpDecoderAbiVersion); + + [DllImport(DllPath, CallingConvention = CallingConvention.Cdecl, EntryPoint = "WebPPictureImportBGR")] + public static extern int WebPPictureImportBGR_x64(ref WebPPicture pic, IntPtr bgr, int stride); + + [DllImport(DllPath, CallingConvention = CallingConvention.Cdecl, EntryPoint = "WebPPictureImportBGRA")] + public static extern int WebPPictureImportBGRA_x64(ref WebPPicture pic, IntPtr bgra, int stride); + + [DllImport(DllPath, CallingConvention = CallingConvention.Cdecl, EntryPoint = "WebPPictureImportBGRX")] + public static extern int WebPPictureImportBGRX_x64(ref WebPPicture pic, IntPtr bgr, int stride); + + [DllImport(DllPath, CallingConvention = CallingConvention.Cdecl, EntryPoint = "WebPEncode")] + public static extern int WebPEncode_x64(ref WebPConfig config, ref WebPPicture picture); + + [DllImport(DllPath, CallingConvention = CallingConvention.Cdecl, EntryPoint = "WebPPictureFree")] + public static extern void WebPPictureFree_x64(ref WebPPicture pic); + + [DllImport(DllPath, CallingConvention = CallingConvention.Cdecl, EntryPoint = "WebPGetInfo")] + public static extern int WebPGetInfo_x64([In] IntPtr data, UIntPtr dataSize, out int width, out int height); + + [DllImport(DllPath, CallingConvention = CallingConvention.Cdecl, EntryPoint = "WebPDecodeBGRInto")] + public static extern int WebPDecodeBGRInto_x64([In] IntPtr data, UIntPtr dataSize, IntPtr outputBuffer, + int outputBufferSize, int outputStride); + + [DllImport(DllPath, CallingConvention = CallingConvention.Cdecl, EntryPoint = "WebPDecodeBGRAInto")] + public static extern int WebPDecodeBGRAInto_x64([In] IntPtr data, UIntPtr dataSize, IntPtr outputBuffer, + int outputBufferSize, int outputStride); + + [DllImport(DllPath, CallingConvention = CallingConvention.Cdecl, EntryPoint = "WebPInitDecoderConfigInternal")] + public static extern int WebPInitDecoderConfigInternal_x64(ref WebPDecoderConfig webPDecoderConfig, + int webpDecoderAbiVersion); + + [DllImport(DllPath, CallingConvention = CallingConvention.Cdecl, EntryPoint = "WebPDecode")] + public static extern Vp8StatusCode WebPDecode_x64(IntPtr data, UIntPtr dataSize, ref WebPDecoderConfig config); + + [DllImport(DllPath, CallingConvention = CallingConvention.Cdecl, EntryPoint = "WebPFreeDecBuffer")] + public static extern void WebPFreeDecBuffer_x64(ref WebPDecBuffer buffer); + + [DllImport(DllPath, CallingConvention = CallingConvention.Cdecl, EntryPoint = "WebPEncodeBGR")] + public static extern int WebPEncodeBGR_x64([In] IntPtr bgr, int width, int height, int stride, float qualityFactor, + out IntPtr output); + + [DllImport(DllPath, CallingConvention = CallingConvention.Cdecl, EntryPoint = "WebPEncodeBGRA")] + public static extern int WebPEncodeBGRA_x64([In] IntPtr bgra, int width, int height, int stride, + float qualityFactor, out IntPtr output); + + [DllImport(DllPath, CallingConvention = CallingConvention.Cdecl, EntryPoint = "WebPEncodeLosslessBGR")] + public static extern int WebPEncodeLosslessBGR_x64([In] IntPtr bgr, int width, int height, int stride, + out IntPtr output); + + [DllImport(DllPath, CallingConvention = CallingConvention.Cdecl, EntryPoint = "WebPEncodeLosslessBGRA")] + public static extern int WebPEncodeLosslessBGRA_x64([In] IntPtr bgra, int width, int height, int stride, + out IntPtr output); + + [DllImport(DllPath, CallingConvention = CallingConvention.Cdecl, EntryPoint = "WebPFree")] + public static extern void WebPFree_x64(IntPtr p); + + [DllImport(DllPath, CallingConvention = CallingConvention.Cdecl, EntryPoint = "WebPGetDecoderVersion")] + public static extern int WebPGetDecoderVersion_x64(); + + [DllImport(DllPath, CallingConvention = CallingConvention.Cdecl, EntryPoint = "WebPPictureDistortion")] + public static extern int WebPPictureDistortion_x64(ref WebPPicture srcPicture, ref WebPPicture refPicture, + int metricType, IntPtr pResult); +} diff --git a/WebP/Natives/Native86.cs b/WebP/Natives/Native86.cs new file mode 100644 index 0000000..00feeb0 --- /dev/null +++ b/WebP/Natives/Native86.cs @@ -0,0 +1,92 @@ +using System; +using System.Runtime.InteropServices; +using System.Security; +using WebP.Natives.Enums; +using WebP.Natives.Structs; + +namespace WebP.Natives; + +[SuppressUnmanagedCodeSecurity] +public static class Native86 { + private const string DllPath = "libwebp.x86.dll"; + + [DllImport(DllPath, CallingConvention = CallingConvention.Cdecl, EntryPoint = "WebPConfigInitInternal")] + public static extern int WebPConfigInitInternal_x86(ref WebPConfig config, WebPPreset preset, float quality, + int webpDecoderAbiVersion); + + [DllImport(DllPath, CallingConvention = CallingConvention.Cdecl, EntryPoint = "WebPGetFeaturesInternal")] + public static extern Vp8StatusCode WebPGetFeaturesInternal_x86([In] IntPtr rawWebP, UIntPtr dataSize, + ref WebPBitstreamFeatures features, + int webpDecoderAbiVersion); + + [DllImport(DllPath, CallingConvention = CallingConvention.Cdecl, EntryPoint = "WebPConfigLosslessPreset")] + public static extern int WebPConfigLosslessPreset_x86(ref WebPConfig config, int level); + + [DllImport(DllPath, CallingConvention = CallingConvention.Cdecl, EntryPoint = "WebPValidateConfig")] + public static extern int WebPValidateConfig_x86(ref WebPConfig config); + + [DllImport(DllPath, CallingConvention = CallingConvention.Cdecl, EntryPoint = "WebPPictureInitInternal")] + public static extern int WebPPictureInitInternal_x86(ref WebPPicture pic, int webpDecoderAbiVersion); + + [DllImport(DllPath, CallingConvention = CallingConvention.Cdecl, EntryPoint = "WebPPictureImportBGR")] + public static extern int WebPPictureImportBGR_x86(ref WebPPicture pic, IntPtr bgr, int stride); + + [DllImport(DllPath, CallingConvention = CallingConvention.Cdecl, EntryPoint = "WebPPictureImportBGRA")] + public static extern int WebPPictureImportBGRA_x86(ref WebPPicture pic, IntPtr bgra, int stride); + + [DllImport(DllPath, CallingConvention = CallingConvention.Cdecl, EntryPoint = "WebPPictureImportBGRX")] + public static extern int WebPPictureImportBGRX_x86(ref WebPPicture pic, IntPtr bgr, int stride); + + [DllImport(DllPath, CallingConvention = CallingConvention.Cdecl, EntryPoint = "WebPEncode")] + public static extern int WebPEncode_x86(ref WebPConfig config, ref WebPPicture picture); + + [DllImport(DllPath, CallingConvention = CallingConvention.Cdecl, EntryPoint = "WebPPictureFree")] + public static extern void WebPPictureFree_x86(ref WebPPicture pic); + + [DllImport(DllPath, CallingConvention = CallingConvention.Cdecl, EntryPoint = "WebPGetInfo")] + public static extern int WebPGetInfo_x86([In] IntPtr data, UIntPtr dataSize, out int width, out int height); + + [DllImport(DllPath, CallingConvention = CallingConvention.Cdecl, EntryPoint = "WebPDecodeBGRInto")] + public static extern int WebPDecodeBGRInto_x86([In] IntPtr data, UIntPtr dataSize, IntPtr outputBuffer, + int outputBufferSize, int outputStride); + + [DllImport(DllPath, CallingConvention = CallingConvention.Cdecl, EntryPoint = "WebPDecodeBGRAInto")] + public static extern int WebPDecodeBGRAInto_x86([In] IntPtr data, UIntPtr dataSize, IntPtr outputBuffer, + int outputBufferSize, int outputStride); + + [DllImport(DllPath, CallingConvention = CallingConvention.Cdecl, EntryPoint = "WebPInitDecoderConfigInternal")] + public static extern int WebPInitDecoderConfigInternal_x86(ref WebPDecoderConfig webPDecoderConfig, + int webpDecoderAbiVersion); + + [DllImport(DllPath, CallingConvention = CallingConvention.Cdecl, EntryPoint = "WebPDecode")] + public static extern Vp8StatusCode WebPDecode_x86(IntPtr data, UIntPtr dataSize, ref WebPDecoderConfig config); + + [DllImport(DllPath, CallingConvention = CallingConvention.Cdecl, EntryPoint = "WebPFreeDecBuffer")] + public static extern void WebPFreeDecBuffer_x86(ref WebPDecBuffer buffer); + + [DllImport(DllPath, CallingConvention = CallingConvention.Cdecl, EntryPoint = "WebPEncodeBGR")] + public static extern int WebPEncodeBGR_x86([In] IntPtr bgr, int width, int height, int stride, float qualityFactor, + out IntPtr output); + + [DllImport(DllPath, CallingConvention = CallingConvention.Cdecl, EntryPoint = "WebPEncodeBGRA")] + public static extern int WebPEncodeBGRA_x86([In] IntPtr bgra, int width, int height, int stride, + float qualityFactor, out IntPtr output); + + [DllImport(DllPath, CallingConvention = CallingConvention.Cdecl, EntryPoint = "WebPEncodeLosslessBGR")] + public static extern int WebPEncodeLosslessBGR_x86([In] IntPtr bgr, int width, int height, int stride, + out IntPtr output); + + [DllImport(DllPath, CallingConvention = CallingConvention.Cdecl, EntryPoint = "WebPEncodeLosslessBGRA")] + public static extern int WebPEncodeLosslessBGRA_x86([In] IntPtr bgra, int width, int height, int stride, + out IntPtr output); + + [DllImport(DllPath, CallingConvention = CallingConvention.Cdecl, EntryPoint = "WebPFree")] + public static extern void WebPFree_x86(IntPtr p); + + [DllImport(DllPath, CallingConvention = CallingConvention.Cdecl, EntryPoint = "WebPGetDecoderVersion")] + public static extern int WebPGetDecoderVersion_x86(); + + [DllImport(DllPath, CallingConvention = CallingConvention.Cdecl, EntryPoint = "WebPPictureDistortion")] + public static extern int WebPPictureDistortion_x86(ref WebPPicture srcPicture, ref WebPPicture refPicture, + int metricType, IntPtr pResult); +} diff --git a/WebP/Natives/Structs/RgbaYuvaBuffer.cs b/WebP/Natives/Structs/RgbaYuvaBuffer.cs new file mode 100644 index 0000000..5553935 --- /dev/null +++ b/WebP/Natives/Structs/RgbaYuvaBuffer.cs @@ -0,0 +1,13 @@ +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; + +namespace WebP.Natives.Structs; + +[StructLayout(LayoutKind.Explicit), + SuppressMessage("ReSharper", "FieldCanBeMadeReadOnly.Global"), + SuppressMessage("ReSharper", "MemberCanBePrivate.Global")] +public struct RgbaYuvaBuffer { + [FieldOffset(0)] public WebPRgbaBuffer Rgba; + + [FieldOffset(0)] public WebPYuvaBuffer Yuva; +} diff --git a/WebP/Natives/Structs/WebPAuxStats.cs b/WebP/Natives/Structs/WebPAuxStats.cs new file mode 100644 index 0000000..de22b30 --- /dev/null +++ b/WebP/Natives/Structs/WebPAuxStats.cs @@ -0,0 +1,58 @@ +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; + +namespace WebP.Natives.Structs; + +[StructLayout(LayoutKind.Sequential), + SuppressMessage("ReSharper", "FieldCanBeMadeReadOnly.Global"), + SuppressMessage("ReSharper", "MemberCanBePrivate.Global")] +public struct WebPAuxStats { + public int coded_size; + public float PSNR_Y; + public float PSNR_U; + public float PSNR_V; + public float PSNR_ALL; + public float PSNRAlpha; + public int block_count_intra4; + public int block_count_intra16; + public int block_count_skipped; + public int header_bytes; + public int mode_partition_0; + public int residual_bytes_DC_segments0; + public int residual_bytes_AC_segments0; + public int residual_bytes_uv_segments0; + public int residual_bytes_DC_segments1; + public int residual_bytes_AC_segments1; + public int residual_bytes_uv_segments1; + public int residual_bytes_DC_segments2; + public int residual_bytes_AC_segments2; + public int residual_bytes_uv_segments2; + public int residual_bytes_DC_segments3; + public int residual_bytes_AC_segments3; + public int residual_bytes_uv_segments3; + public int segment_size_segments0; + public int segment_size_segments1; + public int segment_size_segments2; + public int segment_size_segments3; + public int segment_quant_segments0; + public int segment_quant_segments1; + public int segment_quant_segments2; + public int segment_quant_segments3; + public int segment_level_segments0; + public int segment_level_segments1; + public int segment_level_segments2; + public int segment_level_segments3; + public int alpha_data_size; + public int layer_data_size; + public int lossless_features; + public int histogram_bits; + public int transform_bits; + public int cache_bits; + public int palette_size; + public int lossless_size; + public int lossless_hdr_size; + public int lossless_data_size; + + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 2, ArraySubType = UnmanagedType.U4)] + private readonly uint[] pad; +} diff --git a/WebP/Natives/Structs/WebPBitstreamFeatures.cs b/WebP/Natives/Structs/WebPBitstreamFeatures.cs new file mode 100644 index 0000000..c83361b --- /dev/null +++ b/WebP/Natives/Structs/WebPBitstreamFeatures.cs @@ -0,0 +1,18 @@ +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; + +namespace WebP.Natives.Structs; + +[StructLayout(LayoutKind.Sequential), + SuppressMessage("ReSharper", "FieldCanBeMadeReadOnly.Global"), + SuppressMessage("ReSharper", "MemberCanBePrivate.Global")] +public struct WebPBitstreamFeatures { + public int Width; + public int Height; + public int Has_alpha; + public int Has_animation; + public int Format; + + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 5, ArraySubType = UnmanagedType.U4)] + private readonly uint[] pad; +} diff --git a/WebP/Natives/Structs/WebPConfig.cs b/WebP/Natives/Structs/WebPConfig.cs new file mode 100644 index 0000000..663e746 --- /dev/null +++ b/WebP/Natives/Structs/WebPConfig.cs @@ -0,0 +1,40 @@ +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; +using WebP.Natives.Enums; + +namespace WebP.Natives.Structs; + +[StructLayout(LayoutKind.Sequential), + SuppressMessage("ReSharper", "FieldCanBeMadeReadOnly.Global"), + SuppressMessage("ReSharper", "MemberCanBePrivate.Global")] +public struct WebPConfig { + public int lossless; + public float quality; + public int method; + public WebPImageHint image_hint; + public int target_size; + public float target_PSNR; + public int segments; + public int sns_strength; + public int filter_strength; + public int filter_sharpness; + public int filter_type; + public int auto_filter; + public int alpha_compression; + public int alpha_filtering; + public int alpha_quality; + public int pass; + public int show_compressed; + public int preprocessing; + public int partitions; + public int partition_limit; + public int emulate_jpeg_size; + public int thread_level; + public int low_memory; + public int near_lossless; + public int exact; + public int delta_palletization; + public int use_sharp_yuv; + private readonly int pad1; + private readonly int pad2; +} diff --git a/WebP/Natives/Structs/WebPDecBuffer.cs b/WebP/Natives/Structs/WebPDecBuffer.cs new file mode 100644 index 0000000..486bd94 --- /dev/null +++ b/WebP/Natives/Structs/WebPDecBuffer.cs @@ -0,0 +1,22 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; +using WebP.Natives.Enums; + +namespace WebP.Natives.Structs; + +[StructLayout(LayoutKind.Sequential), + SuppressMessage("ReSharper", "FieldCanBeMadeReadOnly.Global"), + SuppressMessage("ReSharper", "MemberCanBePrivate.Global")] +public struct WebPDecBuffer { + public WebpCspMode colorSpace; + public int width; + public int height; + public int isExternalMemory; + public RgbaYuvaBuffer u; + private readonly uint pad1; + private readonly uint pad2; + private readonly uint pad3; + private readonly uint pad4; + public IntPtr private_memory; +} diff --git a/WebP/Natives/Structs/WebPDecoderConfig.cs b/WebP/Natives/Structs/WebPDecoderConfig.cs new file mode 100644 index 0000000..dba09b5 --- /dev/null +++ b/WebP/Natives/Structs/WebPDecoderConfig.cs @@ -0,0 +1,13 @@ +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; + +namespace WebP.Natives.Structs; + +[StructLayout(LayoutKind.Sequential), + SuppressMessage("ReSharper", "FieldCanBeMadeReadOnly.Global"), + SuppressMessage("ReSharper", "MemberCanBePrivate.Global")] +public struct WebPDecoderConfig { + public WebPBitstreamFeatures input; + public WebPDecBuffer output; + public WebPDecoderOptions options; +} diff --git a/WebP/Natives/Structs/WebPDecoderOptions.cs b/WebP/Natives/Structs/WebPDecoderOptions.cs new file mode 100644 index 0000000..fcc0de8 --- /dev/null +++ b/WebP/Natives/Structs/WebPDecoderOptions.cs @@ -0,0 +1,29 @@ +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; + +namespace WebP.Natives.Structs; + +[StructLayout(LayoutKind.Sequential), + SuppressMessage("ReSharper", "FieldCanBeMadeReadOnly.Global"), + SuppressMessage("ReSharper", "MemberCanBePrivate.Global")] +public struct WebPDecoderOptions { + public int bypass_filtering; + public int no_fancy_upsampling; + public int use_cropping; + public int crop_left; + public int crop_top; + public int crop_width; + public int crop_height; + public int use_scaling; + public int scaled_width; + public int scaled_height; + public int use_threads; + public int dithering_strength; + public int flip; + public int alpha_dithering_strength; + private readonly uint pad1; + private readonly uint pad2; + private readonly uint pad3; + private readonly uint pad4; + private readonly uint pad5; +} diff --git a/WebP/Natives/Structs/WebPPicture.cs b/WebP/Natives/Structs/WebPPicture.cs new file mode 100644 index 0000000..83a767b --- /dev/null +++ b/WebP/Natives/Structs/WebPPicture.cs @@ -0,0 +1,52 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; + +namespace WebP.Natives.Structs; + +[StructLayout(LayoutKind.Sequential), + SuppressMessage("ReSharper", "FieldCanBeMadeReadOnly.Global"), + SuppressMessage("ReSharper", "MemberCanBePrivate.Global")] +public struct WebPPicture : IDisposable { + public int use_argb; + public uint colorspace; + public int width; + public int height; + public IntPtr y; + public IntPtr u; + public IntPtr v; + public IntPtr a; + public int y_stride; + public int uv_stride; + public int a_stride; + + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 2, ArraySubType = UnmanagedType.U4)] + private readonly uint[] pad1; + + public IntPtr argb; + public int argb_stride; + + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 3, ArraySubType = UnmanagedType.U4)] + private readonly uint[] pad2; + + public IntPtr writer; + public IntPtr custom_ptr; + public int extra_info_type; + public IntPtr extra_info; + public IntPtr stats; + public uint error_code; + public IntPtr progress_hook; + public IntPtr user_data; + + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 13, ArraySubType = UnmanagedType.U4)] + private readonly uint[] pad3; + + private readonly IntPtr memory; + private readonly IntPtr memory_argb; + + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 2, ArraySubType = UnmanagedType.U4)] + private readonly uint[] pad4; + + public void Dispose() { + } +} diff --git a/WebP/Natives/Structs/WebPRgbaBuffer.cs b/WebP/Natives/Structs/WebPRgbaBuffer.cs new file mode 100644 index 0000000..84599b8 --- /dev/null +++ b/WebP/Natives/Structs/WebPRgbaBuffer.cs @@ -0,0 +1,14 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; + +namespace WebP.Natives.Structs; + +[StructLayout(LayoutKind.Sequential), + SuppressMessage("ReSharper", "FieldCanBeMadeReadOnly.Global"), + SuppressMessage("ReSharper", "MemberCanBePrivate.Global")] +public struct WebPRgbaBuffer { + public IntPtr rgba; + public int stride; + public UIntPtr size; +} diff --git a/WebP/Natives/Structs/WebPYuvaBuffer.cs b/WebP/Natives/Structs/WebPYuvaBuffer.cs new file mode 100644 index 0000000..d4f559e --- /dev/null +++ b/WebP/Natives/Structs/WebPYuvaBuffer.cs @@ -0,0 +1,23 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; + +namespace WebP.Natives.Structs; + +[StructLayout(LayoutKind.Sequential), + SuppressMessage("ReSharper", "FieldCanBeMadeReadOnly.Global"), + SuppressMessage("ReSharper", "MemberCanBePrivate.Global")] +public struct WebPYuvaBuffer { + public IntPtr y; + public IntPtr u; + public IntPtr v; + public IntPtr a; + public int y_stride; + public int u_stride; + public int v_stride; + public int a_stride; + public UIntPtr y_size; + public UIntPtr u_size; + public UIntPtr v_size; + public UIntPtr a_size; +} diff --git a/WebP/Natives/libwebp.x64.dll b/WebP/Natives/libwebp.x64.dll new file mode 100644 index 0000000..9fcabfd Binary files /dev/null and b/WebP/Natives/libwebp.x64.dll differ diff --git a/WebP/Natives/libwebp.x86.dll b/WebP/Natives/libwebp.x86.dll new file mode 100644 index 0000000..6209467 Binary files /dev/null and b/WebP/Natives/libwebp.x86.dll differ diff --git a/WebP/Properties/AssemblyInfo.cs b/WebP/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..d4e2095 --- /dev/null +++ b/WebP/Properties/AssemblyInfo.cs @@ -0,0 +1,35 @@ +using System.Reflection; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("WebP")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("WebP")] +[assembly: AssemblyCopyright("Copyright © 2025")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("20E1114B-C211-46E5-A2E0-10A598FF4A44")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/WebP/README.md b/WebP/README.md new file mode 100644 index 0000000..8a29bd5 --- /dev/null +++ b/WebP/README.md @@ -0,0 +1,74 @@ +# WebP.Net + +![](https://img.shields.io/badge/C%23-239120?style=for-the-badge&logo=c-sharp&logoColor=white) +[![](https://img.shields.io/badge/NuGet-004880?style=for-the-badge&logo=nuget&logoColor=white)](https://www.nuget.org/packages/WebP_Net/) + +## What is this? + +This library provides a simple encoder and decoder for webp. + +## How to use? + +### Install + +In your csproj : + +```xml + +``` + +Or, if you using .net cli : + +``` +dotnet add package WebP_Net --version 1.1.1 +``` + +### Encode / Decode + +```c# +using System.Drawing; +using WebP.Net; + +static byte[] EncodeLossy(Bitmap bitmap, float quality) +{ + using var webp = new WebPObject(bitmap); + return webp.GetWebPLossy(quality); +} +static byte[] EncodeLossless(Bitmap bitmap) +{ + using var webp = new WebPObject(bitmap); + return webp.GetLossless(); +} +static Image DecodeWebP(byte[] webP) +{ + using var webp = new WebPObject(webP); + return webp.GetImage(); +} +``` + +### Get info + +```c# +using WebP.Net; + +static WebPInfo GetInfo(byte[] webP) +{ + using var webp = new WebPObject(webP); + return webP.GetInfo(); +} +``` + +### Get version + +```c# +using WebP.Net; + +static WebPVersion GetVersion() +{ + return WebPObject.GetVersion(); // get version of libwebp +} +static string GetVersionAsString() +{ + return WebPObject.GetVersion().ToString(); // Major.Minor.Revision +} +``` diff --git a/WebP/WebP.csproj b/WebP/WebP.csproj new file mode 100644 index 0000000..6c8333c --- /dev/null +++ b/WebP/WebP.csproj @@ -0,0 +1,86 @@ + + + + + Debug + AnyCPU + {20E1114B-C211-46E5-A2E0-10A598FF4A44} + Library + Properties + WebP + WebP + v4.8 + 512 + latest + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Always + libwebp.x64.dll + + + Always + libwebp.x86.dll + + + + + + + + diff --git a/WebP/WebPInfo.cs b/WebP/WebPInfo.cs new file mode 100644 index 0000000..99702bf --- /dev/null +++ b/WebP/WebPInfo.cs @@ -0,0 +1,40 @@ +using System; +using System.Runtime.InteropServices; +using WebP.Helpers; +using WebP.Natives; +using WebP.Natives.Enums; +using WebP.Natives.Structs; + +namespace WebP; + +public readonly struct WebPInfo { + [method: Obsolete("WebPInfo.GetFrom is obsolete. Use WebPObject instead of this.")] + public static WebPInfo GetFrom(byte[] webP) { + var handle = GCHandle.Alloc(webP, GCHandleType.Pinned); + + try { + var features = new WebPBitstreamFeatures(); + var status = Native.WebPGetFeatures(handle.AddrOfPinnedObject(), webP.Length, ref features); + if (status is not Vp8StatusCode.Ok) + throw new ExternalException(status.ToString()); + return new WebPInfo(features); + } catch (Exception ex) { + throw ThrowHelper.Create(ex); + } finally { + if (handle.IsAllocated) + handle.Free(); + } + } + + internal WebPInfo(WebPBitstreamFeatures features) { + Width = features.Width; + Height = features.Height; + HasAlpha = features.Has_alpha is not 0; + IsAnimated = features.Has_animation is not 0; + } + + public int Width { get; } + public int Height { get; } + public bool HasAlpha { get; } + public bool IsAnimated { get; } +} diff --git a/WebP/WebPObject.cs b/WebP/WebPObject.cs new file mode 100644 index 0000000..9235d8d --- /dev/null +++ b/WebP/WebPObject.cs @@ -0,0 +1,211 @@ +using System; +using System.Data; +using System.Drawing; +using System.Drawing.Imaging; +using System.IO; +using System.Net.Mime; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using WebP.Natives; +using WebP.Natives.Enums; +using WebP.Natives.Structs; + +namespace WebP; + +public sealed class WebPObject : IDisposable { + #region Ctors + + public WebPObject(Image image) { + _initWithImage = true; + ImageCache = image; + } + + public WebPObject(byte[] bytes) { + BytesCache = bytes; + } + + #endregion + + #region Fields + + private readonly object _cacheLockHandle = new(); + + private byte[] _bytesCache; + + private readonly bool _initWithImage; + + internal const int WebpMaxDimension = 16383; + + #endregion + + #region Properties + + private (IntPtr Pointer, int Size) DynamicArray { get; set; } = (IntPtr.Zero, 0); + + private byte[] BytesCache { + get { + lock (_cacheLockHandle) { + if (_bytesCache is not null) + return _bytesCache; + if (DynamicArray.Pointer == IntPtr.Zero) + return null; + _bytesCache = new byte[DynamicArray.Size]; + Marshal.Copy(DynamicArray.Pointer, _bytesCache, 0, DynamicArray.Size); + return _bytesCache; + } + } + set => _bytesCache = value; + } + + private Image ImageCache { get; set; } + + private WebPInfo? InfoCache { get; set; } + + #endregion + + #region Private methods + + [method: MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void VerifyImage(Image image) { + switch (image) { + case null: + throw new ArgumentNullException(nameof(image)); + case { Width: 0 } or { Height: 0 }: + throw new DataException("Image contains no data."); + case { Width: > WebpMaxDimension } or { Height: > WebpMaxDimension }: + throw new IOException($"Image too big. {WebpMaxDimension}x{WebpMaxDimension} is maximal size."); + } + } + + [method: MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Bitmap ConvertTo32Argb(Image image) { + var bitmap = new Bitmap(image.Width, image.Height, PixelFormat.Format32bppArgb); + using var graphic = Graphics.FromImage(bitmap); + graphic.DrawImage(image, new Rectangle(0, 0, bitmap.Width, bitmap.Height)); + return bitmap; + } + + [method: MethodImpl(MethodImplOptions.AggressiveInlining)] + private static BitmapData GetBitmapData(Bitmap bitmap, ImageLockMode mode) { + return bitmap.LockBits(new Rectangle(0, 0, bitmap.Width, bitmap.Height), mode, bitmap.PixelFormat); + } + + [method: MethodImpl(MethodImplOptions.AggressiveInlining)] + private static WebPInfo GetInfoFrom(IntPtr pointer, int size) { + var features = new WebPBitstreamFeatures(); + var status = Native.WebPGetFeatures(pointer, size, ref features); + if (status is not Vp8StatusCode.Ok) + throw new ExternalException(status.ToString()); + return new WebPInfo(features); + } + + [method: MethodImpl(MethodImplOptions.AggressiveInlining)] + private void StoreEncodedResult(Image image, bool lossy, float quality) { + VerifyImage(image); + using var bitmap = ConvertTo32Argb(image); + BitmapData data = null; + try { + data = GetBitmapData(bitmap, ImageLockMode.ReadOnly); + var size = lossy + ? Native.WebPEncodeBgra(data.Scan0, data.Width, data.Height, data.Stride, quality, out var ptr) + : Native.WebPEncodeLosslessBgra(data.Scan0, data.Width, data.Height, data.Stride, out ptr); + if (size is 0) + throw new IOException("Cannot encode image by unknown cause."); + DynamicArray = (ptr, size); + } finally { + if (data is not null) bitmap.UnlockBits(data); + } + } + + private delegate int DecodeInto(IntPtr ptr, int size, IntPtr output, int outputSize, int outputStride); + + [method: MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Bitmap Decode(IntPtr pointer, int size) { + Bitmap bmp = null; + BitmapData data = null; + int length; + + try { + var info = GetInfoFrom(pointer, size); + bmp = new Bitmap(info.Width, info.Height, info.HasAlpha + ? PixelFormat.Format32bppArgb + : PixelFormat.Format24bppRgb); + data = GetBitmapData(bmp, ImageLockMode.WriteOnly); + length = ((DecodeInto)(info.HasAlpha + ? Native.WebPDecodeBgraInto + : Native.WebPDecodeBgrInto)) + .Invoke(pointer, size, data.Scan0, data.Stride * info.Height, data.Stride); + } finally { + if (data is not null) bmp.UnlockBits(data); + } + + if (length is 0) + throw new IOException("Cannot decode image by unknown cause."); + + return bmp; + } + + #endregion + + #region Public methods + + public Image GetImage() { + if (DynamicArray.Pointer != IntPtr.Zero) + return ImageCache ??= Decode(DynamicArray.Pointer, DynamicArray.Size); + + var cache = BytesCache; + var handle = GCHandle.Alloc(cache, GCHandleType.Pinned); + try { + return ImageCache ??= Decode(handle.AddrOfPinnedObject(), cache.Length); + } finally { + if (handle.IsAllocated) handle.Free(); + } + } + + public byte[] GetWebPLossy(float quality = 70, bool forceLossy = false) { + if (BytesCache is null) + StoreEncodedResult(ImageCache, true, quality); + else if (forceLossy) + StoreEncodedResult(GetImage(), true, quality); + return BytesCache; + } + + public byte[] GetWebPLossless() { + if (BytesCache is null) + StoreEncodedResult(ImageCache, false, 0); + return BytesCache; + } + + public WebPInfo GetInfo() { + if (InfoCache.HasValue) + return InfoCache.Value; + if (DynamicArray.Pointer != IntPtr.Zero) + return InfoCache ??= GetInfoFrom(DynamicArray.Pointer, DynamicArray.Size); + + var cache = BytesCache; + var handle = GCHandle.Alloc(cache, GCHandleType.Pinned); + try { + return InfoCache ??= GetInfoFrom(handle.AddrOfPinnedObject(), cache.Length); + } finally { + if (handle.IsAllocated) handle.Free(); + } + } + + #endregion + + #region Dtors + + ~WebPObject() { + Dispose(); + } + + public void Dispose() { + if (!_initWithImage) + ImageCache?.Dispose(); + if (DynamicArray.Pointer != IntPtr.Zero) + Native.WebPFree(DynamicArray.Pointer); + GC.SuppressFinalize(this); + } + + #endregion +}