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
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,19 @@ Entries land here as they merge.

### Internal

- **CV / cover-letter template icons moved from PNG to recolorable SVG.**
The bundled contact / social glyphs (phone, email, location, website,
LinkedIn, GitHub, …) and the sidebar-portrait avatar now ship as SVG
instead of raster PNG. A new internal `SvgGlyph` helper flattens an icon's
filled layers into one outline that the presets fill with each template's
own accent colour via `rich.shape(...)` — so one bundled glyph recolours
per template with no per-template copies, and the icons stay crisp at any
zoom. The sidebar-portrait avatar is a swappable SVG placeholder. This
shrinks the bundled `templates/cv` assets from ~717 KB to ~133 KB (the
431 KB `portrait.png` alone becomes a ~4 KB SVG), trimming the published
jar. No public API change; the CV / cover-letter presets render the same
layout (visual baselines refreshed for the new glyphs; the sidebar-portrait
layout snapshot updated for the vector avatar).
- **Benchmark suite cleanup (not shipped).** Removed three redundant
benchmark mains: `FullCvBenchmark` (superseded by the JMH
`TemplateCvJmhBenchmark`), `GraphComposeBenchmark` (early-engine relic
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@
import com.demcha.compose.document.api.DocumentSession;
import com.demcha.compose.document.dsl.PageFlowBuilder;
import com.demcha.compose.document.dsl.SectionBuilder;
import com.demcha.compose.document.image.DocumentImageData;
import com.demcha.compose.document.node.DocumentLinkOptions;
import com.demcha.compose.document.node.InlineImageAlignment;
import com.demcha.compose.document.node.TextAlign;
import com.demcha.compose.document.style.DocumentColor;
import com.demcha.compose.document.style.DocumentInsets;
import com.demcha.compose.document.style.DocumentTextDecoration;
import com.demcha.compose.document.style.DocumentTextStyle;
Expand All @@ -19,19 +19,16 @@
import com.demcha.compose.document.templates.cv.v2.data.CvIdentity;
import com.demcha.compose.document.templates.cv.v2.data.CvLink;
import com.demcha.compose.document.templates.cv.v2.theme.CvTheme;
import com.demcha.compose.document.templates.cv.v2.widgets.SvgGlyph;

import java.io.IOException;
import java.io.InputStream;
import java.io.UncheckedIOException;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

/**
* v2 cover-letter pair for the {@code TimelineMinimal} CV preset.
*
* <p>Reproduces the CV's masthead: a left spaced-caps Barlow-Condensed
* name + UPPERCASE role line, balanced by a right-aligned contact stack
* where each line ends with its PNG glyph icon (LinkedIn / GitHub /
* where each line ends with its recolorable SVG glyph icon (LinkedIn / GitHub /
* location / phone / email), all under a thin full-width rule — the same
* header as
* {@link com.demcha.compose.document.templates.cv.v2.presets.TimelineMinimal}.
Expand Down Expand Up @@ -72,8 +69,7 @@ public final class TimelineMinimalLetter {
private static final double CONTACT_ICON_BASELINE_OFFSET = -1.35;
private static final String CONTACT_ICON_ROOT =
"/templates/cv/timeline-minimal/icons/";
private static final Map<String, byte[]> CONTACT_ICON_CACHE =
new ConcurrentHashMap<>();
private static final DocumentColor ICON_COLOR = DocumentColor.rgb(58, 58, 58);

private TimelineMinimalLetter() {
}
Expand Down Expand Up @@ -170,9 +166,8 @@ private void addContact(SectionBuilder section, CvIdentity identity) {
rich.style(item.text(), textStyle);
rich.plain(" ");
if (item.iconFile() != null) {
rich.image(contactIcon(item.iconFile()),
CONTACT_ICON_SIZE,
CONTACT_ICON_SIZE,
rich.shape(glyph(item.iconFile()).outline(CONTACT_ICON_SIZE),
ICON_COLOR, null,
InlineImageAlignment.CENTER,
CONTACT_ICON_BASELINE_OFFSET,
item.linkOptions());
Expand All @@ -188,13 +183,13 @@ private List<ContactItem> contactItems(CvIdentity identity) {
return List.of();
}
List<ContactItem> items = new ArrayList<>();
addContactItem(items, "LOC", "location.png",
addContactItem(items, "LOC", "location.svg",
identity.contact().address(), null);
addContactItem(items, "TEL", "phone.png",
addContactItem(items, "TEL", "phone.svg",
identity.contact().phone(), null);
String email = identity.contact().email();
if (!email.isBlank()) {
addContactItem(items, "@", "email.png", email,
addContactItem(items, "@", "email.svg", email,
new DocumentLinkOptions("mailto:" + email));
}
for (CvLink link : identity.links()) {
Expand All @@ -210,10 +205,8 @@ private List<ContactItem> contactItems(CvIdentity identity) {
return List.copyOf(items);
}

private DocumentImageData contactIcon(String iconFile) {
return DocumentImageData.fromBytes(
CONTACT_ICON_CACHE.computeIfAbsent(iconFile,
TimelineMinimalLetter::readIconBytes));
private SvgGlyph glyph(String iconFile) {
return SvgGlyph.fromResource(CONTACT_ICON_ROOT + iconFile);
}

private DocumentTextStyle nameStyle() {
Expand Down Expand Up @@ -250,16 +243,16 @@ private static void addContactItem(List<ContactItem> items,
private static String pickIconFile(String label) {
String normalized = SectionLookup.normalize(label);
if (normalized.contains("linkedin")) {
return "linkedin.png";
return "linkedin.svg";
}
if (normalized.contains("github")) {
return "github.png";
return "github.svg";
}
if (normalized.contains("dribbble")) {
return "dribbble.png";
return "dribbble.svg";
}
if (normalized.contains("google")) {
return "google.png";
return "google.svg";
}
return null;
}
Expand All @@ -275,20 +268,6 @@ private static String pickFallbackIcon(String label) {
return "@";
}

private static byte[] readIconBytes(String iconFile) {
try (InputStream input = TimelineMinimalLetter.class.getResourceAsStream(
CONTACT_ICON_ROOT + iconFile)) {
if (input == null) {
throw new IllegalStateException(
"Missing timeline minimal contact icon: " + iconFile);
}
return input.readAllBytes();
} catch (IOException e) {
throw new UncheckedIOException(
"Failed to read timeline minimal contact icon: " + iconFile, e);
}
}

private record ContactItem(String fallbackIcon, String iconFile,
String text, DocumentLinkOptions linkOptions) {
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
import com.demcha.compose.document.dsl.ParagraphBuilder;
import com.demcha.compose.document.dsl.RichText;
import com.demcha.compose.document.dsl.SectionBuilder;
import com.demcha.compose.document.image.DocumentImageData;
import com.demcha.compose.document.node.DocumentLinkOptions;
import com.demcha.compose.document.node.InlineImageAlignment;
import com.demcha.compose.document.node.InlineRun;
Expand All @@ -21,6 +20,7 @@
import com.demcha.compose.document.style.DocumentTextDecoration;
import com.demcha.compose.document.style.DocumentTextStyle;
import com.demcha.compose.document.templates.api.DocumentTemplate;
import com.demcha.compose.document.templates.cv.v2.widgets.SvgGlyph;
import com.demcha.compose.document.templates.blocks.Block;
import com.demcha.compose.document.templates.blocks.BulletListBlock;
import com.demcha.compose.document.templates.blocks.IndentedBlock;
Expand All @@ -35,15 +35,10 @@
import com.demcha.compose.document.theme.BusinessTheme;
import com.demcha.compose.font.FontName;

import java.io.IOException;
import java.io.InputStream;
import java.io.UncheckedIOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;

/**
* Templates v2 "Monogram Sidebar" CV preset.
Expand Down Expand Up @@ -81,6 +76,8 @@ public final class MonogramSidebar {
private static final DocumentColor MAIN_RULE = DocumentColor.rgb(72, 79, 84);
private static final DocumentColor ACCENT = DocumentColor.rgb(158, 146, 104);
private static final DocumentColor MONOGRAM_RING = DocumentColor.rgb(54, 62, 74);
/** Contact glyph fill — the muted gold accent, readable on the pale sidebar. */
private static final DocumentColor ICON_COLOR = DocumentColor.rgb(158, 146, 104);

private static final FontName HEADLINE_FONT = FontName.CRIMSON_TEXT;
private static final FontName MONOGRAM_FONT = FontName.PT_SERIF;
Expand All @@ -91,7 +88,6 @@ public final class MonogramSidebar {
private static final double CONTACT_ICON_SIZE = 18;

private static final String CONTACT_ICON_ROOT = "/templates/cv/monogram-sidebar/icons/";
private static final Map<String, byte[]> CONTACT_ICON_CACHE = new ConcurrentHashMap<>();

private static final List<String> EDUCATION_KEYS = List.of("education", "certifications");
private static final List<String> SKILL_KEYS = List.of("skills", "technical skills", "expertise");
Expand Down Expand Up @@ -258,10 +254,10 @@ private void addContactBlock(SectionBuilder section, CvHeader header) {
.textStyle(textStyle)
.align(TextAlign.CENTER)
.margin(DocumentInsets.top(4))
.rich(rich -> rich.image(
contactIcon(contact.iconFile()),
CONTACT_ICON_SIZE,
CONTACT_ICON_SIZE,
.rich(rich -> rich.shape(
glyph(contact.iconFile()).outline(CONTACT_ICON_SIZE),
ICON_COLOR,
null,
InlineImageAlignment.CENTER,
0.0,
contact.linkOptions())));
Expand Down Expand Up @@ -660,13 +656,13 @@ private static List<ContactLine> contactLines(CvHeader header) {
return List.of();
}
List<ContactLine> lines = new ArrayList<>();
addContactLine(lines, "phone.png", safe(header.phone()), null);
addContactLine(lines, "phone.svg", safe(header.phone()), null);
String email = safe(header.email());
if (!email.isBlank()) {
addContactLine(lines, "email.png", email,
addContactLine(lines, "email.svg", email,
new DocumentLinkOptions("mailto:" + email));
}
addContactLine(lines, "location.png", safe(header.address()), null);
addContactLine(lines, "location.svg", safe(header.address()), null);
for (CvHeader.Link link : header.links()) {
String label = safe(link.label());
if (label.isBlank()) {
Expand All @@ -691,29 +687,14 @@ private static void addContactLine(List<ContactLine> lines, String iconFile,
private static String pickIconFile(String label) {
String n = normalize(label);
if (n.contains("github")) {
return "github.png";
return "github.svg";
}
if (n.contains("linkedin")) {
return "linkedin.png";
}
return "linkedin.png";
}

private static DocumentImageData contactIcon(String iconFile) {
return DocumentImageData.fromBytes(
CONTACT_ICON_CACHE.computeIfAbsent(CONTACT_ICON_ROOT + iconFile,
MonogramSidebar::readIconBytes));
// LinkedIn and any other link → the LinkedIn glyph (V1 fallback).
return "linkedin.svg";
}

private static byte[] readIconBytes(String resourcePath) {
try (InputStream input = MonogramSidebar.class.getResourceAsStream(resourcePath)) {
if (input == null) {
throw new IllegalStateException("Missing monogram sidebar icon: " + resourcePath);
}
return input.readAllBytes();
} catch (IOException e) {
throw new UncheckedIOException("Failed to read monogram sidebar icon: " + resourcePath, e);
}
private static SvgGlyph glyph(String iconFile) {
return SvgGlyph.fromResource(CONTACT_ICON_ROOT + iconFile);
}

private static String stripBasicMarkdown(String value) {
Expand Down
Loading