Browse Source

WebP support :)

Former-commit-id: a5eadb739308ab7e2b63ab936f0c069430f91154
af/merge-core
James South 12 years ago
parent
commit
bc06684111
  1. 2
      src/ImageProcessor.Web/NET45/Caching/CachedImage.cs
  2. 52
      src/ImageProcessor.Web/NET45/Caching/DiskCache.cs
  3. 19
      src/ImageProcessor.Web/NET45/HttpModules/ImageProcessingModule.cs
  4. 22
      src/ImageProcessor/ImageFactory.cs
  5. 8
      src/ImageProcessor/ImageProcessor.csproj
  6. 316
      src/ImageProcessor/Imaging/Formats/WebPFormat.cs
  7. 1
      src/ImageProcessor/Settings.StyleCop
  8. 1
      src/ImageProcessor/libwebp.dll.REMOVED.git-id
  9. 6
      src/ImageProcessorConsole/Program.cs
  10. 3
      src/ImageProcessorConsole/images/input/4.sm.webp
  11. 3
      src/ImageProcessorConsole/images/input/test.webp
  12. 3
      src/ImageProcessorConsole/images/output/4.sm.webp
  13. 3
      src/ImageProcessorConsole/images/output/test.webp

2
src/ImageProcessor.Web/NET45/Caching/CachedImage.cs

