Skip to content
Open
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,4 @@ _UpgradeReport_Files/
Thumbs.db
Desktop.ini
.DS_Store
/deploy.cmd
6 changes: 6 additions & 0 deletions global.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"sdk": {
"version": "8.0.406",
"rollForward": "latestFeature"
}
}
18 changes: 18 additions & 0 deletions src/QPdfSharp/Converters/NullableIntToStringConverter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// Copyright © Stephen (Sven) Vernyi and Contributors. Licensed under the MIT License (MIT). See License.md in the repository root for more information.

using System.Text.Json;
using System.Text.Json.Serialization;

namespace QPdfSharp.Converters;

public class NullableIntToStringConverter : JsonConverter<int?>
{
public override int? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
=> throw new NotImplementedException();

public override void Write(Utf8JsonWriter writer, int? value, JsonSerializerOptions options)
{
if (value.HasValue)
writer.WriteStringValue(value.Value.ToString());
}
}
24 changes: 24 additions & 0 deletions src/QPdfSharp/Converters/QPdfCompressStreamConverter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Copyright © Stephen (Sven) Vernyi and Contributors. Licensed under the MIT License (MIT). See License.md in the repository root for more information.

using System.Text.Json;
using System.Text.Json.Serialization;
using QPdfSharp.Enums;

namespace QPdfSharp.Converters;

public class QPdfCompressStreamConverter : JsonConverter<QPdfCompressStream>
{
public override QPdfCompressStream Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
=> throw new NotImplementedException();

public override void Write(Utf8JsonWriter writer, QPdfCompressStream value, JsonSerializerOptions options)
{
var result = value switch
{
QPdfCompressStream.No => "n",
QPdfCompressStream.Yes => "y",
_ => throw new JsonException($"Unknown value enum: {value}")
};
writer.WriteStringValue(result);
}
}
9 changes: 9 additions & 0 deletions src/QPdfSharp/Enums/QPdfCompressStream.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// Copyright © Stephen (Sven) Vernyi and Contributors. Licensed under the MIT License (MIT). See License.md in the repository root for more information.

namespace QPdfSharp.Enums;

public enum QPdfCompressStream : uint
{
No = 0,
Yes
}
37 changes: 37 additions & 0 deletions src/QPdfSharp/Options/QPdfJobOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// Copyright © Stephen (Sven) Vernyi and Contributors. Licensed under the MIT License (MIT). See License.md in the repository root for more information.

using QPdfSharp.Converters;
using System.Text.Json.Serialization;
using QPdfSharp.Enums;

namespace QPdfSharp.Options;

public sealed class QPdfJobOptions
{
// qpdf.exe in.pdf out.pdf --compress-streams=y --recompress-flate --compression-level=9 --object-streams=generate --decode-level=generalized --optimize-images
public string? InputFile { get; set; }
public string? OutputFile { get; set; }

[JsonConverter(typeof(QPdfCompressStreamConverter))]
public QPdfCompressStream? CompressStreams { get; set; }

[JsonConverter(typeof(NullableIntToStringConverter))]
public int? CompressionLevel { get; set; }
public string? RecompressFlate { get; set; }
public string? OptimizeImages { get; set; }
//public bool? NormalizeContent { get; set; }
public QPdfObjectStream? ObjectStreams { get; set; }
public QPdfStreamDecodeLevel? DecodeLevel { get; set; }

public static QPdfJobOptions CreateWithMaxCompression(string inputFile, string outputFile)
=> new() {
InputFile = inputFile,
OutputFile = outputFile,
CompressStreams = QPdfCompressStream.Yes,
CompressionLevel = 9,
RecompressFlate = "",
OptimizeImages = "",
ObjectStreams = QPdfObjectStream.Generate,
DecodeLevel = QPdfStreamDecodeLevel.Generalized
};
}
2 changes: 1 addition & 1 deletion src/QPdfSharp/Options/QPdfWriteOptions.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright © Stephen (Sven) Vernyi and Contributors. Licensed under the MIT License (MIT). See License.md in the repository root for more information.
// Copyright © Stephen (Sven) Vernyi and Contributors. Licensed under the MIT License (MIT). See License.md in the repository root for more information.

using QPdfSharp.Enums;

Expand Down
79 changes: 79 additions & 0 deletions src/QPdfSharp/QPdfJob.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// Copyright © Stephen (Sven) Vernyi and Contributors. Licensed under the MIT License (MIT). See License.md in the repository root for more information.

using System.Text;
using System.Text.Json.Serialization;
using System.Text.Json;
using QPdfSharp.Extensions;
using QPdfSharp.Interop;
using QPdfSharp.Options;

namespace QPdfSharp;

