Skip to content

Commit

Permalink
Adds ability to read/write/update from JSON (minor)
Browse files Browse the repository at this point in the history
  • Loading branch information
svengeance committed Jan 4, 2024
1 parent 21934c1 commit 21e8535
Show file tree
Hide file tree
Showing 9 changed files with 1,909 additions and 18 deletions.
5 changes: 1 addition & 4 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,2 @@
### Breaking Changes
- Adjusts a few enum names

### Features
- Provides operations for adding or removing pages in a PDF document.
- Adds read/write/update operations from JSON files created by QPDF.
6 changes: 4 additions & 2 deletions src/QPdfSharp/Options/QPdfReadOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ namespace QPdfSharp.Options;

public sealed class QPdfReadOptions
{
public bool AttemptRecovery { get; set; }
public bool IgnoreXrefStreams { get; set; }
public bool? AttemptRecovery { get; set; }
public bool? IgnoreXrefStreams { get; set; }

public bool IsJsonFormat { get; set; }
}
79 changes: 79 additions & 0 deletions src/QPdfSharp/QPdf.Writing.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
// Copyright © Stephen (Sven) Vernyi and Contributors. Licensed under the MIT License (MIT). See License.md in the repository root for more information.

using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
using QPdfSharp.Extensions;
using QPdfSharp.Interop;
using QPdfSharp.Options;
Expand Down Expand Up @@ -49,6 +52,82 @@ public Stream WriteStream(QPdfWriteOptions? writeOptions = null)
return _outputStream = new QPdfStream(buffer, (long)bufferLength);
}

public string WriteJson(QPdfWriteOptions? writeOptions = null, string[]? wantedObjects = null)
{
var sb = new StringBuilder();

QPdfWriteFunction writeJson = (data, len, _) => {
sb.Append(new string(data, 0, (int)len));
return 0;
};

WriteJson(writeJson, writeOptions, wantedObjects);

return sb.ToString();
}

public void WriteJsonFile(string outputFilePath, QPdfWriteOptions? writeOptions = null, string[]? wantedObjects = null)
{
using var fs = new FileStream(outputFilePath, FileMode.Create, FileAccess.Write, FileShare.None);

QPdfWriteFunction writeJson = (data, len, _) => {
using var ums = new UnmanagedMemoryStream((byte*)data, (int)len);
ums.CopyTo(fs);
ums.Flush();
return 0;
};

WriteJson(writeJson, writeOptions, wantedObjects);
}

private void WriteJson(QPdfWriteFunction writeFunction, QPdfWriteOptions? writeOptions = null, string[]? wantedObjects = null)
{
MarkDataWritten();
ApplyWriteOptions(writeOptions);

var wantedObjectsBytes = (wantedObjects ?? [""]).Select(s => Unsafe.As<sbyte[]>(Encoding.UTF8.GetBytes(s))).ToArray();
var wantedObjectsPins = wantedObjectsBytes.Select(s => GCHandle.Alloc(s, GCHandleType.Pinned)).ToArray();
var wantedObjectsPtrs = Unsafe.As<sbyte*[]>(wantedObjectsPins.Select(s => s.AddrOfPinnedObject()).ToArray());

fixed (sbyte** wantedObjectsRootPtr = &wantedObjectsPtrs[0])
fixed (sbyte* filePrefix = "in-memory".ToSByte())
{
try
{
CheckError(
QPdfInterop.qpdf_write_json(
qpdf: _qPdfData,
version: 2,
fn: writeFunction,
udata: (void*)IntPtr.Zero,
decode_level: QPdfStreamDecodeLevel.qpdf_dl_all,
json_stream_data: QPdfJsonStreamData.qpdf_sj_inline,
file_prefix: filePrefix,
wanted_objects: wantedObjectsRootPtr
)
);
}
finally
{
foreach (var pin in wantedObjectsPins)
pin.Free();
}
}
}

public void UpdateFromJsonFile(string filePath)
{
fixed (sbyte* filePathBytes = filePath.ToSByte())
CheckError(QPdfInterop.qpdf_update_from_json_file(_qPdfData, filePathBytes));
}

