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
43 changes: 43 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# AGENTS.md

## Purpose
- `Weasyprint.Wrapped` is a .NET wrapper that bundles a standalone `weasyprint` CLI (zip asset) so consumers do not install Python/system deps manually (`readme.md`, `src/Weasyprint.Wrapped/Printer.cs`).
- Runtime behavior is OS-specific: Windows runs `weasyprint.exe`, Linux runs `weasyprint` binary extracted from platform zip (`src/Weasyprint.Wrapped/Printer.cs`, `src/Weasyprint.Wrapped/Configuration/ConfigurationProvider.cs`).

## Repo map (what matters first)
- Library: `src/Weasyprint.Wrapped/` (`Printer`, config, result DTOs, init exception).
- Integration tests: `src/Weasyprint.Wrapped.Tests/Tests/PrinterTests.cs` (best source of expected behavior).
- Usage samples: `src/Weasyprint.Wrapped.Example/Program.cs` and `src/Weasyprint.Wrapped.ExampleApi/Controllers/PrintController.cs`.
- Asset builders: `build-on-windows.ps1`, `build-on-linux.sh`, `test_on_linux.sh`.
- CI/release behavior: `.github/workflows/build-test-code-scan.yml`, `.github/workflows/release-assets.yml`.

## Core runtime flow
- Call `await printer.Initialize()` before print/version calls; it extracts `assets/standalone-{windows|linux}-64.zip` to working folder only when `version-*` marker changes.
- Version pinning is file-based: zip must contain `version-*`; `Initialize()` skips re-extract when matching marker exists (`Printer.Initialize`).
- Print from HTML string uses stdin/stdout (`- -`) and returns bytes via `PrintResult.Bytes`; stream variant returns open stream (`PrintStreamResult.DocumentStream`).
- Print from file path writes directly to output file and returns metadata only (`Print(string htmlFile, string pdfFile, ...)`).
- Known noisy stderr is filtered (`GLib-GIO-WARNING`) via `IgnoreCertainErrors`; do not treat that warning as functional failure.

## Developer workflows
- Build library/package locally from repo root:
- `dotnet build src/Weasyprint.Wrapped/Weasyprint.Wrapped.csproj`
- `dotnet pack src/Weasyprint.Wrapped/Weasyprint.Wrapped.csproj -c Release --output src/Weasyprint.Wrapped/nupkgs`
- Run tests (requires platform asset zip present under `assets/`):
- `dotnet test src/Weasyprint.Wrapped.Tests/Weasyprint.Wrapped.Tests.csproj`
- Rebuild bundled assets:
- Windows: `./build-on-windows.ps1`
- Linux: `./build-on-linux.sh` (Docker required), optional smoke test `./test_on_linux.sh`.

## Project-specific conventions (follow these)
- `ConfigurationProvider` defaults to `AppContext.BaseDirectory`; tests override with relative asset path (`../../../../../assets/`) and working folder `weasyprinter`.
- Additional CLI args are passed through as raw strings (example: `"--optimize-images"` in tests and example app).
- Cross-platform behavior is validated by selecting expected files per OS (`PrinterTests` chooses Windows vs Linux expected PDF).
- `src/Weasyprint.Wrapped/Weasyprint.Wrapped.csproj` packs assets into `contentFiles/any/any`; changing asset names/paths requires updating both packaging and runtime lookup.
- `nuget.config` includes local source `./src/Weasyprint.Wrapped/nupkgs/` for iterative testing with sample projects.

## Integration and release points
- CI builds assets on both OS runners, uploads artifacts, then runs tests against downloaded artifacts (`build-test-code-scan.yml`).
- Linux CI sets isolated font config env vars before tests; font issues in CI are often environment-related, not wrapper logic.
- Tag-based release (`v*`) creates draft prerelease, rewrites `version-*` files inside zips, packs NuGet, pushes to GitHub/NuGet, and publishes zip assets (`release-assets.yml`).
- Docker image `.docker/net-sdk-weayprint/Dockerfile` is published in release workflow and provides runtime deps for WeasyPrint scenarios.

45 changes: 34 additions & 11 deletions src/Weasyprint.Wrapped.Tests/Tests/PrinterTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@ public async Task Initialize_UnzipsAssetToFolder_LeaveFolderIfVersionIsSame()
var fileStream = File.Create($"./weasyprinter/{version}");
fileStream.Close();

var executableFileName = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "weasyprint.exe" : "weasyprint";
await File.WriteAllTextAsync(Path.Combine("./weasyprinter", executableFileName), "placeholder");

var creationTimeBeforeAction = new DirectoryInfo("./weasyprinter").CreationTime;
await Task.Delay(10);
await GetPrinter().Initialize();
Expand All @@ -88,11 +91,8 @@ public async Task Print_RunsCommand_Simple()
Assert.True(string.IsNullOrWhiteSpace(result.Error), $"Should have no error but found {result.Error}");
Assert.Equal(0, result.ExitCode);
Assert.False(result.HasError);

var filename = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "Print_RunsCommand_Result_Windows_Expected.pdf" : "Print_RunsCommand_Result_Linux_Expected.pdf";
var expectedOutputBytes = File.ReadAllBytes(Path.Combine(testingProjectRoot, $"Expected/{filename}"));
File.WriteAllBytes(Path.Combine(testingProjectRoot, "Expected/Print_RunsCommand_Result_Actual.pdf"), result.Bytes);
Assert.True(result.Bytes.Length > 0);
AssertLooksLikePdf(result.Bytes);
}

