拖放式工具,用于降低 JPEG 质量(和文件大小)






4.89/5 (19投票s)
如今,高分辨率的相机生成的图像文件非常大,而您可能愿意牺牲一些画质。
引言
此应用程序是一个简单的单类Windows窗体项目,它提供了一个文件拖放目标,可以降低JPEG文件的质量设置,同时保持分辨率。
这里没有神奇之处,但初学者会发现这些概念得到解释
- Windows Forms 中的拖放
- 拖放到应用程序图标上
- 使用 System.Drawing.Bitmap类来处理 JPEG 图像质量
- 使用 BackgroundWorker进行简单的多线程处理
背景
| 质量 | 相对寻址 大小 | 
|---|---|
| 95 | 50% | 
| 90 | 30% | 
| 80 | 20% | 
| 70 | 15% | 
| 60 | 12% | 
| 50 | 10% | 
| 40 | 9% | 
| 30 | 8% | 
| 20 | 7% | 
| 10 | 5% | 
| 5 | 4% | 
我最近购买了一台尼康 D3300 数码单反相机,用于技术文档(这是我公司的一项服务)。我总是以最高分辨率(6000×4000像素≈24MP)和精细设置进行拍摄。这可以确保在需要放大图像的某个部分进行复制时,能够获得所需的细节。
即使拍摄不太重要的内容,我也会保持分辨率和质量设置不变。否则,我担心以后会后悔,因为忘记将设置改回来。问题是,每张图片都高达 7-13MB。昨天拍摄的 125 张照片占用了超过 1GB 的磁盘空间,对于我的 3TB NAS 来说还可以,但对于 64GB 的 SSD 来说太大了。
JPEG 质量
对于这些不太重要的照片,我想使用一个拖放工具来降低 JPEG 质量,而不降低分辨率,所以我写了 JpegQuality。
JPEG 图像使用有损压缩算法,其目标是以最少的可感知图像退化来减小文件大小。该系统使用介于 0 和 100 之间的质量因子。JPEG 质量的微小降低可能会对文件大小产生显著影响(见表格)。
表格中的数值仅为示例,实际数值很大程度上取决于主题内容。我认为,对于较低分辨率的图像,质量因子为 70,对于较高分辨率的图像,质量因子为 60,可以产生几乎察觉不到的质量损失。
JPEG 算法通过将像素分组为 8×8 块,并应用 离散余弦变换 来量化和丢弃人眼难以区分的颜色和亮度频率。

手机照片,质量为100,原尺寸为2592×1944,已裁剪和缩放。

同一张图片在裁剪和缩放之前转换为质量20。
如何使用它
有两种操作模式。每种模式都会在源图像的同一目录下写入源图像的较低质量副本,并添加一个表示质量因子的文件名后缀。
拖放到控件
正常打开应用程序会显示一个简单的用户界面。

- 使用下拉框设置质量因子。
- 将文件拖放到指定区域。
- 进度条指示已处理的文件数量。
拖放到应用程序图标
可以将文件直接拖放到应用程序的图标上。

工作原理
设置
通过 ComboBox 选择的质量因子会保存到注册表中,使用 Quality 属性。
private const int DefaultQuality = 60; 
private const string RegistryRoot = @"HKEY_CURRENT_USER\Software\Red Cell Innovation Inc.\JpegQuality";
/// <summary>
/// Gets/sets the quality.
/// </summary>
/// <value>The quality.</value>
public int Quality
{
    get { return (int) Registry.GetValue(RegistryRoot, "Quality", DefaultQuality); }
    set { Registry.SetValue(RegistryRoot, "Quality", value); }
}
/// <summary>
/// Handles the SelectedIndexChanged event of the QualityBox control.
/// </summary>
/// <param name="sender">The source of the event.</param>
/// <param name="e">The <see cref="EventArgs"/> instance containing the event data.</param>
private void QualityBox_SelectedIndexChanged(object sender, EventArgs e)
{
    Quality = int.Parse((sender as ToolStripComboBox).Text);
}
拖放到应用程序图标
当文件被拖放到应用程序图标上时,Windows会将文件的绝对路径作为命令行参数传递给应用程序。这些参数在主窗体激活时进行评估。
        /// <summary>
        /// Raises the <see cref="E:System.Windows.Forms.Form.Activated" /> event.
        /// </summary>
        /// <param name="e">An <see cref="T:System.EventArgs" /> that contains the event data.</param>
        protected override void OnActivated(EventArgs e)
        {
            base.OnActivated(e);
            try
            {
                var args = Environment.GetCommandLineArgs();
                if (args.Length > 1)
                {
                    var list = new List<string>(args);
                    list.RemoveAt(0); // First argument is path to exe.
                    ProcessImagesAsync(this, new DoWorkEventArgs(list.ToArray()));
                    Close();
                }
            }
            catch (Exception ex)
            {
                MessageBox.Show(ex.Message, ex.GetType().Name, MessageBoxButtons.OK, MessageBoxIcon.Error);
            }
        }
