Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 14 additions & 8 deletions PhotoLocator/ImageTransformCommands.cs
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ await Task.Run(() =>
metadata = ExifHandler.LoadMetadata(file);
}
catch { } // Ignore if there is no supported metadata
GeneralFileFormatHandler.SaveToFile(pictureSource, sourceFileName, metadata, _mainViewModel.Settings.JpegQuality);
GeneralFileFormatHandler.SaveToFile(pictureSource, sourceFileName, metadata, _mainViewModel.Settings);
}, ct);
}
await Task.Run(() => JpegTransformations.Crop(sourceFileName, targetFileName, cropRectangle), ct);
Expand Down Expand Up @@ -170,7 +170,7 @@ private async Task SaveProcessedImageAsync(LocalContrastViewModel localContrastV
await using var pause = _mainViewModel.PauseFileSystemWatcher();
var sameDir = Path.GetDirectoryName(selectedItem.FullPath) == Path.GetDirectoryName(dlg.FileName);
await Task.Run(() => GeneralFileFormatHandler.SaveToFile(localContrastViewModel.PreviewPictureSource!, dlg.FileName,
ExifHandler.ResetOrientation(metadata), _mainViewModel.Settings.JpegQuality));
ExifHandler.ResetOrientation(metadata), _mainViewModel.Settings));
if (sameDir)
await _mainViewModel.AddOrUpdateItemAsync(dlg.FileName, false, false);
}
Expand All @@ -187,14 +187,14 @@ await _mainViewModel.RunProcessWithProgressBarAsync((progressCallback, ct) => Ta
if (item == selectedItem)
{
GeneralFileFormatHandler.SaveToFile(localContrastViewModel.PreviewPictureSource!, targetFileName,
ExifHandler.ResetOrientation(metadata), _mainViewModel.Settings.JpegQuality);
ExifHandler.ResetOrientation(metadata), _mainViewModel.Settings);
}
else
{
var (image, itemMetadata) = await LoadImageWithMetadataAsync(item);
image = localContrastViewModel.ApplyOperations(image);
GeneralFileFormatHandler.SaveToFile(image, targetFileName,
ExifHandler.ResetOrientation(itemMetadata), _mainViewModel.Settings.JpegQuality);
ExifHandler.ResetOrientation(itemMetadata), _mainViewModel.Settings);
}
progressCallback((double)(++i) / allSelected.Length);
}
Expand Down Expand Up @@ -233,10 +233,16 @@ await _mainViewModel.RunProcessWithProgressBarAsync(async (progressCallback, ct)
overwriteAll = true;
}

var (image, itemMetadata) = await LoadImageWithMetadataAsync(item);
await Task.Run(() => GeneralFileFormatHandler.SaveToFile(image, targetFileName,
ExifHandler.ResetOrientation(itemMetadata), _mainViewModel.Settings.JpegQuality), ct);

if (targetType == "jxl" && Path.GetExtension(item.Name).ToLowerInvariant() is ".jpg" or ".jpeg")
{
await Task.Run(() => JpegXlFileFormatHandler.TranscodeToJxl(item.FullPath, targetFileName, null, ct), ct);
}
else
{
var (image, itemMetadata) = await LoadImageWithMetadataAsync(item);
await Task.Run(() => GeneralFileFormatHandler.SaveToFile(image, targetFileName,
ExifHandler.ResetOrientation(itemMetadata), _mainViewModel.Settings), ct);
}
Comment thread
meesoft marked this conversation as resolved.
progressCallback((double)(++i) / allSelected.Length);
}
}, "Convert to " + targetType);
Expand Down
1 change: 1 addition & 0 deletions PhotoLocator/MainWindow.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,7 @@
<MenuItem Header="TIFF (LZW)" Command="{Binding ImageTransformCommands.ConvertFileFormatCommand}" CommandParameter="tif"/>
<MenuItem Header="PNG" Command="{Binding ImageTransformCommands.ConvertFileFormatCommand}" CommandParameter="png"/>
<MenuItem Header="JPEG XR (lossless)" Command="{Binding ImageTransformCommands.ConvertFileFormatCommand}" CommandParameter="jxr"/>
<MenuItem Header="JPEG XL" Command="{Binding ImageTransformCommands.ConvertFileFormatCommand}" CommandParameter="jxl"/>
<MenuItem Header="BMP" Command="{Binding ImageTransformCommands.ConvertFileFormatCommand}" CommandParameter="bmp"/>
</MenuItem>
<Separator />
Expand Down
2 changes: 1 addition & 1 deletion PhotoLocator/Metadata/ExifHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ public static BitmapMetadata EncodePngMetadata(Rational? exposureTime, Location?
return result;
}

