Skip to content

Commit f76a156

Browse files
Add support of all CSS page sizes and orientations
Closes #12
1 parent 6138f6b commit f76a156

File tree

3 files changed

+204
-24
lines changed

3 files changed

+204
-24
lines changed

HtmlPdf/HtmlToPdfConversion.cs

Lines changed: 53 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,13 @@ internal static class HtmlToPdfConversion
3232
private static readonly CompositeFormat s_PageTemplate = CompositeFormat.Parse(PageTemplate);
3333
#endif
3434

35-
internal static string? ConvertToPdf(List<string> pages, string title, string pdfPath, bool dryRun = false)
35+
internal static string? ConvertToPdf(
36+
List<string> pages,
37+
string title,
38+
string pdfPath,
39+
PageSize pageSize = PageSize.A4,
40+
PageOrientation pageOrientation = PageOrientation.Portrait,
41+
bool dryRun = false)
3642
{
3743
#if NET
3844
ArgumentNullException.ThrowIfNull(pages);
@@ -49,13 +55,39 @@ internal static class HtmlToPdfConversion
4955
}
5056
#endif
5157

58+
#if NET
59+
if (!Enum.IsDefined(pageSize))
60+
{
61+
throw new ArgumentOutOfRangeException(nameof(pageSize));
62+
}
63+
64+
if (!Enum.IsDefined(pageOrientation))
65+
{
66+
throw new ArgumentOutOfRangeException(nameof(pageOrientation));
67+
}
68+
#else
69+
if (!Enum.IsDefined(typeof(PageSize), pageSize))
70+
{
71+
throw new ArgumentOutOfRangeException(nameof(pageSize));
72+
}
73+
74+
if (!Enum.IsDefined(typeof(PageOrientation), pageOrientation))
75+
{
76+
throw new ArgumentOutOfRangeException(nameof(pageOrientation));
77+
}
78+
#endif
79+
5280
title ??= string.Empty;
5381

5482
CultureInfo culture = CultureInfo.InvariantCulture;
5583
StringBuilder html = StringBuilderCache.Acquire(capacity: 16 * 1024);
5684
html.Append(HtmlTemplate);
5785

5886
html.Replace("%%TITLE%%", title);
87+
#pragma warning disable CA1308 // STFU! Actually need lowercase here
88+
html.Replace("%%SIZE%%", pageSize.ToString().ToLowerInvariant());
89+
html.Replace("%%ORIENTATION%%", pageOrientation.ToString().ToLowerInvariant());
90+
#pragma warning restore CA1308
5991

6092
for (int i = 0; i < pages.Count; i++)
6193
{
@@ -95,32 +127,39 @@ internal static class HtmlToPdfConversion
95127
return generatedHtml;
96128
}
97129

98-
// TODO: Implement portrait/landscape orientation switch
99-
private const string PageTemplate = " <img class=\"a4page\" src=\"{0}\" />\r\n";
100-
private const string PageBreakTemplate = " <img class=\"a4pagebreak\" src=\"{0}\" />\r\n";
130+
private const string PageTemplate = " <img class=\"apage\" src=\"{0}\"/>\r\n";
131+
private const string PageBreakTemplate = " <img class=\"apagebreak\" src=\"{0}\"/>\r\n";
101132
private const string HtmlTemplate =
102133
"""
103134
<!DOCTYPE html>
104135
<html lang="en">
105136
<head>
106-
<meta charset="utf-8" />
137+
<meta charset="utf-8"/>
107138
<title>%%TITLE%%</title>
108139
<style type="text/css">
109-
@page { margin: 0pt; }
110-
.a4page {
111-
object-fit: scale-down;
112-
margin: 0; padding: 0;
113-
width: 794px; height: 1122px;
140+
@page {
141+
size: %%SIZE%% %%ORIENTATION%%;
142+
margin: 0pt;
114143
}
115-
.a4pagebreak {
144+
html, body {
145+
margin: 0;
146+
padding: 0;
147+
}
148+
img.apage,
149+
img.apagebreak {
116150
object-fit: scale-down;
117-
margin: 0; padding: 0;
118-
width: 794px; height: 1122px;
151+
margin: 0;
152+
padding: 0;
153+
width: 100%;
154+
height: auto;
155+
max-width: 100%;
156+
}
157+
img.apagebreak {
119158
page-break-before: always;
120159
}
121160
</style>
122161
</head>
123-
<body style="margin: 0; padding: 0;">
162+
<body>
124163
125164
""";
126165
private const string HtmlFooter = "</body>\r\n</html>\r\n";

