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
4 changes: 4 additions & 0 deletions ACadSharp.Image.Cli/CliOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ internal sealed record CliOptions(
string? Format,
int Width,
int Height,
int PaddingLeft,
int PaddingTop,
int PaddingRight,
int PaddingBottom,
string BackgroundColor,
int Quality,
bool ExportPaperLayouts,
Expand Down
38 changes: 37 additions & 1 deletion ACadSharp.Image.Cli/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ private static void Configure(ImageConfiguration configuration, CliOptions optio
{
configuration.Width = options.Width;
configuration.Height = options.Height;
configuration.SetPadding(options.PaddingLeft, options.PaddingTop, options.PaddingRight, options.PaddingBottom);
configuration.OutputQuality = options.Quality;
configuration.BackgroundColor = ParseColor(options.BackgroundColor);

Expand Down Expand Up @@ -141,6 +142,10 @@ private static CliOptions ParseArgs(IReadOnlyList<string> args)
string backgroundColor = "white";
int width = ImageConfiguration.DefaultWidth;
int height = ImageConfiguration.DefaultHeight;
int paddingLeft = 0;
int paddingTop = 0;
int paddingRight = 0;
int paddingBottom = 0;
int quality = 90;
bool exportPaperLayouts = false;
List<string> hideLayers = new();
Expand Down Expand Up @@ -168,6 +173,10 @@ private static CliOptions ParseArgs(IReadOnlyList<string> args)
case "-H":
height = ParsePositiveInt(GetRequiredValue(args, ref i, current), current);
break;
case "--padding":
case "-p":
(paddingLeft, paddingTop, paddingRight, paddingBottom) = ParsePadding(GetRequiredValue(args, ref i, current), current);
break;
case "--background":
case "-b":
backgroundColor = GetRequiredValue(args, ref i, current);
Expand Down Expand Up @@ -196,7 +205,7 @@ private static CliOptions ParseArgs(IReadOnlyList<string> args)
throw new InvalidOperationException("An input .dxf or .dwg file is required.");
}

return new CliOptions(inputPath, outputPath, format, width, height, backgroundColor, quality, exportPaperLayouts, hideLayers);
return new CliOptions(inputPath, outputPath, format, width, height, paddingLeft, paddingTop, paddingRight, paddingBottom, backgroundColor, quality, exportPaperLayouts, hideLayers);
}

private static int ParsePositiveInt(string value, string argumentName)
Expand All @@ -219,6 +228,32 @@ private static int ParseQuality(string value, string argumentName)
throw new InvalidOperationException($"Argument {argumentName} must be between 1 and 100.");
}

private static (int Left, int Top, int Right, int Bottom) ParsePadding(string value, string argumentName)
{
string[] parts = value.Split(',', StringSplitOptions.TrimEntries);
int[] parsed = parts
.Select(part => ParseNonNegativeInt(part, argumentName))
.ToArray();

return parsed.Length switch
{
1 => (parsed[0], parsed[0], parsed[0], parsed[0]),
2 => (parsed[0], parsed[1], parsed[0], parsed[1]),
4 => (parsed[0], parsed[1], parsed[2], parsed[3]),
_ => throw new InvalidOperationException($"Argument {argumentName} must contain 1, 2, or 4 comma-separated integers."),
};
}

private static int ParseNonNegativeInt(string value, string argumentName)
{
if (int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out int parsed) && parsed >= 0)
{
return parsed;
}

throw new InvalidOperationException($"Argument {argumentName} values must be zero or greater.");
}

private static string GetRequiredValue(IReadOnlyList<string> args, ref int index, string argumentName)
{
if (index + 1 >= args.Count || args[index + 1].StartsWith('-'))
Expand Down Expand Up @@ -261,6 +296,7 @@ private static void WriteHelp()
-f, --format <format> png, bmp, jpg, jpeg, gif, webp.
-w, --width <pixels> Output width in pixels. Default: 1600.
-H, --height <pixels> Output height in pixels. Default: 900.
-p, --padding <value> Padding in pixels: <all>, <x,y>, or <left,top,right,bottom>.
-b, --background <color> Background color name or hex value. Default: white.
-q, --quality <1-100> Output quality for lossy formats. Default: 90.
--paper-layouts Export paper layouts instead of model space.
Expand Down
52 changes: 52 additions & 0 deletions ACadSharp.Image.Tests/ImageExporterTests.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
using ACadSharp.Entities;
using ACadSharp.IO;
using ACadSharp.Image.Rendering;
using ACadSharp.Objects;
using ACadSharp.Tables;
using CSMath;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;

namespace ACadSharp.Image.Tests;

Expand All @@ -22,6 +26,10 @@ public void ConfigurationUsesDefaultCanvasSize()

Assert.Equal(ImageConfiguration.DefaultWidth, configuration.Width);
Assert.Equal(ImageConfiguration.DefaultHeight, configuration.Height);
Assert.Equal(0, configuration.PaddingLeft);
Assert.Equal(0, configuration.PaddingTop);
Assert.Equal(0, configuration.PaddingRight);
Assert.Equal(0, configuration.PaddingBottom);
}