static (Rational? ExposureTime, Location? Location) DecodePngMetadata(BitmapMetadata metadata)
internal static (Rational? ExposureTime, Location? Location) DecodePngMetadata(BitmapMetadata metadata)
{
if (metadata.GetQuery("/Text/Description") is not string str)
return (null, null);
Expand Down
65 changes: 55 additions & 10 deletions PhotoLocator/Metadata/ExifTool.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Media.Imaging;
Expand All @@ -21,19 +22,19 @@ public static async Task AdjustTimestampAsync(string sourceFileName, string targ
throw new UserMessageException("Offset must have a sign followed by a value");
var sign = offset[0];
offset = offset[1..];
var startInfo = new ProcessStartInfo(exifToolPath, $"\"-AllDates{sign}={offset}\" \"{sourceFileName}\" ");
var startInfo = new ProcessStartInfo(exifToolPath, $"\"-AllDates{sign}={offset}\" \"{sourceFileName}\"");
await RunExifToolAsync(sourceFileName, targetFileName, startInfo, ct);
}

public static async Task SetTimestampAsync(string sourceFileName, string targetFileName, string timestamp, string exifToolPath, CancellationToken ct)
{
var startInfo = new ProcessStartInfo(exifToolPath, $"\"-AllDates={timestamp}\" \"{sourceFileName}\" ");
var startInfo = new ProcessStartInfo(exifToolPath, $"\"-AllDates={timestamp}\" \"{sourceFileName}\"");
await RunExifToolAsync(sourceFileName, targetFileName, startInfo, ct);
}

public static async Task TransferMetadataAsync(string metadataFileName, string sourceFileName, string targetFileName, string exifToolPath, CancellationToken ct)
{
var startInfo = new ProcessStartInfo(exifToolPath, $"-tagsfromfile \"{metadataFileName}\" \"{sourceFileName}\" "); // -exif
var startInfo = new ProcessStartInfo(exifToolPath, $"-tagsfromfile \"{metadataFileName}\" \"{sourceFileName}\""); // -exif
await RunExifToolAsync(sourceFileName, targetFileName, startInfo, ct);
}

Expand All @@ -44,25 +45,69 @@ public static async Task SetGeotagAsync(string sourceFileName, string targetFile
ExifHandler.SetJpegGeotag(sourceFileName, targetFileName, location);
return;
}
var startInfo = new ProcessStartInfo(exifToolPath, GetLocationParameters(location) + $"\"{sourceFileName}\"");
await RunExifToolAsync(sourceFileName, targetFileName, startInfo, ct);
}

public static async Task SetMetadataAsync(string sourceFileName, string targetFileName, BitmapMetadata metadata, string exifToolPath, CancellationToken ct)
{
var sb = new StringBuilder();

var cameraModel = metadata.CameraModel;
if (!string.IsNullOrEmpty(cameraModel))
sb.Append(CultureInfo.InvariantCulture, $"-Model=\"{cameraModel.Replace('"', '\'')}\" ");

Comment thread
meesoft marked this conversation as resolved.
var location = ExifHandler.GetGeotag(metadata);
if (location is not null)
sb.Append(GetLocationParameters(location));

var altitude = ExifHandler.GetRelativeAltitude(metadata);
if (altitude.HasValue)
sb.Append(CultureInfo.InvariantCulture, $"-RelativeAltitude={altitude.Value} ");

