Browse Source

Bug fixes in cache

Former-commit-id: 07050d4d50fcaadffd6dda4300ddc27b69b15127
af/merge-core
James South 13 years ago
parent
commit
64a1fedb07
  1. 12
      src/ImageProcessor.Web/Caching/CachedImage.cs
  2. 221
      src/ImageProcessor.Web/Caching/DiskCache.cs
  3. 4
      src/ImageProcessor.Web/Caching/SQLContext.cs
  4. 72
      src/ImageProcessor.Web/HttpModules/ImageProcessingModule.cs
  5. 6
      src/ImageProcessor.Web/ImageFactoryExtensions.cs
  6. 46
      src/Test/Test/Web.config

12
src/ImageProcessor.Web/Caching/CachedImage.cs

@ -14,7 +14,7 @@ namespace ImageProcessor.Web.Caching
/// <summary> /// <summary>
/// Describes a cached image /// Describes a cached image
/// </summary> /// </summary>
internal sealed class CachedImage internal sealed class CachedImage : IComparable<CachedImage>
{ {
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="CachedImage"/> class. /// Initializes a new instance of the <see cref="CachedImage"/> class.
@ -49,5 +49,15 @@ namespace ImageProcessor.Web.Caching
/// Gets or sets when the cached image should expire from the cache. /// Gets or sets when the cached image should expire from the cache.
/// </summary> /// </summary>
public DateTime ExpiresUtc { get; set; } public DateTime ExpiresUtc { get; set; }
/// <summary>
///
/// </summary>
/// <param name="other"></param>
/// <returns></returns>
public int CompareTo(CachedImage other)
{
return this.ExpiresUtc.CompareTo(other.ExpiresUtc);
}
} }
} }

221
src/ImageProcessor.Web/Caching/DiskCache.cs