[Fact]
Expand All @@ -41,6 +49,50 @@ public void RenderUsesConfiguredCanvasSize()
Assert.Equal(600, page.Canvas.Height);
}

[Fact]
public void PageContextUsesConfiguredPadding()
{
ImageConfiguration configuration = new()
{
Width = 100,
Height = 80,
};
configuration.SetPadding(10, 20, 30, 20);

ImagePage page = new()
{
Layout = new Layout("padding-page")
{
PaperWidth = 12,
PaperHeight = 8,
},
};

using Image<Rgba32> canvas = new(configuration.Width, configuration.Height);
ImageRenderContext context = ImageRenderContext.CreatePageContext(canvas, page, configuration);

Assert.Equal(5f, context.PixelsPerUnit);
Assert.Equal(10f, context.OffsetX);
Assert.Equal(20f, context.OffsetY);
}

[Fact]
public void RenderThrowsWhenPaddingConsumesCanvas()
{
BlockRecord block = new("padding-overflow-block");
block.Entities.Add(new Line(new XYZ(0, 0, 0), new XYZ(10, 10, 0)));

ImageExporter exporter = new();
exporter.Configuration.Width = 20;
exporter.Configuration.Height = 20;
exporter.Configuration.SetPadding(10, 0, 10, 0);
exporter.Add(block);

InvalidOperationException ex = Assert.Throws<InvalidOperationException>(() => exporter.Render());

Assert.Contains("Padding", ex.Message, StringComparison.Ordinal);
}

[Fact]
public void RenderSplineBlockDoesNotReportNotImplemented()
{
Expand Down
85 changes: 85 additions & 0 deletions ACadSharp.Image/ImageConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,14 @@ public sealed class ImageConfiguration

private int _outputQuality = 90;

private int _paddingTop;

private int _paddingRight;

private int _paddingBottom;

private int _paddingLeft;

/// <summary>
/// Gets or sets the number of segments used to approximate arcs and circles during polygonal tessellation.
/// </summary>
Expand Down Expand Up @@ -128,6 +136,42 @@ public sealed class ImageConfiguration
/// </remarks>
public float LineWeightScale { get; set; } = 1f;

/// <summary>
/// Gets or sets the top padding in pixels applied inside the output canvas.
/// </summary>
public int PaddingTop
{
get => this._paddingTop;
set => this._paddingTop = validateNonNegative(value, nameof(this.PaddingTop));
}

/// <summary>
/// Gets or sets the right padding in pixels applied inside the output canvas.
/// </summary>
public int PaddingRight
{
get => this._paddingRight;
set => this._paddingRight = validateNonNegative(value, nameof(this.PaddingRight));
}

/// <summary>
/// Gets or sets the bottom padding in pixels applied inside the output canvas.
/// </summary>
public int PaddingBottom
{
get => this._paddingBottom;
set => this._paddingBottom = validateNonNegative(value, nameof(this.PaddingBottom));
}

/// <summary>
/// Gets or sets the left padding in pixels applied inside the output canvas.
/// </summary>
public int PaddingLeft
{
get => this._paddingLeft;
set => this._paddingLeft = validateNonNegative(value, nameof(this.PaddingLeft));
}

/// <summary>
/// Gets or sets the background color of the rendered image.
/// </summary>
Expand Down Expand Up @@ -250,8 +294,49 @@ public float GetLineWeightPixels(LineWeightType lineWeight)
return Math.Max(1f, pixels * this.LineWeightScale);
}

/// <summary>
/// Applies the same padding to all four sides of the output canvas.
/// </summary>
/// <param name="padding">The padding in pixels for each side.</param>
public void SetPadding(int padding)
{
this.SetPadding(padding, padding, padding, padding);
}

/// <summary>
/// Applies horizontal and vertical padding to the output canvas.
/// </summary>
/// <param name="horizontal">The padding in pixels for the left and right sides.</param>
/// <param name="vertical">The padding in pixels for the top and bottom sides.</param>
public void SetPadding(int horizontal, int vertical)
{
this.SetPadding(horizontal, vertical, horizontal, vertical);
}

/// <summary>
/// Applies padding to each side of the output canvas.
/// </summary>
/// <param name="left">The left padding in pixels.</param>
/// <param name="top">The top padding in pixels.</param>
/// <param name="right">The right padding in pixels.</param>
/// <param name="bottom">The bottom padding in pixels.</param>
public void SetPadding(int left, int top, int right, int bottom)
{
this.PaddingLeft = left;
this.PaddingTop = top;
this.PaddingRight = right;
this.PaddingBottom = bottom;
}

