Skip to content

Commit

Permalink
Launch viewer for PDF, RTF, and Word files
Browse files Browse the repository at this point in the history
Files are identified by extension, HFS file type, and signature.
The file is extracted into a temp file and handed to the default
viewer for that extension.

Windows doesn't allow you to delete an open file, so if the file
is open in the external program when the file viewer is closed, the
temporary file won't be removed.  We can scan for these at launch
and scrub them, ideally with user confirmation since we can't
absolutely guarantee that we created them.

(Issue #7.)
  • Loading branch information
fadden committed Jan 19, 2024
1 parent 9d17064 commit c827e55
Show file tree
Hide file tree
Showing 3 changed files with 142 additions and 9 deletions.
24 changes: 24 additions & 0 deletions FileConv/Gfx/HostImage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,16 @@ public HostImage(FileAttribs attrs, Stream? dataStream, Stream? rsrcStream,
private static byte[] PNG_SIG =
new byte[] { 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a };

private static uint PDF_TYPE = MacChar.IntifyMacConstantString("PDF ");
private static byte[] PDF_SIG = new byte[] { 0x25, 0x50, 0x44, 0x46 }; // "%PDF" is enough

private static uint RTF_TYPE = MacChar.IntifyMacConstantString("RTF ");
private static byte[] RTF_SIG = new byte[] { 0x7b, 0x5c, 0x72, 0x74 }; // "{\rtf"

private static uint WORD_TYPE = MacChar.IntifyMacConstantString("WDBN");
private static uint WORD6_TYPE = MacChar.IntifyMacConstantString("W6BN");
private static uint WORD8_TYPE = MacChar.IntifyMacConstantString("W8BN");

protected override Applicability TestApplicability() {
const int TEST_LEN = 8;
if (DataStream == null || DataStream.Length < TEST_LEN) {
Expand All @@ -82,6 +92,20 @@ protected override Applicability TestApplicability() {
mKind = HostConv.FileKind.PNG;
return Applicability.Yes;
}
} else if (ext == ".pdf" || FileAttrs.HFSFileType == PDF_TYPE) {
if (RawData.CompareBytes(buf, PDF_SIG, PDF_SIG.Length)) {
mKind = HostConv.FileKind.PDF;
return Applicability.Yes;
}
} else if (ext == ".rtf" || FileAttrs.HFSFileType == RTF_TYPE) {
if (RawData.CompareBytes(buf, RTF_SIG, RTF_SIG.Length)) {
mKind = HostConv.FileKind.RTF;
return Applicability.Yes;
}
} else if (FileAttrs.HFSFileType == WORD_TYPE || FileAttrs.HFSFileType == WORD6_TYPE ||
FileAttrs.HFSFileType == WORD8_TYPE) {
mKind = HostConv.FileKind.Word;
return Applicability.Yes;
}
return Applicability.Not;
}
Expand Down
2 changes: 1 addition & 1 deletion FileConv/HostConv.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public class HostConv : IConvOutput {
public Notes Notes { get; } = new Notes();

public enum FileKind {
Unknown = 0, GIF, JPEG, PNG
Unknown = 0, GIF, JPEG, PNG, PDF, RTF, Word
}

public FileKind Kind { get; private set; }
Expand Down
125 changes: 117 additions & 8 deletions cp2_wpf/FileViewer.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,12 @@ private void OnPropertyChanged([CallerMemberName] string propertyName = "") {
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}

// List of temporary files created for viewing "host" files in an external viewer.
// On Windows you can't delete a file when another application has it open, so the
// best we can do is scrub them when we close the window.
private List<string> mTmpFiles = new List<string>();
private const string TEMP_FILE_PREFIX = "cp2tmp_";

private object mArchiveOrFileSystem;
private List<IFileEntry> mSelected;
private int mCurIndex; // index into mSelected
Expand Down Expand Up @@ -270,6 +276,7 @@ private void Window_SourceInitialized(object sender, EventArgs e) {
/// </summary>
private void Window_Closed(object sender, EventArgs e) {
CloseStreams();
DeleteTempFiles();
}

private void UpdatePrevNextControls() {
Expand Down Expand Up @@ -541,14 +548,32 @@ private void FormatFile() {
IsExportEnabled = true;
} else if (mCurDataOutput is HostConv) {
HostConv.FileKind kind = ((HostConv)mCurDataOutput).Kind;
BitmapSource? bsrc = PrepareHostImage(mDataFork, kind);
if (bsrc == null) {
DataPlainText = "Unable to decode " + kind + " image.";
SetDisplayType(DisplayItemType.SimpleText);
} else {
previewImage.Source = bsrc;
ConfigureMagnification();
SetDisplayType(DisplayItemType.Bitmap);
switch (kind) {
case HostConv.FileKind.GIF:
case HostConv.FileKind.JPEG:
case HostConv.FileKind.PNG:
BitmapSource? bsrc = PrepareHostImage(mDataFork, kind);
if (bsrc == null) {
DataPlainText = "Unable to decode " + kind + " image.";
SetDisplayType(DisplayItemType.SimpleText);
} else {
previewImage.Source = bsrc;
ConfigureMagnification();
SetDisplayType(DisplayItemType.Bitmap);
}
break;
case HostConv.FileKind.PDF:
case HostConv.FileKind.RTF:
case HostConv.FileKind.Word:
if (LaunchExternalViewer(mDataFork, kind)) {
DataPlainText = "(Displaying with external command)";
} else {
DataPlainText = "Failed to launch external viewer";
}
dataForkTextBox.HorizontalContentAlignment = HorizontalAlignment.Center;
dataForkTextBox.VerticalContentAlignment = VerticalAlignment.Center;
SetDisplayType(DisplayItemType.SimpleText);
break;
}
// Exporting these is a little weird, and probably wrong. It makes more sense to
// extract them in their native form. Handling them requires a separate path in
Expand Down Expand Up @@ -646,6 +671,90 @@ private void SelectEnabledTab() {
}
}

/// <summary>
/// Launches an external viewer to display a "host" file.
/// </summary>
/// <returns>True on success.</returns>
private bool LaunchExternalViewer(Stream? stream, HostConv.FileKind kind) {
if (stream == null) {
return false;
}

string ext;
switch (kind) {
case HostConv.FileKind.GIF:
ext = ".gif";
break;
case HostConv.FileKind.JPEG:
ext = ".jpg";
break;
case HostConv.FileKind.PNG:
ext = ".png";
break;
case HostConv.FileKind.PDF:
ext = ".pdf";
break;
case HostConv.FileKind.RTF:
ext = ".rtf";
break;
case HostConv.FileKind.Word:
ext = ".doc";
break;
default:
Debug.Assert(false, "Unhandled kind: " + kind);
return false;
}

// The GUID-based filename isn't guaranteed to be unique, but it should be absurdly
// close to it, especially if we use an app-specific prefix and scrub our old temp
// files. This isn't mission-critical even if it does fail.
string tmpFileName = Path.GetTempPath() +
TEMP_FILE_PREFIX + Guid.NewGuid().ToString() + ext;
try {
// Can't use DeleteOnClose here because we don't control the lifetime of the
// external process.
using (Stream tmpFile = new FileStream(tmpFileName, FileMode.Create)) {
stream.Position = 0;
stream.CopyTo(tmpFile);
}
mAppHook.LogI("Created temp file '" + tmpFileName + "'");
mTmpFiles.Add(tmpFileName);
} catch (IOException ex) {
mAppHook.LogW("Failed to create temp file '" + tmpFileName + "': " + ex.Message);
return false;
}

Process? proc = new Process();
proc.StartInfo = new ProcessStartInfo(tmpFileName);
proc.StartInfo.UseShellExecute = true;
return proc.Start();
}

/// <summary>
/// Removes temporary files we created. Call this when the file viewer is closed.
/// </summary>
private void DeleteTempFiles() {
// TODO: we probably also want to scrub any leftovers from previous iterations
// when the program launches. Scan of local temp directory should be fast, though
// we might want to put it in a dedicated directory. Should get user confirmation,
// ideally with a non-intrusive launch screen affordance rather than a modal
// dialog. If we can open the file read/write then the external viewer should be
// done with it.
string tempPathRoot = Path.GetTempPath();
foreach (string path in mTmpFiles) {
if (!path.StartsWith(tempPathRoot)) {
throw new Exception("whoops"); // be paranoid about removing files
}
try {
File.Delete(path);
mAppHook.LogI("Removed temp '" + path + "'");
} catch (Exception ex) {
// Exception message includes path name.
mAppHook.LogW("Unable to remove temp: " + ex.Message);
}
}
}

/// <summary>
/// Handles movement on the graphics zoom slider.
/// </summary>
Expand Down

0 comments on commit c827e55

Please sign in to comment.