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
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,18 @@ PDF `GoTo` actions. External links are unchanged.
A new sealed `InlineRun` variant (`InlineSvgRun`) joins text / image / shape;
the inline render reuses the existing SVG paint pipeline (shared with the block
path fragment), so flat-colour output stays byte-identical.
- **Inline highlight chips** (`@since 1.9.0`). An inline run can now sit on a
rounded, padded background fill — the GitHub inline `code` look and inline
status badges. `RichText.highlight(text, style, bg, radius, padding)` is the
primitive; `code(text)` ships engine defaults (a monospace font, a muted ink
and a light chip) and `chip(text, fg, bg)` colours a badge — with the matching
`ParagraphBuilder.inlineHighlight` / `inlineCode` / `inlineChip`. A `highlight`
overload takes `DocumentLinkOptions`, so a chip can also be a link. The fill is
a new `InlineBackground(fill, cornerRadius, padding)` carried by a new sealed
`InlineRun` variant, `InlineHighlightRun`; horizontal padding widens the run's
advance, vertical padding overflows the line box without changing line metrics.
Text-only backends (DOCX) keep the text and drop the fill. (A follow-up adds
multi-word coalescing and wrapping the chip across line breaks.)
- **Colour emoji by shortcode** (`@since 1.9.0`). `RichText.emoji(":star:", size)`
and `ParagraphBuilder.inlineEmoji(...)` resolve a GitHub-style shortcode to an inline
vector colour glyph. Resolution is lenient — an unknown shortcode (or no emoji
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import com.demcha.examples.features.text.EmojiClipPathReportExample;
import com.demcha.examples.features.text.InlineShapesExample;
import com.demcha.examples.features.text.InlineSvgIconExample;
import com.demcha.examples.features.text.InlineHighlightExample;
import com.demcha.examples.features.navigation.InPdfNavigationExample;
import com.demcha.examples.features.text.RichTextShowcaseExample;
import com.demcha.examples.features.text.SectionPresetsExample;
Expand Down Expand Up @@ -150,6 +151,7 @@ public static void main(String[] args) throws Exception {
// Text + sections
System.out.println("Generated: " + InlineShapesExample.generate());
System.out.println("Generated: " + InlineSvgIconExample.generate());
System.out.println("Generated: " + InlineHighlightExample.generate());
System.out.println("Generated: " + EmojiShortcodeExample.generate());
System.out.println("Generated: " + EmojiSvgVsPngExample.generate());
System.out.println("Generated: " + EmojiGalleryExample.generate());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
package com.demcha.examples.features.text;

import com.demcha.compose.GraphCompose;
import com.demcha.compose.document.api.DocumentPageSize;
import com.demcha.compose.document.api.DocumentSession;
import com.demcha.compose.document.dsl.RichText;
import com.demcha.compose.document.dsl.SectionBuilder;
import com.demcha.compose.document.style.DocumentColor;
import com.demcha.compose.document.style.DocumentInsets;
import com.demcha.compose.document.style.DocumentTextStyle;
import com.demcha.compose.document.theme.BusinessTheme;
import com.demcha.compose.font.FontName;
import com.demcha.examples.support.ExampleOutputPaths;

import java.nio.file.Path;
import java.util.function.Consumer;

/**
* Runnable showcase for inline highlight "chips" ({@code @since 1.9.0}).
*
* <p>Styled text drawn on a rounded, padded background fill, on the text
* baseline, flowing inside a paragraph — the GitHub inline {@code code} look and
* inline status badges. {@code RichText.code(text)} ships engine defaults
* (monospace + a light chip); {@code chip(text, fg, bg)} colours a badge; and
* {@code highlight(text, style, bg, radius, padding)} is the full primitive.
* On {@code ParagraphBuilder} the calls are {@code inlineCode} / {@code inlineChip}
* / {@code inlineHighlight}.</p>
*/
public final class InlineHighlightExample {
private static final BusinessTheme THEME = BusinessTheme.modern();
private static final DocumentColor MUTED = DocumentColor.rgb(112, 116, 128);
private static final DocumentColor BRAND = DocumentColor.rgb(20, 80, 95);
private static final DocumentColor PANEL = DocumentColor.rgb(248, 244, 234);

// Status-badge palette: ink + a soft tinted fill.
private static final DocumentColor PAID_FG = DocumentColor.rgb(22, 101, 52);
private static final DocumentColor PAID_BG = DocumentColor.rgb(220, 252, 231);
private static final DocumentColor DUE_FG = DocumentColor.rgb(153, 27, 27);
private static final DocumentColor DUE_BG = DocumentColor.rgb(254, 226, 226);
private static final DocumentColor HOLD_FG = DocumentColor.rgb(146, 64, 14);
private static final DocumentColor HOLD_BG = DocumentColor.rgb(254, 243, 199);

private InlineHighlightExample() {
}

public static Path generate() throws Exception {
Path outputFile = ExampleOutputPaths.prepare("features/text", "inline-highlight-chips.pdf");

try (DocumentSession document = GraphCompose.document(outputFile)
.pageSize(DocumentPageSize.A4)
.pageBackground(THEME.pageBackground())
.margin(34, 34, 34, 34)
.create()) {

document.pageFlow()
.name("InlineHighlightShowcase")
.spacing(14)
.addSection("Hero", section -> section
.softPanel(THEME.palette().surfaceMuted(), 10, 16)
.accentLeft(DocumentColor.rgb(97, 40, 217), 4)
.spacing(6)
.addParagraph(p -> p
.text("Inline highlight chips")
.textStyle(THEME.text().h1())
.margin(DocumentInsets.zero()))
.addRich(rich -> rich
.plain("Text on a rounded background, on the baseline ")
.accent("— inline code and status badges", BRAND)
.plain(". Add the ")
.code("graph-compose")
.plain(" dependency from ")
.code("io.github.demchaav")
.plain(".")))
.addSection("Code", section -> labelledRow(section,
"code(text) — monospace on a light chip, engine defaults",
rich -> rich
.plain("Run ").code("./mvnw verify").plain(" then tag ")
.code("v1.9.0").plain(" to publish ").code("graph-compose-emoji")))
.addSection("Badges", section -> labelledRow(section,
"chip(text, fg, bg) — a coloured status badge between words",
rich -> rich
.chip(" Paid ", PAID_FG, PAID_BG).plain(" ")
.chip(" Overdue ", DUE_FG, DUE_BG).plain(" ")
.chip(" On hold ", HOLD_FG, HOLD_BG)))
.addSection("Custom", section -> labelledRow(section,
"highlight(text, style, bg, radius, padding) — the full primitive",
rich -> rich
.plain("Pill ")
.highlight("rounded", chipText(), DocumentColor.rgb(224, 231, 255),
8.0, DocumentInsets.symmetric(2, 8))
.plain(" square ")
.highlight("sharp", chipText(), DocumentColor.rgb(255, 228, 230),
0.0, DocumentInsets.symmetric(2, 6))))
.addSection("Footer", section -> section
.accentTop(THEME.palette().rule(), 0.6)
.padding(new DocumentInsets(8, 0, 0, 0))
.addRich(rich -> rich
.plain("Source: ")
.style("examples/.../InlineHighlightExample.java",
DocumentTextStyle.builder()
.fontName(FontName.COURIER)
.size(8)
.color(MUTED)
.build())))
.build();

document.buildPdf();
}

return outputFile;
}

public static void main(String[] args) throws Exception {
System.out.println("Generated: " + generate());
}

private static void labelledRow(SectionBuilder section, String label, Consumer<RichText> body) {
section
.softPanel(PANEL, 6, 12)
.spacing(4)
.addParagraph(p -> p
.text(label)
.textStyle(caption())
.margin(DocumentInsets.zero()))
.addRich(body::accept);
}

private static DocumentTextStyle chipText() {
return DocumentTextStyle.builder()
.fontName(FontName.HELVETICA_BOLD)
.size(9)
.color(DocumentColor.rgb(55, 48, 163))
.build();
}

private static DocumentTextStyle caption() {
return DocumentTextStyle.builder()
.fontName(FontName.HELVETICA_BOLD)
.size(8.5)
.color(MUTED)
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
import com.demcha.compose.document.node.InlineImageAlignment;
import com.demcha.compose.document.node.TextVerticalAlign;
import com.demcha.compose.document.style.DocumentCornerRadius;
import com.demcha.compose.document.style.DocumentInsets;
import com.demcha.compose.document.style.InlineBackground;
import com.demcha.compose.document.style.ShapeOutline;
import com.demcha.compose.engine.render.pdf.PdfFont;
import com.demcha.compose.font.FontLibrary;
Expand Down Expand Up @@ -117,6 +119,45 @@ private static double resolveInlineGraphicBottom(double graphicHeight,
return base + baselineOffset;
}

/**
* Draws a highlight "chip": a rounded fill behind the span's glyphs, then the
* glyphs in their own text block offset right by the chip's left padding. The
* fill band is the line's text box expanded by vertical padding, so the chip
* overflows the line box like a browser highlight without enlarging it.
*/
private static void renderChip(PDPageContentStream stream,
FontLibrary fonts,
ParagraphTextSpan span,
double cursorX,
double baselineY,
ParagraphLine line,
TextRenderState textState) throws IOException {
PdfFont font = fonts.getFont(span.textStyle().fontName(), PdfFont.class).orElseThrow();
String text = font.sanitizeForRender(span.textStyle(), span.text());
if (text.isEmpty()) {
return; // nothing to paint — no glyph-less fill
}
InlineBackground background = span.background();
DocumentInsets pad = background.padding();
float chipWidth = (float) span.width(); // glyphs + left + right padding
float chipBottom = (float) (baselineY - line.baselineOffsetFromBottom() - pad.bottom());
float chipHeight = (float) (line.textLineHeight() + pad.vertical());
Color fill = background.fill() == null ? null : background.fill().color();
if (fill != null && chipWidth > 0 && chipHeight > 0) {
float radius = (float) Math.min(background.cornerRadius(), Math.min(chipWidth, chipHeight) / 2.0f);
PdfShapeGeometry.fillAndStrokePath(stream, fill, null, s ->
PdfShapeFragmentRenderHandler.drawRoundedRectangle(
s, (float) cursorX, chipBottom, chipWidth, chipHeight, radius, radius, radius, radius));
}
stream.beginText();
stream.newLineAtOffset((float) (cursorX + pad.left()), (float) baselineY);
textState.invalidate();
textState.applyFont(stream, font.fontType(span.textStyle().decoration()), (float) span.textStyle().size());
textState.applyColor(stream, span.textStyle().color());
stream.showText(text);
stream.endText();
}

private static void renderShape(PDPageContentStream stream,
ParagraphShapeSpan span,
double cursorX,
Expand Down Expand Up @@ -284,6 +325,19 @@ private void renderLine(PDPageContentStream stream,
try {
for (ParagraphSpan span : spans) {
if (span instanceof ParagraphTextSpan textSpan) {
if (textSpan.background() != null) {
// A chip span paints a rounded fill plus its glyphs in an
// isolated text block (offset by the left padding), so it
// closes any open run first and is a fragment boundary.
if (inTextBlock) {
stream.endText();
inTextBlock = false;
}
renderChip(stream, fonts, textSpan, cursorX, baselineY, line, textState);
textState.invalidate();
cursorX += textSpan.width();
continue;
}
PdfFont font = fonts.getFont(textSpan.textStyle().fontName(), PdfFont.class).orElseThrow();
// Font-aware sanitization keeps width measurement
// (PdfFont.getTextWidth) and the bytes emitted here
Expand Down
31 changes: 31 additions & 0 deletions src/main/java/com/demcha/compose/document/dsl/CodeChip.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.demcha.compose.document.dsl;

import com.demcha.compose.document.style.DocumentColor;
import com.demcha.compose.document.style.DocumentInsets;
import com.demcha.compose.document.style.DocumentTextStyle;
import com.demcha.compose.document.style.InlineBackground;
import com.demcha.compose.font.FontName;

/**
* Default styling for the {@code code(...)} inline-chip sugar, single-sourced so
* {@link RichText} and {@link ParagraphBuilder} stay in lockstep. GitHub-ish:
* a monospace glyph in a muted ink on a light, translucent rounded fill.
*/
final class CodeChip {

/** Muted code ink (GitHub light {@code #24292f}). */
static final DocumentColor TEXT_COLOR = DocumentColor.rgb(36, 41, 47);

/** Light translucent chip fill (GitHub {@code rgba(175,184,193,.2)}). */
static final InlineBackground BACKGROUND = new InlineBackground(
DocumentColor.rgb(175, 184, 193).withOpacity(0.20), 3.0, DocumentInsets.symmetric(1.0, 4.0));

/** Monospace code glyph style. */
static final DocumentTextStyle STYLE = DocumentTextStyle.builder()
.fontName(FontName.COURIER)
.color(TEXT_COLOR)
.build();

private CodeChip() {
}
}
Loading