internal void Notify(string message, NotificationType notificationType, Exception? ex = null)
{
this.OnNotification?.Invoke(this, new NotificationEventArgs(message, notificationType, ex));
}

private static int validateNonNegative(int value, string propertyName)
{
return value >= 0
? value
: throw new ArgumentOutOfRangeException(propertyName, "Padding must be zero or greater.");
}
}
15 changes: 11 additions & 4 deletions ACadSharp.Image/Rendering/ImageRenderContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,17 +57,24 @@ public static ImageRenderContext CreatePageContext(
ImagePage page,
ImageConfiguration configuration)
{
int drawableWidth = configuration.Width - configuration.PaddingLeft - configuration.PaddingRight;
int drawableHeight = configuration.Height - configuration.PaddingTop - configuration.PaddingBottom;
if (drawableWidth <= 0 || drawableHeight <= 0)
{
throw new InvalidOperationException("Padding must leave at least one drawable pixel in both dimensions.");
}

Layout layout = page.Layout ?? new Layout("default_page");
double pageWidth = Math.Max(1d, layout.PaperWidth);
double pageHeight = Math.Max(1d, layout.PaperHeight);
float pixelsPerUnit = Math.Min(
configuration.Width / (float)pageWidth,
configuration.Height / (float)pageHeight);
drawableWidth / (float)pageWidth,
drawableHeight / (float)pageHeight);

float scaledWidth = (float)pageWidth * pixelsPerUnit;
float scaledHeight = (float)pageHeight * pixelsPerUnit;
float offsetX = (configuration.Width - scaledWidth) / 2f;
float offsetY = (configuration.Height - scaledHeight) / 2f;
float offsetX = configuration.PaddingLeft + ((drawableWidth - scaledWidth) / 2f);
float offsetY = configuration.PaddingBottom + ((drawableHeight - scaledHeight) / 2f);

double originX = -page.Translation.X - layout.UnprintableMargin.Left;
double originY = -page.Translation.Y - layout.UnprintableMargin.Bottom;
Expand Down
13 changes: 11 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ Transform CAD drawings into raster images for **previews**, **CI/CD pipelines**,

- 🎨 **Multi-format export** — PNG, BMP, JPEG, GIF, and WebP support
- 📐 **Full CAD support** — Render DXF and DWG files with ACadSharp
- 🖼️ **Customizable output** — Control width, height, background color, and quality
- 🖼️ **Customizable output** — Control width, height, padding, background color, and quality
- 📊 **Space support** — Model space, paper layouts, and viewports
- 🎭 **Layer filtering** — Hide specific layers with `--hide-layer` option
- ⚡ **CLI tool** — Cross-platform command-line interface for automation
Expand Down Expand Up @@ -67,6 +67,7 @@ var document = DwgReader.Read("part.dwg");
var exporter = new ImageExporter("output.webp");
exporter.Configuration.Width = 2000;
exporter.Configuration.Height = 1400;
exporter.Configuration.SetPadding(24, 12);
exporter.Configuration.BackgroundColor = Color.Parse("#ffffff");
exporter.Configuration.OutputQuality = 90;

Expand Down Expand Up @@ -100,6 +101,14 @@ cad-to-image "drawing.dxf" --format webp --width 1400 --height 1400 --quality 85
cad-to-image "part.dwg" --format png --width 1800 --height 1200 --background "#0c0c0c"
```

**Add padding around the drawing:**

```bash
cad-to-image "part.dwg" --format png --padding 24
cad-to-image "part.dwg" --format png --padding 24,12
cad-to-image "part.dwg" --format png --padding 24,12,40,20
```

**Hide multiple layers:**

```bash
Expand All @@ -125,6 +134,7 @@ Options:
-f, --format <format> png, bmp, jpg, jpeg, gif, webp.
-w, --width <pixels> Output width in pixels. Default: 1600.
-H, --height <pixels> Output height in pixels. Default: 900.
-p, --padding <value> Padding in pixels: <all>, <x,y>, or <left,top,right,bottom>.
-b, --background <color> Background color name or hex value. Default: white.
-q, --quality <1-100> Output quality for lossy formats. Default: 90.
--paper-layouts Export paper layouts instead of model space.
Expand Down Expand Up @@ -272,4 +282,3 @@ This project is released under the [MIT License](LICENSE).
If you find this project helpful, please consider giving it a ⭐️ on GitHub! It helps others discover the project.

**Questions or issues?** [Open an issue](https://github.com/slaveoftime/ACadSharp.Image/issues) or start a [Discussion](https://github.com/slaveoftime/ACadSharp.Image/discussions).

Loading