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
2 changes: 1 addition & 1 deletion .github/workflows/stale.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:
pull-requests: write

steps:
- uses: actions/stale@v6
- uses: actions/stale@v7
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
stale-issue-message: 'Stale issue message'
Expand Down
8 changes: 4 additions & 4 deletions PdfjsSharp/IRasterizer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,16 @@
namespace Codeuctivity.PdfjsSharp
{
/// <summary>
/// Rasterizes PDFs, Depands on node
/// Rasterizes PDFs, Depends on node
/// </summary>
public interface IRasterizer
{
/// <summary>
/// Converts a pdf to pngs
/// Converts a PDF to PNGs
/// </summary>
/// <param name="pathToPdf"></param>
/// <param name="pathToPngOutput">Prefix of file path to pngs created for each page of pdf. E.g. c:\temp\PdfPage will result in c:\temp\PdfPage1.png and c:\temp\PdfPage2.png for a Pdf with two pages.</param>
/// <returns>Collection of paths to pngs for each page in the pdf</returns>
/// <param name="pathToPngOutput">Prefix of file path to PNGs created for each page of pdf. E.g. c:\temp\PdfPage will result in c:\temp\PdfPage1.PNG and c:\temp\PdfPage2.PNG for a Pdf with two pages.</param>
/// <returns>Collection of paths to PNGs for each page in the pdf</returns>
Task<IReadOnlyList<string>> ConvertToPngAsync(string pathToPdf, string pathToPngOutput);
}
}
16 changes: 9 additions & 7 deletions PdfjsSharp/NodeVersionDetector.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
Expand All @@ -16,11 +17,11 @@ public static class NodeVersionDetector
/// <summary>
/// Reads installed node version
/// </summary>
public static Version? DetectVersion()
public static Version? DetectVersion(string nodeExecuteablePath)
{
using var process = new Process();
process.StartInfo.RedirectStandardOutput = true;
process.StartInfo.FileName = "node";
process.StartInfo.FileName = nodeExecuteablePath;
process.StartInfo.Arguments = "-v";
process.Start();
process.WaitForExit();
Expand All @@ -43,11 +44,11 @@ public static class NodeVersionDetector
/// <summary>
/// Reads installed node bitness version
/// </summary>
public static string DetectBittness()
public static string DetectBittness(string nodeExecuteablePath)
{
using var process = new Process();
process.StartInfo.RedirectStandardOutput = true;
process.StartInfo.FileName = "node";
process.StartInfo.FileName = nodeExecuteablePath;
process.StartInfo.Arguments = "-p \"process.arch\"";
process.Start();
process.WaitForExit();
Expand All @@ -63,17 +64,18 @@ public static string DetectBittness()
/// <summary>
/// Check that node with majorNodeVersion is installed
/// </summary>
/// <param name="nodeExecuteablePath"></param>
/// <param name="supportedMajorNodeVersions"></param>
public static int CheckRequiredNodeVersionInstalled(int[] supportedMajorNodeVersions)
public static int CheckRequiredNodeVersionInstalled(string nodeExecuteablePath, IEnumerable<int> supportedMajorNodeVersions)
{
var foundMajorVersion = (DetectVersion())?.Major;
var foundMajorVersion = (DetectVersion(nodeExecuteablePath))?.Major;

if (foundMajorVersion == null)
{
throw new NotSupportedException($"No supported node version found. Expected node {supportedMajorNodeVersions} to be installed.");
}

if (DetectBittness() != "x64")
if (DetectBittness(nodeExecuteablePath) != "x64")
{
throw new NotSupportedException($"No supported node version found. Expected 64bit node to be installed.");
}
Expand Down
118 changes: 108 additions & 10 deletions PdfjsSharp/PdfJsWrapper.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
using Jering.Javascript.NodeJS;
using System;
using System.Collections.Immutable;
using System.Diagnostics;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Threading;
Expand All @@ -9,19 +13,46 @@
namespace Codeuctivity.PdfjsSharp
{
/// <summary>
/// Wraps pdfjs boilerplaiting
/// Wraps PDFjs boiler plaiting
/// </summary>
public class PdfJsWrapper : IDisposable
{
internal const int someMaxPathLength = 206;
internal const int windowsMaxPathLength = 206;
internal static readonly SemaphoreSlim semaphore = new SemaphoreSlim(1, 1);
internal bool useCustomNodeModulePath;
internal string pathToNodeModules = default!;
internal string pathToTempFolder = default!;
private bool disposed;

/// <summary>
/// Supported node versions
/// </summary>
private readonly ImmutableArray<int> SupportedNodeVersions = ImmutableArray.Create(new[] { 18, 16 });

internal bool IsInitialized { get; set; }

/// <summary>
/// Path to node executable
/// </summary>
public string NodeExecuteablePath { get; private set; }

/// <summary>
/// Ctor used with custom NodeExecuteablePath
/// </summary>
/// <param name="nodeExecuteablePath"></param>
public PdfJsWrapper(string nodeExecuteablePath)
{
NodeExecuteablePath = nodeExecuteablePath;
}

/// <summary>
/// Default ctor, NodeExecuteablePath gets auto detected
/// </summary>
public PdfJsWrapper()
{
NodeExecuteablePath = "node";
}

/// <summary>
/// Disposing packaged node_modules folder
/// </summary>
Expand Down Expand Up @@ -55,25 +86,38 @@ protected virtual void Dispose(bool disposing)
disposed = true;
}

internal async Task InitNodeModules()
/// <summary>
/// Needs to be called before any method can be used
/// </summary>
/// <returns></returns>
/// <exception cref="PathTooLongException"></exception>
public async Task InitPdfJsWrapper()
{
if (IsInitialized)
{
return;
}

await semaphore.WaitAsync().ConfigureAwait(false);

if (IsInitialized)
{
return;
}

try
{
if (IsInitialized)
{
return;
}
InitializeNodeExecuteablePath();

pathToTempFolder = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());

if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
if (pathToTempFolder.Length > someMaxPathLength)
if (pathToTempFolder.Length > windowsMaxPathLength)
{
throw new PathTooLongException(pathToTempFolder);
}
var foundVersion = NodeVersionDetector.CheckRequiredNodeVersionInstalled(new[] { 18, 16 });
var foundVersion = NodeVersionDetector.CheckRequiredNodeVersionInstalled(NodeExecuteablePath, SupportedNodeVersions);

Directory.CreateDirectory(pathToTempFolder);

Expand All @@ -83,7 +127,7 @@ internal async Task InitNodeModules()
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
var foundVersion = NodeVersionDetector.CheckRequiredNodeVersionInstalled(new[] { 18, 16 });
var foundVersion = NodeVersionDetector.CheckRequiredNodeVersionInstalled(NodeExecuteablePath, SupportedNodeVersions);
Directory.CreateDirectory(pathToTempFolder);
await ExtractBinaryFromManifest($"Codeuctivity.PdfjsSharp.node_modules.linux.node{foundVersion}.zip").ConfigureAwait(false);

Expand All @@ -95,6 +139,11 @@ internal async Task InitNodeModules()
useCustomNodeModulePath = true;
}

if (!string.IsNullOrEmpty(NodeExecuteablePath) && NodeExecuteablePath != "node")
{
StaticNodeJSService.Configure<NodeJSProcessOptions>(options => options.ExecutablePath = NodeExecuteablePath);
}

IsInitialized = true;
}
finally
Expand All @@ -103,6 +152,55 @@ internal async Task InitNodeModules()
}
}

private void InitializeNodeExecuteablePath()
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
var filePathWhich = "/usr/bin/which";

if (File.Exists(filePathWhich))
{
using var process = new Process();
process.StartInfo.RedirectStandardOutput = true;
process.StartInfo.FileName = filePathWhich;
process.StartInfo.Arguments = "node";
process.Start();
process.WaitForExit();

var detectedPath = process.StandardOutput.ReadToEnd();
if (!string.IsNullOrEmpty(detectedPath) && File.Exists(detectedPath))
{
NodeExecuteablePath = detectedPath;
return;
}

// not finding node is a WSL only issue
// known location of node installed by nvm in wsl - https://learn.microsoft.com/en-us/windows/dev-environment/javascript/nodejs-on-wsl

var home = Environment.GetEnvironmentVariable("HOME");

var path = Path.Combine(home, ".nvm", "versions", "node");
if (!string.IsNullOrEmpty(home) && Directory.Exists(path))
{
var installedNodeVersions = Directory.GetDirectories(path);

var nodeExecutableDirectory = installedNodeVersions.FirstOrDefault(directory => SupportedNodeVersions.Any(version => Path.GetFileName(directory).StartsWith("v" + version.ToString())));

if (nodeExecutableDirectory != null)
{
var nodePath = Path.Combine(path, nodeExecutableDirectory, "bin", "node");
if (File.Exists(nodePath))
{
NodeExecuteablePath = nodePath;
return;
}
}
}
}
}
NodeExecuteablePath = "node";
}

