Last active
July 14, 2025 02:10
-
-
Save gitcrtn/c5edbea00c2b43ddb4c0e6c91d2f4a07 to your computer and use it in GitHub Desktop.
JXR to JPG Converter for Windows11
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /* | |
| * 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: ", "")); | |
| } | |
| } | |
| } | |
| } | |
| } |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
ビルド済み実行ファイルもあります。
https://carrot.games/dl/jxr2jpg.exe