Skip to content

Instantly share code, notes, and snippets.

@ericlaw1979
Created November 3, 2026 16:54
Show Gist options
  • Select an option

  • Save ericlaw1979/95f8739e1de5479ce6d05045084fff80 to your computer and use it in GitHub Desktop.

Select an option

Save ericlaw1979/95f8739e1de5479ce6d05045084fff80 to your computer and use it in GitHub Desktop.
JPEGify for Clearinet.app
using Clearinet;
using System;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Windows.Forms;
[assembly: Clearinet.RequiredVersion("0.0.3.0")]
namespace MakeJPEG
{
public class JPEGify : IAutoTamper
{
private bool _autoConvert;
private long _autoJPEGQuality = 80;
private readonly ToolStripMenuItem _rulesMenuItem = new ToolStripMenuItem("JPEGify I&mages...");
private readonly ToolStripMenuItem _contextMenuItem = new ToolStripMenuItem("&JPEGify...");
public void OnPeekAtRequestHeaders(Exchange exchange) { }
public void AutoTamperRequestBefore(Exchange exchange) { }
public void AutoTamperRequestAfter(Exchange exchange) { }
public void OnPeekAtResponseHeaders(Exchange exchange)
{
if (_autoConvert && exchange?.ResponseHeaders != null && exchange.ResponseHeaders.ExistsAndContains("Content-Type", "image/"))
{
// TODO: Enable response buffering.
// Clearinet handles the actual response tampering in AutoTamperResponseBefore.
}
}
public void AutoTamperResponseBefore(Exchange exchange)
{
if (!_autoConvert) return;
ProcessExchange(exchange, _autoJPEGQuality);
}
public void AutoTamperResponseAfter(Exchange exchange) { }
public void OnBeforeReturningGeneratedError(Exchange exchange) { }
private void ProcessExchange(Exchange exchange, long quality)
{
if (exchange == null || !exchange.bHasResponse || exchange.responseCode != 200)
{
return;
}
if (!exchange.ResponseBody.HasData()
|| exchange.ResponseHeaders == null
|| !exchange.MediaType.OICStartsWith("image/"))
{
return;
}
if (!exchange.MediaTypeIs("image/jpeg") &&
!exchange.MediaTypeIs("image/webp") &&
!exchange.MediaTypeIs("image/png") &&
!exchange.MediaTypeIs("image/gif") &&
!exchange.MediaTypeIs("image/jxl") &&
!exchange.MediaTypeIs("image/bmp") &&
!exchange.MediaType.OICStartsWith("image/jp"))
{
return;
}
try
{
int oldSize = exchange.ResponseBody?.Length ?? 0;
MakeJPEGOfQuality(exchange, quality);
exchange.oFlags["Size-Before-JPEGify"] = oldSize.ToString("N0");
float percent = oldSize == 0 ? 0F : 100F * (exchange.ResponseBody.Length / (float)oldSize);
string summary = $"JPEGify to {exchange.ResponseBody.Length:N0} from {oldSize:N0} bytes ({percent:N1}%)";
CApp.UI.SetStatusText(summary);
}
catch (Exception ex)
{
CApp.Log.Log(ex.ToString());
}
}
private void MakeJPEGOfQuality(Exchange exchange, long quality)
{
if (quality < 1 || quality > 100)
{
throw new ArgumentOutOfRangeException(nameof(quality), quality, "Quality must be between 1 and 100");
}
if (!exchange.ResponseBody.HasData())
{
return;
}
byte[] arrBody = exchange.ResponseBody;
if (exchange.MediaTypeIs("image/webp"))
{
arrBody = Clearinet.Utilities.ConvertWebPToPNG(arrBody);
}
else if (exchange.MediaTypeIs("image/jxl"))
{
arrBody = Clearinet.Utilities.ConvertJXLToJPEG(arrBody);
}
using (MemoryStream inputStream = new MemoryStream(arrBody))
using (Bitmap bitmap = new Bitmap(inputStream))
using (MemoryStream outputStream = new MemoryStream())
{
ImageCodecInfo encoder = GetEncoder(ImageFormat.Jpeg) ?? throw new InvalidOperationException("Unable to find a JPEG encoder.");
EncoderParameters parameters = new EncoderParameters(1);
parameters.Param[0] = new EncoderParameter(Encoder.Quality, quality);
bitmap.Save(outputStream, encoder, parameters);
exchange.ResponseBody = outputStream.ToArray();
exchange.ResponseHeaders["Content-Type"] = "image/jpeg";
exchange.ResponseHeaders["Cache-Control"] = "no-cache";
}
}
private static ImageCodecInfo GetEncoder(ImageFormat format)
{
ImageCodecInfo[] codecs = ImageCodecInfo.GetImageDecoders();
foreach (ImageCodecInfo codec in codecs)
{
if (codec.FormatID == format.Guid)
{
return codec;
}
}
return null;
}
public void OnBeforeUnload()
{
_autoConvert = false;
_rulesMenuItem.GetCurrentParent()?.Items.Remove(_rulesMenuItem);
_contextMenuItem.GetCurrentParent()?.Items.Remove(_contextMenuItem);
_rulesMenuItem.Dispose();
_contextMenuItem.Dispose();
}
public void OnLoad()
{
CApp.UI.AddExtensionRulesMenuItem(_rulesMenuItem);
CApp.UI.lvExchanges.AddExtensionMenuItem(_contextMenuItem);
_rulesMenuItem.Click += (s, e) =>
{
bool newValue = !_rulesMenuItem.Checked;
if (newValue)
{
string qualityText = frmPrompt.GetUserString(
"Set JPEG Quality",
"Enter a value, 1 to 100",
_autoJPEGQuality.ToString(),
true,
frmPrompt.PromptKind.Numbers);
if (!qualityText.HasText())
{
newValue = false;
}
else
{
if (!long.TryParse(qualityText, out long parsedQuality) || parsedQuality < 1 || parsedQuality > 100)
{
newValue = false;
}
else
{
_autoJPEGQuality = parsedQuality;
}
}
}
_rulesMenuItem.Checked = _autoConvert = newValue;
};
_contextMenuItem.Click += (s, e) =>
{
string qualityText = frmPrompt.GetUserString(
"Set JPEG Quality",
"Enter a value, 1 to 100",
_autoJPEGQuality.ToString(),
true,
frmPrompt.PromptKind.Numbers);
if (!qualityText.HasText()) return;
if (!long.TryParse(qualityText, out long quality) || quality < 1 || quality > 100) return;
Exchange[] selectedExchanges = CApp.UI.GetSelectedExchanges();
foreach (Exchange exchange in selectedExchanges)
{
ProcessExchange(exchange, quality);
}
CApp.UI.actRefreshUI(false);
CApp.UI.SetStatusText("JPEGify Complete");
};
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment