diff --git a/DemoApp/DemoApp.csproj b/DemoApp/DemoApp.csproj
new file mode 100644
index 0000000..dc7fd3f
--- /dev/null
+++ b/DemoApp/DemoApp.csproj
@@ -0,0 +1,16 @@
+
+
+
+ Exe
+ net8.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
diff --git a/DemoApp/Program.cs b/DemoApp/Program.cs
new file mode 100644
index 0000000..f0ef2eb
--- /dev/null
+++ b/DemoApp/Program.cs
@@ -0,0 +1,203 @@
+// DemoApp – exercises all README examples to verify they compile and run.
+
+using Pmad.Cartography;
+using Pmad.Cartography.Contours;
+using Pmad.Cartography.Databases;
+using Pmad.Cartography.DataCells;
+using Pmad.Cartography.Drawing.Contours;
+using Pmad.Cartography.Drawing.Topographic;
+using Pmad.Cartography.Hillshading;
+using Pmad.Cartography.Projections;
+using Pmad.Drawing;
+using Pmad.Drawing.PdfRender;
+using Pmad.Geometry;
+using Pmad.ProgressTracking;
+using SixLabors.ImageSharp;
+
+// ─────────────────────────────────────────────────────────────────────────────
+// Pmad.Cartography README examples
+// ─────────────────────────────────────────────────────────────────────────────
+
+Console.WriteLine("=== Pmad.Cartography ===");
+
+// Query elevation from a well-known database
+// Uses SRTM1 data hosted on cdn.dem.pmad.net (downloaded on demand and cached locally)
+var database = WellKnownDatabases.GetSRTM1();
+
+// Single point – bilinear interpolation
+var elevation = await database.GetElevationAsync(
+ new Coordinates(51.509865, -0.118092),
+ DefaultInterpolation.Instance);
+Console.WriteLine($"Elevation at London: {elevation:F1} m");
+
+// Load an area into memory as a float raster
+var area = await database.CreateView(
+ new Coordinates(51, -1),
+ new Coordinates(52, 0));
+Console.WriteLine($"Area loaded: {area.PointsLon}x{area.PointsLat} px");
+
+// Contour lines
+var contour = new ContourGraph();
+// 10-metre interval starting at 10 m
+contour.Add(area, new ContourLevelGenerator(10, 10));
+
+foreach (var line in contour.Lines.Take(3))
+{
+ Console.WriteLine($"Level {line.Level}: {line.Points.Count} points");
+}
+
+// Hillshading – each pixel represents a 10x10-metre cell
+var hillshader = new HillshaderFast(new Vector2D(10, 10));
+
+// Returns an image where alpha encodes shadow intensity
+var hillshadeImg = hillshader.GetPixelsAlphaBelowFlat(area);
+Console.WriteLine($"Hillshade image: {hillshadeImg.Width}x{hillshadeImg.Height}");
+
+// Custom cache path example (not actually downloaded here)
+var _ = WellKnownDatabases.GetSRTM1(localCache: Path.Combine(Path.GetTempPath(), "dem-cache"));
+
+// ─────────────────────────────────────────────────────────────────────────────
+// Pmad.Drawing README examples
+// ─────────────────────────────────────────────────────────────────────────────
+
+Console.WriteLine("\n=== Pmad.Drawing ===");
+
+var myPoints = new Vector2D[] { new(10, 10), new(200, 10), new(200, 200), new(10, 200) };
+var myLine = new Vector2D[] { new(50, 50), new(400, 300), new(700, 100) };
+
+// Render to SVG
+Render.ToSvg("output.svg", new Vector2D(800, 600), surface =>
+{
+ var fill = new SolidColorBrush(Color.LightBlue);
+ var stroke = new Pen(new SolidColorBrush(Color.DarkBlue), 2);
+ var style = surface.AllocateStyle(fill, stroke);
+
+ surface.DrawPolygon(new[] { myPoints }, style);
+ surface.DrawPolyline(myLine, style);
+});
+Console.WriteLine("SVG written: output.svg");
+
+// Render to PNG
+Render.ToPng("output.png", new Vector2D(800, 600), surface =>
+{
+ // same drawing calls as SVG
+ var fill = new SolidColorBrush(Color.LightBlue);
+ var stroke = new Pen(new SolidColorBrush(Color.DarkBlue), 2);
+ var style = surface.AllocateStyle(fill, stroke);
+ surface.DrawPolygon(new[] { myPoints }, style);
+ surface.DrawPolyline(myLine, style);
+});
+Console.WriteLine("PNG written: output.png");
+
+// Render to PDF (sizeInPixels, not a PaperSize enum)
+Render.ToPdf("output.pdf", new Vector2D(800, 600), surface =>
+{
+ // same drawing calls
+ var fill = new SolidColorBrush(Color.LightBlue);
+ var stroke = new Pen(new SolidColorBrush(Color.DarkBlue), 2);
+ var style = surface.AllocateStyle(fill, stroke);
+ surface.DrawPolygon(new[] { myPoints }, style);
+});
+Console.WriteLine("PDF written: output.pdf");
+
+// Generate map tiles (Leaflet-compatible)
+TilingInfos info = Render.ToSvgTiled(
+ "tiles/map.svg",
+ new Vector2D(1024, 1024),
+ SvgFallBackFormats.Webp,
+ drawLod1: surface =>
+ {
+ var style = surface.AllocateStyle(new SolidColorBrush(Color.LightGreen), null);
+ surface.DrawPolygon(new[] { myPoints }, style);
+ },
+ drawLod2: surface =>
+ {
+ var style = surface.AllocateStyle(new SolidColorBrush(Color.Green), null);
+ surface.DrawPolygon(new[] { myPoints }, style);
+ });
+Console.WriteLine($"Tiles written – zoom {info.MinZoom}–{info.MaxZoom}");
+
+// Tile an existing Image directly
+using var fullImage = new SixLabors.ImageSharp.Image(512, 512);
+TilingInfos info2 = ImageTiler.DefaultToWebp(fullImage, "tiles-img/");
+Console.WriteLine($"Image tiles written – zoom {info2.MinZoom}–{info2.MaxZoom}");
+
+// ─────────────────────────────────────────────────────────────────────────────
+// Pmad.Cartography.Drawing README examples
+// ─────────────────────────────────────────────────────────────────────────────
+
+Console.WriteLine("\n=== Pmad.Cartography.Drawing ===");
+
+// Projection area covering the loaded DEM view
+var proj = new NoProjectionArea(
+ new Coordinates(51, -1),
+ new Coordinates(52, 0),
+ new Vector2D(1024, 1024));
+
+// Render contour lines
+Render.ToSvg("contours.svg", proj.Size, surface =>
+{
+ var renderer = new ContourRender(surface);
+ renderer.Render(contour, proj, hillshadeImg);
+});
+Console.WriteLine("Contour SVG written: contours.svg");
+
+// Topographic map rendering
+using var scope = new NoProgress();
+
+// Build a minimal ITopoMapData implementation using the DEM view we already loaded
+var topoData = new DemoTopoMapData(area);
+
+var renderData = TopoMapRenderData.Create(topoData, scope);
+
+Render.ToSvgTiled("topo/map.svg", proj.Size, SvgFallBackFormats.Webp,
+ drawLod1: surface => new TopoMapRender(renderData, proj).Render(surface),
+ drawLod2: surface => new TopoMapRender(renderData, proj).RenderLod2(surface),
+ drawLod3: surface => new TopoMapRender(renderData, proj).RenderLod3(surface));
+Console.WriteLine("Topographic tiled SVG written: topo/map.svg");
+
+// PDF export
+// Note: TopoMapPdfRender is designed for metric units, but the demo data is in degrees, so rescale to have something useful
+topoData = new DemoTopoMapData(new DemDataCellPixelIsPoint(new Coordinates(0,0), new Coordinates(10240, 10240), area.ToDataCell().Data));
+
+System.IO.Directory.CreateDirectory("topo-pdf");
+var pdfFiles = TopoMapPdfRender.RenderPDF("topo-pdf", "demo", topoData, scope);
+Console.WriteLine($"PDF files written: {pdfFiles.Count}");
+
+// Map metadata example
+var metadata = new TopoMapMetadata(
+ attribution: "Map data (c) OpenStreetMap contributors",
+ title: "My Topographic Map",
+ licenseNotice: "CC BY-SA 4.0",
+ exportCreator: "DemoApp 1.0",
+ upperTitle: "Sheet 1");
+Console.WriteLine($"Metadata title: {metadata.Title}");
+
+Console.WriteLine("\nAll examples completed successfully.");
+
+// ─────────────────────────────────────────────────────────────────────────────
+// Minimal ITopoMapData implementation used by the demo
+// ─────────────────────────────────────────────────────────────────────────────
+
+internal sealed class DemoTopoMapData : ITopoMapData
+{
+ public DemoTopoMapData(Pmad.Cartography.DataCells.IDemDataView dem)
+ {
+ DemDataCell = dem;
+ }
+
+ public TopoMapMetadata Metadata => TopoMapMetadata.None;
+ public Pmad.Cartography.DataCells.IDemDataView DemDataCell { get; }
+ public Dictionary>? Roads => null;
+ public Dictionary>? Bridges => null;
+ public Pmad.Geometry.Shapes.MultiPolygon? ForestPolygons => null;
+ public Pmad.Geometry.Shapes.MultiPolygon? RockPolygons => null;
+ public Pmad.Geometry.Shapes.MultiPolygon? BuildingPolygons => null;
+ public Pmad.Geometry.Shapes.MultiPolygon? WaterPolygons => null;
+ public Pmad.Geometry.Shapes.MultiPolygon? FortPolygons => null;
+ public List? Names => null;
+ public List? Icons => null;
+ public Pmad.Geometry.Shapes.MultiPath? Powerlines => null;
+ public Pmad.Geometry.Shapes.MultiPath? Railways => null;
+ public List? PlottedPoints => null;
+}
diff --git a/MapToolkit.Test/Databases/HttpClientHelperTest.cs b/MapToolkit.Test/Databases/HttpClientHelperTest.cs
new file mode 100644
index 0000000..7efa662
--- /dev/null
+++ b/MapToolkit.Test/Databases/HttpClientHelperTest.cs
@@ -0,0 +1,71 @@
+using System;
+using System.Linq;
+using System.Net.Http;
+using Pmad.Cartography.Databases;
+
+namespace Pmad.Cartography.Test.Databases
+{
+ public class HttpClientHelperTest
+ {
+ private const string BaseAddressString = "https://cdn.dem.pmad.net/SRTM1/";
+ private static readonly Uri BaseAddressUri = new Uri(BaseAddressString);
+
+ [Fact]
+ public void CreateClient_WithUri_SetsBaseAddress()
+ {
+ using var client = HttpClientHelper.CreateClient(BaseAddressUri);
+
+ Assert.Equal(BaseAddressUri, client.BaseAddress);
+ }
+
+ [Fact]
+ public void CreateClient_WithString_SetsBaseAddress()
+ {
+ using var client = HttpClientHelper.CreateClient(BaseAddressString);
+
+ Assert.Equal(BaseAddressUri, client.BaseAddress);
+ }
+
+ [Fact]
+ public void CreateClient_WithUri_SetsUserAgentHeader()
+ {
+ using var client = HttpClientHelper.CreateClient(BaseAddressUri);
+
+ var userAgentValues = client.DefaultRequestHeaders.UserAgent.ToList();
+ Assert.Equal(2, userAgentValues.Count);
+ Assert.Equal("Mozilla", userAgentValues[0].Product?.Name);
+ Assert.Equal("5.0", userAgentValues[0].Product?.Version);
+ Assert.Equal("(Pmad-Cartography; Default)", userAgentValues[1].Comment);
+ }
+
+ [Fact]
+ public void CreateClient_WithString_SetsUserAgentHeader()
+ {
+ using var client = HttpClientHelper.CreateClient(BaseAddressString);
+
+ var userAgentValues = client.DefaultRequestHeaders.UserAgent.ToList();
+ Assert.Equal(2, userAgentValues.Count);
+ Assert.Equal("Mozilla", userAgentValues[0].Product?.Name);
+ Assert.Equal("5.0", userAgentValues[0].Product?.Version);
+ Assert.Equal("(Pmad-Cartography; Default)", userAgentValues[1].Comment);
+ }
+
+ [Fact]
+ public void CreateClient_ReturnsDifferentInstances()
+ {
+ using var client1 = HttpClientHelper.CreateClient(BaseAddressUri);
+ using var client2 = HttpClientHelper.CreateClient(BaseAddressUri);
+
+ Assert.NotSame(client1, client2);
+ }
+
+ [Fact]
+ public void CreateClient_WithString_AndWithUri_ProduceSameBaseAddress()
+ {
+ using var clientFromString = HttpClientHelper.CreateClient(BaseAddressString);
+ using var clientFromUri = HttpClientHelper.CreateClient(BaseAddressUri);
+
+ Assert.Equal(clientFromString.BaseAddress, clientFromUri.BaseAddress);
+ }
+ }
+}
diff --git a/MapToolkit/Databases/DemHttpStorage.cs b/MapToolkit/Databases/DemHttpStorage.cs
index 465c1d9..391b93d 100644
--- a/MapToolkit/Databases/DemHttpStorage.cs
+++ b/MapToolkit/Databases/DemHttpStorage.cs
@@ -8,10 +8,20 @@
namespace Pmad.Cartography.Databases
{
+ ///
+ /// An implementation that downloads DEM tiles on demand from an HTTP
+ /// server and caches them locally on disk.
+ ///
+ ///
+ /// Downloaded tiles are stored under (or a custom path supplied
+ /// to the constructor) and reused on subsequent requests, so network access only occurs when a
+ /// tile is not yet present in the cache.
+ ///
public class DemHttpStorage : IDemStorage
{
///
- /// How long a cached index.json is considered fresh before being re-downloaded.
+ /// Gets or sets how long a cached index.json is considered fresh before being
+ /// re-downloaded. Defaults to 24 hours.
///
public static TimeSpan IndexCacheDuration { get; set; } = TimeSpan.FromHours(24);
@@ -20,26 +30,60 @@ public class DemHttpStorage : IDemStorage
private readonly string localCache;
private readonly HttpClient client;
+ ///
+ /// Initialises a new instance of with an already-configured
+ /// .
+ ///
+ ///
+ /// Directory used to cache downloaded tiles. Pass to use
+ /// .
+ ///
+ ///
+ /// An whose points to the DEM
+ /// server root.
+ ///
public DemHttpStorage(string? localCache, HttpClient client)
{
this.localCache = localCache ?? DefaultCacheLocation;
this.client = client;
}
+ ///
+ /// Initialises a new instance of that creates its own
+ /// for the given base address.
+ ///
+ ///
+ /// Directory used to cache downloaded tiles. Pass to use
+ /// .
+ ///
+ /// Base URI of the DEM HTTP server.
public DemHttpStorage(string? localCache, Uri baseAddress)
- : this(localCache, new HttpClient() { BaseAddress = baseAddress })
+ : this(localCache, HttpClientHelper.CreateClient(baseAddress))
{
}
+ ///
+ /// Initialises a new instance of using
+ /// as the local cache directory.
+ ///
+ /// Base URI of the DEM HTTP server.
public DemHttpStorage(Uri baseAddress)
: this(null, baseAddress)
{
}
+ ///
+ /// Gets the default directory used to cache downloaded tiles when no explicit path is
+ /// provided. Resolves to a dem sub-directory inside the system temporary folder.
+ ///
public static string DefaultCacheLocation => Path.Combine(Path.GetTempPath(), "dem");
+ ///
+ /// Deletes all files in , freeing disk space occupied by
+ /// previously downloaded tiles. Has no effect when the directory does not exist.
+ ///
public static void ClearDefaultCache()
{
var cacheDir = DefaultCacheLocation;
@@ -55,6 +99,21 @@ private string GetCacheFile(string path)
return Path.Combine(localCache, uri.DnsSafeHost, uri.AbsolutePath.Substring(1).Replace('/', Path.DirectorySeparatorChar));
}
+ ///
+ /// Loads a DEM tile from the cache, downloading it first if necessary.
+ ///
+ /// Path of the tile relative to the configured dataset root (e.g. N00E006.SRTMGL1.hgt.zst).
+ ///
+ /// Optional expected SHA-256 hex digest. When provided the cached file is verified after
+ /// download; a mismatch causes the file to be deleted and an
+ /// to be thrown.
+ ///
+ /// Token to cancel the asynchronous operation.
+ /// The loaded .
+ ///
+ /// Thrown when is set and the downloaded file does not
+ /// match the expected digest.
+ ///
public async Task LoadAsync(string path, string? expectedSha256 = null, CancellationToken cancellationToken = default)
{
var cacheFile = GetCacheFile(path);
@@ -117,8 +176,14 @@ private async Task DownloadFile(string path, string cacheFile, CancellationToken
}
}
+ ///
public Task Load(string path) => LoadAsync(path, null);
+ ///
+ /// Downloads (if required) and deserialises the server's index.json file.
+ /// The cached copy is reused as long as it is younger than .
+ ///
+ /// The deserialized .
public async Task ReadIndex()
{
var cacheFile = GetCacheFile("index.json");
@@ -134,6 +199,16 @@ public async Task ReadIndex()
}
}
+ ///
+ /// Downloads the file at and returns its SHA-256 hex digest, or
+ /// if the file does not exist on the server.
+ ///
+ /// Server-relative path of the file to hash.
+ /// Token to cancel the asynchronous operation.
+ ///
+ /// A lowercase hex string representing the SHA-256 digest, or when
+ /// the server returns HTTP 404.
+ ///
public async Task GetSha256Async(string path, CancellationToken cancellationToken = default)
{
var cacheFile = GetCacheFile(path);
diff --git a/MapToolkit/Databases/HttpClientHelper.cs b/MapToolkit/Databases/HttpClientHelper.cs
new file mode 100644
index 0000000..7a9d9cc
--- /dev/null
+++ b/MapToolkit/Databases/HttpClientHelper.cs
@@ -0,0 +1,22 @@
+using System;
+using System.Net.Http;
+
+namespace Pmad.Cartography.Databases
+{
+ internal static class HttpClientHelper
+ {
+ private const string DefaultUserAgent = "Mozilla/5.0 (Pmad-Cartography; Default)";
+
+ internal static HttpClient CreateClient(string baseAddress)
+ {
+ return CreateClient(new Uri(baseAddress));
+ }
+
+ internal static HttpClient CreateClient(Uri baseAddress)
+ {
+ var httpClient = new HttpClient { BaseAddress = baseAddress };
+ httpClient.DefaultRequestHeaders.UserAgent.ParseAdd(DefaultUserAgent);
+ return httpClient;
+ }
+ }
+}
diff --git a/MapToolkit/Databases/WellKnownDatabases.cs b/MapToolkit/Databases/WellKnownDatabases.cs
index 61ec130..4516cac 100644
--- a/MapToolkit/Databases/WellKnownDatabases.cs
+++ b/MapToolkit/Databases/WellKnownDatabases.cs
@@ -1,20 +1,7 @@
-using System;
-using System.Net.Http;
-
-namespace Pmad.Cartography.Databases
+namespace Pmad.Cartography.Databases
{
public static class WellKnownDatabases
{
- private const string DefaultUserAgent = "Mozilla/5.0 (Pmad-Cartography; Default)";
-
- internal static HttpClient CreateClient(string baseAddress)
- {
- var httpClient = new HttpClient { BaseAddress = new Uri(baseAddress) };
- // OVH CDN Requires a user agent
- httpClient.DefaultRequestHeaders.UserAgent.ParseAdd(DefaultUserAgent);
- return httpClient;
- }
-
///
/// Get the AW3D 30 data hosted on cdn.dem.pmad.net:
/// Japan Aerospace Exploration Agency (2021). ALOS World 3D 30 meter DEM. V3.2, Jan 2021.
@@ -29,7 +16,7 @@ internal static HttpClient CreateClient(string baseAddress)
///
public static DemHttpStorage GetAW3D30Storage(string? localCache = null)
{
- return new DemHttpStorage(localCache, CreateClient("https://cdn.dem.pmad.net/AW3D30/"));
+ return new DemHttpStorage(localCache, HttpClientHelper.CreateClient("https://cdn.dem.pmad.net/AW3D30/"));
}
///
@@ -43,7 +30,7 @@ public static DemHttpStorage GetAW3D30Storage(string? localCache = null)
///
public static DemHttpStorage GetSRTM1Storage(string? localCache = null)
{
- return new DemHttpStorage(localCache, CreateClient("https://cdn.dem.pmad.net/SRTM1/"));
+ return new DemHttpStorage(localCache, HttpClientHelper.CreateClient("https://cdn.dem.pmad.net/SRTM1/"));
}
///
@@ -58,7 +45,7 @@ public static DemHttpStorage GetSRTM1Storage(string? localCache = null)
///
public static DemHttpStorage GetSRTM15PlusStorage(string? localCache = null)
{
- return new DemHttpStorage(localCache, CreateClient("https://cdn.dem.pmad.net/SRTM15Plus/"));
+ return new DemHttpStorage(localCache, HttpClientHelper.CreateClient("https://cdn.dem.pmad.net/SRTM15Plus/"));
}
///
diff --git a/Pmad.Cartography.Drawing/README.md b/Pmad.Cartography.Drawing/README.md
index b5bebf7..edf96ea 100644
--- a/Pmad.Cartography.Drawing/README.md
+++ b/Pmad.Cartography.Drawing/README.md
@@ -32,25 +32,25 @@ Render.ToSvg("contours.svg", new Vector2D(1024, 1024), surface =>
```csharp
// Populate map data (roads, forests, water, buildings, etc.)
-var data = new TopoMapRenderData
-{
- Data = myTopoMapData, // implements ITopoMapData
- Img = hillshadeImage // optional pre-computed hillshade
-};
+// myTopoMapData implements ITopoMapData
+using var scope = new NoProgress();
+var data = TopoMapRenderData.Create(myTopoMapData, scope);
-var proj = new NoProjectionArea(origin, new Vector2D(width, height), scale);
+var proj = new NoProjectionArea(min, max, new Vector2D(width, height));
Render.ToSvgTiled("map.svg", proj.Size, SvgFallBackFormats.Webp,
- lod1: surface => new TopoMapRender(data, proj).Render(surface),
- lod2: surface => new TopoMapRender(data, proj).RenderLod2(surface),
- lod3: surface => new TopoMapRender(data, proj).RenderLod3(surface));
+ drawLod1: surface => new TopoMapRender(data, proj).Render(surface),
+ drawLod2: surface => new TopoMapRender(data, proj).RenderLod2(surface),
+ drawLod3: surface => new TopoMapRender(data, proj).RenderLod3(surface));
```
### Export as PDF
```csharp
-var pdfRender = new TopoMapPdfRender(data, proj);
-pdfRender.Render("map.pdf");
+// myTopoMapData implements ITopoMapData
+System.IO.Directory.CreateDirectory("output-dir");
+using var scope = new NoProgress();
+var pdfFiles = TopoMapPdfRender.RenderPDF("output-dir", "map", myTopoMapData, scope);
```
## Map data model
diff --git a/Pmad.Cartography.sln b/Pmad.Cartography.sln
index 542dd9b..0a511a3 100644
--- a/Pmad.Cartography.sln
+++ b/Pmad.Cartography.sln
@@ -1,7 +1,7 @@
Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio Version 17
-VisualStudioVersion = 17.2.32616.157
+# Visual Studio Version 18
+VisualStudioVersion = 18.5.11723.231 stable
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Pmad.Cartography", "MapToolkit\Pmad.Cartography.csproj", "{A7C154E7-F4A6-49AD-91DD-C1CF32650B50}"
EndProject
@@ -27,40 +27,114 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Pmad.Cartography.Drawing.Te
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{1BBD068A-5F5B-49FE-B391-B762ECDB31C9}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DemoApp", "DemoApp\DemoApp.csproj", "{DD594970-E132-4712-9963-AC8979369DA0}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
+ Debug|x64 = Debug|x64
+ Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
+ Release|x64 = Release|x64
+ Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{A7C154E7-F4A6-49AD-91DD-C1CF32650B50}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A7C154E7-F4A6-49AD-91DD-C1CF32650B50}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {A7C154E7-F4A6-49AD-91DD-C1CF32650B50}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {A7C154E7-F4A6-49AD-91DD-C1CF32650B50}.Debug|x64.Build.0 = Debug|Any CPU
+ {A7C154E7-F4A6-49AD-91DD-C1CF32650B50}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {A7C154E7-F4A6-49AD-91DD-C1CF32650B50}.Debug|x86.Build.0 = Debug|Any CPU
{A7C154E7-F4A6-49AD-91DD-C1CF32650B50}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A7C154E7-F4A6-49AD-91DD-C1CF32650B50}.Release|Any CPU.Build.0 = Release|Any CPU
+ {A7C154E7-F4A6-49AD-91DD-C1CF32650B50}.Release|x64.ActiveCfg = Release|Any CPU
+ {A7C154E7-F4A6-49AD-91DD-C1CF32650B50}.Release|x64.Build.0 = Release|Any CPU
+ {A7C154E7-F4A6-49AD-91DD-C1CF32650B50}.Release|x86.ActiveCfg = Release|Any CPU
+ {A7C154E7-F4A6-49AD-91DD-C1CF32650B50}.Release|x86.Build.0 = Release|Any CPU
{76B611A8-6A27-4338-8F84-BA4C74F2E518}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{76B611A8-6A27-4338-8F84-BA4C74F2E518}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {76B611A8-6A27-4338-8F84-BA4C74F2E518}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {76B611A8-6A27-4338-8F84-BA4C74F2E518}.Debug|x64.Build.0 = Debug|Any CPU
+ {76B611A8-6A27-4338-8F84-BA4C74F2E518}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {76B611A8-6A27-4338-8F84-BA4C74F2E518}.Debug|x86.Build.0 = Debug|Any CPU
{76B611A8-6A27-4338-8F84-BA4C74F2E518}.Release|Any CPU.ActiveCfg = Release|Any CPU
{76B611A8-6A27-4338-8F84-BA4C74F2E518}.Release|Any CPU.Build.0 = Release|Any CPU
+ {76B611A8-6A27-4338-8F84-BA4C74F2E518}.Release|x64.ActiveCfg = Release|Any CPU
+ {76B611A8-6A27-4338-8F84-BA4C74F2E518}.Release|x64.Build.0 = Release|Any CPU
+ {76B611A8-6A27-4338-8F84-BA4C74F2E518}.Release|x86.ActiveCfg = Release|Any CPU
+ {76B611A8-6A27-4338-8F84-BA4C74F2E518}.Release|x86.Build.0 = Release|Any CPU
{F797D011-4BE7-435F-9F5F-0E52140B787A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F797D011-4BE7-435F-9F5F-0E52140B787A}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {F797D011-4BE7-435F-9F5F-0E52140B787A}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {F797D011-4BE7-435F-9F5F-0E52140B787A}.Debug|x64.Build.0 = Debug|Any CPU
+ {F797D011-4BE7-435F-9F5F-0E52140B787A}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {F797D011-4BE7-435F-9F5F-0E52140B787A}.Debug|x86.Build.0 = Debug|Any CPU
{F797D011-4BE7-435F-9F5F-0E52140B787A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F797D011-4BE7-435F-9F5F-0E52140B787A}.Release|Any CPU.Build.0 = Release|Any CPU
+ {F797D011-4BE7-435F-9F5F-0E52140B787A}.Release|x64.ActiveCfg = Release|Any CPU
+ {F797D011-4BE7-435F-9F5F-0E52140B787A}.Release|x64.Build.0 = Release|Any CPU
+ {F797D011-4BE7-435F-9F5F-0E52140B787A}.Release|x86.ActiveCfg = Release|Any CPU
+ {F797D011-4BE7-435F-9F5F-0E52140B787A}.Release|x86.Build.0 = Release|Any CPU
{CD89FBC4-5213-4908-9B77-421C73CFFB6D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CD89FBC4-5213-4908-9B77-421C73CFFB6D}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {CD89FBC4-5213-4908-9B77-421C73CFFB6D}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {CD89FBC4-5213-4908-9B77-421C73CFFB6D}.Debug|x64.Build.0 = Debug|Any CPU
+ {CD89FBC4-5213-4908-9B77-421C73CFFB6D}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {CD89FBC4-5213-4908-9B77-421C73CFFB6D}.Debug|x86.Build.0 = Debug|Any CPU
{CD89FBC4-5213-4908-9B77-421C73CFFB6D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CD89FBC4-5213-4908-9B77-421C73CFFB6D}.Release|Any CPU.Build.0 = Release|Any CPU
+ {CD89FBC4-5213-4908-9B77-421C73CFFB6D}.Release|x64.ActiveCfg = Release|Any CPU
+ {CD89FBC4-5213-4908-9B77-421C73CFFB6D}.Release|x64.Build.0 = Release|Any CPU
+ {CD89FBC4-5213-4908-9B77-421C73CFFB6D}.Release|x86.ActiveCfg = Release|Any CPU
+ {CD89FBC4-5213-4908-9B77-421C73CFFB6D}.Release|x86.Build.0 = Release|Any CPU
{789FFCD4-375D-4201-BCEA-776B3A8D2EAB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{789FFCD4-375D-4201-BCEA-776B3A8D2EAB}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {789FFCD4-375D-4201-BCEA-776B3A8D2EAB}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {789FFCD4-375D-4201-BCEA-776B3A8D2EAB}.Debug|x64.Build.0 = Debug|Any CPU
+ {789FFCD4-375D-4201-BCEA-776B3A8D2EAB}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {789FFCD4-375D-4201-BCEA-776B3A8D2EAB}.Debug|x86.Build.0 = Debug|Any CPU
{789FFCD4-375D-4201-BCEA-776B3A8D2EAB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{789FFCD4-375D-4201-BCEA-776B3A8D2EAB}.Release|Any CPU.Build.0 = Release|Any CPU
+ {789FFCD4-375D-4201-BCEA-776B3A8D2EAB}.Release|x64.ActiveCfg = Release|Any CPU
+ {789FFCD4-375D-4201-BCEA-776B3A8D2EAB}.Release|x64.Build.0 = Release|Any CPU
+ {789FFCD4-375D-4201-BCEA-776B3A8D2EAB}.Release|x86.ActiveCfg = Release|Any CPU
+ {789FFCD4-375D-4201-BCEA-776B3A8D2EAB}.Release|x86.Build.0 = Release|Any CPU
{A4A7588A-0141-4D37-A614-4E889006CF1F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A4A7588A-0141-4D37-A614-4E889006CF1F}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {A4A7588A-0141-4D37-A614-4E889006CF1F}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {A4A7588A-0141-4D37-A614-4E889006CF1F}.Debug|x64.Build.0 = Debug|Any CPU
+ {A4A7588A-0141-4D37-A614-4E889006CF1F}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {A4A7588A-0141-4D37-A614-4E889006CF1F}.Debug|x86.Build.0 = Debug|Any CPU
{A4A7588A-0141-4D37-A614-4E889006CF1F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A4A7588A-0141-4D37-A614-4E889006CF1F}.Release|Any CPU.Build.0 = Release|Any CPU
+ {A4A7588A-0141-4D37-A614-4E889006CF1F}.Release|x64.ActiveCfg = Release|Any CPU
+ {A4A7588A-0141-4D37-A614-4E889006CF1F}.Release|x64.Build.0 = Release|Any CPU
+ {A4A7588A-0141-4D37-A614-4E889006CF1F}.Release|x86.ActiveCfg = Release|Any CPU
+ {A4A7588A-0141-4D37-A614-4E889006CF1F}.Release|x86.Build.0 = Release|Any CPU
{DC21E7FB-E6D1-4EF9-8401-D0D4AA784DF7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{DC21E7FB-E6D1-4EF9-8401-D0D4AA784DF7}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {DC21E7FB-E6D1-4EF9-8401-D0D4AA784DF7}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {DC21E7FB-E6D1-4EF9-8401-D0D4AA784DF7}.Debug|x64.Build.0 = Debug|Any CPU
+ {DC21E7FB-E6D1-4EF9-8401-D0D4AA784DF7}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {DC21E7FB-E6D1-4EF9-8401-D0D4AA784DF7}.Debug|x86.Build.0 = Debug|Any CPU
{DC21E7FB-E6D1-4EF9-8401-D0D4AA784DF7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{DC21E7FB-E6D1-4EF9-8401-D0D4AA784DF7}.Release|Any CPU.Build.0 = Release|Any CPU
+ {DC21E7FB-E6D1-4EF9-8401-D0D4AA784DF7}.Release|x64.ActiveCfg = Release|Any CPU
+ {DC21E7FB-E6D1-4EF9-8401-D0D4AA784DF7}.Release|x64.Build.0 = Release|Any CPU
+ {DC21E7FB-E6D1-4EF9-8401-D0D4AA784DF7}.Release|x86.ActiveCfg = Release|Any CPU
+ {DC21E7FB-E6D1-4EF9-8401-D0D4AA784DF7}.Release|x86.Build.0 = Release|Any CPU
+ {DD594970-E132-4712-9963-AC8979369DA0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {DD594970-E132-4712-9963-AC8979369DA0}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {DD594970-E132-4712-9963-AC8979369DA0}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {DD594970-E132-4712-9963-AC8979369DA0}.Debug|x64.Build.0 = Debug|Any CPU
+ {DD594970-E132-4712-9963-AC8979369DA0}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {DD594970-E132-4712-9963-AC8979369DA0}.Debug|x86.Build.0 = Debug|Any CPU
+ {DD594970-E132-4712-9963-AC8979369DA0}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {DD594970-E132-4712-9963-AC8979369DA0}.Release|Any CPU.Build.0 = Release|Any CPU
+ {DD594970-E132-4712-9963-AC8979369DA0}.Release|x64.ActiveCfg = Release|Any CPU
+ {DD594970-E132-4712-9963-AC8979369DA0}.Release|x64.Build.0 = Release|Any CPU
+ {DD594970-E132-4712-9963-AC8979369DA0}.Release|x86.ActiveCfg = Release|Any CPU
+ {DD594970-E132-4712-9963-AC8979369DA0}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -69,6 +143,7 @@ Global
{F797D011-4BE7-435F-9F5F-0E52140B787A} = {1BBD068A-5F5B-49FE-B391-B762ECDB31C9}
{789FFCD4-375D-4201-BCEA-776B3A8D2EAB} = {1BBD068A-5F5B-49FE-B391-B762ECDB31C9}
{DC21E7FB-E6D1-4EF9-8401-D0D4AA784DF7} = {1BBD068A-5F5B-49FE-B391-B762ECDB31C9}
+ {DD594970-E132-4712-9963-AC8979369DA0} = {1BBD068A-5F5B-49FE-B391-B762ECDB31C9}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {2496CF15-57E0-4409-AD08-7A98882B0088}
diff --git a/Pmad.Drawing/README.md b/Pmad.Drawing/README.md
index 7177741..b418bc1 100644
--- a/Pmad.Drawing/README.md
+++ b/Pmad.Drawing/README.md
@@ -39,7 +39,7 @@ Render.ToPng("output.png", new Vector2D(800, 600), surface =>
### Render to PDF
```csharp
-Render.ToPdf("output.pdf", PaperSize.A4Landscape, surface =>
+Render.ToPdf("output.pdf", new Vector2D(800, 600), surface =>
{
// same drawing calls
});