@ -10,9 +10,7 @@
namespace ImageProcessor.Web.Caching
{
#region Using
using System;
#endregion
/// <summary>
/// Describes a cached image

52
src/ImageProcessor.Web/NET45/Caching/DiskCache.cs

@ -13,7 +13,6 @@ namespace ImageProcessor.Web.Caching
#region Using
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using System.Linq;
@ -96,24 +95,33 @@ namespace ImageProcessor.Web.Caching
this.requestPath = requestPath;
this.fullPath = fullPath;
this.imageName = imageName;
this.CachedPath = this.GetCachePath();
}
#endregion
#region Properties
#region Methods
#region Internal
/// <summary>
/// Gets the cached path.
/// Gets the full transformed cached path for the image.
/// The images are stored in paths that are based upon the SHA1 of their full request path
/// taking the individual characters of the hash to determine their location.
/// This allows us to store millions of images.
/// </summary>
internal string CachedPath { get; private set; }
#endregion
/// <returns>The full cached path for the image.</returns>
internal Task<string> GetCachePathAsync()
{
return TaskHelpers.Run<string>(this.GetCachePath);
}
#region Methods
#region Internal
/// <summary>
/// Gets the virtual path to the cached processed image.
/// </summary>
/// <returns>The virtual path to the cached processed image.</returns>
internal string GetVirtualCachedPath()
/// <param name="cachedPath">
/// The path to the cached image.
/// </param>
/// <returns>
/// The virtual path to the cached processed image.
/// </returns>
internal string GetVirtualCachedPath(string cachedPath)
{
string applicationPath = this.request.PhysicalApplicationPath;
string virtualDir = this.request.ApplicationPath;
@ -121,7 +129,7 @@ namespace ImageProcessor.Web.Caching
if (applicationPath != null)
{
return this.CachedPath.Replace(applicationPath, virtualDir).Replace(@"\", "/");
return cachedPath.Replace(applicationPath, virtualDir).Replace(@"\", "/");
}
throw new InvalidOperationException(
@ -131,13 +139,16 @@ namespace ImageProcessor.Web.Caching
/// <summary>
/// Adds an image to the cache.
/// </summary>
internal void AddImageToCache()
/// <param name="cachedPath">
/// The path to the cached image.
/// </param>
internal void AddImageToCache(string cachedPath)
{
string key = Path.GetFileNameWithoutExtension(this.CachedPath);
string key = Path.GetFileNameWithoutExtension(cachedPath);
CachedImage cachedImage = new CachedImage
{
Key = key,
Path = this.CachedPath,
Path = cachedPath,
CreationTimeUtc = DateTime.UtcNow
};
@ -147,14 +158,16 @@ namespace ImageProcessor.Web.Caching
/// <summary>
/// Returns a value indicating whether the original file is new or has been updated.
/// </summary>
/// <param name="cachedPath">
/// The path to the cached image.
/// </param>
/// <returns>
/// True if the the original file is new or has been updated; otherwise, false.
/// </returns>
internal async Task<bool> IsNewOrUpdatedFileAsync()
internal async Task<bool> IsNewOrUpdatedFileAsync(string cachedPath)
{
string path = this.CachedPath;
bool isUpdated = false;
CachedImage cachedImage = await CacheIndexer.GetValueAsync(path);
CachedImage cachedImage = await CacheIndexer.GetValueAsync(cachedPath);
if (cachedImage == null)
{
@ -166,7 +179,7 @@ namespace ImageProcessor.Web.Caching
// Check to see if the cached image is set to expire.
if (this.IsExpired(cachedImage.CreationTimeUtc))
{
CacheIndexer.Remove(path);
CacheIndexer.Remove(cachedPath);
isUpdated = true;
}
}
@ -236,12 +249,11 @@ namespace ImageProcessor.Web.Caching
/// <summary>
/// Gets the full transformed cached path for the image.
/// The images are stored in paths that are based upon the sha1 of their full request path
/// The images are stored in paths that are based upon the SHA1 of their full request path
/// taking the individual characters of the hash to determine their location.
/// This allows us to store millions of images.
/// </summary>
/// <returns>The full cached path for the image.</returns>
[SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1650:ElementDocumentationMustBeSpelledCorrectly", Justification = "Reviewed. Suppression is OK here.")]
private string GetCachePath()
{
string cachedPath = string.Empty;

19
src/ImageProcessor.Web/NET45/HttpModules/ImageProcessingModule.cs

@ -161,7 +161,6 @@ namespace ImageProcessor.Web.HttpModules
/// </returns>
private static SemaphoreSlim GetSemaphoreSlim(string id)
{
id = id.ToMD5Fingerprint();
SemaphoreSlim semaphore = SemaphoreSlims.GetOrAdd(id, new SemaphoreSlim(1, 1));
return semaphore;
}
@ -348,13 +347,14 @@ namespace ImageProcessor.Web.HttpModules
// Create a new cache to help process and cache the request.
DiskCache cache = new DiskCache(request, requestPath, fullPath, imageName);
string cachedPath = await cache.GetCachePathAsync();
// Since we are now rewriting the path we need to check again that the current user has access
// to the rewritten path.
// Get the user for the current request
// If the user is anonymous or authentication doesn't work for this suffix avoid a NullReferenceException
// in the UrlAuthorizationModule by creating a generic identity.
string virtualCachedPath = cache.GetVirtualCachedPath();
string virtualCachedPath = cache.GetVirtualCachedPath(cachedPath);
IPrincipal user = context.User ?? new GenericPrincipal(new GenericIdentity(string.Empty, string.Empty), new string[0]);
@ -362,6 +362,7 @@ namespace ImageProcessor.Web.HttpModules
PermissionSet permission = new PermissionSet(PermissionState.None);
permission.AddPermission(new AspNetHostingPermission(AspNetHostingPermissionLevel.Unrestricted));
bool hasPermission = permission.IsSubsetOf(AppDomain.CurrentDomain.PermissionSet);
bool isAllowed = true;
// Run the rewritten path past the auth system again, using the result as the default "AllowAccess" value
@ -373,13 +374,11 @@ namespace ImageProcessor.Web.HttpModules
if (isAllowed)
{
// Is the file new or updated?
bool isNewOrUpdated = await cache.IsNewOrUpdatedFileAsync();
bool isNewOrUpdated = await cache.IsNewOrUpdatedFileAsync(cachedPath);
// Only process if the file has been updated.
if (isNewOrUpdated)
{
string cachedPath = cache.CachedPath;
// Process the image.
using (ImageFactory imageFactory = new ImageFactory(preserveExifMetaData != null && preserveExifMetaData.Value))
{
@ -419,7 +418,7 @@ namespace ImageProcessor.Web.HttpModules
context.Items[CachedResponseTypeKey] = imageFactory.CurrentImageFormat.MimeType;
// Add to the cache.
cache.AddImageToCache();
cache.AddImageToCache(cachedPath);
// Trim the cache.
await cache.TrimCachedFolderAsync(cachedPath);
@ -458,7 +457,7 @@ namespace ImageProcessor.Web.HttpModules
context.Items[CachedResponseTypeKey] = imageFactory.CurrentImageFormat.MimeType;
// Add to the cache.
cache.AddImageToCache();
cache.AddImageToCache(cachedPath);
// Trim the cache.
await cache.TrimCachedFolderAsync(cachedPath);
@ -480,7 +479,7 @@ namespace ImageProcessor.Web.HttpModules
context.Response.AddHeader("Content-Length", "0");
context.Response.StatusCode = (int)HttpStatusCode.NotModified;
context.Response.SuppressContent = true;
context.Response.AddFileDependency(context.Server.MapPath(cache.GetVirtualCachedPath()));
context.Response.AddFileDependency(context.Server.MapPath(virtualCachedPath));
this.SetHeaders(context, (string)context.Items[CachedResponseTypeKey]);
if (!isRemote)
@ -489,10 +488,8 @@ namespace ImageProcessor.Web.HttpModules
}
}
string virtualPath = cache.GetVirtualCachedPath();
// The cached file is valid so just rewrite the path.
context.RewritePath(virtualPath, false);
context.RewritePath(virtualCachedPath, false);
}
else
{

22
src/ImageProcessor/ImageFactory.cs

@ -120,24 +120,24 @@ namespace ImageProcessor
internal Image Image { get; set; }
/// <summary>
/// Gets or sets the memory stream for storing any input stream to prevent disposal.
/// Gets or sets the stream for storing any input stream to prevent disposal.
/// </summary>
internal MemoryStream InputStream { get; set; }
internal Stream InputStream { get; set; }
#endregion
#region Methods
/// <summary>
/// Loads the image to process. Always call this method first.
/// </summary>
/// <param name="memoryStream">
/// The <see cref="T:System.IO.MemoryStream"/> containing the image information.
/// <param name="stream">
/// The <see cref="T:System.IO.Stream"/> containing the image information.
/// </param>
/// <returns>
/// The current instance of the <see cref="T:ImageProcessor.ImageFactory"/> class.
/// </returns>
public ImageFactory Load(MemoryStream memoryStream)
public ImageFactory Load(Stream stream)
{
ISupportedImageFormat format = FormatUtilities.GetFormat(memoryStream);
ISupportedImageFormat format = FormatUtilities.GetFormat(stream);
if (format == null)
{
@ -145,10 +145,10 @@ namespace ImageProcessor
}
// Set our image as the memory stream value.
this.Image = format.Load(memoryStream);
this.Image = format.Load(stream);
// Store the stream so we can dispose of it later.
this.InputStream = memoryStream;
this.InputStream = stream;
// Set the other properties.
format.Quality = DefaultQuality;
@ -831,17 +831,17 @@ namespace ImageProcessor
/// <summary>
/// Saves the current image to the specified output stream.
/// </summary>
/// <param name="memoryStream">
/// <param name="stream">
/// The <see cref="T:System.IO.MemoryStream"/> to save the image information to.
/// </param>
/// <returns>
/// The current instance of the <see cref="T:ImageProcessor.ImageFactory"/> class.
/// </returns>
public ImageFactory Save(MemoryStream memoryStream)
public ImageFactory Save(Stream stream)
{
if (this.ShouldProcess)
{
this.Image = this.CurrentImageFormat.Save(memoryStream, this.Image);
this.Image = this.CurrentImageFormat.Save(stream, this.Image);
}
return this;

8
src/ImageProcessor/ImageProcessor.csproj

@ -52,7 +52,6 @@
<Reference Include="System.Configuration" />
<Reference Include="System.Core" />
<Reference Include="System.Drawing" />
<Reference Include="System.Web" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" />
@ -84,6 +83,7 @@
<Compile Include="Imaging\Formats\FormatUtilities.cs" />
<Compile Include="Imaging\Formats\FormatBase.cs" />
<Compile Include="Imaging\Formats\ISupportedImageFormat.cs" />
<Compile Include="Imaging\Formats\WebPFormat.cs" />
<Compile Include="Imaging\GaussianLayer.cs" />
<Compile Include="Imaging\Formats\GifEncoder.cs" />
<Compile Include="Imaging\Formats\GifFrame.cs" />
@ -128,7 +128,11 @@
<Compile Include="Processors\Watermark.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup>
<ItemGroup />
<ItemGroup>
<Content Include="libwebp.dll">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<Import Project="$(SolutionDir)\.nuget\NuGet.targets" Condition="Exists('$(SolutionDir)\.nuget\NuGet.targets')" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.

316
src/ImageProcessor/Imaging/Formats/WebPFormat.cs

@ -0,0 +1,316 @@
// --------------------------------------------------------------------------------------------------------------------
// <copyright file="WebPFormat.cs" company="James South">
// Copyright (c) James South.
// Licensed under the Apache License, Version 2.0.
// </copyright>
// <summary>
// Provides the necessary information to support webp images.
// Adapted from <see cref="https://groups.google.com/a/webmproject.org/forum/#!topic/webp-discuss/1coeidT0rQU"/>
// by Jose M. Piñeiro
// </summary>
// --------------------------------------------------------------------------------------------------------------------
namespace ImageProcessor.Imaging.Formats
{
using System;
using System.Diagnostics.CodeAnalysis;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
/// <summary>
/// Provides the necessary information to support webp images.
/// Adapted from <see cref="http://groups.google.com/a/webmproject.org/forum/#!topic/webp-discuss/1coeidT0rQU"/>
/// by Jose M. Piñeiro
/// </summary>
[SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1650:ElementDocumentationMustBeSpelledCorrectly", Justification = "Reviewed. Suppression is OK here.")]
public class WebPFormat : FormatBase
{
/// <summary>
/// Gets the file headers.
/// </summary>
public override byte[][] FileHeaders
{
get
{
return new[] { Encoding.ASCII.GetBytes("RIFF") };
}
}
/// <summary>
/// Gets the list of file extensions.
/// </summary>
public override string[] FileExtensions
{
get
{
return new[] { "webp" };
}
}
/// <summary>
/// Gets the standard identifier used on the Internet to indicate the type of data that a file contains.
/// </summary>
public override string MimeType
{
get
{
return "image/webp";
}
}
/// <summary>
/// Gets the file format of the image.
/// </summary>
public override ImageFormat ImageFormat
{
get
{
return null;
}
}
/// <summary>
/// Decodes the image to process.
/// </summary>
/// <param name="stream">
/// The <see cref="T:System.IO.stream" /> containing the image information.
/// </param>
/// <returns>
/// The <see cref="T:System.Drawing.Image" />.
/// </returns>
public override Image Load(Stream stream)
{
byte[] bytes = new byte[stream.Length];
stream.Read(bytes, 0, bytes.Length);
return Decode(bytes);
}
/// <summary>
/// Saves the current image to the specified file path.
/// </summary>
/// <param name="path">The path to save the image to.</param>
/// <param name="image">The
/// <see cref="T:System.Drawing.Image" /> to save.</param>
/// <returns>
/// The <see cref="T:System.Drawing.Image" />.
/// </returns>
public override Image Save(string path, Image image)
{
byte[] bytes;
// Encode in webP format.
if (EncodeLossly((Bitmap)image, this.Quality, out bytes))
{
File.WriteAllBytes(path, bytes);
}
return image;
}
/// <summary>
/// Saves the current image to the specified output stream.
/// </summary>
/// <param name="stream">The <see cref="T:System.IO.Stream" /> to save the image information to.</param>
/// <param name="image">The <see cref="T:System.Drawing.Image" /> to save.</param>
/// <returns>
/// The <see cref="T:System.Drawing.Image" />.
/// </returns>
public override Image Save(Stream stream, Image image)
{
byte[] bytes;
// Encode in webP format.
if (EncodeLossly((Bitmap)image, this.Quality, out bytes))
{
using (MemoryStream memoryStream = new MemoryStream(bytes))
{
memoryStream.CopyTo(stream);
memoryStream.Position = stream.Position = 0;
}
}
return image;
}
/// <summary>
/// Decodes a WebP image
/// </summary>
/// <param name="webpData">
/// The data to uncompress
/// </param>
/// <returns>
/// The <see cref="T:System.Drawing.Bitmap" />.
/// </returns>
private static Bitmap Decode(byte[] webpData)
{
// Get the image width and height
GCHandle pinnedWebP = GCHandle.Alloc(webpData, GCHandleType.Pinned);
IntPtr ptrData = pinnedWebP.AddrOfPinnedObject();
uint dataSize = (uint)webpData.Length;
int imgWidth;
int imgHeight;
if (WebPGetInfo(ptrData, dataSize, out imgWidth, out imgHeight) != 1)
{
// TODO: Throw error?
return null;
}
// Create a BitmapData and Lock all pixels to be written
Bitmap bmp = new Bitmap(imgWidth, imgHeight, PixelFormat.Format24bppRgb);
BitmapData bmpData = bmp.LockBits(new Rectangle(0, 0, bmp.Width, bmp.Height), ImageLockMode.WriteOnly, bmp.PixelFormat);
// Allocate memory for uncompress image
int outputBufferSize = bmpData.Stride * imgHeight;
IntPtr outputBuffer = Marshal.AllocHGlobal(outputBufferSize);
// Uncompress the image
outputBuffer = WebPDecodeBGRInto(ptrData, dataSize, outputBuffer, outputBufferSize, bmpData.Stride);
// Write image to bitmap using Marshal
byte[] buffer = new byte[outputBufferSize];
Marshal.Copy(outputBuffer, buffer, 0, outputBufferSize);
Marshal.Copy(buffer, 0, bmpData.Scan0, outputBufferSize);
// Unlock the pixels
bmp.UnlockBits(bmpData);
// Free memory
pinnedWebP.Free();
Marshal.FreeHGlobal(outputBuffer);
return bmp;
}
/// <summary>
/// Lossly encodes the image in bitmap.
/// </summary>
/// <param name="bitmap">
/// Bitmap with the image
/// </param>
/// <param name="quality">
/// Quality. 0 = minimum ... 100 = maximimun quality
/// </param>
/// <param name="webpData">
/// The byte array containing the encoded image data.
/// </param>
/// <returns>
/// True if success; False otherwise
/// </returns>
private static bool EncodeLossly(Bitmap bitmap, int quality, out byte[] webpData)
{
webpData = null;
try
{
BitmapData bmpData = bitmap.LockBits(new Rectangle(0, 0, bitmap.Width, bitmap.Height), ImageLockMode.ReadOnly, PixelFormat.Format24bppRgb);
IntPtr unmanagedData;
int size = WebPEncodeBGR(bmpData.Scan0, bitmap.Width, bitmap.Height, bmpData.Stride, quality, out unmanagedData);
// Copy image compress data to output array
webpData = new byte[size];
Marshal.Copy(unmanagedData, webpData, 0, size);
// Unlock the pixels
bitmap.UnlockBits(bmpData);
// Free memory
WebPFree(unmanagedData);
return true;
}
catch
{
return false;
}
}
/// <summary>
/// Validate the WebP image header and retrieve the image height and width. Pointers *width and *height can be passed NULL if deemed irrelevant
/// </summary>
/// <param name="data">
/// Pointer to WebP image data
/// </param>
/// <param name="dataSize">
/// This is the size of the memory block pointed to by data containing the image data
/// </param>
/// <param name="width">
/// The width range is limited currently from 1 to 16383
/// </param>
/// <param name="height">
/// The height range is limited currently from 1 to 16383
/// </param>
/// <returns>
/// 1 if success, otherwise error code returned in the case of (a) formatting error(s).
/// </returns>
[DllImport("libwebp.dll", CallingConvention = CallingConvention.Cdecl)]
private static extern int WebPGetInfo(IntPtr data, uint dataSize, out int width, out int height);
/// <summary>
/// Decode WEBP image pointed to by *data and returns BGR samples into a pre-allocated buffer
/// </summary>
/// <param name="data">
/// Pointer to WebP image data
/// </param>
/// <param name="dataSize">
/// This is the size of the memory block pointed to by data containing the image data
/// </param>
/// <param name="outputBuffer">
/// Pointer to decoded WebP image
/// </param>
/// <param name="outputBufferSize">
/// Size of allocated buffer
/// </param>
/// <param name="outputStride">
/// Specifies the distance between scanlines
/// </param>
/// <returns>
/// output_buffer if function succeeds; NULL otherwise
/// </returns>
[DllImport("libwebp.dll", CallingConvention = CallingConvention.Cdecl)]
private static extern IntPtr WebPDecodeBGRInto(IntPtr data, uint dataSize, IntPtr outputBuffer, int outputBufferSize, int outputStride);
/// <summary>
/// Lossless encoding images pointed to by *data in WebP format
/// </summary>
/// <param name="rgb">
/// Pointer to RGB image data
/// </param>
/// <param name="width">
/// The width range is limited currently from 1 to 16383
/// </param>
/// <param name="height">
/// The height range is limited currently from 1 to 16383
/// </param>
/// <param name="stride">
/// The stride.
/// </param>
/// <param name="qualityFactor">
/// Ranges from 0 (lower quality) to 100 (highest quality). Controls the loss and quality during compression
/// </param>
/// <param name="output">
/// output_buffer with WebP image
/// </param>
/// <returns>
/// Size of WebP Image
/// </returns>
[DllImport("libwebp.dll", CallingConvention = CallingConvention.Cdecl)]
private static extern int WebPEncodeBGR(IntPtr rgb, int width, int height, int stride, float qualityFactor, out IntPtr output);
/// <summary>
/// Frees the unmanaged memory.
/// </summary>
/// <param name="p">
/// The pointer.
/// </param>
/// <returns>
/// 1 if success, otherwise error code returned in the case of (a) error(s).
/// </returns>
[DllImport("libwebp.dll", CallingConvention = CallingConvention.Cdecl)]
private static extern int WebPFree(IntPtr p);
}
}

1
src/ImageProcessor/Settings.StyleCop

@ -13,6 +13,7 @@
<Value>octree</Value>
<Value>png</Value>
<Value>quantizer</Value>
<Value>webp</Value>
</CollectionProperty>
</GlobalSettings>
<Analyzers>

1
src/ImageProcessor/libwebp.dll.REMOVED.git-id

@ -0,0 +1 @@
ed0333da619a813e8337d77ba7ee206ea5f20a51

6
src/ImageProcessorConsole/Program.cs

@ -13,6 +13,7 @@ namespace ImageProcessorConsole
using System.Drawing;
using System.IO;
using ImageProcessor;
using ImageProcessor.Imaging.Formats;
/// <summary>
/// The program.
@ -36,7 +37,8 @@ namespace ImageProcessorConsole
di.Create();
}
FileInfo[] files = di.GetFiles("*.gif");
//FileInfo[] files = di.GetFiles("*.gif");
FileInfo[] files = di.GetFiles();
foreach (FileInfo fileInfo in files)
{
@ -52,7 +54,9 @@ namespace ImageProcessorConsole
// Load, resize, set the format and quality and save an image.
imageFactory.Load(inStream)
.Constrain(size)
//.Format(new JpegFormat())
// ReSharper disable once AssignNullToNotNullAttribute
// .Save(Path.GetFullPath(Path.Combine(Path.GetDirectoryName(path), @"..\..\images\output", Path.GetFileNameWithoutExtension(fileInfo.Name) + ".jpg")));
.Save(Path.GetFullPath(Path.Combine(Path.GetDirectoryName(path), @"..\..\images\output", fileInfo.Name)));
}
}

3
src/ImageProcessorConsole/images/input/4.sm.webp

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:5858927e93548de019aa2f18c4437e4fa9d50e87054ef1e44a3e38fedb36c5ae
size 20772

3
src/ImageProcessorConsole/images/input/test.webp

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:1bf9449d529b491c6eb22ec84d42217e43cf9af0ffe391fa7a30b29b037f098a
size 4880

3
src/ImageProcessorConsole/images/output/4.sm.webp

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:bc9ccd29734ed06d2916389cc363955924f4bbb200fea91c113ae2b9334eca1f
size 12162

3
src/ImageProcessorConsole/images/output/test.webp

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b41b4fe144f9d3eaee706b516021efc7abc598f17655d9592866963bfa62f3ad
size 5590
Loading…
Cancel
Save