@ -27,17 +27,6 @@ namespace ImageProcessor.Web.Caching
internal sealed class DiskCache internal sealed class DiskCache
{ {
#region Fields #region Fields
/// <summary>
/// The maximum number or time a new file should be cached before checking the
/// cache controller and running any clearing mechanisms.
/// </summary>
/// <remarks>
/// NTFS file systems can handle up to 8000 files in one directory. The Cache controller will clear out any
/// time we hit 6000 so if we tell the handler to run at every 1000 times an image is added to the cache we
/// should have a 1000 file buffer.
/// </remarks>
internal const int MaxRunsBeforeCacheClear = 1000;
/// <summary> /// <summary>
/// The maximum number of days to cache files on the system for. /// The maximum number of days to cache files on the system for.
/// </summary> /// </summary>
@ -47,10 +36,10 @@ namespace ImageProcessor.Web.Caching
/// The maximum number of files allowed in the directory. /// The maximum number of files allowed in the directory.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// NTFS Folder can handle up to 8000 files in a directory. /// NTFS directories can handle up to 8000 files in the directory before slowing down.
/// This buffer will help us to ensure that we rarely hit anywhere near that limit. /// This buffer will help us to ensure that we rarely hit anywhere near that limit.
/// </remarks> /// </remarks>
private const int MaxFilesCount = 6500; private const int MaxFilesCount = 7500;
/// <summary> /// <summary>
/// The regular expression to search strings for extension changes. /// The regular expression to search strings for extension changes.
@ -58,12 +47,7 @@ namespace ImageProcessor.Web.Caching
private static readonly Regex FormatRegex = new Regex(@"format=(jpeg|png|bmp|gif)", RegexOptions.Compiled); private static readonly Regex FormatRegex = new Regex(@"format=(jpeg|png|bmp|gif)", RegexOptions.Compiled);
/// <summary> /// <summary>
/// The object to lock against. /// The default paths for Cached folders on the server.
/// </summary>
private static readonly object SyncRoot = new object();
/// <summary>
/// The default paths for Cached folders on the server.
/// </summary> /// </summary>
private static readonly string CachePath = ImageProcessorConfig.Instance.VirtualCachePath; private static readonly string CachePath = ImageProcessorConfig.Instance.VirtualCachePath;
#endregion #endregion
@ -122,8 +106,8 @@ namespace ImageProcessor.Web.Caching
internal static void AddImageToCache(string cachedPath, DateTime lastWriteTimeUtc) internal static void AddImageToCache(string cachedPath, DateTime lastWriteTimeUtc)
{ {
string key = Path.GetFileNameWithoutExtension(cachedPath); string key = Path.GetFileNameWithoutExtension(cachedPath);
DateTime expires = lastWriteTimeUtc.AddDays(MaxFileCachedDuration).ToUniversalTime(); DateTime expires = DateTime.UtcNow.AddDays(MaxFileCachedDuration).ToUniversalTime();
CachedImage cachedImage = new CachedImage(key, lastWriteTimeUtc, expires); CachedImage cachedImage = new CachedImage(cachedPath, lastWriteTimeUtc, expires);
PersistantDictionary.Instance.Add(key, cachedImage); PersistantDictionary.Instance.Add(key, cachedImage);
} }
@ -171,17 +155,27 @@ namespace ImageProcessor.Web.Caching
/// </returns> /// </returns>
internal static bool IsUpdatedFile(string imagePath, string cachedImagePath) internal static bool IsUpdatedFile(string imagePath, string cachedImagePath)
{ {
string key = Path.GetFileNameWithoutExtension(cachedImagePath);
CachedImage cachedImage;
bool isUpdated = false;
if (File.Exists(imagePath)) if (File.Exists(imagePath))
{ {
CachedImage image;
string key = Path.GetFileNameWithoutExtension(cachedImagePath);
PersistantDictionary.Instance.TryGetValue(key, out image);
FileInfo imageFileInfo = new FileInfo(imagePath); FileInfo imageFileInfo = new FileInfo(imagePath);
return image != null && imageFileInfo.LastWriteTimeUtc.Equals(image.LastWriteTimeUtc); if (PersistantDictionary.Instance.TryGetValue(key, out cachedImage))
{
if (!imageFileInfo.LastWriteTimeUtc.Equals(cachedImage.LastWriteTimeUtc))
{
if (PersistantDictionary.Instance.TryRemove(key, out cachedImage))
{
isUpdated = true;
}
}
}
} }
return true; return isUpdated;
} }
/// <summary> /// <summary>
@ -198,99 +192,124 @@ namespace ImageProcessor.Web.Caching
/// </returns> /// </returns>
internal static DateTime SetCachedLastWriteTime(string imagePath, string cachedImagePath) internal static DateTime SetCachedLastWriteTime(string imagePath, string cachedImagePath)
{ {
lock (SyncRoot) if (File.Exists(imagePath) && File.Exists(cachedImagePath))
{ {
if (File.Exists(imagePath) && File.Exists(cachedImagePath)) DateTime dateTime = File.GetLastWriteTimeUtc(imagePath);
{ File.SetLastWriteTimeUtc(cachedImagePath, dateTime);
DateTime dateTime = File.GetLastWriteTimeUtc(imagePath); return dateTime;
File.SetLastWriteTimeUtc(cachedImagePath, dateTime);
return dateTime;
}
} }
return DateTime.MinValue; return DateTime.MinValue.ToUniversalTime();
} }
/// <summary> /// <summary>
/// Purges any files from the file-system cache in the given folders. /// Purges any files from the file-system cache in the given folders.
/// </summary> /// </summary>
private static void PurgeFolders() internal static void PurgeFolders()
{ {
// Group each cache folder and clear any expired items or any that exeed
// the maximum allowable count.
Regex searchTerm = new Regex(@"(jpeg|png|bmp|gif)"); Regex searchTerm = new Regex(@"(jpeg|png|bmp|gif)");
var list = PersistantDictionary.Instance.ToList() var groups = PersistantDictionary.Instance.ToList()
.GroupBy(x => searchTerm.Match(x.Value.Path)) .GroupBy(x => searchTerm.Match(x.Value.Path).Value)
.Select(y => new .Where(g => g.Count() > MaxFilesCount);
{ //.Where(g => g.Count() > MaxFilesCount
Path = y.Key, // || g.Select(a => a.Value.ExpiresUtc < DateTime.UtcNow.AddDays(-MaxFileCachedDuration)).Count() > 0);
Expires = y.Select(z => z.Value.ExpiresUtc),
Count = y.Sum(z => z.Key.Count())
})
.AsEnumerable();
foreach (var path in list)
{
}
foreach (var group in groups)
string folder = HostingEnvironment.MapPath(CachePath);
if (folder != null)
{ {
DirectoryInfo directoryInfo = new DirectoryInfo(folder); int groupCount = group.Count();
if (directoryInfo.Exists) foreach (KeyValuePair<string, CachedImage> pair in group.OrderBy(x => x.Value.ExpiresUtc))
{ {
List<DirectoryInfo> directoryInfos = directoryInfo // If the group count is equal to the max count minus 1 then we know we
.EnumerateDirectories("*", SearchOption.TopDirectoryOnly) // are counting down from a full directory not simply clearing out
.ToList(); // expired items.
if (groupCount == MaxFilesCount - 1)
{
break;
}
// Delete each CachedImage.
try
{
FileInfo fileInfo = new FileInfo(pair.Value.Path);
// Remove from the cache.
string key = Path.GetFileNameWithoutExtension(fileInfo.Name);
CachedImage cachedImage;
Parallel.ForEach( if (PersistantDictionary.Instance.TryRemove(key, out cachedImage))
directoryInfos,
subDirectoryInfo =>
{ {
// Get all the files in the cache ordered by LastAccessTime - oldest first. fileInfo.Delete();
List<FileInfo> fileInfos = subDirectoryInfo.EnumerateFiles("*", SearchOption.TopDirectoryOnly) groupCount -= 1;
.OrderBy(x => x.LastAccessTimeUtc).ToList(); }
}
int counter = fileInfos.Count; catch (Exception)
{
Parallel.ForEach( // Do Nothing, skip to the next.
fileInfos, // TODO: Should we handle this?
fileInfo => continue;
{ }
// Delete the file if we are nearing our limit buffer.
if (counter >= MaxFilesCount || fileInfo.LastAccessTimeUtc < DateTime.UtcNow.AddDays(-MaxFileCachedDuration))
{
lock (SyncRoot)
{
try
{
// Remove from the cache.
string key = Path.GetFileNameWithoutExtension(fileInfo.Name);
CachedImage cachedImage;
if (PersistantDictionary.Instance.TryGetValue(key, out cachedImage))
{
if (PersistantDictionary.Instance.TryRemove(key, out cachedImage))
{
fileInfo.Delete();
counter -= 1;
}
}
}
catch (IOException)
{
// Do Nothing, skip to the next.
// TODO: Should we handle this?
}
}
}
});
});
} }
} }
//string folder = HostingEnvironment.MapPath(CachePath);
//if (folder != null)
//{
// DirectoryInfo directoryInfo = new DirectoryInfo(folder);
// if (directoryInfo.Exists)
// {
// List<DirectoryInfo> directoryInfos = directoryInfo
// .EnumerateDirectories("*", SearchOption.TopDirectoryOnly)
// .ToList();
// Parallel.ForEach(
// directoryInfos,
// subDirectoryInfo =>
// {
// // Get all the files in the cache ordered by LastAccessTime - oldest first.
// List<FileInfo> fileInfos = subDirectoryInfo.EnumerateFiles("*", SearchOption.TopDirectoryOnly)
// .OrderBy(x => x.LastAccessTimeUtc).ToList();
// int counter = fileInfos.Count;
// Parallel.ForEach(
// fileInfos,
// fileInfo =>
// {
// // Delete the file if we are nearing our limit buffer.
// if (counter >= MaxFilesCount || fileInfo.LastAccessTimeUtc < DateTime.UtcNow.AddDays(-MaxFileCachedDuration))
// {
// lock (SyncRoot)
// {
// try
// {
// // Remove from the cache.
// string key = Path.GetFileNameWithoutExtension(fileInfo.Name);
// CachedImage cachedImage;
// if (PersistantDictionary.Instance.TryGetValue(key, out cachedImage))
// {
// if (PersistantDictionary.Instance.TryRemove(key, out cachedImage))
// {
// fileInfo.Delete();
// counter -= 1;
// }
// }
// }
// catch (IOException)
// {
// // Do Nothing, skip to the next.
// // TODO: Should we handle this?
// }
// }
// }
// });
// });
// }
//}
} }
/// <summary> /// <summary>

4
src/ImageProcessor.Web/Caching/SQLContext.cs

@ -72,7 +72,7 @@ namespace ImageProcessor.Web.Caching
LastWriteTimeUtc TEXT, LastWriteTimeUtc TEXT,
ExpiresUtc TEXT, ExpiresUtc TEXT,
PRIMARY KEY (Key), PRIMARY KEY (Key),
UNIQUE (Value));"; UNIQUE (Path));";
command.ExecuteNonQuery(); command.ExecuteNonQuery();
} }
@ -198,7 +198,7 @@ namespace ImageProcessor.Web.Caching
CachedImage image = new CachedImage( CachedImage image = new CachedImage(
reader["Path"].ToString(), reader["Path"].ToString(),
DateTime.Parse(reader["LastWriteTimeUtc"].ToString()).ToUniversalTime(), DateTime.Parse(reader["LastWriteTimeUtc"].ToString()).ToUniversalTime(),
DateTime.Parse(reader["LastWriteTimeUtc"].ToString()).ToUniversalTime()); DateTime.Parse(reader["ExpiresUtc"].ToString()).ToUniversalTime());
dictionary.Add(key, image); dictionary.Add(key, image);
} }

