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
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,4 @@ On Linux (or with `SHINYPDF_DEVCONTAINER=true`) the Linux native assets are adde

## Current focus

Open issues: more underline options (#11), template designer app (#12). Markdown follow-ups not yet built: tables, images, task lists.
Open issues: template designer app (#12). Markdown follow-ups not yet built: tables, images, task lists.
70 changes: 64 additions & 6 deletions docs/reference/text.md
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,9 @@ container.Column(column =>
| Italic | `Italic(bool value = true)` | same |
| Underline | `Underline(bool value = true)` | same |
| Strikethrough | `Strikethrough(bool value = true)` | same |
| Decoration line | `DecorationColor(string)`, `DecorationThickness(float)`, `DecorationSolid()`, `DecorationDouble()`, `DecorationDotted()`, `DecorationDashed()`, `DecorationWavy()` | same |
| Underline position | `UnderlinePositionAuto()`, `UnderlineAtBaseline()`, `UnderlineBelowGlyphs()` | same |
| Strikethrough position | `StrikethroughThroughGlyphs()`, `StrikethroughAtBaseline()` | same |
| Position | `NormalPosition()`, `Subscript()`, `Superscript()` | same |
| Wrapping | `WrapAnywhere(bool value = true)` | same |
| Direction | `DirectionAuto()`, `DirectionFromLeftToRight()`, `DirectionFromRightToLeft()` | same |
Expand Down Expand Up @@ -255,12 +258,6 @@ container.Text(text =>

`Italic()`, `Underline()` and `Strikethrough()` accept an optional `bool`, so you can switch an inherited decoration off, for example `Underline(false)`.

Decoration lines use the text color and the position and thickness defined by the font. There are no options for line color, thickness or style. Current behavior with sub- and superscript:

- Superscript: the underline is drawn at the position of normal-sized text, so it lines up with the surrounding text.
- Subscript: the underline follows the lowered, smaller glyphs.
- Strikethrough follows the glyphs; its thickness is scaled to 62.5% for sub- and superscript.

```csharp
container.Text(text =>
{
Expand All @@ -272,6 +269,65 @@ container.Text(text =>
});
```

#### Decoration line color, thickness and style

By default, decoration lines use the text color, the thickness defined by the font, and a solid stroke. These settings apply to both underline and strikethrough:

- `DecorationColor(string value)`: line color, independent of the text color. Same color formats and validation as `FontColor`.
- `DecorationThickness(float value)`: line thickness in points. Values `<= 0` throw `ArgumentException`. For `DecorationDouble()` it is the thickness of each of the two lines.
- `DecorationSolid()` (default), `DecorationDouble()`, `DecorationDotted()`, `DecorationDashed()`, `DecorationWavy()`: line style. Dots, dashes and waves scale with the thickness.

```csharp
container.Text(text =>
{
text.Span("spelling mistake").Underline().DecorationWavy().DecorationColor(Colors.Red.Medium);
text.Span(" and ");
text.Span("removed").Strikethrough().DecorationDouble().DecorationThickness(0.75f);
});
```

#### Underline position with sub- and superscript

The underline position decides where underlines of sub- and superscript spans are drawn. Normal text is not affected.

| Method | Superscript | Subscript |
|---|---|---|
| `UnderlinePositionAuto()` (default) | at the position of normal-sized text, in line with the surrounding text | follows the lowered, smaller glyphs |
| `UnderlineAtBaseline()` | at the position of normal-sized text | at the position of normal-sized text |
| `UnderlineBelowGlyphs()` | directly below the raised glyphs | follows the lowered, smaller glyphs |

Use `UnderlineAtBaseline()` for a continuous underline across formulas like `H鈧侽`.

```csharp
container.Text(text =>
{
text.DefaultTextStyle(x => x.Underline().UnderlineAtBaseline());
text.Span("H");
text.Span("2").Subscript();
text.Span("O");
});
```

#### Strikethrough position with sub- and superscript

The strikethrough position decides where strikethroughs of sub- and superscript spans are drawn. Normal text is not affected.

| Method | Sub- and superscript |
|---|---|
| `StrikethroughThroughGlyphs()` (default) | through the shifted, smaller glyphs; unless `DecorationThickness` is set, the thickness is scaled to 62.5% |
| `StrikethroughAtBaseline()` | at the position and thickness of normal-sized text, in line with the surrounding text |

Use `StrikethroughAtBaseline()` for a single continuous line across formulas like `E=mc虏`.

```csharp
container.Text(text =>
{
text.DefaultTextStyle(x => x.Strikethrough().StrikethroughAtBaseline());
text.Span("E=mc");
text.Span("2").Superscript();
});
```

### Subscript and superscript

`Subscript()` and `Superscript()` render the span at 62.5% of the font size, shifted down by 10% or up by 35% of the font size. To keep strokes visually as thick as the surrounding text, the font is matched one weight step heavier (+100). `NormalPosition()` resets an inherited position.
Expand Down Expand Up @@ -396,6 +452,8 @@ Values used when nothing else sets a property (`TextStyle.LibraryDefault`, inter
| Weight | Normal (400) |
| Position | Normal |
| Italic, underline, strikethrough, wrap anywhere | off |
| Decoration line | text color, font thickness, solid |
| Underline position | Auto |
| Direction | Auto |
| Fallback | `Noto Color Emoji` (embedded), black |

Expand Down
119 changes: 119 additions & 0 deletions src/ShinyPDF.Examples/TextExamples.cs
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,125 @@ public void SuperscriptSubscript_Effects()
});
}

[Test]
public void SuperscriptSubscript_UnderlinePosition()
{
RenderingTest
.Create()
.PageSize(800, 300)
.ProduceImages()
.ShowResults()
.Render(container =>
{
container
.Padding(25)
.DefaultTextStyle(x => x.FontSize(30).Underline())
.Column(column =>
{
column.Spacing(25);

column.Item().Text(text =>
{
text.Span("Auto: E = mc");
text.Span("2").Superscript();
text.Span(", H");
text.Span("2").Subscript();
text.Span("O");
});

column.Item().Text(text =>
{
text.DefaultTextStyle(x => x.UnderlineAtBaseline());

text.Span("Baseline: E = mc");
text.Span("2").Superscript();
text.Span(", H");
text.Span("2").Subscript();
text.Span("O");
});

column.Item().Text(text =>
{
text.DefaultTextStyle(x => x.UnderlineBelowGlyphs());

text.Span("Below glyphs: E = mc");
text.Span("2").Superscript();
text.Span(", H");
text.Span("2").Subscript();
text.Span("O");
});
});
});
}

[Test]
public void SuperscriptSubscript_StrikethroughPosition()
{
RenderingTest
.Create()
.PageSize(800, 200)
.ProduceImages()
.ShowResults()
.Render(container =>
{
container
.Padding(25)
.DefaultTextStyle(x => x.FontSize(30).Strikethrough())
.Column(column =>
{
column.Spacing(25);

column.Item().Text(text =>
{
text.Span("Through glyphs: (E = mc");
text.Span("2").Superscript();
text.Span("), H");
text.Span("2").Subscript();
text.Span("O");
});

column.Item().Text(text =>
{
text.DefaultTextStyle(x => x.StrikethroughAtBaseline());

text.Span("Baseline: (E = mc");
text.Span("2").Superscript();
text.Span("), H");
text.Span("2").Subscript();
text.Span("O");
});
});
});
}

[Test]
public void DecorationStyles()
{
RenderingTest
.Create()
.PageSize(600, 420)
.ProduceImages()
.ShowResults()
.Render(container =>
{
container
.Padding(25)
.DefaultTextStyle(x => x.FontSize(24))
.Column(column =>
{
column.Spacing(15);

column.Item().Text("Solid underline").Underline();
column.Item().Text("Double underline").Underline().DecorationDouble();
column.Item().Text("Dotted underline").Underline().DecorationDotted();
column.Item().Text("Dashed underline").Underline().DecorationDashed();
column.Item().Text("Wavy red underline").Underline().DecorationWavy().DecorationColor(Colors.Red.Medium);
column.Item().Text("Thick blue underline").Underline().DecorationThickness(3).DecorationColor(Colors.Blue.Medium);
column.Item().Text("Dashed strikethrough").Strikethrough().DecorationDashed();
});
});
}

[Test]
public void ParagraphSpacing()
{
Expand Down
1 change: 1 addition & 0 deletions src/ShinyPDF.UnitTests/TestEngine/MockCanvas.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ internal class MockCanvas : ICanvas
public void Scale(float scaleX, float scaleY) => ScaleFunc(scaleX, scaleY);

public void DrawRectangle(Position vector, Size size, string color) => DrawRectFunc(vector, size, color);
public void DrawTextDecoration(Position vector, float width, float thickness, string color, TextDecorationStyle style) => throw new NotImplementedException();
public void DrawText(SKTextBlob skTextBlob, Position position, TextStyle style) => throw new NotImplementedException();
public void DrawImage(SKImage image, Position position, Size size) => DrawImageFunc(image, position, size);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ internal class OperationRecordingCanvas : ICanvas
public void Scale(float scaleX, float scaleY) => Operations.Add(new CanvasScaleOperation(scaleX, scaleY));

public void DrawRectangle(Position vector, Size size, string color) => Operations.Add(new CanvasDrawRectangleOperation(vector, size, color));
public void DrawTextDecoration(Position vector, float width, float thickness, string color, TextDecorationStyle style) => throw new NotImplementedException();
public void DrawText(SKTextBlob skTextBlob, Position position, TextStyle style) => throw new NotImplementedException();
public void DrawImage(SKImage image, Position position, Size size) => Operations.Add(new CanvasDrawImageOperation(position, size));

Expand Down
Loading
Loading