Skip to content

Commit a55c906

Browse files
committed
fix: fonts
1 parent 97caa7d commit a55c906

8 files changed

Lines changed: 101 additions & 21 deletions

File tree

bitext/src/lib/components/editor/GlossInputRow.svelte

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,14 @@
33
import type { Token } from '$lib/domain/tokens.js';
44
import { settingsStore } from '$lib/state/settings.svelte.js';
55
import { resolveVisualizationFontCss } from '$lib/fonts/visualization-font.js';
6-
import { defaultGlossFontSizePx } from '$lib/serialization/schema.js';
76
87
const inputClass =
98
'block w-full rounded-none border border-gray-300 bg-gray-50 p-2 text-gray-900 placeholder-gray-500 focus:border-primary-500 focus:ring-primary-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:focus:border-primary-500 dark:focus:ring-primary-500';
109
10+
/** Form UI: fixed; Appearance “Gloss size” is for preview + export only. */
11+
const GLOSS_EDITOR_FIELD_FONT_PX = 14;
12+
1113
const glossFont = $derived(resolveVisualizationFontCss(settingsStore.settings, 'gloss'));
12-
const glossSizePx = $derived(defaultGlossFontSizePx(settingsStore.settings));
1314
1415
let {
1516
tokens,
@@ -33,7 +34,7 @@
3334
placeholder=" "
3435
value={t.gloss ?? ''}
3536
style:font-family={glossFont}
36-
style:font-size="{glossSizePx}px"
37+
style:font-size="{GLOSS_EDITOR_FIELD_FONT_PX}px"
3738
oninput={(e) => onGloss(t.id, (e.currentTarget as HTMLInputElement).value)}
3839
/>
3940
</div>

bitext/src/lib/components/preview/AlignmentSvg.svelte

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@
101101
void settingsStore.settings.glossLineGapPx;
102102
void settingsStore.settings.sourceTextSizePx;
103103
void settingsStore.settings.targetTextSizePx;
104+
void settingsStore.settings.glossTextSizePx;
104105
void settingsStore.settings.glossFontFamily;
105106
void settingsStore.settings.glossFontSource;
106107
void projectStore.sourceTokens;

bitext/src/lib/components/preview/GlossRow.svelte

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,12 @@
22
import type { Token } from '$lib/domain/tokens.js';
33
import { primaryLinkForToken } from '$lib/domain/alignment.js';
44
import { settingsStore } from '$lib/state/settings.svelte.js';
5-
import { defaultGlossFontSizePx } from '$lib/serialization/schema.js';
65
import { projectStore } from '$lib/state/project.svelte.js';
76
87
let { tokens, side }: { tokens: Token[]; side: 'source' | 'target' } = $props();
98
109
const gap = $derived(settingsStore.settings.gapWordPx);
11-
const sz = $derived(defaultGlossFontSizePx(settingsStore.settings));
10+
const sz = $derived(settingsStore.settings.glossTextSizePx);
1211
const links = $derived(projectStore.links);
1312
const colorByLink = $derived(settingsStore.settings.colorTokensByLink);
1413

bitext/src/lib/components/settings/AppearanceTab.svelte

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@
7171
/>
7272
</div>
7373
{/if}
74-
<div class="col-span-12 md:col-span-6">
74+
<div class="col-span-12 md:col-span-4">
7575
<Label class="mb-2">Source size ({s.sourceTextSizePx}px)</Label>
7676
<Range
7777
appearance="auto"
@@ -87,7 +87,7 @@
8787
})}
8888
/>
8989
</div>
90-
<div class="col-span-12 md:col-span-6">
90+
<div class="col-span-12 md:col-span-4">
9191
<Label class="mb-2">Target size ({s.targetTextSizePx}px)</Label>
9292
<Range
9393
appearance="auto"
@@ -103,6 +103,22 @@
103103
})}
104104
/>
105105
</div>
106+
<div class="col-span-12 md:col-span-4">
107+
<Label class="mb-2">Gloss size ({s.glossTextSizePx}px)</Label>
108+
<Range
109+
appearance="auto"
110+
color="indigo"
111+
size="lg"
112+
min={MIN_TEXT_SIZE_PX}
113+
max={MAX_TEXT_SIZE_PX}
114+
step={1}
115+
value={s.glossTextSizePx}
116+
oninput={(e) =>
117+
settingsStore.patch({
118+
glossTextSizePx: Number((e.currentTarget as HTMLInputElement).value)
119+
})}
120+
/>
121+
</div>
106122
<div class="col-span-12 md:col-span-6">
107123
<Label class="mb-2">Word gap ({s.gapWordPx}px)</Label>
108124
<Range