72
src/ImageProcessor.Web/HttpModules/ImageProcessingModule.cs

@ -37,14 +37,9 @@ namespace ImageProcessor.Web.HttpModules
private static readonly string RemotePrefix = ImageProcessorConfig.Instance.RemotePrefix; private static readonly string RemotePrefix = ImageProcessorConfig.Instance.RemotePrefix;
/// <summary> /// <summary>
/// Whether this is the first run of the handler. /// The object to lock against.
/// </summary> /// </summary>
private static bool isFirstRun = true; private static readonly object SyncRoot = new object();
/// <summary>
/// A counter for keeping track of how many images have been added to the cache.
/// </summary>
private static int cachedImageCounter;
#endregion #endregion
#region IHttpModule Members #region IHttpModule Members
@ -106,16 +101,6 @@ namespace ImageProcessor.Web.HttpModules
if (ImageUtils.IsValidImageExtension(path) && !string.IsNullOrWhiteSpace(queryString)) if (ImageUtils.IsValidImageExtension(path) && !string.IsNullOrWhiteSpace(queryString))
{ {
// Check to see if this is the first run and if so run the cache controller.
if (isFirstRun)
{
// Trim the cache.
DiskCache.PurgeCachedFolders();
// Disable the controller.
isFirstRun = false;
}
string fullPath = string.Format("{0}?{1}", path, queryString); string fullPath = string.Format("{0}?{1}", path, queryString);
string imageName = Path.GetFileName(path); string imageName = Path.GetFileName(path);
string cachedPath = DiskCache.GetCachePath(fullPath, imageName); string cachedPath = DiskCache.GetCachePath(fullPath, imageName);
@ -141,46 +126,51 @@ namespace ImageProcessor.Web.HttpModules
{ {
if (responseStream != null) if (responseStream != null)
{ {
responseStream.CopyTo(memoryStream); lock (SyncRoot)
{
// Trim the cache.
DiskCache.PurgeFolders();
responseStream.CopyTo(memoryStream);
imageFactory.Load(memoryStream)
.AddQueryString(queryString)
.Format(ImageUtils.GetImageFormat(imageName))
.AutoProcess().Save(cachedPath);
// Ensure that the LastWriteTime property of the source and cached file match.
DateTime dateTime = DiskCache.SetCachedLastWriteTime(path, cachedPath);
imageFactory.Load(memoryStream) // Add to the cache.
.AddQueryString(queryString) DiskCache.AddImageToCache(cachedPath, dateTime);
.Format(ImageUtils.GetImageFormat(imageName)) }
.AutoProcess().Save(cachedPath);
} }
} }
} }
} }
else else
{ {
imageFactory.Load(fullPath).AutoProcess().Save(cachedPath); lock (SyncRoot)
} {
} // Trim the cache.
DiskCache.PurgeFolders();
// Add 1 to the counter imageFactory.Load(fullPath).AutoProcess().Save(cachedPath);
cachedImageCounter += 1;
// Ensure that the LastWriteTime property of the source and cached file match. // Ensure that the LastWriteTime property of the source and cached file match.
DateTime dateTime = DiskCache.SetCachedLastWriteTime(path, cachedPath); DateTime dateTime = DiskCache.SetCachedLastWriteTime(path, cachedPath);
// Add to the cache. // Add to the cache.
DiskCache.AddImageToCache(cachedPath, dateTime); DiskCache.AddImageToCache(cachedPath, dateTime);
}
}
}
} }
context.Items[CachedResponseTypeKey] = ImageUtils.GetResponseType(imageName).ToDescription(); context.Items[CachedResponseTypeKey] = ImageUtils.GetResponseType(imageName).ToDescription();
// The cached file is valid so just rewrite the path. // The cached file is valid so just rewrite the path.
context.RewritePath(DiskCache.GetVirtualPath(cachedPath, context.Request), false); context.RewritePath(DiskCache.GetVirtualPath(cachedPath, context.Request), false);
// If the number of cached imaged hits the maximum allowed for this session then we clear
// the cache again and reset the counter.
// TODO: There is a potential concurrency issue here but collision probability is very low
// it would be nice to nail it though.
if (cachedImageCounter >= DiskCache.MaxRunsBeforeCacheClear)
{
DiskCache.PurgeCachedFolders();
cachedImageCounter = 0;
}
} }
} }
} }