HtmlPdf/ModuleInitializer.cs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -74,10 +74,10 @@ private static Assembly OnAssemblyResolve(object sender, ResolveEventArgs args)
7474
"ITEXT.STYLEDXMLPARSER",
7575
"ITEXT.SVG",
7676
"MICROSOFT.BCL.ASYNCINTERFACES",
77-
"MICROSOFT.EXTENSIONS.DEPENDENCYINJECTION.ABSTRACTIONS",
7877
"MICROSOFT.EXTENSIONS.DEPENDENCYINJECTION",
79-
"MICROSOFT.EXTENSIONS.LOGGING.ABSTRACTIONS",
78+
"MICROSOFT.EXTENSIONS.DEPENDENCYINJECTION.ABSTRACTIONS",
8079
"MICROSOFT.EXTENSIONS.LOGGING",
80+
"MICROSOFT.EXTENSIONS.LOGGING.ABSTRACTIONS",
8181
"MICROSOFT.EXTENSIONS.OPTIONS",
8282
"MICROSOFT.EXTENSIONS.PRIMITIVES",
8383
"NEWTONSOFT.JSON",
@@ -87,7 +87,6 @@ private static Assembly OnAssemblyResolve(object sender, ResolveEventArgs args)
8787
"SYSTEM.NUMERICS.VECTORS",
8888
"SYSTEM.RUNTIME.COMPILERSERVICES.UNSAFE",
8989
"SYSTEM.THREADING.TASKS.EXTENSIONS",
90-
"SYSTEM.VALUETUPLE",
9190
};
9291
}
9392

HtmlPdf/OutPdfCommand.cs

Lines changed: 149 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ namespace HtmlPdf;
1919

2020
using System;
2121
using System.Collections.Generic;
22+
using System.IO;
2223

2324
[Cmdlet(VerbsData.Out, "Pdf", ConfirmImpact = ConfirmImpact.Low, RemotingCapability = RemotingCapability.None, SupportsShouldProcess = true)]
2425
public sealed class OutPdfCommand : PSCmdlet
@@ -54,6 +55,14 @@ public string? Title
5455
get; set;
5556
}
5657

