Skip to content
Open
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
1 change: 1 addition & 0 deletions apps/desktop/electron/main/plugin-host-process.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,7 @@ function buildApi() {
ui: {
openPanel: (options) => call("ui.openPanel", [options]),
closePanel: () => call("ui.closePanel"),
openWorkPanelFile: (input) => call("ui.openWorkPanelFile", [input ?? {}]),
showToast: (message, level) => call("ui.showToast", [message, level]),
notify: (input) => call("ui.notify", [input]),
getNotificationPermission: () => call("ui.getNotificationPermission"),
Expand Down
26 changes: 26 additions & 0 deletions apps/desktop/electron/main/plugin-runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,8 @@ export type PluginHostServices = {
readClipboardHistory: () => Promise<ClipboardHistoryEntry[]>;
openPanel: (request: PluginPanelRequest) => Promise<void>;
closePanel: (pluginId: string) => Promise<void>;
/** Ask the renderer to open a file through the normal work-panel router. */
openWorkPanelFile?: (input: { path: string; mimeType?: string }) => void | Promise<void>;
fetch?: (input: {
url: string;
method?: string;
Expand Down Expand Up @@ -457,6 +459,7 @@ const HOST_API_ALLOWLIST = new Set([
"plugin.getDataPath",
"ui.openPanel",
"ui.closePanel",
"ui.openWorkPanelFile",
"ui.showToast",
"ui.notify",
"ui.getNotificationPermission",
Expand Down Expand Up @@ -4750,6 +4753,29 @@ export class PluginRuntime {
closePanel: async () => {
await this.services.closePanel(pluginId);
},
openWorkPanelFile: async (input: { path: string; mimeType?: string }) => {
this.assertPermission(loaded, "ui.view");
const path = typeof input?.path === "string" ? input.path.trim() : "";
if (!path || path.length > 4096) {
throw apiError("INVALID_ARGUMENT", "file path must be a non-empty string");
}
const service = this.services.openWorkPanelFile;
if (!service) {
throw apiError("UNSUPPORTED", "host api not available: ui.openWorkPanelFile");
}
await service({
path,
...(typeof input?.mimeType === "string" && input.mimeType
? { mimeType: input.mimeType }
: {}),
});
this.services.audit?.({
pluginId,
api: "ui.openWorkPanelFile",
ok: true,
ts: Date.now(),
});
},
showToast: async (message: string, level?: "info" | "warn" | "error") => {
this.services.showToast(message, level);
},
Expand Down
6 changes: 6 additions & 0 deletions apps/desktop/electron/main/services/plugin-services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,12 @@ export function createPluginServices({
readClipboardHistory: async () => clipboardHistory.getHistory(),
getLocale: () => getUpdaterLocale(),
getAppearance: () => getAppearance(),
openWorkPanelFile: ({ path, mimeType }) => {
sendToRenderer(IPC.event.pluginOpenWorkPanelFile, {
path,
...(mimeType ? { mimeType } : {}),
});
},
openPanel: async (request) => {
await pluginPanels.open({
...request,
Expand Down
7 changes: 5 additions & 2 deletions apps/desktop/resources/plugins/pi.file-manager/UPSTREAM.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ stores LF, and this repository's `.gitattributes` keeps it that way.

| File | Bytes | sha256 |
| --- | --- | --- |
| `main.js` | 63234 | `43cface10124728f16e72530e699678177f97353b57190532e89c03186e6960d` |
| `main.js` | 63507 | `195156e89d197a563e0a5fad1dbe0c8e4f866ae309ec05b049541d3e1a412d5f` |
| `README.md` | 20039 | `8c524f6d13eac557e286fa0ec9b9cf5138bed0bd66d4a7914f3484443e627a01` |
| `views/index.html` | 345 | `771fd3d8afdea7fca75ed1f1918c1ce93ad1c87babdb321cfb85e910465cd2c1` |
| `views/assets/index.js` | 1345417 | `d0a1dc369764bed2ab12ce0e65fe983fe0b4f9919f2f8ff4546b736208d66dac` |
Expand All @@ -45,7 +45,7 @@ directory carries the built view the plugin publishes, not its React source.

## Local changes

Two, so a re-sync stays a copy:
Three, so a re-sync stays a copy:

- `manifest.json` gains `"license": "MIT"` (after `author`), making the vendored
copy 14191 bytes
Expand All @@ -62,6 +62,9 @@ Two, so a re-sync stays a copy:
ancestor's `"module"`, which is why the marker cannot live one directory up.
In a packaged app the file is inert, and deleting it only costs the developer
experience, never a user.
- `main.js` routes `.docx` reads through the host's `ui.openWorkPanelFile`
bridge so the bundled Office view can own DOCX editing. Other extensions keep
the upstream file-manager behavior.

## Re-syncing a newer release

Expand Down
9 changes: 9 additions & 0 deletions apps/desktop/resources/plugins/pi.file-manager/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -775,6 +775,15 @@ async function handleRead(payload) {
const base = { path: rel, size: stat.size, mtimeMs: stat.mtimeMs };
const extension = path.extname(rel).toLowerCase();

if (extension === ".docx" && typeof pi.ui.openWorkPanelFile === "function") {
await pi.ui
.openWorkPanelFile({
path: abs,
mimeType: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
})
.catch(() => {});
}

// 数据库:只读 100 字节的头部就能认出它,所以这一支**不做体积限制**——
// 几百 MB 的 .db 也会在这里秒开(真正的取数走 fm.sqlite.*,一次只拿一页)。
// 扩展名像但魔数不对的(比如 Windows 的 thumbs.db 其实是 OLE 文件)继续按普通文件走。
Expand Down
201 changes: 201 additions & 0 deletions apps/desktop/resources/plugins/pi.office/FONTS-README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
# Metric-compatible fallback fonts

Source: fonts bundled with LibreOffice (`/Applications/LibreOffice.app/Contents/Resources/fonts/truetype`),
freely redistributable with the app. Licenses: Carlito and Liberation are
**SIL Open Font License 1.1** (see `LICENSE-OFL.txt`); Caladea is
**Apache License 2.0** (copyright Huerta Tipografica).

| Font | License | Metric-compatible Word counterpart |
| ---------------- | ---------- | ---------------------------------- |
| Carlito GO | OFL 1.1 | Calibri (Word's default body font) |
| Caladea | Apache-2.0 | Cambria |
| Liberation Serif | OFL 1.1 | Times New Roman |
| Liberation Sans | OFL 1.1 | Arial |
| Liberation Mono | OFL 1.1 | Courier New |
| Aptos GO | OFL 1.1 | Aptos (Carlito, size-adjusted) |
| Aptos Display GO | OFL 1.1 | Aptos Display (Carlito, adjusted) |

"Carlito GO" (`Carlito-*.ttf`) is a derivative of Carlito 1.103: a build-time patch
(`tools/patch-carlito-vi.py`) rebuilds Vietnamese precomposed glyphs whose above mark
(circumflex/breve) was dropped (Ậ/Ệ/Ộ in Regular/Bold); advance widths are unchanged.
Renamed per OFL 1.1 §2 — "Carlito" is a Reserved Font Name. The files live in
`packages/ui/src/fonts/` (shared with sheets and slides, which alias Calibri/Aptos
to the same faces) and are referenced here as `@genoffice/ui/fonts/Carlito-*.ttf`.

Purpose: when a Word font declared by the document is missing on this machine, the
browser's silent fallback (Helvetica etc.) changes glyph widths, so line-break points
and pagination diverge from Word. Falling back to a metric-compatible font keeps
canvas line breaking aligned with Word, and stays consistent with the offline
pagination model (`tests/helpers/lo-fonts.ts` measures the same set of files).

Registration lives in `fonts.css`; family-name mapping in `cssFontFamily()` of `line-metrics.ts`.

"Aptos GO" / "Aptos Display GO" are not separate files: they are `size-adjust`ed
views of the Carlito faces (Word probe 2026-09-03 against Word's bundled Aptos).
Aptos letters run ~6.8% wider than Carlito's, digits +5.4%, the space 10%
narrower, so each weight registers three faces (general, `U+0030-0039`,
`U+0020/00A0`) — later faces win inside their unicode-range. Aptos Display is
close to Carlito for letters but shares the narrow space. Pitch stays Calibri's
1.22 (`lineHeightFactor`). `tests/aptos-alias-metrics.test.ts` holds the probed
sentence widths.

## CJK fallback

| Font | Role |
| --------------------------------------- | ------------------------------------------ |
| Noto Sans CJK SC (GB2312-subset woff2) | fallback for heiti-style (sans) families |
| Noto Serif CJK SC (GB2312-subset woff2) | fallback for songti-style (serif) families |

Source: [notofonts/noto-cjk](https://github.com/notofonts/noto-cjk) (SIL OFL 1.1),
subset with fonttools to all 7,445 GB2312 Han characters + CJK punctuation/fullwidth
forms + basic Latin
(`pyftsubset --text-file=gb2312 --unicodes="U+0020-024F,U+2000-206F,U+3000-303F,U+FF00-FFEF" --flavor=woff2`).
Rare characters outside the subset still fall through to system fonts (shown as
missing glyphs in minimal environments); bold is synthesized by the browser.

The serif subset also backs the `GenOffice Fullwidth TC` face (`fonts.css`), a
unicode-range shim (U+FF0D/FF0F/FF3C/FF3F/FF5E) slotted before Songti TC in the
Traditional Chinese serif chain: Songti TC draws those fullwidth glyphs at
~0.2-0.5em of ink inside the 1em advance, so a PMingLiU document's U+FF0F
rendered as a spaced half-width slash. Real PMingLiU (Windows) still wins by
chain order; advances are 1.0em everywhere, so line breaking is unchanged.

## Korean fallback

| Font | Role |
| ------------------------------------ | ----------------------------------------------------------- |
| GenOffice Serif KR (subset woff2) | Batang-metric stand-in for Korean serif families |
| GenOffice Sans KR (subset woff2) | fallback for Korean sans families (Malgun etc.) |
| GenOffice Che Latin KR (ASCII woff2) | half-width Latin for BatangChe/GulimChe/DotumChe/GungsuhChe |

Source: Noto Serif/Sans CJK KR Regular from [notofonts/noto-cjk](https://github.com/notofonts/noto-cjk)
(SIL OFL 1.1), subset with fonttools to the 2,350 KS X 1001 syllables + jamo
(U+1100-11FF, U+3130-318F) + basic Latin/CJK punctuation/fullwidth forms
(`U+0020-024F,U+2000-206F,U+3000-303F,U+FF00-FFEF`), then hmtx-normalized to the
metrics of the Windows faces Word substitutes for missing Korean fonts: hangul
syllables/compatibility jamo → 1.0em (Noto CJK KR ships 0.92/0.966em, which
would shift line breaks ~8% vs Word), serif digits → 0.596em and space →
0.333em (measured Batang values), sans Basic Latin (U+0020-007E, U+00A0) →
measured Malgun Gothic advances (space 0.352em, digits 0.551em; Noto's 0.224em
space alone drifted Korean sans line breaks ~3%/line —
`tools/normalize-kr-sans-hmtx.py`, asserted by `tests/kr-font-metrics.test.ts`).
The printable Latin outlines are also horizontally transformed to the measured
ink widths and side bearings of Batang/Malgun
(`tools/normalize-kr-latin-metrics.py`,
`tools/scale-kr-sans-latin-ink.py`).

`GenOfficeCheLatinKR.woff2` is an ASCII-only derivative of GenOffice Sans KR.
`tools/build-kr-che-latin-font.py` gives its Noto-derived outlines fixed 0.5em
advances and transforms them to measured DotumChe ink boxes. Microsoft Office
fonts are build-time measurement references only; no Microsoft outlines are
included. All three derivatives are renamed because the upstream OFL notices
reserve the name "Source"; "Noto" is the distribution family name, not the
Reserved Font Name. Conjoining jamo keep native advances (shaping). Word
counterpart line factors live in `lineHeightFactor()` of `line-metrics.ts`.
The Sans/Che source copyright (Adobe 2014–2021 and Google LLC), Serif source
copyright (Adobe 2017–2024), and full OFL 1.1 text are in `LICENSE-OFL.txt`.

### GenOffice Gothic KR

| Font | Role |
| ---------------------------------- | ----------------------------------------------- |
| GenOffice Gothic KR (subset woff2) | real-metric face for NanumGothic-declaring docs |

Source: NanumGothic Regular from [google/fonts](https://github.com/google/fonts/tree/main/ofl/nanumgothic)
(SIL OFL 1.1). Word for Mac renders NanumGothic documents with the OS
_downloadable_ Nanum asset (FontServices subset Chromium cannot see): hangul
0.94em, space 0.28em, digits 0.606em (M3 probe 2026-08-14), while the
Batang-normalized subset above ships 1.0/0.333/0.596 — +6.4% per hangul line.
Subset to the same ranges as the KR fallbacks (KS X 1001 syllables + jamo +
Basic Latin/punctuation/fullwidth forms), advances **unmodified**
(`tools/build-gothic-kr-font.py`) and checked in as
`GenOfficeGothicKR-Regular-subset.woff2`. Renamed per OFL (the upstream
Reserved Font Names include "Nanum" and "NanumGothic"; subsetting is a
modification). The exact NHN copyright/Reserved Font Name notice and the full
OFL 1.1 text are in `LICENSE-OFL.txt`.

### GenOffice UI Kana JP

| Font | Role |
| ----------------------------------------- | --------------------------------------------------- |
| GenOffice UI Kana JP (Regular/Bold woff2) | Meiryo UI-advance kana/JP punctuation for the alias |

Source: Noto Sans JP variable font from [notofonts/noto-cjk](https://github.com/notofonts/noto-cjk)
(SIL OFL 1.1), instanced at wght 400/700. Word for Mac renders Meiryo UI with
its private copy whose kana are proportional (Word probe 2026-09-03: あ
0.816em, う 0.639em, ア 0.754em, ideographic space and 、。 0.664em, corner
brackets and ・ 0.5em) at full glyph height; the Hiragino fallback keeps them
at 1em, and a size-adjust alias shrinks height along with width. Subset to
U+3000-30FF code points whose Meiryo UI advance differs from 1em, each glyph
given that exact advance (`tools/meiryo-ui-kana-advances.json`) with the
outline condensed horizontally to fit; vertical metrics set to the Hiragino
class (0.88/-0.12) the glyphs sit next to (`tools/build-meiryo-ui-kana-font.py`).
Renamed per OFL ("Source" is a Reserved Font Name of the upstream and the
outlines are modified).

## Poppins (M365 cloud font)

| Font | Role |
| -------------------------------- | ------------------------------------ |
| GenOffice Poppins (subset woff2) | real face for Poppins-declaring docs |

Source: Poppins Regular/Bold from [google/fonts](https://github.com/google/fonts/tree/main/ofl/poppins)
(SIL OFL 1.1). Poppins is an M365 cloud font: Word downloads the real face and
lays out with its metrics (line box hhea = typo = 1.500em; Word probe
2026-09-01 measured factor exactly 1.500 at 10/12/16/28pt, regular and bold,
with the PDF embedding Poppins-Regular/Bold), while the Helvetica-class
fallback runs ~12.6% narrower per line and 1.172-spaced — a 13-page document
paginated as 11. Subset to Latin + Latin Extended + punctuation/currency,
advances and vertical metrics **unmodified** (`tools/build-poppins-font.py`),
checked in as `GenOfficePoppins-{Regular,Bold}-subset.woff2`. Renamed (no
Reserved Font Name upstream) so a locally installed Poppins wins by chain
order. Italic synthesizes oblique from these faces.

## Tamil fallback

| Font | Role |
| ----------------------- | ---------------------------------------- |
| GenOffice Tamil (woff2) | Latha-metric stand-in for Tamil families |

Source: Noto Sans Tamil Regular from [notofonts](https://github.com/notofonts/notofonts.github.io)
(SIL OFL 1.1). Word substitutes missing Tamil families with Latha; Chromium's
macOS fallback (Tamil Sangam MN) shapes ~27% narrower (M3 probe: sentence R
0.728, space 0.39×), far past what size-adjust can fix without inflating glyph
ink (137%). Advances are rewritten to Latha's: the 109 cmap-shared codepoints
exactly, remaining glyphs (GSUB conjunct/matra outputs) by the median
Tamil-letter ratio (`tools/build-tamil-font.py`; shaped sentence R vs Latha
0.994, every probe sentence within ±2.3%). The face ships no Latin letters
(upstream Noto Sans Tamil has none); Latin falls through the chain. Renamed
per OFL ("Noto" is a Reserved Font Name; advances are modified).

## Arabic fallback

| Font | Role |
| -------------------------------- | -------------------------------------------------------- |
| Noto Naskh Arabic (subset woff2) | fallback for naskh/serif-class Arabic families (default) |
| Noto Sans Arabic (subset woff2) | fallback for kufi/sans-class Arabic families |

Source: Noto Naskh/Sans Arabic Regular from [notofonts/arabic](https://github.com/notofonts/arabic)
(SIL OFL 1.1), subset with fonttools to the Arabic blocks + presentation forms +
digits/punctuation, keeping all shaping features
(`pyftsubset --unicodes="U+0020-024F,U+0600-06FF,U+0750-077F,U+08A0-08FF,U+FB50-FDFF,U+FE70-FEFF,U+2000-206F" --layout-features='*' --flavor=woff2`).
Names are kept ("Noto ..."): glyphs and advances are unmodified, so the OFL
Reserved Font Name clause does not apply. The upstream fonts carry no Latin
letters (only digits/punctuation); Latin text in a cs-font run falls through to
the rest of the chain. Word substitutes a missing Arabic font with a naskh-style
serif, so unknown Arabic families default to the Naskh chain.

## PUA blanker

| Font | Role |
| --------------------------------- | ------------------------------------------------ |
| GenOffice PUA Blank (woff2, 312B) | blank 1em glyph for all of U+E000-F8FF (BMP PUA) |

Generated from scratch by `tools/build-pua-blank-font.py` (no upstream font;
two glyphs, both empty). Chromium never system-falls-back for Private Use
codepoints: an unmapped PUA character renders the chain's primary font's
`.notdef`. Chains headed by a real face (Calibri, Carlito GO) therefore draw
tofu boxes for AI-residue PUA tokens, while Word — and chains headed by the
bundled CJK subsets, whose subsetted `.notdef` is blank — show nothing. This
face sits in the Aptos chain and behind the range-limited `Noto Sans/Serif
CJK GO` aliases so PUA stays invisible there too.
Loading
Loading