public void UpdateFromJson(string json)
{
var jsonSpan = json.ToSByte();
fixed (sbyte* jsonBytes = jsonSpan)
CheckError(QPdfInterop.qpdf_update_from_json_data(_qPdfData, jsonBytes, (ulong)jsonSpan.Length));
}

private void ApplyWriteOptions(QPdfWriteOptions? writeOptions)
{
if (writeOptions is null)
Expand Down
16 changes: 14 additions & 2 deletions src/QPdfSharp/QPdf.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,13 @@ public QPdf(string filePath, string password = "", QPdfReadOptions? readOptions

fixed (sbyte* filePathBytes = filePath.ToSByte())
fixed (sbyte* passwordBytes = password.ToSByte())
CheckError(QPdfInterop.qpdf_read(_qPdfData, filePathBytes, passwordBytes));
{
var qpdReadResult = readOptions?.IsJsonFormat == true
? QPdfInterop.qpdf_create_from_json_file(_qPdfData, filePathBytes)
: QPdfInterop.qpdf_read(_qPdfData, filePathBytes, passwordBytes);

CheckError(qpdReadResult);
}
}

public QPdf(ReadOnlyMemory<byte> bytes, string name = "in-memory pdf", string password = "", QPdfReadOptions? readOptions = null)
Expand All @@ -36,7 +42,13 @@ public QPdf(ReadOnlyMemory<byte> bytes, string name = "in-memory pdf", string pa

fixed (sbyte* fileNameBytes = name.ToSByte())
fixed (sbyte* passwordBytes = password.ToSByte())
CheckError(QPdfInterop.qpdf_read_memory(_qPdfData, fileNameBytes, (sbyte*)fileBytesHandle.Pointer, (ulong)bytes.Length, passwordBytes));
{
var qpdReadResult = readOptions?.IsJsonFormat == true
? QPdfInterop.qpdf_create_from_json_data(_qPdfData, (sbyte*)fileBytesHandle.Pointer, (ulong)bytes.Length)
: QPdfInterop.qpdf_read_memory(_qPdfData, fileNameBytes, (sbyte*)fileBytesHandle.Pointer, (ulong)bytes.Length, passwordBytes);

CheckError(qpdReadResult);
}
}

public void Dispose()
Expand Down
1,684 changes: 1,684 additions & 0 deletions tests/QPdfSharp.Tests/Assets/grug.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions tests/QPdfSharp.Tests/QPdfSharp.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

<ItemGroup>
<None Update="Assets\grug.pdf" CopyToOutputDirectory="Always" />
<None Update="Assets\grug.json" CopyToOutputDirectory="Always" />
</ItemGroup>

<ItemGroup>
Expand Down
24 changes: 15 additions & 9 deletions tests/QPdfSharp.Tests/QPdfTests.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using QPdfSharp.Options;

namespace QPdfSharp.Tests;

public class QPdfTests
Expand All @@ -13,28 +15,32 @@ public void Can_read_qpdf_version()
Version.TryParse(version, out _).Should().BeTrue();
}

[Fact]
public void Can_read_pdf_from_file_path()
[Theory]
[InlineData(TestAssets.Grug)]
[InlineData(TestAssets.GrugJson)]
public void Can_read_pdf_from_file_path(string filePath)
{
// Arrange
var fileName = TestAssets.Grug;
var readOptions = new QPdfReadOptions { IsJsonFormat = filePath.EndsWith(".json") };

// Act
var createPdfFromFile = () => new QPdf(fileName);
var createPdfFromFile = () => new QPdf(filePath, readOptions: readOptions);

// Assert
createPdfFromFile.Should().NotThrow();
}

[Fact]
public async Task Can_read_pdf_from_memory()
[Theory]
[InlineData(TestAssets.Grug)]
[InlineData(TestAssets.GrugJson)]
public async Task Can_read_pdf_from_memory(string filePath)
{
// Arrange
var fileName = TestAssets.Grug;
var fileBytes = await File.ReadAllBytesAsync(fileName);
var readOptions = new QPdfReadOptions { IsJsonFormat = filePath.EndsWith(".json") };
var fileBytes = await File.ReadAllBytesAsync(filePath);

// Act
var createPdfFromBytes = () => new QPdf(fileBytes);
var createPdfFromBytes = () => new QPdf(fileBytes, readOptions: readOptions);

// Assert
createPdfFromBytes.Should().NotThrow();
Expand Down
109 changes: 109 additions & 0 deletions tests/QPdfSharp.Tests/QPdfWritingTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,48 @@ public void Can_write_pdf_stream()
pdfStream.Should().BeReadOnly();
}