当然,这意味着该应用程序也可以通过传递空格分隔的文件名列表来在命令行中使用。
拖放到控件
当文件被拖放到其 AllowDrop 属性为 true 的控件上时(在本例中为 DropLabel 控件),将调用与拖放相关的 Windows Forms 事件。对于简单的拖放操作,我们处理 DragOver 和 DragDrop 事件。
当鼠标指针位于控件上方时,会触发 DragOver 事件。处理此事件可以向用户提供反馈,指示控件是否可以接收正在拖动的对象。传递给事件处理程序的 DragEventArgs 对象引用了我们确定是否可以接收对象所需的一切。在这种情况下,我们使用 GetData 方法查询数据,查看是否存在 FileDrop 对象,该对象是字符串格式的绝对文件路径数组。我们使用三个标准来确定我们是否可以处理被拖动的对象:
- 存在 FileDrop对象。
- FileDrop对象中的所有文件都具有- .jpg或- .jpeg文件扩展名。
- 我们的工作线程尚未繁忙处理。
如果所有这些条件都为 true,我们将 DragEventArgs.Effect 设置为 DragDropEffects.Copy,这将通过在光标旁添加一个 + 号来向用户指示成功条件。
        /// <summary>
        /// Handles the DragOver event of the DropLabel control.
        /// </summary>
        /// <param name="sender">The source of the event.</param>
        /// <param name="e">The <see cref="DragEventArgs"/> instance containing the event data.</param>
        private void DropLabel_DragOver(object sender, DragEventArgs e)
        {
            var files = new List<string>(e.Data.GetData("FileDrop") as string[] ?? new string[0]);
            bool valid = !Worker.IsBusy;
            foreach (string file in files)
                if (!Regex.IsMatch(file, @"\.jpe?g$", RegexOptions.IgnoreCase))
                    valid = false;
            e.Effect = valid ? DragDropEffects.Copy : DragDropEffects.None;
        }
更改质量
使用静态 Bitmap.FromFile(string filename) 构造函数打开图像,然后通过调用带 ImageEncoder 的 Save 方法进行保存。
/// <summary>
/// Processes the image.
/// </summary>
/// <param name="source">The source.</param>
/// <param name="destination">The destination.</param>
private void ProcessImage(string source, string destination)
{
    using (var original = Bitmap.FromFile(source))
    {
        var encoder =  ImageCodecInfo.GetImageEncoders().Single(e => e.MimeType == "image/jpeg");
        var options = new EncoderParameters(1);
        options.Param[0] = new EncoderParameter(Encoder.Quality, Quality);
        original.Save(destination, encoder, options);
    }
}
多线程
我想目标版本是 .NET Runtime 3.5,因此 BackgroundWorker 是在单独线程上执行后台操作的最简单方法。它就是为这类应用程序设计的,内置了对取消和进度报告的支持。
要使用它,我们通过处理 DoWork 事件来定义要执行的工作。当调用 RunWorkerAsync 方法时,这项工作将在线程池中的一个线程上启动。
进度报告通过在工作线程中调用 ReportProgress 方法并在主 UI 线程上处理 ProgressChanged 事件来实现。同样,取消操作通过在 UI 线程上调用 CancelAsync 方法,并在工作线程循环中检查 CancellationPending 属性来实现。
历史
- 2015 年 1 月 24 日:文章发布。
- 2015 年 4 月 14 日:添加了示例图片。
- 2018 年 1 月 28 日:少量格式和语法清理。