var exposureTime = Rational.Decode(metadata.GetQuery(ExifHandler.ExposureTimeQuery1) ?? metadata.GetQuery(ExifHandler.ExposureTimeQuery2));
if (exposureTime is null && metadata.TryGetFormat() == "png")
(exposureTime, var _) = ExifHandler.DecodePngMetadata(metadata);
if (exposureTime is not null && exposureTime.Numerator > 0 && exposureTime.Denominator > 0)
sb.Append(CultureInfo.InvariantCulture, $"-ExposureTime={exposureTime.ToDouble()} ");

var startInfo = new ProcessStartInfo(exifToolPath, string.Create(CultureInfo.InvariantCulture,
//"-m " + // Ignore minor errors and warnings
var lensAperture = Rational.Decode(metadata.GetQuery(ExifHandler.LensApertureQuery1) ?? metadata.GetQuery(ExifHandler.LensApertureQuery2));
if (lensAperture is not null && lensAperture.Numerator > 0 && lensAperture.Denominator > 0)
sb.Append(CultureInfo.InvariantCulture, $"-FNumber={lensAperture.ToDouble()} ");
Comment thread
meesoft marked this conversation as resolved.

var focalLength = Rational.Decode(metadata.GetQuery(ExifHandler.FocalLengthQuery1) ?? metadata.GetQuery(ExifHandler.FocalLengthQuery2));
if (focalLength is not null && focalLength.Numerator > 0 && focalLength.Denominator > 0)
sb.Append(CultureInfo.InvariantCulture, $"-FocalLength={focalLength.ToDouble()} ");

var iso = metadata.GetQuery(ExifHandler.IsoQuery1) ?? metadata.GetQuery(ExifHandler.IsoQuery2);
if (iso is not null)
sb.Append(CultureInfo.InvariantCulture, $"-ISO={iso} ");

if (sb.Length == 0)
return;
sb.Append(CultureInfo.InvariantCulture, $"\"{sourceFileName}\"");
var startInfo = new ProcessStartInfo(exifToolPath, sb.ToString());
await RunExifToolAsync(sourceFileName, targetFileName, startInfo, ct).ConfigureAwait(false);
}

private static string GetLocationParameters(Location location)
{
return string.Create(CultureInfo.InvariantCulture,
//$"-m " + // Ignore minor errors and warnings
$"-GPSLatitude={location.Latitude} " +
$"-GPSLatitudeRef={Math.Sign(location.Latitude)} " +
$"-GPSLongitude={location.Longitude} " +
$"-GPSLongitudeRef={Math.Sign(location.Longitude)} " +
$"\"{sourceFileName}\" "));
await RunExifToolAsync(sourceFileName, targetFileName, startInfo, ct);
$"-GPSLongitudeRef={Math.Sign(location.Longitude)} ");
}