private async Task ExtractBinaryFromManifest(string resourceName)
{
var pathNodeModules = Path.Combine(pathToTempFolder, "node_modules.zip");
Expand Down
3 changes: 2 additions & 1 deletion PdfjsSharp/PdfjsSharp.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,11 @@

<ItemGroup>
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="all" />
<PackageReference Include="SonarAnalyzer.CSharp" Version="8.50.0.58025">
<PackageReference Include="SonarAnalyzer.CSharp" Version="8.51.0.59060">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="System.Collections.Immutable" Version="7.0.0" />
</ItemGroup>

<ItemGroup>
Expand Down
23 changes: 19 additions & 4 deletions PdfjsSharp/Rasterizer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,29 @@ namespace Codeuctivity.PdfjsSharp
public class Rasterizer : PdfJsWrapper, IRasterizer
{
/// <summary>
/// Converts a pdf to PNGs
/// Default ctor
/// </summary>
public Rasterizer()
{
}

/// <summary>
/// Ctor used with custom NodeExecuteablePath
/// </summary>
/// <param name="nodeExecuteablePath"></param>
public Rasterizer(string nodeExecuteablePath) : base(nodeExecuteablePath)
{
}

/// <summary>
/// Converts a PDF to PNGs
/// </summary>
/// <param name="pathToPdf"></param>
/// <param name="pathToPngOutput">Prefix of file path to PNGs created for each page of pdf. E.g. c:\temp\PdfPage will result in c:\temp\PdfPage1.png and c:\temp\PdfPage2.png for a Pdf with two pages.</param>
/// <returns>Collection of paths to PNGs for each page in the pdf</returns>
/// <param name="pathToPngOutput">Prefix of file path to PNGs created for each page of PDF. E.g. c:\temp\PdfPage will result in c:\temp\PdfPage1.png and c:\temp\PdfPage2.png for a PDF with two pages.</param>
/// <returns>Collection of paths to PNGs for each page in the PDF</returns>
public async Task<IReadOnlyList<string>> ConvertToPngAsync(string pathToPdf, string pathToPngOutput)
{
await InitNodeModules().ConfigureAwait(false);
await InitPdfJsWrapper().ConfigureAwait(false);
var assembly = Assembly.GetExecutingAssembly();
var stream = assembly.GetManifestResourceStream("Codeuctivity.PdfjsSharp.Rasterize.js");
using var reader = new StreamReader(stream!);
Expand Down
Loading