[Fact]
public void Can_write_pdf_json()
{
// Arrange
using var qpdf = new QPdf(TestAssets.Grug);

// Act
var pdfJson = qpdf.WriteJson();

// Assert
pdfJson.Should().NotBeEmpty();
}

[Fact]
public void Can_write_pdf_json_with_specific_object_key()
{
// Arrange
using var qpdf = new QPdf(TestAssets.Grug);
var objectKey = "obj:94 0 R"; // Corresponds to Title

// Act
var pdfJson = qpdf.WriteJson(wantedObjects: [objectKey]);

// Assert
pdfJson.Should().NotBeEmpty();
}


[Fact]
public void Can_write_pdf_json_file()
{
// Arrange
using var qpdf = new QPdf(TestAssets.Grug);

// Act
qpdf.WriteJsonFile("out.json");

// Assert
var json = File.ReadAllText("out.json");
json.Should().NotBeEmpty();
}

[Fact]
public void Throws_on_consecutive_writes_to_stream()
{
Expand Down Expand Up @@ -94,4 +136,71 @@ public void Throws_on_consecutive_writes_to_file()
writeAgain.Should().Throw<InvalidOperationException>();
File.Delete(outputFileName);
}

[Fact]
public void Throws_on_consecutive_writes_to_json()
{
// Arrange
using var qpdf = new QPdf(TestAssets.Grug);

_ = qpdf.WriteJson();

// Act
var writeAgain = () => qpdf.WriteJson();

// Assert
writeAgain.Should().Throw<InvalidOperationException>();
}

[Fact]
public void Throws_on_consecutive_writes_to_json_file()
{
// Arrange
using var qpdf = new QPdf(TestAssets.Grug);

qpdf.WriteJsonFile("out.json");

// Act
var writeAgain = () => qpdf.WriteJsonFile("out.json");

// Assert
writeAgain.Should().Throw<InvalidOperationException>();
}

[Fact]
public void Can_update_from_json_string()
{
// Arrange
using var qpdf = new QPdf(TestAssets.Grug);
var titleKey = "obj:94 0 R"; // Corresponds to Title
var updatedTitle = "Grug Brained Developer (Updated)";
var titleJson = qpdf.WriteJson(wantedObjects: [titleKey]).Replace("Grug Brained Developer", updatedTitle);

// Act
using var qpdfToUpdate = new QPdf(TestAssets.Grug);
qpdfToUpdate.UpdateFromJson(titleJson);

// Assert
qpdfToUpdate.GetPageCount().Should().Be(19);
qpdfToUpdate.WriteJson(wantedObjects: [titleKey]).Should().Contain(updatedTitle);
}

[Fact]
public void Can_update_from_json_file()
{
// Arrange
using var qpdf = new QPdf(TestAssets.Grug);
var titleKey = "obj:94 0 R"; // Corresponds to Title
var updatedTitle = "Grug Brained Developer (Updated)";
var jsonFileName = $"{nameof(Can_update_from_json_file)}.json";
File.WriteAllText(jsonFileName, qpdf.WriteJson(wantedObjects: [titleKey]).Replace("Grug Brained Developer", updatedTitle));

// Act
using var qpdfToUpdate = new QPdf(TestAssets.Grug);
qpdfToUpdate.UpdateFromJsonFile(jsonFileName);

// Assert
qpdfToUpdate.GetPageCount().Should().Be(19);
qpdfToUpdate.WriteJson(wantedObjects: [titleKey]).Should().Contain(updatedTitle);
}
}
3 changes: 2 additions & 1 deletion tests/QPdfSharp.Tests/TestAssets.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@ namespace QPdfSharp.Tests;

public static class TestAssets
{
public static string Grug = "Assets/grug.pdf";
public const string Grug = "Assets/grug.pdf";
public const string GrugJson = "Assets/grug.json";
}

0 comments on commit 21e8535

Please sign in to comment.