private static async Task RunExifToolAsync(string sourceFileName, string targetFileName, ProcessStartInfo startInfo, CancellationToken ct)
{
if (targetFileName == sourceFileName)
startInfo.Arguments += "-overwrite_original";
startInfo.Arguments += " -overwrite_original";
else
{
File.Delete(targetFileName);
startInfo.Arguments += $"-out \"{targetFileName}\"";
startInfo.Arguments += $" -out \"{targetFileName}\"";
}
startInfo.CreateNoWindow = true;
startInfo.RedirectStandardOutput = true;
Expand Down
20 changes: 14 additions & 6 deletions PhotoLocator/PictureFileFormats/GeneralFileFormatHandler.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using PhotoLocator.Helpers;
using PhotoLocator.Metadata;
using PhotoLocator.Settings;
using System;
using System.IO;
using System.Threading;
Expand All @@ -9,7 +10,9 @@ namespace PhotoLocator.PictureFileFormats
{
static class GeneralFileFormatHandler
{
public const string SaveImageFilter = "JPEG|*.jpg|PNG|*.png|TIFF|*.tif|JPEG XR lossless|*.jxr|BMP|*.bmp";
public const string SaveImageFilter = "JPEG|*.jpg|PNG|*.png|TIFF|*.tif|JPEG XR lossless|*.jxr|JPEG XL|*.jxl|BMP|*.bmp";

public const int DefaultJpegQuality = 90;

static string? _jpegliPath;
static bool _jpegliChecked;
Expand Down Expand Up @@ -43,7 +46,7 @@ public static BitmapSource LoadFromStream(Stream source, Rotation rotation, int
return bitmap;
}

public static void SaveToFile(BitmapSource image, string targetPath, BitmapMetadata? metadata = null, int jpegQuality = 95)
public static void SaveToFile(BitmapSource bitmap, string targetPath, BitmapMetadata? metadata = null, ISettings? settings = null)
{
var ext = Path.GetExtension(targetPath).ToLowerInvariant();
BitmapEncoder encoder;
Expand All @@ -62,10 +65,10 @@ public static void SaveToFile(BitmapSource image, string targetPath, BitmapMetad
if (_jpegliPath is not null)
{
Log.Write("Saving using " + _jpegliPath);
JpegliEncoder.SaveToFile(image, targetPath, metadata, jpegQuality, _jpegliPath);
JpegliEncoder.SaveToFile(bitmap, targetPath, metadata, settings?.JpegQuality ?? DefaultJpegQuality, _jpegliPath);
return;
}
encoder = new JpegBitmapEncoder() { QualityLevel = jpegQuality };
encoder = new JpegBitmapEncoder() { QualityLevel = settings?.JpegQuality ?? DefaultJpegQuality };
}
else if (ext is ".tif" or ".tiff")
encoder = new TiffBitmapEncoder(); // Default is best compression
Expand All @@ -75,13 +78,18 @@ public static void SaveToFile(BitmapSource image, string targetPath, BitmapMetad
encoder = new BmpBitmapEncoder();
else if (ext is ".jxr")
encoder = new WmpBitmapEncoder() { Lossless = true };
else if (ext is ".jxl")
{
JpegXlFileFormatHandler.SaveToFile(bitmap, targetPath, metadata, settings);
return;
}
else
throw new UserMessageException("Unsupported file format " + ext);

if (metadata is null)
encoder.Frames.Add(BitmapFrame.Create(image));
encoder.Frames.Add(BitmapFrame.Create(bitmap));
else
encoder.Frames.Add(BitmapFrame.Create(image, null, ExifHandler.CreateMetadataForEncoder(metadata, encoder), null));
encoder.Frames.Add(BitmapFrame.Create(bitmap, null, ExifHandler.CreateMetadataForEncoder(metadata, encoder), null));

using var fileStream = new FileStream(targetPath, FileMode.Create);
encoder.Save(fileStream);
Expand Down
2 changes: 1 addition & 1 deletion PhotoLocator/PictureFileFormats/JpegTransformations.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ static class JpegTransformations
{
public static bool IsFileTypeSupported(string fileName)
{
return Path.GetExtension(fileName).ToUpperInvariant() is ".JPG" or ".JPEG";
return Path.GetExtension(fileName).ToLowerInvariant() is ".jpg" or ".jpeg";
}

public static void Rotate(string sourceFileName, string newFileName, int angleDegrees)
Expand Down
72 changes: 72 additions & 0 deletions PhotoLocator/PictureFileFormats/JpegXlFileFormatHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
using PhotoLocator.Helpers;
using PhotoLocator.Metadata;
using System;
using System.Diagnostics;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Media.Imaging;

namespace PhotoLocator.PictureFileFormats
{
public static class JpegXlFileFormatHandler
{
public const string EncoderName = "cjxl.exe";
internal static string EncoderPath { get; } = Path.Combine(AppContext.BaseDirectory, "jpegli", EncoderName);

public static void SaveToStream(BitmapSource bitmap, Stream dest, string encoderPath, BitmapMetadata? metadata, int quality)
{
using var srcStream = new MemoryStream();
var encoder = new PngBitmapEncoder();
if (metadata is null)
encoder.Frames.Add(BitmapFrame.Create(bitmap));
else
encoder.Frames.Add(BitmapFrame.Create(bitmap, null, ExifHandler.CreateMetadataForEncoder(metadata, encoder), null));
encoder.Save(srcStream);
srcStream.Position = 0;
JpegliEncoder.Process(encoderPath, srcStream, ".png", dest, ".jxl", $"-q {quality}");
}

public static void SaveToFile(BitmapSource image, string targetPath, BitmapMetadata? metadata = null, Settings.ISettings? settings = null)
{
using (var stream = new FileStream(targetPath, FileMode.Create, FileAccess.Write))
SaveToStream(image, stream, EncoderPath, metadata, settings?.JpegQuality ?? GeneralFileFormatHandler.DefaultJpegQuality);
if (metadata is not null && !string.IsNullOrEmpty(settings?.ExifToolPath))
ExifTool.SetMetadataAsync(targetPath, targetPath, metadata, settings.ExifToolPath, CancellationToken.None).Wait();
Comment on lines +34 to +35

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the handler file and show the relevant region
ls -la
fd -i "JpegXlFileFormatHandler.cs" .
echo "---- File context around handler ----"
FILE=$(fd -i "JpegXlFileFormatHandler.cs" . -a | head -n 1)
echo "Using: $FILE"
sed -n '1,120p' "$FILE" | nl -ba | sed -n '25,80p'
echo "---- Exact lines 34-60 (best effort) ----"
sed -n '20,60p' "$FILE" | nl -ba

# Find ExifTool and SetMetadataAsync definition/overloads
echo "---- Searching for SetMetadataAsync ----"
rg -n "SetMetadataAsync" -S . -g'*.cs'

# Show the ExifTool.SetMetadataAsync implementation(s)
echo "---- Showing implementation(s) ----"
rg -n "Task\s+SetMetadataAsync|SetMetadataAsync\s*\(" -S . -g'*.cs' | head -n 50

# Attempt to open the first implementation file(s)
# (We use sed ranges after locating filenames.)
IMPL_FILES=$(rg -l "SetMetadataAsync" -S . -g'*.cs' | tr '\n' ' ')
echo "---- Implementation files (top 10) ----"
for f in $(rg -l "SetMetadataAsync" -S . -g'*.cs' | head -n 10); do echo "$f"; done

# Show likely ExifTool class file(s)
for f in $(rg -l "class\s+ExifTool" -S . -g'*.cs' | head -n 5); do
  echo "---- ExifTool class snippet: $f ----"
  sed -n '1,220p' "$f" | nl -ba | head -n 220
done

# Find call sites of ExifTool.SetMetadataAsync that may also use .Wait() or .Result
echo "---- Call sites using .Wait() / .Result ----"
rg -n "SetMetadataAsync\([^)]*\)\.(Wait|Result)" -S . -g'*.cs'
rg -n "\.Wait\(\)\s*;" -S . -g'*.cs' | head -n 50

# Find where JpegXlFileFormatHandler is invoked from the WPF save flow (async vs sync)
echo "---- Searching for JpegXlFileFormatHandler usage ----"
rg -n "JpegXlFileFormatHandler" -S . -g'*.cs'

Repository: meesoft/PhotoLocator

Length of output: 2051


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="PhotoLocator/PictureFileFormats/JpegXlFileFormatHandler.cs"
echo "---- Handler snippet (lines 1-120) ----"
sed -n '1,120p' "$FILE" | head -n 120

echo "---- Handler snippet (lines 30-55) ----"
sed -n '30,55p' "$FILE"

echo "---- Search for SetMetadataAsync in repo ----"
rg -n "SetMetadataAsync" PhotoLocator -S --glob='*.cs'

echo "---- Search for SetMetadataAsync(...).Wait() ----"
rg -n "SetMetadataAsync\s*\([^)]*\)\s*\.\s*Wait\s*\(\s*\)" PhotoLocator -S --glob='*.cs' || true

echo "---- Search for ExifTool class ----"
rg -n "class\s+ExifTool\b" PhotoLocator -S --glob='*.cs'

echo "---- Show ExifTool.* implementation(s) ----"
rg -n "Task\s+SetMetadataAsync|SetMetadataAsync\s*\(" PhotoLocator -S --glob='*.cs'

# List candidate implementation files and show surrounding code
IMPL_FILES=$(rg -l "SetMetadataAsync" PhotoLocator -S --glob='*.cs' | head -n 20)
echo "---- Candidate implementation files (first 20) ----"
echo "$IMPL_FILES"

for f in $IMPL_FILES; do
  echo "==== $f (first 220 lines) ===="
  sed -n '1,220p' "$f"
done

Repository: meesoft/PhotoLocator

Length of output: 20589


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n "JpegXlFileFormatHandler\.SaveToFile" -S . --glob='*.cs'
echo "----"
rg -n "SaveToFile\s*\(" PhotoLocator -S --glob='*.cs' | head -n 200
echo "----"
# Try to find typical WPF command/save entry points mentioning "Save" and "PictureFileFormats"
rg -n "Save.*(Async|Task)|Dispatcher|SyncContext|SynchronizationContext" PhotoLocator -S --glob='*.cs' | head -n 200
echo "----"
# Search for ExifTool usage and any async caller patterns
rg -n "ExifTool\.SetMetadataAsync" -S . --glob='*.cs'

Repository: meesoft/PhotoLocator

Length of output: 7226


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILELIST=(
  PhotoLocator/PictureFileFormats/GeneralFileFormatHandler.cs
  PhotoLocator/MainViewModel.cs
  PhotoLocator/ImageTransformCommands.cs
  PhotoLocator/VideoTransformCommands.cs
  PhotoLocator/PictureItemViewModel.cs
  PhotoLocator/Helpers/ExceptionHandler.cs
  PhotoLocator/App.xaml.cs
)
for f in "${FILELIST[@]}"; do
  echo "==== $f (head 220) ===="
  sed -n '1,220p' "$f" | nl -ba | head -n 220
done

# Print focused ranges around the identified call sites
echo "==== Focus: GeneralFileFormatHandler.cs around line ~60-120 ===="
sed -n '40,120p' PhotoLocator/PictureFileFormats/GeneralFileFormatHandler.cs | nl -ba

echo "==== Focus: MainViewModel.cs around line ~430-490 ===="
sed -n '420,500p' PhotoLocator/MainViewModel.cs | nl -ba

echo "==== Focus: ImageTransformCommands.cs around line ~60-280 ===="
sed -n '60,280p' PhotoLocator/ImageTransformCommands.cs | nl -ba

echo "==== Focus: VideoTransformCommands.cs around lines ~1030-1120 ===="
sed -n '1020,1125p' PhotoLocator/VideoTransformCommands.cs | nl -ba

echo "==== Focus: PictureItemViewModel.cs around line ~330-420 (SaveGeoTagAsync mention) ===="
sed -n '300,420p' PhotoLocator/PictureItemViewModel.cs | nl -ba

echo "==== Focus: ExceptionHandler.cs around line ~1-120 ===="
sed -n '1,120p' PhotoLocator/Helpers/ExceptionHandler.cs | nl -ba

echo "==== Focus: App.xaml.cs around dispatcher exception handler ===="
sed -n '1,120p' PhotoLocator/App.xaml.cs | nl -ba

Repository: meesoft/PhotoLocator

Length of output: 207


🏁 Script executed:

#!/bin/bash
set -euo pipefail

show_range () {
  local file="$1"
  local start="$2"
  local end="$3"
  echo "==== $file ($start-$end) ===="
  sed -n "${start},${end}p" "$file" | cat -n
}

show_range PhotoLocator/PictureFileFormats/GeneralFileFormatHandler.cs 1 140
show_range PhotoLocator/PictureFileFormats/GeneralFileFormatHandler.cs 140 220

show_range PhotoLocator/MainViewModel.cs 420 560

show_range PhotoLocator/ImageTransformCommands.cs 1 260
show_range PhotoLocator/ImageTransformCommands.cs 260 340

show_range PhotoLocator/VideoTransformCommands.cs 1000 1125

show_range PhotoLocator/PictureItemViewModel.cs 300 430

show_range PhotoLocator/Helpers/ExceptionHandler.cs 1 120
show_range PhotoLocator/App.xaml.cs 1 120

Repository: meesoft/PhotoLocator

Length of output: 46048


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show more context around the clipboard image paste loop in MainViewModel
# (Search for the clipboard image / Clipboard.ContainsImage() block)
rg -n "Clipboard\.ContainsImage\(\)" PhotoLocator/MainViewModel.cs --glob='*.cs'

# Print a wider window around that match
MATCH_LINE=$(rg -n "Clipboard\.ContainsImage\(\)" PhotoLocator/MainViewModel.cs --glob='*.cs' | head -n1 | cut -d: -f1)
echo "Match at line: $MATCH_LINE"
start=$((MATCH_LINE-60))
end=$((MATCH_LINE+80))
sed -n "${start},${end}p" PhotoLocator/MainViewModel.cs | cat -n

# Inspect VideoTransformCommands area around a method that calls GeneralFileFormatHandler.SaveToFile
# Find the enclosing command/delegate for the snippet lines
rg -n "GeneralFileFormatHandler\.SaveToFile\(" PhotoLocator/VideoTransformCommands.cs --glob='*.cs'
# For first occurrence near earlier snippet, show surrounding context
# We'll just show around each occurrence line.
rg -n "GeneralFileFormatHandler\.SaveToFile\(" PhotoLocator/VideoTransformCommands.cs --glob='*.cs' | while read -r line; do
  ln=$(echo "$line" | cut -d: -f1)
  file=$(echo "$line" | cut -d: -f1)
  : # noop
done

# Actually extract for each match: line number and show window
while IFS=: read -r file ln rest; do
  echo "---- $file match at $ln ----"
  s=$((ln-60)); e=$((ln+80))
  sed -n "${s},${e}p" "$file" | cat -n | head -n 220
done < <(rg -n "GeneralFileFormatHandler\.SaveToFile\(" PhotoLocator/VideoTransformCommands.cs --glob='*.cs' | head -n 10)

# Search exception handling patterns for AggregateException / Wait usage
rg -n "AggregateException" PhotoLocator --glob='*.cs'
rg -n "\.Wait\(\)" PhotoLocator --glob='*.cs' | head -n 50

# Find any callers that don't unwrap AggregateException from tasks
rg -n "await .*SaveToFile|SaveToFile\(" PhotoLocator --glob='*.cs' | head -n 100

Repository: meesoft/PhotoLocator

Length of output: 7508


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) Find the command/method in VideoTransformCommands that contains the SaveToFile call.
#    We'll print surrounding larger region and then locate its entry point.
FILE="PhotoLocator/VideoTransformCommands.cs"
# show matches for SaveToFile
echo "---- SaveToFile call sites in VideoTransformCommands.cs ----"
rg -n "GeneralFileFormatHandler\.SaveToFile\(" "$FILE" --glob='*.cs'

# Pick first match and dump a bigger region above it
FIRST_LN=$(rg -n "GeneralFileFormatHandler\.SaveToFile\(" "$FILE" --glob='*.cs' | head -n1 | cut -d: -f1)
echo "First SaveToFile line: $FIRST_LN"
START=$((FIRST_LN-220))
END=$((FIRST_LN+120))
echo "---- Context $START-$END ----"
sed -n "${START},${END}p" "$FILE" | cat -n

# 2) Look for wrapping Task.Run/await patterns around that processing delegate.
echo "---- Search for Task.Run / RunProcessWithProgressBar in VideoTransformCommands.cs ----"
rg -n "RunProcessWithProgressBarAsync|Task\.Run\(|Dispatcher\.|await Task\.Run" "$FILE" --glob='*.cs'

# 3) Find where output file extension is determined for image sequence/video/time-slice processing.
#    Search for ".jxl" usage.
echo "---- Search for jxl in VideoTransformCommands.cs ----"
rg -n "\"?\.jxl|jxl\"" "$FILE" --glob='*.cs'

# 4) Find any other direct SaveToFile calls (not inside Task.Run) in repo.
echo "---- Find SaveToFile invocations not wrapped in Task.Run ----"
# Heuristic: show lines with SaveToFile( and look at nearest few lines above for Task.Run.
# We'll just list all SaveToFile calls with 20 lines context.
rg -n "GeneralFileFormatHandler\.SaveToFile\(" PhotoLocator --glob='*.cs' -C 2

# 5) Determine if any call to JpegXlFileFormatHandler.SaveToFile happens outside Task.Run.
echo "---- Calls to JpegXlFileFormatHandler.SaveToFile ----"
rg -n "JpegXlFileFormatHandler\.SaveToFile\(" PhotoLocator --glob='*.cs' -C 2

Repository: meesoft/PhotoLocator

Length of output: 21394


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "---- GeneralFileFormatHandler.SaveToFile call sites (with small context) ----"
rg -n "GeneralFileFormatHandler\.SaveToFile\(" PhotoLocator --glob='*.cs' -C 3

echo "---- Any direct JpegXlFileFormatHandler.SaveToFile call sites (with context) ----"
rg -n "JpegXlFileFormatHandler\.SaveToFile\(" PhotoLocator --glob='*.cs' -C 3

echo "---- Call sites of JpegXlFileFormatHandler.SaveToFile within Task.Run bodies (heuristic) ----"
rg -n "Task\.Run\([^)]*\)[\s\S]*JpegXlFileFormatHandler\.SaveToFile" PhotoLocator --glob='*.cs' || true

echo "---- Identify if any SaveToFile call happens outside Task.Run ----"
# Heuristic: list SaveToFile call lines and 5 lines above, then user can see if Task.Run is near.
rg -n "GeneralFileFormatHandler\.SaveToFile\(" PhotoLocator --glob='*.cs' | cut -d: -f1 | sort -u | while read -r file; do
  : 
done

Repository: meesoft/PhotoLocator

Length of output: 8668


Remove the .Wait() on ExifTool.SetMetadataAsync in JpegXlFileFormatHandler.SaveToFile.

  • SaveToFile currently does ExifTool.SetMetadataAsync(...).Wait(); while SetMetadataAsync/RunExifToolAsync use await without ConfigureAwait(false), so blocking on it under a WPF SynchronizationContext can deadlock.
  • Blocking also wraps failures (e.g., UserMessageException/IOException) in AggregateException.

if (metadata is not null && !string.IsNullOrEmpty(settings?.ExifToolPath)) ExifTool.SetMetadataAsync(...).Wait();

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@PhotoLocator/PictureFileFormats/JpegXlFileFormatHandler.cs` around lines 34 -
35, In JpegXlFileFormatHandler.SaveToFile remove the blocking .Wait() call and
await ExifTool.SetMetadataAsync instead: change SaveToFile to an async method
(e.g., Task or Task<T> as appropriate), replace the .Wait() usage with await
ExifTool.SetMetadataAsync(targetPath, targetPath, metadata,
settings.ExifToolPath, CancellationToken.None), and propagate the async change
to callers so exceptions are not wrapped in AggregateException and you avoid
deadlocks on the WPF SynchronizationContext.

}

public static void TranscodeToJxl(string sourcePath, string targetPath, string? arguments, CancellationToken ct)
{
using var process = new Process();
process.StartInfo.FileName = EncoderPath;
process.StartInfo.UseShellExecute = false;
process.StartInfo.RedirectStandardError = true;
process.StartInfo.CreateNoWindow = true;
process.StartInfo.Arguments = $"\"{sourcePath}\" \"{targetPath}\" {arguments}";
process.Start();
string? output = null;
var outputTask = Task.Run(() => output = process.StandardError.ReadToEnd(), ct);
try
{
try
{
if (!process.WaitForExit(60000))
throw new TimeoutException();
if (process.ExitCode != 0)
throw new IOException("Codec failed with exit code " + process.ExitCode);
}
finally
{
if (outputTask.Wait(1000, ct))
Log.Write(output);
}
}
catch (Exception ex)
{
if (output is null)
throw;
throw new IOException("Codec failed with: " + output, ex);
Comment thread
meesoft marked this conversation as resolved.
}
}
}
}
Loading
Loading