58+
[Parameter(Position = 2, ValueFromPipelineByPropertyName = true)]
59+
[PSDefaultValue(Value = PageSize.A4)]
60+
public PageSize Size { get; set; } = PageSize.A4;
61+
62+
[Parameter(Position = 3, ValueFromPipelineByPropertyName = true)]
63+
[PSDefaultValue(Value = PageOrientation.Portrait)]
64+
public PageOrientation Orientation { get; set; } = PageOrientation.Portrait;
65+
5766
protected override void ProcessRecord()
5867
{
5968
if (this.Page is { Length: > 0 })
@@ -64,16 +73,149 @@ protected override void ProcessRecord()
6473

6574
protected override void EndProcessing()
6675
{
67-
if (_pages is { Count: > 0 })
76+
if (_pages is not { Count: > 0 })
77+
{
78+
ErrorRecord erNone = new(
79+
new FileNotFoundException("Pages were not found."),
80+
"NoPagesFound",
81+
ErrorCategory.ObjectNotFound,
82+
targetObject: null);
83+
84+
this.ThrowTerminatingError(erNone);
85+
return;
86+
}
87+
88+
string outputPath;
89+
try
90+
{
91+
outputPath = ResolveOutputPath();
92+
}
93+
catch (ItemNotFoundException ex)
94+
{
95+
var er = new ErrorRecord(
96+
ex,
97+
"UnresolvedPath",
98+
ErrorCategory.ObjectNotFound,
99+
targetObject: IsLiteralPath ? LiteralPath : Path);
100+
this.ThrowTerminatingError(er);
101+
return;
102+
}
103+
catch (Exception ex) when (ex is ArgumentException or PSArgumentException or DirectoryNotFoundException)
104+
{
105+
var er = new ErrorRecord(
106+
ex,
107+
"UnresolvedPath",
108+
ErrorCategory.InvalidArgument,
109+
targetObject: IsLiteralPath ? LiteralPath : Path);
110+
this.ThrowTerminatingError(er);
111+
return;
112+
}
113+
114+
bool dryRun = !this.ShouldProcess(outputPath, "Create");
115+
116+
var notFoundPages = new List<string>(capacity: _pages.Count);
117+
var resolvedPages = new List<string>(capacity: _pages.Count);
118+
119+
foreach (var page in _pages)
120+
{
121+
try
122+
{
123+
foreach (var resolvedPage in this.SessionState.Path.GetResolvedPSPathFromPSPath(page))
124+
{
125+
var rp = GetFilePathOfExistingFile(this, resolvedPage.ProviderPath);
126+
if (rp is not null)
127+
{
128+
resolvedPages.Add(rp);
129+
}
130+
else
131+
{
132+
notFoundPages.Add(page);
133+
}
134+
}
135+
}
136+
catch (ItemNotFoundException)
137+
{
138+
notFoundPages.Add(page);
139+
}
140+
}
141+
142+
if (notFoundPages.Count > 0)
143+
{
144+
ErrorRecord er = new(
145+
new FileNotFoundException("Pages were not found."),
146+
"NoPagesFound",
147+
ErrorCategory.ObjectNotFound,
148+
targetObject: notFoundPages);
149+
150+
this.ThrowTerminatingError(er);
151+
}
152+
153+
string? html = HtmlToPdfConversion.ConvertToPdf(resolvedPages, this.Title!, outputPath, this.Size, this.Orientation, dryRun);
154+
this.WriteVerbose(html);
155+
}
156+
157+
private bool IsLiteralPath => string.Equals(this.ParameterSetName, "LiteralPath", StringComparison.Ordinal);
158+
159+
private string ResolveOutputPath()
160+
{
161+
string? raw = IsLiteralPath ? this.LiteralPath : this.Path;
162+
if (string.IsNullOrWhiteSpace(raw))
68163
{
69-
string path = string.Equals(this.ParameterSetName, "Path", StringComparison.Ordinal)
70-
? this.SessionState.Path.GetUnresolvedProviderPathFromPSPath(this.Path!)
71-
: System.IO.Path.GetFullPath(this.LiteralPath!);
164+
throw new PSArgumentNullException(IsLiteralPath ? nameof(this.LiteralPath) : nameof(this.Path));
165+
}
166+
167+
// If the (non-literal) user input contains wildcards, resolve existing items
168+
if (!IsLiteralPath && WildcardPattern.ContainsWildcardCharacters(raw))
169+
{
170+
var matches = this.GetResolvedProviderPathFromPSPath(raw, out _);
171+
if (matches.Count == 0)
172+
{
173+
throw new ItemNotFoundException($"Path '{raw}' could not be resolved.");
174+
}
175+
176+
if (matches.Count > 1)
177+
{
178+
throw new PSArgumentException($"Path '{raw}' resolved to multiple locations. Use -LiteralPath to specify an exact output file.");
179+
}
180+
181+
// If the resolved path is a directory, you might choose to derive a filename; enforce it's not a container
182+
var singlePath = matches[0];
183+
if (Directory.Exists(singlePath))
184+
{
185+
throw new PSArgumentException($"Path '{raw}' resolves to a directory. Specify a file name.");
186+
}
187+
188+
return singlePath;
189+
}
72190

73-
bool dryRun = !this.ShouldProcess("Pdf", "Create");
191+
// Treat as a (possibly new) file path. This yields the full provider path even if the file does not yet exist
192+
var full = this.SessionState.Path.GetUnresolvedProviderPathFromPSPath(raw);
74193

75-
string? html = HtmlToPdfConversion.ConvertToPdf(_pages, this.Title!, path, dryRun);
76-
this.WriteVerbose(html);
194+
// Validate directory existence (optional but safer)
195+
var dir = System.IO.Path.GetDirectoryName(full);
196+
if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir))
197+
{
198+
throw new DirectoryNotFoundException($"Directory '{dir}' does not exist.");
77199
}
200+
201+
return full;
202+
}
203+
204+
private static string? GetFilePathOfExistingFile(PSCmdlet cmdlet, string path)
205+
{
206+
var resolvedProviderPath = cmdlet.SessionState.Path.GetUnresolvedProviderPathFromPSPath(path);
207+
return File.Exists(resolvedProviderPath) ? resolvedProviderPath : null;
78208
}
79209
}
210+
211+
// See CSS @page size keyword values:
212+
// https://developer.mozilla.org/en-US/docs/Web/CSS/@page/size
213+
public enum PageSize
214+
{
215+
A5, A4, A3, B5, B4, JISB5, JISB4, Letter, Legal, Ledger,
216+
}
217+
218+
public enum PageOrientation
219+
{
220+
Portrait, Landscape,
221+
}

0 commit comments

Comments
 (0)