[Fact]
Expand All @@ -110,11 +110,8 @@ public async Task Print_RunsStreamCommand_Simple()
Assert.True(string.IsNullOrWhiteSpace(result.Error), $"Should have no error but found {result.Error}");
Assert.Equal(0, result.ExitCode);
Assert.False(result.HasError);

var filename = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "Print_RunsCommand_Result_Windows_Expected.pdf" : "Print_RunsCommand_Result_Linux_Expected.pdf";
var expectedOutputBytes = File.ReadAllBytes(Path.Combine(testingProjectRoot, $"Expected/{filename}"));

Assert.True(actualOutputBytes.Length > 0);
AssertLooksLikePdf(actualOutputBytes);
}

[Fact]
Expand All @@ -133,6 +130,7 @@ public async Task Print_RunsCommand_WithFilePaths_Simple()

var outputFileBytes = await File.ReadAllBytesAsync(outputFile);
Assert.True(outputFileBytes.Length > 0);
AssertLooksLikePdf(outputFileBytes);
}

[Fact]
Expand All @@ -152,6 +150,8 @@ public async Task Print_RunsCommand_WithParameters()
Assert.False(resultOptimized.HasError);

Assert.True(resultNormal.Bytes.Length > resultOptimized.Bytes.Length, $"Expected {resultNormal.Bytes.Length} to be greater than {resultOptimized.Bytes.Length}");
AssertLooksLikePdf(resultNormal.Bytes);
AssertLooksLikePdf(resultOptimized.Bytes);
}

[Fact]
Expand Down Expand Up @@ -191,12 +191,11 @@ public async Task Print_RunsCommand_SpecialCharacters()
var html = await File.ReadAllTextAsync(Path.Combine(testingProjectRoot, "Expected/Print_RunsCommand_SpecialCharacters_Input.html"), Encoding.UTF8);
var result = await printer.Print(html);

await File.WriteAllBytesAsync(Path.Combine(testingProjectRoot, "Expected/Print_RunsCommand_SpecialCharacters_Output.pdf"), result.Bytes);

Assert.True(string.IsNullOrWhiteSpace(result.Error), $"Should have no error but found {result.Error}");
Assert.Equal(0, result.ExitCode);
Assert.False(result.HasError);
Assert.True(result.Bytes.Length > 0);
AssertLooksLikePdf(result.Bytes);
}

[Fact]
Expand All @@ -209,13 +208,27 @@ public async Task Print_RunsStreamCommand_SpecialCharacters()
var result = await printer.PrintStream(html);

var actualBytes = (result.DocumentStream as MemoryStream)?.ToArray();
var expectedBytes = await File.ReadAllBytesAsync(Path.Combine(testingProjectRoot, "Expected/Print_RunsCommand_SpecialCharacters_Output.pdf"));

Assert.True(string.IsNullOrWhiteSpace(result.Error), $"Should have no error but found {result.Error}");
Assert.Equal(0, result.ExitCode);
Assert.False(result.HasError);
Assert.True(result.DocumentStream.Length > 0);
Assert.True(actualBytes?.Length > 0);
AssertLooksLikePdf(actualBytes!);
}

[Fact]
public async Task Initialize_ThrowsHelpfulError_WhenAssetMissing()
{
var missingAssetsFolder = Path.Combine(testingProjectRoot, "assets", "this-directory-does-not-exist");
var config = new ConfigurationProvider(missingAssetsFolder, true, "weasyprinter", false);
var printer = new Printer(config);

var exception = await Assert.ThrowsAsync<InitializeException>(() => printer.Initialize());

Assert.True(
exception.Message.IndexOf("asset was not found", StringComparison.OrdinalIgnoreCase) >= 0,
$"Unexpected error message: {exception.Message}");
}

[Fact]
Expand All @@ -237,4 +250,14 @@ private static Printer GetPrinter()
var config = new ConfigurationProvider("../../../../../assets/", false, "weasyprinter", false);
return new Printer(config);
}

private static void AssertLooksLikePdf(byte[] bytes)
{
Assert.True(bytes.Length > 5, "Expected generated PDF bytes to have a valid length.");
Assert.Equal((byte)'%', bytes[0]);
Assert.Equal((byte)'P', bytes[1]);
Assert.Equal((byte)'D', bytes[2]);
Assert.Equal((byte)'F', bytes[3]);
Assert.Equal((byte)'-', bytes[4]);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,11 @@ private string GetFolder(string folder, bool isAbsolute)

public string GetAsset()
{
var env = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "windows" : "linux";
var env = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
? "windows"
: RuntimeInformation.IsOSPlatform(OSPlatform.Linux)
? "linux"
: throw new PlatformNotSupportedException("Only Windows and Linux are supported by Weasyprint.Wrapped.");
return Path.Combine(assetsFolder, $"standalone-{env}-64.zip");
}

Expand Down
8 changes: 8 additions & 0 deletions src/Weasyprint.Wrapped/Exceptions/InitializeException.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@ namespace Weasyprint.Wrapped;

public class InitializeException : Exception
{
public InitializeException(string message) : base(message)
{
}

public InitializeException(string message, Exception innerException) : base(message, innerException)
{
}

public InitializeException(CommandResult result, string errorOutput) : base(@$"Error happened during weasyprint initialization
ErrorOutput: {errorOutput}
ExitCode: {result.ExitCode}
Expand Down
Loading
Loading