6
src/ImageProcessor.Web/ImageFactoryExtensions.cs

@ -39,8 +39,8 @@ namespace ImageProcessor.Web
if (factory.ShouldProcess) if (factory.ShouldProcess)
{ {
// TODO: This is going to be a bottleneck for speed. Find a faster way. // TODO: This is going to be a bottleneck for speed. Find a faster way.
lock (SyncRoot) //lock (SyncRoot)
{ //{
// Get a list of all graphics processors that have parsed and matched the querystring. // Get a list of all graphics processors that have parsed and matched the querystring.
List<IGraphicsProcessor> list = List<IGraphicsProcessor> list =
ImageProcessorConfig.Instance.GraphicsProcessors ImageProcessorConfig.Instance.GraphicsProcessors
@ -53,7 +53,7 @@ namespace ImageProcessor.Web
{ {
factory.Image = graphicsProcessor.ProcessImage(factory); factory.Image = graphicsProcessor.ProcessImage(factory);
} }
} //}
} }
return factory; return factory;

46
src/Test/Test/Web.config

@ -3,45 +3,40 @@
For more information on how to configure your ASP.NET application, please visit For more information on how to configure your ASP.NET application, please visit
http://go.microsoft.com/fwlink/?LinkId=152368 http://go.microsoft.com/fwlink/?LinkId=152368
--> -->
<configuration> <configuration>
<!-- Configuration section-handler declaration area. --> <!-- Configuration section-handler declaration area. -->
<configSections> <configSections>
<sectionGroup name="imageProcessor"> <sectionGroup name="imageProcessor">
<section name="security" requirePermission="false" type="ImageProcessor.Web.Config.ImageSecuritySection, ImageProcessor.Web"/> <section name="security" requirePermission="false" type="ImageProcessor.Web.Config.ImageSecuritySection, ImageProcessor.Web"/>
<section name="processing" requirePermission="false" type="ImageProcessor.Web.Config.ImageProcessingSection, ImageProcessor.Web" /> <section name="processing" requirePermission="false" type="ImageProcessor.Web.Config.ImageProcessingSection, ImageProcessor.Web"/>
<section name="cache" requirePermission="false" type="ImageProcessor.Web.Config.ImageCacheSection, ImageProcessor.Web" /> <section name="cache" requirePermission="false" type="ImageProcessor.Web.Config.ImageCacheSection, ImageProcessor.Web"/>
</sectionGroup> </sectionGroup>
</configSections> </configSections>
<appSettings> <appSettings>
<add key="webpages:Version" value="1.0.0.0"/> <add key="webpages:Version" value="1.0.0.0"/>
<add key="ClientValidationEnabled" value="true"/> <add key="ClientValidationEnabled" value="true"/>
<add key="UnobtrusiveJavaScriptEnabled" value="true"/> <add key="UnobtrusiveJavaScriptEnabled" value="true"/>
</appSettings> </appSettings>
<system.web> <system.web>
<compilation debug="false" targetFramework="4.0"> <compilation debug="true" targetFramework="4.0">
<assemblies> <assemblies>
<add assembly="System.Web.Abstractions, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" /> <add assembly="System.Web.Abstractions, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"/>
<add assembly="System.Web.Helpers, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" /> <add assembly="System.Web.Helpers, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"/>
<add assembly="System.Web.Routing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" /> <add assembly="System.Web.Routing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"/>
<add assembly="System.Web.Mvc, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" /> <add assembly="System.Web.Mvc, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"/>
<add assembly="System.Web.WebPages, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" /> <add assembly="System.Web.WebPages, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"/>
</assemblies> </assemblies>
</compilation> </compilation>
<authentication mode="Forms"> <authentication mode="Forms">
<forms loginUrl="~/Account/LogOn" timeout="2880" /> <forms loginUrl="~/Account/LogOn" timeout="2880"/>
</authentication> </authentication>
<pages> <pages>
<namespaces> <namespaces>
<add namespace="System.Web.Helpers" /> <add namespace="System.Web.Helpers"/>
<add namespace="System.Web.Mvc" /> <add namespace="System.Web.Mvc"/>
<add namespace="System.Web.Mvc.Ajax" /> <add namespace="System.Web.Mvc.Ajax"/>
<add namespace="System.Web.Mvc.Html" /> <add namespace="System.Web.Mvc.Html"/>
<add namespace="System.Web.Routing" /> <add namespace="System.Web.Routing"/>
<add namespace="System.Web.WebPages"/> <add namespace="System.Web.WebPages"/>
</namespaces> </namespaces>
</pages> </pages>
@ -51,31 +46,28 @@
<!--Set the trust level.--> <!--Set the trust level.-->
<!--<trust level="Medium"/>--> <!--<trust level="Medium"/>-->
</system.web> </system.web>
<system.webServer> <system.webServer>
<validation validateIntegratedModeConfiguration="false"/> <validation validateIntegratedModeConfiguration="false"/>
<modules runAllManagedModulesForAllRequests="true"/> <modules runAllManagedModulesForAllRequests="true"/>
</system.webServer> </system.webServer>
<runtime> <runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1"> <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly> <dependentAssembly>
<assemblyIdentity name="System.Web.Mvc" publicKeyToken="31bf3856ad364e35" /> <assemblyIdentity name="System.Web.Mvc" publicKeyToken="31bf3856ad364e35"/>
<bindingRedirect oldVersion="1.0.0.0-2.0.0.0" newVersion="3.0.0.0" /> <bindingRedirect oldVersion="1.0.0.0-2.0.0.0" newVersion="3.0.0.0"/>
</dependentAssembly> </dependentAssembly>
</assemblyBinding> </assemblyBinding>
</runtime> </runtime>
<imageProcessor> <imageProcessor>
<security allowRemoteDownloads="true" timeout="300000" maxBytes="524288" remotePrefix="/remote.axd"> <security allowRemoteDownloads="true" timeout="300000" maxBytes="524288" remotePrefix="/remote.axd">
<whiteList> <whiteList>
<add url="http://images.mymovies.net"/> <add url="http://images.mymovies.net"/>
</whiteList> </whiteList>
</security> </security>
<cache virtualPath="~/cache" maxDays="28" /> <cache virtualPath="~/cache" maxDays="28"/>
<processing> <processing>
<plugins> <plugins>
<plugin name ="Resize"> <plugin name="Resize">
<settings> <settings>
<setting key="MaxWidth" value="1024"/> <setting key="MaxWidth" value="1024"/>
<setting key="MaxHeight" value="768"/> <setting key="MaxHeight" value="768"/>
@ -84,4 +76,4 @@
</plugins> </plugins>
</processing> </processing>
</imageProcessor> </imageProcessor>
</configuration> </configuration>
Loading…
Cancel
Save