public unsafe class QPdfJob : IDisposable
{
public static readonly string Version = new(QPdfInterop.qpdf_get_qpdf_version());

private readonly QPdfJobHandle* _qPdfJob = QPdfInterop.qpdfjob_init();

private QPdfLoggerHandle* _qPdfLogger = null;

private readonly StringBuilder _errorBuilder = new();

public QPdfJob(string filePath, string outFilePath)
=> InitializeQPdfJob(QPdfJobOptions.CreateWithMaxCompression(filePath, outFilePath));

~QPdfJob() => ReleaseUnmanagedResources();

public void Run()
=> CheckError(QPdfInterop.qpdfjob_run(_qPdfJob));

private void CheckError(int? errorCode = null)
{
if (errorCode == 0)
return;

var errorMessage = _errorBuilder.ToString();
if (string.IsNullOrWhiteSpace(errorMessage))
return;

_errorBuilder.Clear();

throw new QPdfException(errorMessage);
}

public void Dispose()
{
ReleaseUnmanagedResources();
GC.SuppressFinalize(this);
}

private void ReleaseUnmanagedResources()
{
fixed (QPdfLoggerHandle** logHandle = &_qPdfLogger)
QPdfInterop.qpdflogger_cleanup(logHandle);

fixed (QPdfJobHandle** jobHandle = &_qPdfJob)
QPdfInterop.qpdfjob_cleanup(jobHandle);
}

private void InitializeQPdfJob(QPdfJobOptions? options)
{
QPdfLogFunction writeError = (data, len, _) => {
_errorBuilder.Append(new string(data, 0, (int)len));
return 0;
};

_qPdfLogger = QPdfInterop.qpdflogger_create();
QPdfInterop.qpdflogger_set_error(_qPdfLogger, QPdfLogDestination.qpdf_log_dest_custom, writeError, null);
QPdfInterop.qpdfjob_set_logger(_qPdfJob, _qPdfLogger);

var serializerOptions = new JsonSerializerOptions {
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
Converters = { new JsonStringEnumConverter(JsonNamingPolicy.CamelCase) }
};
var json = JsonSerializer.Serialize(options, serializerOptions);
fixed (sbyte* jsonBytes = json.ToSByte())
CheckError(QPdfInterop.qpdfjob_initialize_from_json(_qPdfJob, jsonBytes));
}
}
1 change: 1 addition & 0 deletions src/QPdfSharp/QPdfSharp.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
<ItemGroup>
<PackageReference Include="QPdfSharp.Interop" Version="1.0.1" />
<PackageReference Include="System.Memory" Version="4.5.5" />
<PackageReference Include="System.Text.Json" Version="8.0.5" />
</ItemGroup>

</Project>
55 changes: 55 additions & 0 deletions tests/QPdfSharp.Tests/QPdfJobTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// Copyright © Stephen (Sven) Vernyi and Contributors. Licensed under the MIT License (MIT). See License.md in the repository root for more information.

namespace QPdfSharp.Tests;

public class QPdfJobTests
{
[Fact]
public void Can_read_qpdf_version()
{
// Arrange
// Act
var version = QPdfJob.Version;

// Assert
Version.TryParse(version, out _).Should().BeTrue();
}

[Theory]
[InlineData(TestAssets.Grug)]
public void Can_compress_pdf_from_file_path(string filePath)
{
// Arrange
var outputFileName = "small.pdf";

// Act
var run = () => {
using var qPdfJob = new QPdfJob(filePath, outputFileName);
qPdfJob.Run();
};

// Assert
run.Should().NotThrow();
File.Exists(outputFileName).Should().BeTrue();
new FileInfo(outputFileName).Length.Should().BeLessThan(new FileInfo(filePath).Length);
File.Delete(outputFileName);
}

[Theory]
[InlineData("abc.pdf")]
[InlineData("Assets/abc.pdf")]
public void Should_throw_exception_if_file_not_found(string filePath)
{
// Arrange
var outputFileName = "small.pdf";

// Act
var run = () => {
using var qPdfJob = new QPdfJob(filePath, outputFileName);
qPdfJob.Run();
};

// Assert
run.Should().Throw<QPdfException>($"qpdfjob json: open {filePath}: No such file or directory");
}
}
1 change: 0 additions & 1 deletion tests/QPdfSharp.Tests/QPdfReadingTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,3 @@ public async Task Can_read_pdf_from_memory(string filePath)
createPdfFromBytes.Should().NotThrow();
}
}

4 changes: 4 additions & 0 deletions tests/QPdfSharp.Tests/QPdfSharp.Tests.csproj
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="FluentAssertions" Version="6.12.0" />
<PackageReference Include="FluentAssertions.Analyzers" Version="0.30.0">
Expand Down
2 changes: 1 addition & 1 deletion tests/QPdfSharp.Tests/TestAssets.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright © Stephen (Sven) Vernyi and Contributors. Licensed under the MIT License (MIT). See License.md in the repository root for more information.
// Copyright © Stephen (Sven) Vernyi and Contributors. Licensed under the MIT License (MIT). See License.md in the repository root for more information.

namespace QPdfSharp.Tests;

Expand Down