mirror of
https://github.com/microsoft/PowerToys
synced 2025-08-22 01:58:04 +00:00
* Update SUPPORT.md * Update runner.md * Update guidance.md * Update convert-stringtable-to-resx.ps1 * Update readme.md * Update ControlType.cs * Update README.md * Update CLSID.h * Update GenericProperty`1.cs * Update project-overview.md * Update interop.cpp * Update PreviewHandlerBase.cs * Update indexer.md * Update common.md * Update two_way_pipe_message_ipc.cpp * Update PowerToys.exe.manifest * Update HotkeySettings.cs * push * Update src/tests/win-app-driver/README.md * Update doc/devdocs/akaLinks.md * Update doc/devdocs/modules/launcher/plugins/registry.md
381 lines
14 KiB
C#
381 lines
14 KiB
C#
// Copyright (c) Brice Lambson
|
|
// The Brice Lambson licenses this file to you under the MIT license.
|
|
// See the LICENSE file in the project root for more information. Code forked from Brice Lambson's https://github.com/bricelam/ImageResizer/
|
|
|
|
using System;
|
|
using System.Diagnostics;
|
|
using System.Globalization;
|
|
using System.IO;
|
|
using System.IO.Abstractions;
|
|
using System.Linq;
|
|
using System.Windows;
|
|
using System.Windows.Media;
|
|
using System.Windows.Media.Imaging;
|
|
using ImageResizer.Extensions;
|
|
using ImageResizer.Properties;
|
|
using ImageResizer.Utilities;
|
|
using Microsoft.VisualBasic.FileIO;
|
|
using FileSystem = Microsoft.VisualBasic.FileIO.FileSystem;
|
|
|
|
namespace ImageResizer.Models
|
|
{
|
|
internal class ResizeOperation
|
|
{
|
|
private readonly IFileSystem _fileSystem = new System.IO.Abstractions.FileSystem();
|
|
|
|
private readonly string _file;
|
|
private readonly string _destinationDirectory;
|
|
private readonly Settings _settings;
|
|
|
|
// Filenames to avoid according to https://learn.microsoft.com/windows/win32/fileio/naming-a-file#file-and-directory-names
|
|
private static readonly string[] _avoidFilenames =
|
|
{
|
|
"CON", "PRN", "AUX", "NUL",
|
|
"COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9",
|
|
"LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9",
|
|
};
|
|
|
|
public ResizeOperation(string file, string destinationDirectory, Settings settings)
|
|
{
|
|
_file = file;
|
|
_destinationDirectory = destinationDirectory;
|
|
_settings = settings;
|
|
}
|
|
|
|
public void Execute()
|
|
{
|
|
string path;
|
|
using (var inputStream = _fileSystem.File.OpenRead(_file))
|
|
{
|
|
var decoder = BitmapDecoder.Create(
|
|
inputStream,
|
|
BitmapCreateOptions.PreservePixelFormat,
|
|
BitmapCacheOption.None);
|
|
|
|
var containerFormat = decoder.CodecInfo.ContainerFormat;
|
|
|
|
var encoder = CreateEncoder(containerFormat);
|
|
|
|
if (decoder.Metadata != null)
|
|
{
|
|
try
|
|
{
|
|
encoder.Metadata = decoder.Metadata;
|
|
}
|
|
catch (InvalidOperationException)
|
|
{
|
|
}
|
|
}
|
|
|
|
if (decoder.Palette != null)
|
|
{
|
|
encoder.Palette = decoder.Palette;
|
|
}
|
|
|
|
foreach (var originalFrame in decoder.Frames)
|
|
{
|
|
var transformedBitmap = Transform(originalFrame);
|
|
|
|
// if the frame was not modified, we should not replace the metadata
|
|
if (transformedBitmap == originalFrame)
|
|
{
|
|
encoder.Frames.Add(originalFrame);
|
|
}
|
|
else
|
|
{
|
|
BitmapMetadata originalMetadata = (BitmapMetadata)originalFrame.Metadata;
|
|
|
|
#if DEBUG
|
|
Debug.WriteLine($"### Processing metadata of file {_file}");
|
|
originalMetadata.PrintsAllMetadataToDebugOutput();
|
|
#endif
|
|
|
|
var metadata = GetValidMetadata(originalMetadata, transformedBitmap, containerFormat);
|
|
|
|
if (_settings.RemoveMetadata && metadata != null)
|
|
{
|
|
// strip any metadata that doesn't affect rendering
|
|
var newMetadata = new BitmapMetadata(metadata.Format);
|
|
|
|
metadata.CopyMetadataPropertyTo(newMetadata, "System.Photo.Orientation");
|
|
metadata.CopyMetadataPropertyTo(newMetadata, "System.Image.ColorSpace");
|
|
|
|
metadata = newMetadata;
|
|
}
|
|
|
|
var frame = CreateBitmapFrame(transformedBitmap, metadata);
|
|
|
|
encoder.Frames.Add(frame);
|
|
}
|
|
}
|
|
|
|
path = GetDestinationPath(encoder);
|
|
_fileSystem.Directory.CreateDirectory(_fileSystem.Path.GetDirectoryName(path));
|
|
using (var outputStream = _fileSystem.File.Open(path, FileMode.CreateNew, FileAccess.Write))
|
|
{
|
|
encoder.Save(outputStream);
|
|
}
|
|
}
|
|
|
|
if (_settings.KeepDateModified)
|
|
{
|
|
_fileSystem.File.SetLastWriteTimeUtc(path, _fileSystem.File.GetLastWriteTimeUtc(_file));
|
|
}
|
|
|
|
if (_settings.Replace)
|
|
{
|
|
var backup = GetBackupPath();
|
|
_fileSystem.File.Replace(path, _file, backup, ignoreMetadataErrors: true);
|
|
FileSystem.DeleteFile(backup, UIOption.OnlyErrorDialogs, RecycleOption.SendToRecycleBin);
|
|
}
|
|
}
|
|
|
|
private BitmapEncoder CreateEncoder(Guid containerFormat)
|
|
{
|
|
var createdEncoder = BitmapEncoder.Create(containerFormat);
|
|
if (!createdEncoder.CanEncode())
|
|
{
|
|
createdEncoder = BitmapEncoder.Create(_settings.FallbackEncoder);
|
|
}
|
|
|
|
ConfigureEncoder(createdEncoder);
|
|
|
|
return createdEncoder;
|
|
|
|
void ConfigureEncoder(BitmapEncoder encoder)
|
|
{
|
|
switch (encoder)
|
|
{
|
|
case JpegBitmapEncoder jpegEncoder:
|
|
jpegEncoder.QualityLevel = MathHelpers.Clamp(_settings.JpegQualityLevel, 1, 100);
|
|
break;
|
|
|
|
case PngBitmapEncoder pngBitmapEncoder:
|
|
pngBitmapEncoder.Interlace = _settings.PngInterlaceOption;
|
|
break;
|
|
|
|
case TiffBitmapEncoder tiffEncoder:
|
|
tiffEncoder.Compression = _settings.TiffCompressOption;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
private BitmapSource Transform(BitmapSource source)
|
|
{
|
|
var originalWidth = source.PixelWidth;
|
|
var originalHeight = source.PixelHeight;
|
|
var width = _settings.SelectedSize.GetPixelWidth(originalWidth, source.DpiX);
|
|
var height = _settings.SelectedSize.GetPixelHeight(originalHeight, source.DpiY);
|
|
|
|
if (_settings.IgnoreOrientation
|
|
&& !_settings.SelectedSize.HasAuto
|
|
&& _settings.SelectedSize.Unit != ResizeUnit.Percent
|
|
&& originalWidth < originalHeight != (width < height))
|
|
{
|
|
var temp = width;
|
|
width = height;
|
|
height = temp;
|
|
}
|
|
|
|
var scaleX = width / originalWidth;
|
|
var scaleY = height / originalHeight;
|
|
|
|
if (_settings.SelectedSize.Fit == ResizeFit.Fit)
|
|
{
|
|
scaleX = Math.Min(scaleX, scaleY);
|
|
scaleY = scaleX;
|
|
}
|
|
else if (_settings.SelectedSize.Fit == ResizeFit.Fill)
|
|
{
|
|
scaleX = Math.Max(scaleX, scaleY);
|
|
scaleY = scaleX;
|
|
}
|
|
|
|
if (_settings.ShrinkOnly
|
|
&& _settings.SelectedSize.Unit != ResizeUnit.Percent
|
|
&& (scaleX >= 1 || scaleY >= 1))
|
|
{
|
|
return source;
|
|
}
|
|
|
|
var scaledBitmap = new TransformedBitmap(source, new ScaleTransform(scaleX, scaleY));
|
|
if (_settings.SelectedSize.Fit == ResizeFit.Fill
|
|
&& (scaledBitmap.PixelWidth > width
|
|
|| scaledBitmap.PixelHeight > height))
|
|
{
|
|
var x = (int)(((originalWidth * scaleX) - width) / 2);
|
|
var y = (int)(((originalHeight * scaleY) - height) / 2);
|
|
|
|
return new CroppedBitmap(scaledBitmap, new Int32Rect(x, y, (int)width, (int)height));
|
|
}
|
|
|
|
return scaledBitmap;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks original metadata by writing an image containing the given metadata into a memory stream.
|
|
/// In case of errors, we try to rebuild the metadata object and check again.
|
|
/// We return null if we were not able to get hold of valid metadata.
|
|
/// </summary>
|
|
private BitmapMetadata GetValidMetadata(BitmapMetadata originalMetadata, BitmapSource transformedBitmap, Guid containerFormat)
|
|
{
|
|
if (originalMetadata == null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
// Check if the original metadata is valid
|
|
var frameWithOriginalMetadata = CreateBitmapFrame(transformedBitmap, originalMetadata);
|
|
if (EnsureFrameIsValid(frameWithOriginalMetadata))
|
|
{
|
|
return originalMetadata;
|
|
}
|
|
|
|
// Original metadata was invalid. We try to rebuild the metadata object from the scratch and discard invalid metadata fields
|
|
var recreatedMetadata = BuildMetadataFromTheScratch(originalMetadata);
|
|
var frameWithRecreatedMetadata = CreateBitmapFrame(transformedBitmap, recreatedMetadata);
|
|
if (EnsureFrameIsValid(frameWithRecreatedMetadata))
|
|
{
|
|
return recreatedMetadata;
|
|
}
|
|
|
|
// Seems like we have an invalid metadata object. ImageResizer will fail when trying to write the image to disk. We discard all metadata to be able to save the image.
|
|
return null;
|
|
|
|
// The safest way to check if the metadata object is valid is to call Save() on the encoder.
|
|
// I tried other ways to check if metadata is valid (like calling Clone() on the metadata object) but this was not reliable resulting in a few github issues.
|
|
bool EnsureFrameIsValid(BitmapFrame frameToBeChecked)
|
|
{
|
|
try
|
|
{
|
|
var encoder = CreateEncoder(containerFormat);
|
|
encoder.Frames.Add(frameToBeChecked);
|
|
using (var testStream = new MemoryStream())
|
|
{
|
|
encoder.Save(testStream);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
catch (Exception)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Read all metadata and build up metadata object from the scratch. Discard invalid (unreadable/unwritable) metadata.
|
|
/// </summary>
|
|
private static BitmapMetadata BuildMetadataFromTheScratch(BitmapMetadata originalMetadata)
|
|
{
|
|
try
|
|
{
|
|
var metadata = new BitmapMetadata(originalMetadata.Format);
|
|
var listOfMetadata = originalMetadata.GetListOfMetadata();
|
|
foreach (var (metadataPath, value) in listOfMetadata)
|
|
{
|
|
if (value is BitmapMetadata bitmapMetadata)
|
|
{
|
|
var innerMetadata = new BitmapMetadata(bitmapMetadata.Format);
|
|
metadata.SetQuerySafe(metadataPath, innerMetadata);
|
|
}
|
|
else
|
|
{
|
|
metadata.SetQuerySafe(metadataPath, value);
|
|
}
|
|
}
|
|
|
|
return metadata;
|
|
}
|
|
catch (ArgumentException ex)
|
|
{
|
|
Debug.WriteLine(ex);
|
|
|
|
return null;
|
|
}
|
|
}
|
|
|
|
private static BitmapFrame CreateBitmapFrame(BitmapSource transformedBitmap, BitmapMetadata metadata)
|
|
{
|
|
return BitmapFrame.Create(
|
|
transformedBitmap,
|
|
thumbnail: null, /* should be null, see #15413 */
|
|
metadata,
|
|
colorContexts: null /* should be null, see #14866 */ );
|
|
}
|
|
|
|
private string GetDestinationPath(BitmapEncoder encoder)
|
|
{
|
|
var directory = _destinationDirectory ?? _fileSystem.Path.GetDirectoryName(_file);
|
|
var originalFileName = _fileSystem.Path.GetFileNameWithoutExtension(_file);
|
|
|
|
var supportedExtensions = encoder.CodecInfo.FileExtensions.Split(',');
|
|
var extension = _fileSystem.Path.GetExtension(_file);
|
|
if (!supportedExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase))
|
|
{
|
|
extension = supportedExtensions.FirstOrDefault();
|
|
}
|
|
|
|
// Remove directory characters from the size's name.
|
|
string sizeNameSanitized = _settings.SelectedSize.Name;
|
|
sizeNameSanitized = sizeNameSanitized
|
|
.Replace('\\', '_')
|
|
.Replace('/', '_');
|
|
|
|
// Using CurrentCulture since this is user facing
|
|
var fileName = string.Format(
|
|
CultureInfo.CurrentCulture,
|
|
_settings.FileNameFormat,
|
|
originalFileName,
|
|
sizeNameSanitized,
|
|
_settings.SelectedSize.Width,
|
|
_settings.SelectedSize.Height,
|
|
encoder.Frames[0].PixelWidth,
|
|
encoder.Frames[0].PixelHeight);
|
|
|
|
// Remove invalid characters from the final file name.
|
|
fileName = fileName
|
|
.Replace(':', '_')
|
|
.Replace('*', '_')
|
|
.Replace('?', '_')
|
|
.Replace('"', '_')
|
|
.Replace('<', '_')
|
|
.Replace('>', '_')
|
|
.Replace('|', '_');
|
|
|
|
// Avoid creating not recommended filenames
|
|
if (_avoidFilenames.Contains(fileName.ToUpperInvariant()))
|
|
{
|
|
fileName = fileName + "_";
|
|
}
|
|
|
|
var path = _fileSystem.Path.Combine(directory, fileName + extension);
|
|
var uniquifier = 1;
|
|
while (_fileSystem.File.Exists(path))
|
|
{
|
|
path = _fileSystem.Path.Combine(directory, fileName + " (" + uniquifier++ + ")" + extension);
|
|
}
|
|
|
|
return path;
|
|
}
|
|
|
|
private string GetBackupPath()
|
|
{
|
|
var directory = _fileSystem.Path.GetDirectoryName(_file);
|
|
var fileName = _fileSystem.Path.GetFileNameWithoutExtension(_file);
|
|
var extension = _fileSystem.Path.GetExtension(_file);
|
|
|
|
var path = _fileSystem.Path.Combine(directory, fileName + ".bak" + extension);
|
|
var uniquifier = 1;
|
|
while (_fileSystem.File.Exists(path))
|
|
{
|
|
path = _fileSystem.Path.Combine(directory, fileName + " (" + uniquifier++ + ")" + ".bak" + extension);
|
|
}
|
|
|
|
return path;
|
|
}
|
|
}
|
|
}
|