Hytone-PDF
0.8.1-beta
dotnet add package Hytone-PDF --version 0.8.1-beta
NuGet\Install-Package Hytone-PDF -Version 0.8.1-beta
<PackageReference Include="Hytone-PDF" Version="0.8.1-beta" />
<PackageVersion Include="Hytone-PDF" Version="0.8.1-beta" />
<PackageReference Include="Hytone-PDF" />
paket add Hytone-PDF --version 0.8.1-beta
#r "nuget: Hytone-PDF, 0.8.1-beta"
#:package Hytone-PDF@0.8.1-beta
#addin nuget:?package=Hytone-PDF&version=0.8.1-beta&prerelease
#tool nuget:?package=Hytone-PDF&version=0.8.1-beta&prerelease
Hytone-PDF
Licensed NuGet library for DOCX → PDF conversion, plus legacy DOC → DOCX conversion, on .NET.
Breaking changes in v0.8.0-beta
The conversion API was redesigned around an Aspose-style Save/SaveAsync shape, and PDF
merging's appendToDocument option was folded into the existing MergePdfs utility instead of
being a separate mode on conversion itself. There's no compatibility shim — update call sites
directly:
| Old (pre-0.8.0) | New (0.8.0+) |
|---|---|
doc.ConvertToPDF("out.pdf") |
doc.Save("out.pdf") |
doc.ConvertToPDF("out.pdf", deleteOriginalFile: true) |
doc.Save("out.pdf", new PdfSaveOptions { DeleteOriginalFile = true }) |
HytonePdf.Convert(docxStream, pdfStream) |
HytonePdf.LoadDocument(docxStream).Save(pdfStream) |
doc.ConvertToPDF(appendToDocument: "report.pdf") (grow in place) |
doc.Save("new.pdf"); HytonePdf.MergePdfs(["report.pdf", "new.pdf"], "report.pdf"); |
doc.ConvertToPDF(outputPath: "v2.pdf", appendToDocument: "report.pdf") (non-destructive) |
doc.Save("new.pdf"); HytonePdf.MergePdfs(["report.pdf", "new.pdf"], "v2.pdf"); |
Save/SaveAsync also gained stream overloads (doc.Save(Stream), doc.SaveAsync(Stream, ...)),
and HytonePdf.LoadDocument/LoadDocumentAsync now accept a stream directly — see Convert
below.
- Package id:
Hytone-PDF - No Microsoft Word / Office required
- License key activated in code (RSA-signed, offline validation)
This is a commercial library (like Aspose.Words), not a hosted service. Customers install the package and call C# methods directly in their own process.
Install
dotnet add package Hytone-PDF
On install, NuGet will prompt to accept the license (LICENSE.txt in the package).
Activate a license
using Hytone.Pdf;
// Once at application startup — key issued by Hytone
HytonePdf.SetLicense("HYT1....");
// Or from a .lic file
// License.SetLicenseFromFile("hytone-pdf.lic");
Without a key, the library runs in evaluation mode (production use requires a valid key).
using Hytone.Pdf.Licensing;
if (License.IsLicensed)
{
Console.WriteLine($"Licensed to {License.Current.LicensedTo} ({License.Current.Type})");
}
Pricing & tiers
| Trial | Standard | |
|---|---|---|
| Price | Free | £25/month or £175/year |
| Output | Capped at 1 page | Unlimited |
PDF merging (MergePdfs) |
Not available — throws LicenseException |
Included |
| Watermark | Yes | None |
No key at all, an expired key, or an active Trial-type key all count as the Trial tier — the
relevant check is License.Current.IsRestrictedTier, not IsLicensed (a valid, non-expired
Trial key genuinely activates — IsLicensed is true — but still keeps the tier's limits, since
IsLicensed is about key validity, not tier).
The trial watermark is deliberately not a single text object: it's tiled as many small, independently-positioned fragments whose draw calls are interleaved with the real page content's own draw calls, so there's no single object a PDF-editing tool (including PDFsharp itself) can find and delete to produce a clean file — removing it means correctly identifying and stripping dozens of scattered fragments throughout the whole content stream. Position, size, opacity, and draw order are randomized per document, so there's no fixed pattern to script against either. No purely visual watermark defeats a sufficiently determined, PDF-format-expert attacker with custom tooling — the realistic goal is raising the bar past casual/scripted removal, not claiming unbreakability.
Convert
using Hytone.Pdf;
var doc = HytonePdf.LoadDocument("report.docx");
doc.Save("report.pdf");
LoadDocument returns a HytoneDocument — hold onto the reference and call Save whenever
you're ready. Each instance is independent (no shared state with any other loaded document), so
this is safe to use from a script, a CLI tool, or a server loading and converting many documents
at once from multiple threads.
For streams, or any source that isn't a file on disk, LoadDocument/Save both have stream
overloads — the same object model, just a different source/destination:
using var docxStream = File.OpenRead("report.docx");
using var pdfStream = File.Create("report.pdf");
HytonePdf.LoadDocument(docxStream).Save(pdfStream);
For a server/web-app caller that doesn't want to block a thread pool thread on an I/O-heavy
conversion, LoadDocumentAsync/SaveAsync are available (path and stream overloads, both with
CancellationToken support):
using Hytone.Pdf.Conversion.Pdf;
var doc = await HytonePdf.LoadDocumentAsync("report.docx");
await doc.SaveAsync("report.pdf", new PdfSaveOptions { DeleteOriginalFile = true });
This is honest async, not a fabricated one: reading a file's bytes uses real asynchronous file
I/O, but the DOCX parsing/PDF layout/rendering itself (via OpenXml SDK and PDFsharp, neither of
which expose async APIs) runs via a thread-pool Task.Run under the hood rather than a true
non-blocking pipeline — see the XML doc comments on SaveAsync for the full explanation.
Merge PDFs
The recommended way to combine documents: build up in-memory "parts" — HytoneDocument.ToPdfDocument()
for a freshly-converted DOCX, HytonePdf.LoadPdf(...) for an existing PDF — then combine and
Save once, no intermediate file or stream needed in between:
HytonePdfDocument cover = HytonePdf.LoadPdf("cover.pdf");
HytoneDocument chapter1 = HytonePdf.LoadDocument("chapter1.docx");
HytoneDocument chapter2 = HytonePdf.LoadDocument("chapter2.docx");
cover.Append(chapter1).Append(chapter2).Save("book.pdf");
Append chains fluently and accepts either a HytoneDocument (converts and appends in one call)
or another HytonePdfDocument. Prefer a flat ordered list instead of chaining? MergePdfs has an
overload for that too:
HytonePdf.MergePdfs(
[HytonePdf.LoadPdf("cover.pdf"), chapter1.ToPdfDocument(), chapter2.ToPdfDocument()],
"book.pdf");
For merging PDFs that are already files on disk with no conversion involved at all, the plain path/stream overloads are still there and remain the simplest option:
HytonePdf.MergePdfs(["part1.pdf", "part2.pdf"], "combined.pdf");
using var a = File.OpenRead("part1.pdf");
using var b = File.OpenRead("part2.pdf");
using var merged = File.Create("combined.pdf");
HytonePdf.MergePdfs([a, b], merged);
MergePdfs's path overload also handles the output path being one of its own inputs safely (a
temp file under the hood, swapped in atomically once the merge succeeds) — the "grow an existing
PDF in place" pattern:
HytonePdf.MergePdfs(["report.pdf", "chapter2_only.pdf"], "report.pdf");
Convert legacy .doc
Word 97-2003 binary (.doc) documents convert to .docx first — chain into LoadDocument to go
straight to PDF:
HytonePdf.ConvertDocToDocx("legacy.doc", "legacy.docx");
HytonePdf.LoadDocument("legacy.docx").Save("legacy.pdf");
For streams, or any non-file source, use the stream overload instead:
using var docStream = File.OpenRead("legacy.doc");
using var docxStream = File.Create("legacy.docx");
HytonePdf.ConvertDocToDocx(docStream, docxStream);
License key format
Keys are signed strings:
HYT1.<base64url-json-payload>.<base64url-rsa-sha256-signature>
The NuGet package embeds only the public key (verify). Keys are issued with the vendor tool that holds the private key.
| Field | Meaning |
|---|---|
| Product | Always Hytone-PDF |
| Type | Trial, Standard, Enterprise, OEM |
| Customer | Licensee name |
| Expires | Optional yyyy-MM-dd (omit = perpetual) |
Issue keys (vendor only)
Keep keys/hytone-pdf-private.pem secret. Never publish it or the issuer tool as a public package.
# Perpetual Standard key
dotnet run --project tools/Hytone.Pdf.LicenseIssuer -- issue ^
-c "Acme Ltd" -t Standard -o acme.lic
# 30-day trial
dotnet run --project tools/Hytone.Pdf.LicenseIssuer -- issue ^
-c "Prospect Inc" -t Trial -e 2026-09-01
# Verify
dotnet run --project tools/Hytone.Pdf.LicenseIssuer -- verify acme.lic
Build & pack the licensed NuGet
cd source/Hytone-PDF
dotnet test
dotnet pack src/Hytone-PDF/Hytone-PDF.csproj -c Release -o artifacts
Output: artifacts/Hytone-PDF.<version>.nupkg
Publish to nuget.org or a private feed when ready:
dotnet nuget push artifacts/Hytone-PDF.*.nupkg --api-key <KEY> --source https://api.nuget.org/v3/index.json
Status
| Area | Status |
|---|---|
| Licensed NuGet packaging | Done |
| RSA license keys + evaluation mode | Done |
| Vendor license issuer tool | Done |
| Public convert methods (instance-based document, path + stream + async overloads, all thread-safe) | Done |
PDF merging (standalone, or append-after-conversion via Save + MergePdfs) |
Working |
| DOCX → PDF engine | Section-aware two-pass layout |
| Style inheritance / bullets / fonts | Working |
Sections (portrait/landscape pgSz) |
Working |
Multi-column sections (w:cols, incl. column breaks) |
Working — sequential-fill (see Known limitations) |
| Hard page breaks | Working |
| Headers & footers (incl. PAGE/NUMPAGES, first-page, odd/even) | Working |
Table grid widths (gridCol / tcW) |
Working |
Table vertical cell merge (vMerge) |
Working |
| Nested tables (a table inside a table cell) | Working (see Known limitations) |
| Cell borders (incl. single-edge accents) | Working |
Floating/anchored images (wp:anchor, incl. behindDoc z-order) |
Working (see Known limitations) |
Tracked changes (w:ins/w:del) |
Rendered as accepted (insertions kept, deletions dropped) |
| Hyperlinks | Clickable PDF link annotations |
List restart (w:startOverride) |
Working |
| Character formatting (subscript/superscript, strikethrough, caps/small caps, highlight, character spacing) | Working |
| Images / charts | Working |
| Font resolution | Windows only — matches installed TTF/TTC fonts by parsing their actual name table, with an open-license substitute for a common decorative font that isn't installed (see Known limitations) |
| Pixel-perfect Word layout | Improving (see Known limitations) |
| DOC → DOCX (legacy Word 97-2003 binary) | Working (see Known limitations) |
Known limitations
Deliberately out of scope for now — each is a substantial subsystem on its own rather than a quick addition, and none of the internal validation documents used to harden this engine exercise them:
- Footnotes / endnotes are not rendered.
- Multi-column sections use sequential-fill, not Word's end-of-section column balancing — a column fills top-to-bottom before the next one starts, but the last page of a section isn't re-balanced to even out column heights the way Word's own renderer does.
- Floating/anchored images (
wp:anchor) render at their authored absolute/relative position and respectbehindDocz-order, but surrounding text is never reflowed around them — no wrap-type simulation (Square/Tight/Through), and the image is fixed to whichever page its anchoring paragraph starts on rather than independently floating across a page break. Keyword alignment (wp:align, e.g. "center"/"right" relative to a reference frame) falls back to a zero offset rather than being resolved. - Nested tables don't split across a page break independently of their parent row — the whole row (nested content included) moves to the next page together if it doesn't fit, the same behavior an oversized single cell already had.
- Field computation beyond
PAGE/NUMPAGES—TOC,REF,STYLEREF,DATE, etc. are not evaluated. - Comments are not rendered (matches Word's own default print/PDF behavior).
- Small caps and character spacing are visual approximations (uppercase transform; per-span advance) rather than true sub-glyph resizing / per-glyph tracking.
- Font resolution is Windows-only — it reads the Windows Fonts folder directly, so conversion currently requires a Windows host (not yet validated in Linux/Docker deployments).
- A document-requested font that isn't installed on the host falls back to Arial by default —
except for a curated list of well-known decorative fonts (mostly Microsoft Office/Publisher
fonts like Tempus Sans ITC or Matisse ITC, which aren't part of a bare Windows install and can't
be redistributed by us), where the closest-category open-license font bundled in the assembly is
used instead: a handwriting style, a bold marker/brush style, or an elegant script style. This is
a best-effort visual approximation, not a literal match, and only kicks in for the specific fonts
and keyword patterns
WindowsFontResolverrecognizes as decorative — an unrecognized plain font still falls back to Arial as before. Installing the actual requested font on the host (e.g. by having Office/Publisher installed) always takes priority over any substitute. - DOC → DOCX is built in part using components from
b2xtranslator (BSD-3-Clause; see
THIRD-PARTY-NOTICES.md— used under license, not affiliated with or endorsed by its authors), a small, niche project rather than a widely field-tested one — verified here against a real.docfixture with headings, character formatting, lists, and a page break, but not yet exercised against a broad, adversarial.doccorpus the way the DOCX → PDF engine has been. Report any conversion gaps found in real-world files.
Solution layout
Hytone-PDF/
├── LICENSE.txt # EULA packed into the NuGet
├── keys/
│ ├── hytone-pdf-public.pem # OK to keep with source
│ └── hytone-pdf-private.pem # SECRET — gitignored
├── src/Hytone-PDF/ # PackageId: Hytone-PDF
├── tools/Hytone.Pdf.LicenseIssuer/
└── tests/Hytone-PDF.Tests/
License
Proprietary. See LICENSE.txt. A signed license key is required for production use.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net8.0
- b2xtranslator (>= 1.0.2)
- b2xtranslator.doc (>= 1.0.2)
- DocumentFormat.OpenXml (>= 3.5.1)
- PDFsharp (>= 6.1.1)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 0.8.1-beta | 72 | 8/5/2026 |
| 0.8.0-beta | 61 | 8/5/2026 |
| 0.7.1-beta | 67 | 8/4/2026 |
| 0.7.0-beta | 57 | 8/4/2026 |
| 0.6.0-beta | 58 | 8/4/2026 |
| 0.5.0-beta | 68 | 8/3/2026 |