Add queue for print downloads and automatic cropping for saved prints (#1068)

* Add queue for print downloads

To prevent issues with rate limits in instances with a lot of prints switch downloading prints to use a queue.

* Auto cropping for instance print saving

Add option to automatically crop prints saved using the "Save Instance Prints" feature and a popup to apply crop to all previously saved prints while preserving metadata.

---------

Co-authored-by: Natsumi <11171153+Natsumi-sama@users.noreply.github.com>
This commit is contained in:
Nekromateion
2025-01-09 00:47:02 +01:00
committed by GitHub
parent addad7bc06
commit 35621f27d2
6 changed files with 174 additions and 16 deletions

View File

@@ -213,6 +213,45 @@ namespace VRCX
return imageSaveMemoryStream.ToArray();
}
public async Task CropAllPrints(string ugcFolderPath)
{
var folder = Path.Combine(GetUGCPhotoLocation(ugcFolderPath), "Prints");
var files = Directory.GetFiles(folder, "*.png", SearchOption.AllDirectories);
foreach (var file in files)
{
await CropPrintImage(file);
}
}
public async Task<bool> CropPrintImage(string path)
{
var tempPath = path + ".temp";
var bytes = await File.ReadAllBytesAsync(path);
var ms = new MemoryStream(bytes);
Bitmap print = new Bitmap(ms);
// validation step to ensure image is actually a print
if (print.Width != 2048 || print.Height != 1440)
{
return false;
}
var point = new Point(64, 69);
var size = new Size(1920, 1080);
var rectangle = new Rectangle(point, size);
Bitmap cropped = print.Clone(rectangle, print.PixelFormat);
cropped.Save(tempPath);
if (ScreenshotHelper.HasTXt(path))
{
var success = ScreenshotHelper.CopyTXt(path, tempPath);
if (!success)
{
File.Delete(tempPath);
return false;
}
}
File.Move(tempPath, path, true);
return true;
}
/// <summary>
/// Computes the signature of the file represented by the specified base64-encoded string using the librsync library.
/// </summary>
@@ -581,7 +620,7 @@ namespace VRCX
{
if (enabled)
{
var path = Application.ExecutablePath;
var path = System.Windows.Forms.Application.ExecutablePath;
key.SetValue("VRCX", $"\"{path}\" --startup");
}
else
@@ -615,7 +654,7 @@ namespace VRCX
{
MainForm.Instance.BeginInvoke(new MethodInvoker(() =>
{
var image = Image.FromFile(path);
var image = System.Drawing.Image.FromFile(path);
// Clipboard.SetImage(image);
var data = new DataObject();
data.SetData(DataFormats.Bitmap, image);
@@ -654,26 +693,30 @@ namespace VRCX
return null;
}
public async Task<bool> SavePrintToFile(string url, string ugcFolderPath, string monthFolder, string fileName)
public async Task<string> SavePrintToFile(string url, string ugcFolderPath, string monthFolder, string fileName)
{
var folder = Path.Combine(GetUGCPhotoLocation(ugcFolderPath), "Prints", MakeValidFileName(monthFolder));
Directory.CreateDirectory(folder);
var filePath = Path.Combine(folder, MakeValidFileName(fileName));
if (File.Exists(filePath))
return false;
return null;
return await ImageCache.SaveImageToFile(url, filePath);
var success = await ImageCache.SaveImageToFile(url, filePath);
return success ? filePath : null;
}
public async Task<bool> SaveStickerToFile(string url, string ugcFolderPath, string monthFolder, string fileName)
public async Task<string> SaveStickerToFile(string url, string ugcFolderPath, string monthFolder, string fileName)
{
var folder = Path.Combine(GetUGCPhotoLocation(ugcFolderPath), "Stickers", MakeValidFileName(monthFolder));
Directory.CreateDirectory(folder);
var filePath = Path.Combine(folder, MakeValidFileName(fileName));
if (File.Exists(filePath))
return false;
return null;
return await ImageCache.SaveImageToFile(url, filePath);
var success = await ImageCache.SaveImageToFile(url, filePath);
return success ? filePath : null;
}
public bool IsRunningUnderWine()

View File

@@ -15,7 +15,7 @@ namespace VRCX
{
private static readonly ILogger logger = LogManager.GetCurrentClassLogger();
private static readonly byte[] pngSignatureBytes = { 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A };
private static readonly ScreenshotMetadataDatabase cacheDatabase = new ScreenshotMetadataDatabase(Path.Combine(Program.AppDataDirectory, "metadataCache.db"));
private static readonly ScreenshotMetadataDatabase cacheDatabase = new ScreenshotMetadataDatabase(System.IO.Path.Combine(Program.AppDataDirectory, "metadataCache.db"));
private static readonly Dictionary<string, ScreenshotMetadata> metadataCache = new Dictionary<string, ScreenshotMetadata>();
public enum ScreenshotSearchType
@@ -287,6 +287,34 @@ namespace VRCX
return true;
}
public static bool CopyTXt(string sourceImage, string targetImage)
{
if (!File.Exists(sourceImage) || !IsPNGFile(sourceImage) ||
!File.Exists(targetImage) || !IsPNGFile(targetImage))
return false;
var sourceMetadata = ReadTXt(sourceImage);
if (sourceMetadata == null)
return false;
var targetImageData = File.ReadAllBytes(targetImage);
var newChunkIndex = FindEndOfChunk(targetImageData, "IHDR");
if (newChunkIndex == -1) return false;
// If this file already has a text chunk, chances are it got logged twice for some reason. Stop.
var existingiTXt = FindChunkIndex(targetImageData, "iTXt");
if (existingiTXt != -1) return false;
var newFile = targetImageData.ToList();
newFile.InsertRange(newChunkIndex, sourceMetadata.ConstructChunkByteArray());
File.WriteAllBytes(targetImage, newFile.ToArray());
return true;
}
/// <summary>
/// Reads a text description from a PNG file at the specified path.
/// Reads any existing iTXt PNG chunk in the target file, using the Description tag.
@@ -308,6 +336,30 @@ namespace VRCX
}
}
public static bool HasTXt(string path)
{
if (!File.Exists(path) || !IsPNGFile(path)) return false;
using (var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, 512))
{
var existingiTXt = FindChunk(stream, "iTXt", true);
return existingiTXt != null;
}
}
public static PNGChunk ReadTXt(string path)
{
if (!File.Exists(path) || !IsPNGFile(path)) return null;
using (var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, 512))
{
var existingiTXt = FindChunk(stream, "iTXt", true);
return existingiTXt;
}
}
/// <summary>
/// Reads the PNG resolution.
/// </summary>