Fix icon resolution: imshow + interpolation="none" embeds icons unsampled

The OffsetImage path composites at figure dpi, so the icons were going into
the PDF as 39x39 bitmaps regardless of source resolution (pdfimages-verified;
this is why the SVG switch changed nothing visually). Placing them as
AxesImage with interpolation="none" embeds the full 1024 px rasterisation
with a scaling transform: 4719 ppi at the placed size, print-crisp.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BkRLcc18rwT2Lysu6PbG7v
This commit is contained in:
Giorgio Gilestro 2026-09-07 13:37:53 +01:00
parent c9c4d927ad
commit 438931d67b
3 changed files with 10 additions and 5 deletions

Binary file not shown.

Binary file not shown.

View file

@ -54,7 +54,7 @@ def _icon(svg_name: str):
svg = OUT / "icons" / svg_name
with tempfile.NamedTemporaryFile(suffix=".png") as f:
try:
subprocess.run(["rsvg-convert", "-w", "2048", "-h", "2048", "-o", f.name, str(svg)],
subprocess.run(["rsvg-convert", "-w", "1024", "-h", "1024", "-o", f.name, str(svg)],
check=True, capture_output=True)
except FileNotFoundError as e:
raise RuntimeError("rsvg-convert (librsvg) is required to rasterise the icon SVGs "
@ -122,8 +122,6 @@ def fig1a():
xs = [x0, x0 + cw + sep, x0 + 2 * cw + sep]
row_h, row_top = 0.157, 0.805
from matplotlib.offsetbox import AnnotationBbox, OffsetImage
for j2, (name, arch, guarantee, edge, face, headfill, textcol, icon) in enumerate(TIERS):
x = xs[j2]
xc = x + cw / 2 - 0.02 # text centred left of the icon slot
@ -135,9 +133,16 @@ def fig1a():
linespacing=1.3, color=textcol)
ax.text(xc, 0.833, guarantee, ha="center", va="bottom", fontsize=6.4,
style="italic", color=textcol, alpha=0.85)
# Reason: imshow + interpolation="none" embeds the icon unsampled in the PDF (an
# OffsetImage is always composited at figure dpi, i.e. ~39 px, whatever the source).
img = _icon(icon)
ax.add_artist(AnnotationBbox(OffsetImage(img, zoom=28.0 / img.shape[0]),
(x + cw - gap - 0.024, 0.902), frameon=False))
iw = 28.0 / 1140.0 # 28 display px on an 11.4in/100dpi fig
ih = iw * 11.4 / 5.3
icx, icy = x + cw - gap - 0.024, 0.902
ax.imshow(img, extent=(icx - iw / 2, icx + iw / 2, icy - ih / 2, icy + ih / 2),
interpolation="none", aspect="auto", zorder=5)
ax.set_xlim(0, 1)
ax.set_ylim(0, 1)
for i2, (label, definition, cells) in enumerate(ROWS):
tags = TAGS[i2]