Skip to content

Instantly share code, notes, and snippets.

@gitcrtn
Last active July 14, 2025 02:10
Show Gist options
  • Select an option

  • Save gitcrtn/c5edbea00c2b43ddb4c0e6c91d2f4a07 to your computer and use it in GitHub Desktop.

Select an option

Save gitcrtn/c5edbea00c2b43ddb4c0e6c91d2f4a07 to your computer and use it in GitHub Desktop.
JXR to JPG Converter for Windows11
/*
* JXR to JPG Converter for Windows11.
*
* Author: Carotene
*
* License: MIT
*
* Requires:
* Windows11
* Visual Studio 2022
* Blender 4.4+
*
* How to run:
* 1. Install Blender 4.4+.
* 2. Build jxr2jpg.cs to jxr2jpg.exe with .NET Framework 4.8 and PresentationCore.
* 3. Run jxr2jpg.exe.
*
* Version: v1.0.2
*
* Release note:
* v1.0.0
* - Added a feature to convert JXR to JPG.
* - Use separated python script and a batch file.
*
* v1.0.1
* - Added a feature of batch conversion with progress bar.
* - Added a feature to find blender.exe on startup.
* - Added a feature to dump python scripts on startup.
* - Added a feature to set exposure.
* - Added a feature to set ocio look.
* - Removed dependency on separated python script and a batch file.
*
* v1.0.2
* - Added a feature to convert JXR to OpenEXR.
* - Fixed a bug where progress bar shows a value off by minus one.
*/
using System;
using System.IO;
using System.Drawing;
using System.Diagnostics;
using System.Reflection;
using System.Windows.Forms;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Label = System.Windows.Forms.Label;
namespace jxr2jpg
{
public class ProgressDialog : Form
{
private ProgressBar bar;
private Button btCancel;
private Label progressLabel;
private TableLayoutPanel tlp;
private CancellationTokenSource cts = null;
private int maxCount, doneCount = 0;
public CancellationToken Token => cts.Token;
public ProgressDialog(int maxCount)
{
this.maxCount = maxCount;
Text = "Converting...";
Size = new Size(300, 150);
FormBorderStyle = FormBorderStyle.FixedDialog;
StartPosition = FormStartPosition.CenterScreen;
MaximizeBox = false;
MinimizeBox = false;
progressLabel = new Label();
bar = new ProgressBar
{
Minimum = 0,
Maximum = maxCount,
Value = doneCount + 1,
Dock = DockStyle.Fill,
};
btCancel = new Button()
{
Text = "Cancel",
Dock = DockStyle.Fill,
};
btCancel.Click += (s, e) =>
{
cts.Cancel();
Close();
};
tlp = new TableLayoutPanel();
tlp.ColumnCount = 3;
tlp.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 50F));
tlp.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 220F));
tlp.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 50F));
tlp.Controls.Add(progressLabel, 1, 1);
tlp.Controls.Add(bar, 1, 2);
tlp.Controls.Add(btCancel, 1, 3);
tlp.Dock = DockStyle.Fill;
tlp.RowCount = 5;
tlp.RowStyles.Add(new RowStyle(SizeType.Percent, 50F));
tlp.RowStyles.Add(new RowStyle(SizeType.Absolute, 30F));
tlp.RowStyles.Add(new RowStyle(SizeType.Absolute, 20F));
tlp.RowStyles.Add(new RowStyle(SizeType.Absolute, 30F));
tlp.RowStyles.Add(new RowStyle(SizeType.Percent, 50F));
Controls.Add(tlp);
cts = new CancellationTokenSource();
}
public void Increment()
{
if (InvokeRequired)
{
BeginInvoke((MethodInvoker)Increment);
return;
}
doneCount++;
if (doneCount < maxCount)
bar.Value = doneCount + 1;
progressLabel.Text = $"{doneCount} / {maxCount}";
}
}
public partial class Form1 : Form
{
private TableLayoutPanel tlp;
private Label label, lookLabel, exposureLabel;
private ComboBox cbLooks;
private NumericUpDown nudExposure;
private CheckBox ckExr;
private string exeDirPath;
private string tempBinPath;
private string bin2JpgPath;
private string blenderPath = null;
private List<string> lookNames = new List<string>();
public Form1()
{
InitializeComponent();
exeDirPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
tempBinPath = Path.Combine(exeDirPath, "temp.bin");
FindBlender();
GenerateBin2Jpg();
FetchLooks();
SetupGUI();
}
private void SetupGUI()
{
tlp = new TableLayoutPanel();
label = new Label();
lookLabel = new Label();
exposureLabel = new Label();
cbLooks = new ComboBox();
nudExposure = new NumericUpDown();
ckExr = new CheckBox();
tlp.ColumnCount = 3;
tlp.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 50F));
tlp.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 220F));
tlp.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 50F));
tlp.Controls.Add(label, 1, 1);
tlp.Controls.Add(ckExr, 1, 2);
tlp.Controls.Add(lookLabel, 1, 3);
tlp.Controls.Add(cbLooks, 1, 4);
tlp.Controls.Add(exposureLabel, 1, 5);
tlp.Controls.Add(nudExposure, 1, 6);
tlp.Dock = DockStyle.Fill;
tlp.RowCount = 8;
tlp.RowStyles.Add(new RowStyle(SizeType.Percent, 50F));
tlp.RowStyles.Add(new RowStyle(SizeType.Absolute, 60F));
tlp.RowStyles.Add(new RowStyle(SizeType.Absolute, 30F));
tlp.RowStyles.Add(new RowStyle(SizeType.Absolute, 30F));
tlp.RowStyles.Add(new RowStyle(SizeType.Absolute, 30F));
tlp.RowStyles.Add(new RowStyle(SizeType.Absolute, 30F));
tlp.RowStyles.Add(new RowStyle(SizeType.Absolute, 30F));
tlp.RowStyles.Add(new RowStyle(SizeType.Percent, 50F));
label.Anchor = AnchorStyles.Left | AnchorStyles.Right;
label.TextAlign = ContentAlignment.MiddleCenter;
label.AutoSize = true;
label.Location = new Point(70, 82);
label.Size = new Size(94, 12);
label.Text = "Drop jxr file here.";
ckExr.Text = "To OpenEXR";
ckExr.Checked = false;
ckExr.CheckedChanged += (s, e) =>
{
var enabled = ckExr.Checked;
Text = enabled ? "JXR to EXR" : "JXR to JPG";
lookLabel.Enabled = !enabled;
cbLooks.Enabled = !enabled;
exposureLabel.Enabled = !enabled;
nudExposure.Enabled = !enabled;
};
lookLabel.Anchor = AnchorStyles.Left;
lookLabel.AutoSize = true;
lookLabel.Text = "Look:";
cbLooks.Items.Clear();
cbLooks.Items.AddRange(lookNames.ToArray());
cbLooks.SelectedIndex = 0;
cbLooks.DropDownStyle = ComboBoxStyle.DropDownList;
cbLooks.Dock = DockStyle.Fill;
exposureLabel.Anchor = AnchorStyles.Left;
exposureLabel.AutoSize = true;
exposureLabel.Text = "Exposure:";
nudExposure.Minimum = -100.0M;
nudExposure.Maximum = 100.0M;
nudExposure.DecimalPlaces = 1;
nudExposure.Increment = 0.1M;
nudExposure.Value = -1.5M;
nudExposure.Dock = DockStyle.Fill;
StartPosition = FormStartPosition.CenterScreen;
ClientSize = new Size(300, 250);
Controls.Add(tlp);
Text = "JXR to JPG";
AllowDrop = true;
DragEnter += Form_DragEnter;
DragDrop += Form_DragDrop;
}
private void Form_DragEnter(object sender, DragEventArgs e)
{
if (e.Data.GetDataPresent(DataFormats.FileDrop)) e.Effect = DragDropEffects.Copy;
else e.Effect = DragDropEffects.None;
}
private void Form_DragDrop(object sender, DragEventArgs e)
{
string[] files = (string[])e.Data.GetData(DataFormats.FileDrop);
var targetFiles = new List<string>();
foreach (string file in files)
{
if (Path.GetExtension(file).Equals(".jxr", StringComparison.OrdinalIgnoreCase))
{
targetFiles.Add(file);
}
}
if (targetFiles.Count == 0)
{
MessageBox.Show($"Jxr file not found.", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}
if (targetFiles.Count == 1)
{
Convert(targetFiles[0], true);
return;
}
var progressDialog = new ProgressDialog(targetFiles.Count);
progressDialog.Show(this);
Task.Run(async () =>
{
int successCount = 0;
var token = progressDialog.Token;
foreach (var file in targetFiles)
{
Invoke((MethodInvoker)(() => {
if (Convert(file))
{
successCount++;
}
progressDialog.Increment();
}));
await Task.Delay(10);
if (token.IsCancellationRequested)
{
break;
}
}
Invoke((MethodInvoker)(() =>
{
progressDialog.Close();
MessageBox.Show($"Success files: {successCount} / {targetFiles.Count}", "Done", MessageBoxButtons.OK, MessageBoxIcon.Information);
}));
});
}
private bool Convert(string jxrPath, bool single = false)
{
try
{
var jxrFullPath = Path.GetFullPath(jxrPath);
var outputPath = jxrPath.Replace(".jxr", ckExr.Checked ? ".exr" : ".jpg");
jxr2bin(jxrFullPath, tempBinPath);
var output = bin2output(tempBinPath, outputPath);
if (single)
{
MessageBox.Show($"Done: {outputPath}\r\n\r\n{output}", "Success", MessageBoxButtons.OK, MessageBoxIcon.Information);
return true;
}
return output.Contains("Conversion process completed successfully.");
}
catch (Exception ex)
{
if (single)
{
MessageBox.Show($"Failed: {ex.Message}", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
return false;
}
private void jxr2bin(string jxrPath, string binPath)
{
var decoder = new WmpBitmapDecoder(
new Uri(jxrPath),
BitmapCreateOptions.PreservePixelFormat,
BitmapCacheOption.OnLoad
);
var frame = decoder.Frames[0];
var converted = new FormatConvertedBitmap(
frame,
PixelFormats.Rgba128Float, // 128bit float (32bit per channel)
null,
0.0
);
int width = converted.PixelWidth;
int height = converted.PixelHeight;
int stride = width * 16; // 4 channels x 4 bytes x float = 16 bytes/pixel
byte[] buffer = new byte[height * stride];
converted.CopyPixels(buffer, stride, 0);
using (var fs = new BinaryWriter(File.Open(binPath, FileMode.Create)))
{
fs.Write(width);
fs.Write(height);
// byte[] -> float32
for (int i = 0; i < buffer.Length; i += 4)
{
float value = BitConverter.ToSingle(buffer, i);
fs.Write(value);
}
}
}
private string RunBlender(string pyScriptPath, string args = null)
{
if (blenderPath == null)
{
throw new Exception("blender.exe not found.");
}
string allArgs = $"--background --python \"{pyScriptPath}\"";
if (args != null)
{
allArgs += $" -- {args}";
}
var psi = new ProcessStartInfo
{
FileName = $"\"{blenderPath}\"",
Arguments = allArgs,
UseShellExecute = false,
RedirectStandardOutput = true,
CreateNoWindow = true,
};
using (var process = Process.Start(psi))
{
string output = process.StandardOutput.ReadToEnd();
process.WaitForExit();
return output;
}
}
private string bin2output(string binPath, string outputPath)
{
var look = cbLooks.Items[cbLooks.SelectedIndex].ToString();
var exposure = nudExposure.Value;
var args = $"--input \"{binPath}\" --output \"{outputPath}\" --look \"{look}\" --exposure={exposure}";
if (ckExr.Checked) args += " --exr";
return RunBlender(bin2JpgPath, args);
}
private void FindBlender()
{
var baseDirPath = @"C:\Program Files\Blender Foundation\";
if (Directory.Exists(baseDirPath))
{
foreach (var dir in Directory.GetDirectories(baseDirPath, "Blender *"))
{
var exePath = Path.Combine(dir, "blender.exe");
if (File.Exists(exePath))
{
blenderPath = exePath;
return;
}
}
}
MessageBox.Show("blender.exe not found.", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
private void GenerateBin2Jpg()
{
string pyContent = @"
import sys
import argparse
import struct
import os
import traceback
import bpy
import numpy as np
INPUT_COLOR_SPACE = 'Rec.2020'
LINEAR_COLOR_SPACE_FOR_EXR = 'Linear Rec.2020'
TARGET_DISPLAY_DEVICE_FOR_SRGB = 'sRGB'
TARGET_VIEW_TRANSFORM_FOR_SRGB = 'Standard'
def convert_bin_to_srgb_jpg(bin_filepath, output_filepath, look, exposure, exr_output):
# 1. Read the .bin file
with open(bin_filepath, 'rb') as f:
width_bytes = f.read(4)
height_bytes = f.read(4)
if not width_bytes or len(width_bytes) < 4 or not height_bytes or len(height_bytes) < 4:
raise RuntimeError(f'Could not read dimensions from {bin_filepath}. File might be too short or corrupt.')
width = struct.unpack('<i', width_bytes)[0]
height = struct.unpack('<i', height_bytes)[0]
if width <= 0 or height <= 0:
raise RuntimeError(f'Invalid dimensions read from file: {width}x{height}')
print(f'Read dimensions: {width}x{height}')
expected_pixel_data_bytes = width * height * 4 * 4
pixel_data_bytes = f.read(expected_pixel_data_bytes)
if len(pixel_data_bytes) != expected_pixel_data_bytes:
raise RuntimeError('Unexpected pixel data size.')
pixels_flat_original_order = np.frombuffer(pixel_data_bytes, dtype=np.float32)
if pixels_flat_original_order.size != width * height * 4:
raise RuntimeError('Pixel data size mismatch after numpy conversion.')
# Reshape to (height, width, 4) to easily flip rows
pixels_reshaped = pixels_flat_original_order.reshape((height, width, 4))
# Flip along the first axis (rows/height)
pixels_flipped_vertically = np.flip(pixels_reshaped, axis=0)
# Flatten back for Blender
pixels_to_set = pixels_flipped_vertically.ravel()
# 2. Create a Blender image
image_name = os.path.basename(bin_filepath) + '_srgb_conversion'
if image_name in bpy.data.images:
existing_image = bpy.data.images[image_name]
bpy.data.images.remove(existing_image)
blender_image = bpy.data.images.new(
name=image_name,
width=width,
height=height,
alpha=True,
float_buffer=True
)
# 3. Set the input color space of the raw pixel data
blender_image.colorspace_settings.name = INPUT_COLOR_SPACE
print(f'Using input color space: {INPUT_COLOR_SPACE}')
blender_image.pixels.foreach_set(pixels_to_set)
blender_image.update()
# 4. Configure scene for sRGB output and save
scene = bpy.context.scene
if exr_output:
scene.render.image_settings.file_format = 'OPEN_EXR'
scene.render.image_settings.color_depth = '32' # 32-bit float
scene.render.image_settings.color_mode = 'RGBA'
scene.render.image_settings.color_management = 'OVERRIDE'
scene.render.image_settings.linear_colorspace_settings.name = LINEAR_COLOR_SPACE_FOR_EXR
# Set display/view transforms to neutral to avoid unwanted transformations
# when outputting to a specified linear color space.
scene.display_settings.display_device = 'Rec.2020'
scene.view_settings.view_transform = 'Raw'
print(f'Set scene display device to: {scene.display_settings.display_device}')
print(f'Set scene view transform to: {scene.view_settings.view_transform}')
print(f'Target Color Space: {LINEAR_COLOR_SPACE_FOR_EXR}')
print(f'Configured for EXR output.')
else:
scene.render.image_settings.file_format = 'JPEG'
scene.render.image_settings.quality = 90
scene.render.image_settings.color_mode = 'RGB'
scene.display_settings.display_device = TARGET_DISPLAY_DEVICE_FOR_SRGB
scene.view_settings.view_transform = TARGET_VIEW_TRANSFORM_FOR_SRGB
scene.view_settings.exposure = exposure
scene.view_settings.look = look
print(f'Set scene display device to: {scene.display_settings.display_device}')
print(f'Set scene view transform to: {scene.view_settings.view_transform}')
print(f'This combination will result in an sRGB output for the JPG.')
output_dir = os.path.dirname(output_filepath)
os.makedirs(output_dir, exist_ok=True)
blender_image.save_render(filepath=output_filepath, scene=scene)
print(f'Successfully converted and saved to {output_filepath}')
def main():
parser = argparse.ArgumentParser(description='Convert .bin (float32 RGBA) to sRGB JPG using Blender.')
parser.add_argument('--input', '-i', type=str, required=True, help='Path to the input .bin file.')
parser.add_argument('--output', '-o', type=str, required=True, help='Path to save the output .jpg file.')
parser.add_argument('--look', '-l', type=str, required=True, help='Look name.')
parser.add_argument('--exposure', '-e', type=float, required=True, help='Exposure.')
parser.add_argument('--exr', action='store_true', help='Output as EXR in Linear Rec.2020 color space. If not set, outputs JPG in sRGB.')
try:
argv = sys.argv[sys.argv.index('--') + 1:]
args = parser.parse_args(argv)
print(f'Input BIN file: {args.input}')
print(f'Output JPG file: {args.output}')
if args.exr:
print(f'Output EXR file: {args.output}')
else:
print(f'Output JPG file: {args.output}')
print(f'Look: {args.look}')
print(f'Exposure: {args.exposure}')
convert_bin_to_srgb_jpg(args.input, args.output, args.look, args.exposure, args.exr)
print('Conversion process completed successfully.')
return
except:
traceback.print_exc(file=sys.stdout)
print('Conversion process failed.')
if __name__ == '__main__':
main()
";
bin2JpgPath = Path.Combine(exeDirPath, "bin2jpg.py");
File.WriteAllText(bin2JpgPath, pyContent);
}
private void FetchLooks()
{
lookNames.Clear();
lookNames.Add("None");
if (blenderPath == null)
{
return;
}
string pyContent = @"
import PyOpenColorIO as ocio
for look in ocio.GetCurrentConfig().getLookNames():
print(f'LOOK: {look}')
";
var dumpLooksPath = Path.Combine(exeDirPath, "dump_looks.py");
File.WriteAllText(dumpLooksPath, pyContent);
var looks = RunBlender(dumpLooksPath);
foreach (var look in looks.Split('\n'))
{
if (look.StartsWith("LOOK: ") && !look.Contains(" - "))
{
lookNames.Add(look.Trim().Replace("LOOK: ", ""));
}
}
}
}
}
@gitcrtn
Copy link
Author

gitcrtn commented Jul 14, 2025

ビルド済み実行ファイルもあります。
https://carrot.games/dl/jxr2jpg.exe

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment