Skip to content

Commit c00ecc8

Browse files
authored
refactor(templates): migrate CV/cover-letter icons from PNG to recolorable SVG (#212)
Replace the bundled CV / cover-letter contact-icon PNGs with recolorable SVG via a new SvgGlyph helper (flatten filled layers into one ShapeOutline, recoloured per template in code via rich.shape). The sidebar-portrait avatar becomes a swappable SVG. templates/cv assets drop ~717 KB to ~133 KB. No public API change (the IconTextRow raster overload is retained for binary compatibility). Visual baselines refreshed for the new glyphs; the sidebar-portrait layout snapshot updated for the vector avatar. See CHANGELOG v1.8.0.
1 parent 803a710 commit c00ecc8

79 files changed

Lines changed: 1965 additions & 348 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CHANGELOG.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -407,6 +407,19 @@ Entries land here as they merge.
407407

408408
### Internal
409409

410+
- **CV / cover-letter template icons moved from PNG to recolorable SVG.**
411+
The bundled contact / social glyphs (phone, email, location, website,
412+
LinkedIn, GitHub, …) and the sidebar-portrait avatar now ship as SVG
413+
instead of raster PNG. A new internal `SvgGlyph` helper flattens an icon's
414+
filled layers into one outline that the presets fill with each template's
415+
own accent colour via `rich.shape(...)` — so one bundled glyph recolours
416+
per template with no per-template copies, and the icons stay crisp at any
417+
zoom. The sidebar-portrait avatar is a swappable SVG placeholder. This
418+
shrinks the bundled `templates/cv` assets from ~717 KB to ~133 KB (the
419+
431 KB `portrait.png` alone becomes a ~4 KB SVG), trimming the published
420+
jar. No public API change; the CV / cover-letter presets render the same
421+
layout (visual baselines refreshed for the new glyphs; the sidebar-portrait
422+
layout snapshot updated for the vector avatar).
410423
- **Benchmark suite cleanup (not shipped).** Removed three redundant
411424
benchmark mains: `FullCvBenchmark` (superseded by the JMH
412425
`TemplateCvJmhBenchmark`), `GraphComposeBenchmark` (early-engine relic

src/main/java/com/demcha/compose/document/templates/coverletter/v2/presets/TimelineMinimalLetter.java

Lines changed: 15 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@
33
import com.demcha.compose.document.api.DocumentSession;
44
import com.demcha.compose.document.dsl.PageFlowBuilder;
55
import com.demcha.compose.document.dsl.SectionBuilder;
6-
import com.demcha.compose.document.image.DocumentImageData;
76
import com.demcha.compose.document.node.DocumentLinkOptions;
87
import com.demcha.compose.document.node.InlineImageAlignment;
98
import com.demcha.compose.document.node.TextAlign;
9+
import com.demcha.compose.document.style.DocumentColor;
1010
import com.demcha.compose.document.style.DocumentInsets;
1111
import com.demcha.compose.document.style.DocumentTextDecoration;
1212
import com.demcha.compose.document.style.DocumentTextStyle;
@@ -19,19 +19,16 @@
1919
import com.demcha.compose.document.templates.cv.v2.data.CvIdentity;
2020
import com.demcha.compose.document.templates.cv.v2.data.CvLink;
2121
import com.demcha.compose.document.templates.cv.v2.theme.CvTheme;
22+
import com.demcha.compose.document.templates.cv.v2.widgets.SvgGlyph;
2223

23-
import java.io.IOException;
24-
import java.io.InputStream;
25-
import java.io.UncheckedIOException;
2624
import java.util.*;
27-
import java.util.concurrent.ConcurrentHashMap;
2825

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

7874
private TimelineMinimalLetter() {
7975
}
@@ -170,9 +166,8 @@ private void addContact(SectionBuilder section, CvIdentity identity) {
170166
rich.style(item.text(), textStyle);
171167
rich.plain(" ");
172168
if (item.iconFile() != null) {
173-
rich.image(contactIcon(item.iconFile()),
174-
CONTACT_ICON_SIZE,
175-
CONTACT_ICON_SIZE,
169+
rich.shape(glyph(item.iconFile()).outline(CONTACT_ICON_SIZE),
170+
ICON_COLOR, null,
176171
InlineImageAlignment.CENTER,
177172
CONTACT_ICON_BASELINE_OFFSET,
178173
item.linkOptions());
@@ -188,13 +183,13 @@ private List<ContactItem> contactItems(CvIdentity identity) {
188183
return List.of();
189184
}
190185
List<ContactItem> items = new ArrayList<>();
191-
addContactItem(items, "LOC", "location.png",
186+
addContactItem(items, "LOC", "location.svg",
192187
identity.contact().address(), null);
193-
addContactItem(items, "TEL", "phone.png",
188+
addContactItem(items, "TEL", "phone.svg",
194189
identity.contact().phone(), null);
195190
String email = identity.contact().email();
196191
if (!email.isBlank()) {
197-
addContactItem(items, "@", "email.png", email,
192+
addContactItem(items, "@", "email.svg", email,
198193
new DocumentLinkOptions("mailto:" + email));
199194
}
200195
for (CvLink link : identity.links()) {
@@ -210,10 +205,8 @@ private List<ContactItem> contactItems(CvIdentity identity) {
210205
return List.copyOf(items);
211206
}
212207

213-
private DocumentImageData contactIcon(String iconFile) {
214-
return DocumentImageData.fromBytes(
215-
CONTACT_ICON_CACHE.computeIfAbsent(iconFile,
216-
TimelineMinimalLetter::readIconBytes));
208+
private SvgGlyph glyph(String iconFile) {
209+
return SvgGlyph.fromResource(CONTACT_ICON_ROOT + iconFile);
217210
}
218211

219212
private DocumentTextStyle nameStyle() {
@@ -250,16 +243,16 @@ private static void addContactItem(List<ContactItem> items,
250243
private static String pickIconFile(String label) {
251244
String normalized = SectionLookup.normalize(label);
252245
if (normalized.contains("linkedin")) {
253-
return "linkedin.png";
246+
return "linkedin.svg";
254247
}
255248
if (normalized.contains("github")) {
256-
return "github.png";
249+
return "github.svg";
257250
}
258251
if (normalized.contains("dribbble")) {
259-
return "dribbble.png";
252+
return "dribbble.svg";
260253
}
261254
if (normalized.contains("google")) {
262-
return "google.png";
255+
return "google.svg";
263256
}
264257
return null;
265258
}
@@ -275,20 +268,6 @@ private static String pickFallbackIcon(String label) {
275268
return "@";
276269
}
277270

278-
private static byte[] readIconBytes(String iconFile) {
279-
try (InputStream input = TimelineMinimalLetter.class.getResourceAsStream(
280-
CONTACT_ICON_ROOT + iconFile)) {
281-
if (input == null) {
282-
throw new IllegalStateException(
283-
"Missing timeline minimal contact icon: " + iconFile);
284-
}
285-
return input.readAllBytes();
286-
} catch (IOException e) {
287-
throw new UncheckedIOException(
288-
"Failed to read timeline minimal contact icon: " + iconFile, e);
289-
}
290-
}
291-
292271
private record ContactItem(String fallbackIcon, String iconFile,
293272
String text, DocumentLinkOptions linkOptions) {
294273
}

src/main/java/com/demcha/compose/document/templates/cv/presets/MonogramSidebar.java

Lines changed: 15 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
import com.demcha.compose.document.dsl.ParagraphBuilder;
77
import com.demcha.compose.document.dsl.RichText;
88
import com.demcha.compose.document.dsl.SectionBuilder;
9-
import com.demcha.compose.document.image.DocumentImageData;
109
import com.demcha.compose.document.node.DocumentLinkOptions;
1110
import com.demcha.compose.document.node.InlineImageAlignment;
1211
import com.demcha.compose.document.node.InlineRun;
@@ -21,6 +20,7 @@
2120
import com.demcha.compose.document.style.DocumentTextDecoration;
2221
import com.demcha.compose.document.style.DocumentTextStyle;
2322
import com.demcha.compose.document.templates.api.DocumentTemplate;
23+
import com.demcha.compose.document.templates.cv.v2.widgets.SvgGlyph;
2424
import com.demcha.compose.document.templates.blocks.Block;
2525
import com.demcha.compose.document.templates.blocks.BulletListBlock;
2626
import com.demcha.compose.document.templates.blocks.IndentedBlock;
@@ -35,15 +35,10 @@
3535
import com.demcha.compose.document.theme.BusinessTheme;
3636
import com.demcha.compose.font.FontName;
3737

38-
import java.io.IOException;
39-
import java.io.InputStream;
40-
import java.io.UncheckedIOException;
4138
import java.util.ArrayList;
4239
import java.util.List;
4340
import java.util.Locale;
44-
import java.util.Map;
4541
import java.util.Objects;
46-
import java.util.concurrent.ConcurrentHashMap;
4742

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

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

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

9692
private static final List<String> EDUCATION_KEYS = List.of("education", "certifications");
9793
private static final List<String> SKILL_KEYS = List.of("skills", "technical skills", "expertise");
@@ -258,10 +254,10 @@ private void addContactBlock(SectionBuilder section, CvHeader header) {
258254
.textStyle(textStyle)
259255
.align(TextAlign.CENTER)
260256
.margin(DocumentInsets.top(4))
261-
.rich(rich -> rich.image(
262-
contactIcon(contact.iconFile()),
263-
CONTACT_ICON_SIZE,
264-
CONTACT_ICON_SIZE,
257+
.rich(rich -> rich.shape(
258+
glyph(contact.iconFile()).outline(CONTACT_ICON_SIZE),
259+
ICON_COLOR,
260+
null,
265261
InlineImageAlignment.CENTER,
266262
0.0,
267263
contact.linkOptions())));
@@ -660,13 +656,13 @@ private static List<ContactLine> contactLines(CvHeader header) {
660656
return List.of();
661657
}
662658
List<ContactLine> lines = new ArrayList<>();
663-
addContactLine(lines, "phone.png", safe(header.phone()), null);
659+
addContactLine(lines, "phone.svg", safe(header.phone()), null);
664660
String email = safe(header.email());
665661
if (!email.isBlank()) {
666-
addContactLine(lines, "email.png", email,
662+
addContactLine(lines, "email.svg", email,
667663
new DocumentLinkOptions("mailto:" + email));
668664
}
669-
addContactLine(lines, "location.png", safe(header.address()), null);
665+
addContactLine(lines, "location.svg", safe(header.address()), null);
670666
for (CvHeader.Link link : header.links()) {
671667
String label = safe(link.label());
672668
if (label.isBlank()) {
@@ -691,29 +687,14 @@ private static void addContactLine(List<ContactLine> lines, String iconFile,
691687
private static String pickIconFile(String label) {
692688
String n = normalize(label);
693689
if (n.contains("github")) {
694-
return "github.png";
690+
return "github.svg";
695691
}
696-
if (n.contains("linkedin")) {
697-
return "linkedin.png";
698-
}
699-
return "linkedin.png";
700-
}
701-
702-
private static DocumentImageData contactIcon(String iconFile) {
703-
return DocumentImageData.fromBytes(
704-
CONTACT_ICON_CACHE.computeIfAbsent(CONTACT_ICON_ROOT + iconFile,
705-
MonogramSidebar::readIconBytes));
692+
// LinkedIn and any other link → the LinkedIn glyph (V1 fallback).
693+
return "linkedin.svg";
706694
}
707695

708-
private static byte[] readIconBytes(String resourcePath) {
709-
try (InputStream input = MonogramSidebar.class.getResourceAsStream(resourcePath)) {
710-
if (input == null) {
711-
throw new IllegalStateException("Missing monogram sidebar icon: " + resourcePath);
712-
}
713-
return input.readAllBytes();
714-
} catch (IOException e) {
715-
throw new UncheckedIOException("Failed to read monogram sidebar icon: " + resourcePath, e);
716-
}
696+
private static SvgGlyph glyph(String iconFile) {
697+
return SvgGlyph.fromResource(CONTACT_ICON_ROOT + iconFile);
717698
}
718699

719700
private static String stripBasicMarkdown(String value) {

0 commit comments

Comments
 (0)