bitext/src/lib/components/share/ExportMenu.svelte

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,9 @@
1414
visualizationGoogleFontUrls
1515
} from '$lib/fonts/visualization-font.js';
1616
import { buildInlinedFontCss } from '$lib/fonts/inline-fonts.js';
17+
import { ensureVisualizationCustomFontsInDocument } from '$lib/fonts/ensure-document-fonts.js';
1718
import { encodeState } from '$lib/serialization/encode.js';
18-
import {
19-
SCHEMA_VERSION,
20-
defaultGlossFontSizePx,
21-
type AppStateV1
22-
} from '$lib/serialization/schema.js';
19+
import { SCHEMA_VERSION, type AppStateV1 } from '$lib/serialization/schema.js';
2320
import { getShareUrl } from '$lib/share/url.js';
2421
import { shareUrlToQrDataUrl } from '$lib/share/qr.js';
2522
@@ -67,7 +64,7 @@
6764
fontFamilyGloss: svgFontFamilyStack(s, 'gloss'),
6865
fontSizeSource: s.sourceTextSizePx,
6966
fontSizeTarget: s.targetTextSizePx,
70-
glossFontSize: defaultGlossFontSizePx(s),
67+
glossFontSize: s.glossTextSizePx,
7168
defaultTextColor: exportTextColor(),
7269
colorTokensByLink: s.colorTokensByLink,
7370
lineStyle: s.lineStyle,
@@ -93,6 +90,7 @@
9390
9491
async function downloadPng() {
9592
await flushPreviewLayout();
93+
await ensureVisualizationCustomFontsInDocument(settingsStore.settings);
9694
const embedFontCss = await buildInlinedFontCss(settingsStore.settings);
9795
const svg = buildSvg({ includeAttributionFooter: true, embedFontCss, includeImports: false });
9896
const blob = await svgStringToPngBlob(svg, 2);
@@ -101,6 +99,7 @@
10199
102100
async function downloadPdf() {
103101
await flushPreviewLayout();
102+
await ensureVisualizationCustomFontsInDocument(settingsStore.settings);
104103
const embedFontCss = await buildInlinedFontCss(settingsStore.settings);
105104
const svg = buildSvg({ includeAttributionFooter: true, embedFontCss, includeImports: false });
106105
const blob = await svgStringToPdfBlob(svg);
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { loadCustomFontBlob } from './custom-fonts.js';
2+
import type { VisualSettingsV1 } from '$lib/serialization/schema.js';
3+
4+
const loadedKeys = new Set<string>();
5+
6+
/**
7+
* Custom fonts in Bitext are stored in IDB; SVG rasterization (PNG/PDF) often does not
8+
* apply @font-face from `<defs><style>` in an SVG blob, but the same family names can resolve
9+
* if the faces are registered on `document.fonts` (same as preview).
10+
*/
11+
export async function ensureVisualizationCustomFontsInDocument(
12+
settings: VisualSettingsV1
13+
): Promise<void> {
14+
if (typeof document === 'undefined' || !document.fonts) return;
15+
16+
const names: string[] = [];
17+
if (settings.sourceFontSource === 'custom' && settings.sourceCustomFontName) {
18+
names.push(settings.sourceCustomFontName);
19+
}
20+
if (settings.targetFontSource === 'custom' && settings.targetCustomFontName) {
21+
names.push(settings.targetCustomFontName);
22+
}
23+
if (settings.glossFontSource === 'custom' && settings.glossCustomFontName) {
24+
names.push(settings.glossCustomFontName);
25+
}
26+
27+
for (const name of names) {
28+
if (loadedKeys.has(name)) continue;
29+
let inDocument = false;
30+
for (const f of document.fonts) {
31+
if (f.family === name) {
32+
inDocument = true;
33+
break;
34+
}
35+
}
36+
if (inDocument) {
37+
loadedKeys.add(name);
38+
continue;
39+
}
40+
const blob = await loadCustomFontBlob(name);
41+
if (!blob) continue;
42+
const ff = new FontFace(name, await blob.arrayBuffer());
43+
try {
44+
await ff.load();
45+
document.fonts.add(ff);
46+
loadedKeys.add(name);
47+
} catch {
48+
/* invalid font or decode error */
49+
}
50+
}
51+
52+
await document.fonts.ready;
53+
}

bitext/src/lib/serialization/compact-v2.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,7 @@ function settingsToCompact(rounded: VisualSettingsV1): CompactSettings | undefin
201201
if (rounded.gapWordPx !== def.gapWordPx) o.gw = rounded.gapWordPx;
202202
if (rounded.gapLinePx !== def.gapLinePx) o.gl = rounded.gapLinePx;
203203
if (rounded.glossLineGapPx !== def.glossLineGapPx) o.gg = rounded.glossLineGapPx;
204+
if (rounded.glossTextSizePx !== def.glossTextSizePx) o.gtx = rounded.glossTextSizePx;
204205
if (rounded.lineThickness !== def.lineThickness) o.lt = rounded.lineThickness;
205206
if (rounded.lineOpacity !== def.lineOpacity) o.lo = rounded.lineOpacity;
206207
if (rounded.lineStyle !== def.lineStyle) o.ls = rounded.lineStyle === 'straight' ? 0 : 1;
@@ -249,6 +250,7 @@ function compactToVisualSettings(s: CompactSettings | undefined): VisualSettings
249250
if (s.gw !== undefined) raw.gapWordPx = Number(s.gw);
250251
if (s.gl !== undefined) raw.gapLinePx = Number(s.gl);
251252
if (s.gg !== undefined) raw.glossLineGapPx = Number(s.gg);
253+
if (s.gtx !== undefined) raw.glossTextSizePx = Number(s.gtx);
252254
if (s.lt !== undefined) raw.lineThickness = Number(s.lt);
253255
if (s.lo !== undefined) raw.lineOpacity = Number(s.lo);
254256
if (s.ls !== undefined) raw.lineStyle = Number(s.ls) === 0 ? 'straight' : 'curved';

bitext/src/lib/serialization/schema.ts

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,6 @@ export const MIN_GLOSS_LINE_GAP_PX = 0;
1717
export const MAX_GLOSS_LINE_GAP_PX = 80;
1818
export const DEFAULT_TOKEN_SPLIT_CHARS = '.-';
1919

20-
/** Scales with the smaller of the two line sizes (same rule as in-app gloss preview). */
21-
export function defaultGlossFontSizePx(s: {
22-
sourceTextSizePx: number;
23-
targetTextSizePx: number;
24-
}): number {
25-
return Math.max(12, Math.round(0.75 * Math.min(s.sourceTextSizePx, s.targetTextSizePx)));
26-
}
27-
2820
/** Normalize theme from shared `?data=` payloads to BeerCSS body class `light` | `dark`. */
2921
export function normalizeUiTheme(theme: string): UiTheme {
3022
const t = theme.toLowerCase();
@@ -37,6 +29,8 @@ export interface VisualSettingsV1 {
3729
theme: UiTheme;
3830
sourceTextSizePx: number;
3931
targetTextSizePx: number;
32+
/** Interlinear gloss (preview, editor, export) — independent of line sizes. */
33+
glossTextSizePx: number;
4034
gapWordPx: number;
4135
gapLinePx: number;
4236
/** Vertical gap between a gloss row and its sentence line (preview + export layout). */
@@ -89,6 +83,7 @@ export function defaultVisualSettings(): VisualSettingsV1 {
8983
theme: 'light',
9084
sourceTextSizePx: size,
9185
targetTextSizePx: size,
86+
glossTextSizePx: Math.max(12, Math.round(0.75 * size)),
9287
gapWordPx: 14,
9388
gapLinePx: 120,
9489
glossLineGapPx: MIN_GLOSS_LINE_GAP_PX,
@@ -164,11 +159,25 @@ export function normalizeVisualSettings(
164159
legacyLineSize !== undefined ? legacyLineSize : d.targetTextSizePx
165160
);
166161

162+
const pickGlossTextSize = (v: unknown, fallback: number) =>
163+
typeof v === 'number' && Number.isFinite(v)
164+
? Math.max(MIN_TEXT_SIZE_PX, Math.min(MAX_TEXT_SIZE_PX, v))
165+
: fallback;
166+
167+
const glossTextSizePx = pickGlossTextSize(
168+
rawRest.glossTextSizePx,
169+
Math.max(
170+
MIN_TEXT_SIZE_PX,
171+
Math.min(MAX_TEXT_SIZE_PX, Math.round(0.75 * Math.min(sourceTextSizePx, targetTextSizePx)))
172+
)
173+
);
174+
167175
return {
168176
...d,
169177
...rawRest,
170178
sourceTextSizePx,
171179
targetTextSizePx,
180+
glossTextSizePx,
172181
theme: normalizeUiTheme(String(rawRest.theme ?? d.theme)),
173182
sourceFontFamily:
174183
typeof rawRest.sourceFontFamily === 'string'

0 commit comments

Comments
 (0)