327 lines
12 KiB
C#
327 lines
12 KiB
C#
#nullable enable
|
|
|
|
using System;
|
|
using System.Collections.ObjectModel;
|
|
using System.Globalization;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Threading.Tasks;
|
|
using Avalonia.Platform.Storage;
|
|
using Avalonia.Themes.Fluent;
|
|
using DialogHostAvalonia;
|
|
using ImageMagick;
|
|
using MigraDoc.DocumentObjectModel;
|
|
using MigraDoc.Rendering;
|
|
using PdfSharp.Fonts;
|
|
using PdfSharp.Pdf.IO;
|
|
using PdfSharp.Snippets.Font;
|
|
using ReceiptPDFBuilder.Interfaces;
|
|
using ReceiptPDFBuilder.Models;
|
|
|
|
namespace ReceiptPDFBuilder.ViewModels;
|
|
|
|
class MainViewModel : BaseViewModel, IFontResolver
|
|
{
|
|
private string _baseDir;
|
|
private bool _isCreatingPDF;
|
|
private string _createPDFLog;
|
|
private string _workingFolder;
|
|
|
|
private ObservableCollection<ReportFile> _reportFiles;
|
|
|
|
public MainViewModel(IChangeViewModel viewModelChanger) : base(viewModelChanger)
|
|
{
|
|
_baseDir = Path.GetDirectoryName(Environment.ProcessPath) ?? "";
|
|
_isCreatingPDF = false;
|
|
_createPDFLog = "Ready to create PDF! Choose a folder to begin...";
|
|
_workingFolder = "";
|
|
_reportFiles = new ObservableCollection<ReportFile>();
|
|
_reportFiles.CollectionChanged += ( sender, e ) => { NotifyPropertyChanged(nameof(IsCreatePDFButtonEnabled)); };
|
|
}
|
|
|
|
public bool IsCreatingPDF
|
|
{
|
|
get => _isCreatingPDF;
|
|
set { _isCreatingPDF = value; NotifyPropertyChanged(); NotifyPropertyChanged(nameof(IsCreatePDFButtonEnabled)); }
|
|
}
|
|
|
|
public bool IsCreatePDFButtonEnabled
|
|
{
|
|
get => !_isCreatingPDF && _reportFiles.Count > 0;
|
|
}
|
|
|
|
public string CreatePDFLog
|
|
{
|
|
get => _createPDFLog;
|
|
set { _createPDFLog = value; NotifyPropertyChanged(); }
|
|
}
|
|
|
|
public ObservableCollection<ReportFile> ReportFiles
|
|
{
|
|
get => _reportFiles;
|
|
set { _reportFiles = value; NotifyPropertyChanged(); }
|
|
}
|
|
|
|
private void LogInfo(string message, params object[]? arguments)
|
|
{
|
|
var timestamp = string.Format("[{0:s}]", DateTime.Now);
|
|
Console.WriteLine(timestamp + " " + message, arguments);
|
|
CreatePDFLog += Environment.NewLine + string.Format(message, arguments ?? []);
|
|
}
|
|
|
|
public async void ChooseFolder()
|
|
{
|
|
var topLevel = TopLevelGrabber?.GetTopLevel();
|
|
if (topLevel is not null)
|
|
{
|
|
var folders = await topLevel.StorageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions()
|
|
{
|
|
Title = "Pick a folder of files...",
|
|
AllowMultiple = false,
|
|
});
|
|
if (folders.Count == 1)
|
|
{
|
|
var folder = folders[0];
|
|
LogInfo("Chosen folder: " + folder.Path.LocalPath);
|
|
if (Directory.Exists(folder.Path.LocalPath))
|
|
{
|
|
_workingFolder = folder.Path.LocalPath;
|
|
// TODO: Scan folder for saved info from previous reports and reload if needed
|
|
// Scan folder for files and display in DataGrid
|
|
var filePaths = Directory.GetFiles(_workingFolder);
|
|
filePaths.Sort();
|
|
foreach (var filePath in filePaths)
|
|
{
|
|
if (!filePath.Contains(".DS_Store"))
|
|
{
|
|
// TODO: if date in file name, pull out that date instead
|
|
ReportFiles.Add(new ReportFile()
|
|
{
|
|
Title = Path.GetFileName(filePath),
|
|
ReceiptDateTime = File.GetCreationTime(filePath),
|
|
Notes = "",
|
|
FilePath = filePath,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public async void RemoveFile(ReportFile file)
|
|
{
|
|
var result = await DialogHost.Show(new WarningDeleteItemModel(file));
|
|
if (result != null && (bool)result)
|
|
{
|
|
var idx = ReportFiles.IndexOf(file);
|
|
if (idx != -1)
|
|
{
|
|
ReportFiles.RemoveAt(idx);
|
|
}
|
|
}
|
|
}
|
|
|
|
public async void EditFileProperties(ReportFile file)
|
|
{
|
|
var result = await DialogHost.Show(new EditFileViewModel(file, ViewModelChanger));
|
|
if (result != null && result is ReportFile updatedData)
|
|
{
|
|
file.Title = updatedData.Title;
|
|
file.ReceiptDateTime = updatedData.ReceiptDateTime;
|
|
file.Notes = updatedData.Notes;
|
|
}
|
|
}
|
|
|
|
public async void BuildPDF()
|
|
{
|
|
try
|
|
{
|
|
// TODO: use already found files
|
|
await Task.Run(() => CreatePDF(_workingFolder));
|
|
} catch (Exception e)
|
|
{
|
|
LogInfo("PDF process failed! Reason: " + e.Message);
|
|
if (e.StackTrace != null)
|
|
{
|
|
LogInfo(e.StackTrace);
|
|
}
|
|
if (e.InnerException != null)
|
|
{
|
|
LogInfo("Inner exception: " + e.InnerException.Message);
|
|
if (e.InnerException.StackTrace != null)
|
|
{
|
|
LogInfo(e.InnerException.StackTrace);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public byte[]? GetFont(string faceName)
|
|
{
|
|
LogInfo(string.Format("Loading font {0}", faceName));
|
|
if (faceName == "Noto Sans JP")
|
|
{
|
|
var path = Path.Combine(_baseDir, "Assets/Fonts/Noto_Sans_JP/static/NotoSansJP-Regular.ttf");
|
|
if (!File.Exists(path))
|
|
{
|
|
path = Path.Combine(_baseDir, "../Resources/Assets/Fonts/Noto_Sans_JP/static/NotoSansJP-Regular.ttf");
|
|
}
|
|
return File.ReadAllBytes(path);
|
|
}
|
|
if (faceName == "Noto Sans JP Bold")
|
|
{
|
|
var path = Path.Combine(_baseDir, "Assets/Fonts/Noto_Sans_JP/static/NotoSansJP-SemiBold.ttf");
|
|
if (!File.Exists(path))
|
|
{
|
|
path = Path.Combine(_baseDir, "../Resources/Assets/Fonts/Noto_Sans_JP/static/NotoSansJP-SemiBold.ttf");
|
|
}
|
|
return File.ReadAllBytes(Path.Combine(_baseDir, "Assets/Fonts/Noto_Sans_JP/static/NotoSansJP-SemiBold.ttf"));
|
|
}
|
|
return null;
|
|
}
|
|
|
|
public FontResolverInfo? ResolveTypeface(string familyName, bool bold, bool italic)
|
|
{
|
|
// LogInfo(string.Format("Resolving font name {0}", familyName));
|
|
if (familyName == "Noto Sans JP")
|
|
{
|
|
if (bold)
|
|
{
|
|
return new FontResolverInfo(familyName + " Bold");
|
|
}
|
|
return new FontResolverInfo(familyName);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
// https://forum.pdfsharp.net/viewtopic.php?f=2&t=1025
|
|
private async Task CreatePDF(string folderPath)
|
|
{
|
|
// TODO: calculate needed width for images based on page width and margins and all that?
|
|
// TODO: resize (non-HEIC) images for smaller size...?
|
|
IsCreatingPDF = true;
|
|
var pdfDoc = new Document();
|
|
var outputFileName = "MyReceipts.pdf";
|
|
var folderName = new DirectoryInfo(folderPath).Name;
|
|
const int imageWidth = 425;
|
|
if (folderName.Contains('-'))
|
|
{
|
|
// see if year/month format
|
|
var parts = folderName.Split('-');
|
|
if (parts[0].Length == 4 &&
|
|
parts[1].Length <= 2 &&
|
|
int.TryParse(parts[0], out int year) && int.TryParse(parts[1], out int month))
|
|
{
|
|
outputFileName = string.Format("{0} {1} Receipts.pdf",
|
|
CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(month),
|
|
year);
|
|
LogInfo("Auto-changed output file name to " + outputFileName);
|
|
}
|
|
}
|
|
var section = pdfDoc.AddSection();
|
|
section.PageSetup.PageFormat = PageFormat.Letter;
|
|
section.PageSetup.PageWidth = "8.5in";
|
|
section.PageSetup.PageHeight = "11in";
|
|
section.PageSetup.TopMargin = "0.5in";
|
|
section.PageSetup.RightMargin = "0.5in";
|
|
section.PageSetup.BottomMargin = "0.5in";
|
|
section.PageSetup.LeftMargin = "0.5in";
|
|
// setup footer for page number
|
|
var footerPar = new Paragraph();
|
|
footerPar.Format.Alignment = ParagraphAlignment.Center;
|
|
footerPar.Format.Font.Size = 10;
|
|
footerPar.AddText("--Page ");
|
|
footerPar.AddPageField();
|
|
footerPar.AddText(" of ");
|
|
footerPar.AddNumPagesField();
|
|
footerPar.AddText("--");
|
|
footerPar.AddLineBreak();
|
|
footerPar.AddText("Report generated on " + DateTime.Now.ToString("f"));
|
|
section.Footers.Primary.Add(footerPar);
|
|
//
|
|
var files = Directory.GetFiles(folderPath);
|
|
files.Sort();
|
|
GlobalFontSettings.FontResolver = this;
|
|
GlobalFontSettings.FallbackFontResolver = new FailsafeFontResolver();
|
|
var hasAddedData = false;
|
|
for (var i = 0; i < files.Length; i++)
|
|
{
|
|
var file = files[i];
|
|
var fileName = Path.GetFileName(file);
|
|
if (fileName == ".DS_Store" || fileName == outputFileName)
|
|
{
|
|
continue;
|
|
}
|
|
if (i > 0 && hasAddedData)
|
|
{
|
|
section.AddPageBreak();
|
|
}
|
|
var imageTitlePar = section.AddParagraph();
|
|
imageTitlePar.Format.Alignment = ParagraphAlignment.Center;
|
|
imageTitlePar.Format.Font.Size = 12;
|
|
imageTitlePar.Format.Font.Bold = true;
|
|
imageTitlePar.Format.Font.Name = "Noto Sans JP"; // has english letters in it, too
|
|
imageTitlePar.AddText(fileName);
|
|
section.AddParagraph(); // add empty line for spacing
|
|
// now add the image
|
|
var isPDF = fileName.EndsWith(".pdf");
|
|
var isHEIC = fileName.EndsWith(".HEIC") || fileName.EndsWith(".heic");
|
|
if (isHEIC)
|
|
{
|
|
var convertedDir = Path.Combine(folderPath, "converted");
|
|
if (!Directory.Exists(convertedDir))
|
|
{
|
|
Directory.CreateDirectory(convertedDir);
|
|
}
|
|
var info = new FileInfo(file);
|
|
using var mImage = new MagickImage(info.FullName);
|
|
// Save frame as jpg
|
|
var outputPath = Path.Combine(convertedDir, info.Name + ".jpg");
|
|
mImage.Quality = 80;
|
|
mImage.Scale((uint)Math.Floor(mImage.Width * 0.5), (uint)Math.Floor(mImage.Height * 0.5));
|
|
await mImage.WriteAsync(outputPath);
|
|
fileName = Path.Combine("Converted", info.Name + ".jpg");
|
|
LogInfo(string.Format("Converted HEIC image to JPEG; fileName is now {0}", fileName));
|
|
}
|
|
var paragraph = section.AddParagraph();
|
|
paragraph.Format.Alignment = ParagraphAlignment.Center;
|
|
var image = paragraph.AddImage(fileName);
|
|
image.LockAspectRatio = true;
|
|
image.Width = imageWidth; // can't be too wide now...not sure why...maybe due to margins...
|
|
LogInfo(string.Format("Added image: {0}", fileName));
|
|
if (isPDF)
|
|
{
|
|
// add other PDF pages
|
|
// see: https://stackoverflow.com/a/65091204/3938401
|
|
var pdfFileToAdd = PdfReader.Open(file);
|
|
imageTitlePar.AddText(string.Format(" (PDF with {0} page{1}) ",
|
|
pdfFileToAdd.PageCount,
|
|
pdfFileToAdd.PageCount == 1 ? "" : "s"));
|
|
for (var j = 2; j <= pdfFileToAdd.PageCount; j++)
|
|
{
|
|
section.AddPageBreak();
|
|
paragraph = section.AddParagraph();
|
|
paragraph.Format.Alignment = ParagraphAlignment.Center;
|
|
image = paragraph.AddImage(file + "#" + j);
|
|
image.LockAspectRatio = true;
|
|
image.Width = imageWidth;
|
|
}
|
|
}
|
|
hasAddedData = true;
|
|
}
|
|
var pdfRenderer = new PdfDocumentRenderer
|
|
{
|
|
Document = pdfDoc,
|
|
WorkingDirectory = folderPath
|
|
};
|
|
LogInfo("Rendering document...");
|
|
pdfRenderer.RenderDocument();
|
|
string filename = Path.Join(folderPath, outputFileName);
|
|
LogInfo("Saving document to disk...");
|
|
pdfRenderer.PdfDocument.Save(filename);
|
|
LogInfo("Saved PDF output to: " + filename);
|
|
IsCreatingPDF = false;
|
|
return;
|
|
}
|
|
} |