Skip to content

fix: honor font-family in PDF export without LibreOffice (#8245) - #8249

Merged
JohnMcLear merged 6 commits into
developfrom
fix/8245-pdf-fonts
Sep 20, 2026
Merged

JohnMcLear merged 6 commits into
developfrom
fix/8245-pdf-fonts

Conversation

@JohnMcLear

Copy link
Copy Markdown
Member

What

The built-in PDF export path — src/node/utils/ExportPdfNative.ts, which runs on every install that has no soffice configured, i.e. the default — only ever drew with pdfkit's Helvetica and Courier. Any font-family applied in the pad was silently dropped from the PDF, while HTML, ODT and DOCX carried it correctly (ep_font_family #173).

The mapping, and why

pdfkit's built-in PDF "standard 14" fonts cover three families — Helvetica, Times and Courier — each with regular/bold/italic/bold-italic variants, and they need no font files on disk. Anything else has to be registered from a TTF/OTF, which core cannot do out of the box without bundling fonts, and that is a licensing decision rather than a technical one.

So the renderer resolves a CSS font-family list to a built-in by category:

CSS family PDF font
sans-serif, Arial, Helvetica, Calibri, Verdana, Avant Garde, … Helvetica
serif, Times New Roman, Georgia, Garamond, Palatino, Bookman, … Times
monospace, Courier, Courier New, Consolas, Menlo, … Courier

A pad set in Garamond renders as Times rather than Helvetica — not the exact face, but the right kind of face. That covers every family ep_font_family offers and is the difference between a readable export and one that loses the distinction entirely. Bold and italic select the matching variant, so <b> inside a serif span gets Times-Bold.

The family list is walked in order, so font-family: 'Fancy Face', Georgia, serif resolves to Times. A family that matches nothing inherits the enclosing font rather than resetting it, which is why a pad with no font styling produces byte-identical output to before (verified against develop for plain text, headings, bold/italic, lists, <code>/<pre>, alignment and links).

Exact faces: the exportPdfFonts setting

Operators who need a real face register font files themselves:

"exportPdfFonts": {
  "Garamond": {
    "regular":    "/usr/share/fonts/truetype/EBGaramond-Regular.ttf",
    "bold":       "/usr/share/fonts/truetype/EBGaramond-Bold.ttf",
    "italic":     "/usr/share/fonts/truetype/EBGaramond-Italic.ttf",
    "boldItalic": "/usr/share/fonts/truetype/EBGaramond-BoldItalic.ttf",
    "fallback":   "times"
  },
  "Inter": "/usr/share/fonts/truetype/Inter-Regular.ttf"
}

Keys are CSS family names, matched case-insensitively and treating -, _ and spaces alike, so Times New Roman and the times-new-roman that ep_font_family emits are the same key. A bare string is used for every variant; a variant with no file configured degrades to the regular face. Relative paths resolve against the Etherpad root. Registered families take priority over the built-in mapping, so an install with a real Arial can use it.

Every failure path degrades: a missing file, an unreadable file or a malformed TTF logs a warning and falls back to fallback (or Helvetica). An export is never failed because of a font. Etherpad ships no fonts for this setting.

What remains unsupported

Exact non-standard faces without operator configuration. Shipping them would mean bundling font files in an Apache-2.0 project, so that is deliberately left to the operator.

Security

Family names reaching the renderer come from pad content (plugins emit <span style="font-family:…"> via getLineHTMLForExport). They are normalised and looked up in an allow-list using own-property checks — so constructor, __proto__ and friends cannot resolve to anything — and are never used as a file path or passed to pdfkit verbatim. Only the operator-controlled setting can name a file. Nothing unsanitised is reintroduced into the PDF path; the export-HTML restrictions from GHSA-6vx2-3gwr-958v are untouched.

Tests

New backend coverage in src/tests/backend/specs/export.ts, asserting on the /BaseFont entries of the produced PDF:

  • serif / sans-serif / monospace map to Times-Roman / Helvetica / Courier
  • families with no built-in equivalent (Garamond, Palatino, Bookman; Calibri, Avant Garde) map by category
  • bold / italic / bold-italic variants
  • the font-family list is walked and the first known family wins
  • data-font-family (the form exportHtmlAdditionalTagsWithData plugins produce) is honoured
  • an unknown family falls back without throwing
  • an unstyled document still uses exactly Helvetica / Helvetica-Bold / Helvetica-Oblique, and <code> still uses Courier
  • Object.prototype property names resolve to nothing
  • exportPdfFonts: file embedded, per-variant files, bare-string form, missing file → configured fallback, non-font file → fallback, override of a built-in family

Verification

Real pad on a local instance with ep_font_family installed: three lines, one set to Times New Roman, one to Monospace, one left plain. GET /p/<pad>/export/pdf returns a PDF whose base fonts are Times-Roman, Courier and Helvetica. With exportPdfFonts pointing the Times family at a TTF, the same export embeds that font file instead.

Full backend suite, vitest and tsc --noEmit run locally.

Fixes #8245

🤖 Generated with Claude Code

https://claude.ai/code/session_01EVGSvGqsCVzHFPneVbLVrB

`ExportPdfNative.ts` — the in-process PDF renderer that runs on every
install with no `soffice` configured, which is the default — only ever
drew with pdfkit's Helvetica and Courier. Any `font-family` applied in
the pad was dropped from the PDF, while HTML, ODT and DOCX carried it
correctly (ep_font_family #173).

pdfkit's built-in PDF "standard 14" fonts cover three families
(Helvetica, Times, Courier) with regular/bold/italic/bold-italic
variants and need no font files on disk; anything else must be
registered from a TTF/OTF, which core cannot ship without taking on a
font licensing decision. So the renderer now resolves a CSS font-family
list to a built-in by category — sans-serif faces to Helvetica, serif
faces to Times, monospace faces to Courier — which keeps serif, sans and
monospace text visually distinct and covers every family ep_font_family
offers. Bold and italic pick the matching variant.

Operators who need an exact face register font files through the new
`exportPdfFonts` setting, keyed by CSS family name. A missing, unreadable
or malformed font file logs a warning and degrades to a built-in font;
an export is never failed over a font. Families that match nothing
inherit the enclosing font, so a pad with no font styling produces
byte-identical output to before.

Font names reaching the renderer come from pad content, so they are
normalised and looked up in an allow-list (with own-property checks, so
names like `constructor` cannot resolve) and never used as a file path
or passed to pdfkit verbatim. Only the operator-controlled setting can
name a file, which keeps the export-HTML restrictions from
GHSA-6vx2-3gwr-958v intact.

Adds backend coverage for the category mapping, bold/italic variants,
font-family list walking, the `data-font-family` form plugins using
exportHtmlAdditionalTagsWithData emit, unknown-family fallback,
unchanged unstyled output, and every `exportPdfFonts` degradation path.
Documents the setting in settings.json.template and doc/faq.md.

Fixes #8245

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EVGSvGqsCVzHFPneVbLVrB
@qodo-code-review

Copy link
Copy Markdown

ⓘ Qodo reviews are paused because the subscription is no longer active. Ask your workspace admin to reactivate the subscription to resume reviews. Manage billing

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

Honor font families in native PDF exports

🐞 Bug fix ✨ Enhancement 🧪 Tests 📝 Documentation ⚙️ Configuration changes 🕐 40+ Minutes

Grey Divider

AI Description

• Preserve CSS font categories and style variants in native PDF exports.
• Allow operators to embed exact faces through resilient exportPdfFonts configuration.
• Cover mappings, fallbacks, security edge cases, and configuration with tests and documentation.
Diagram

graph TD
  html["Export HTML"] --> renderer["Native renderer"] --> resolver["Family resolver"]
  settings["Font settings"] --> resolver
  resolver -->|"Configured"| cache["Font file cache"] --> pdfkit["PDFKit document"] --> output["PDF output"]
  resolver -->|"Mapped / fallback"| builtins["Standard fonts"] --> pdfkit
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Discover installed system fonts
  • ➕ Could resolve exact faces without per-family operator configuration
  • ➕ Avoids bundling font binaries with Etherpad
  • ➖ Introduces platform-specific discovery and matching behavior
  • ➖ Produces non-deterministic exports across hosts
  • ➖ Expands security, caching, and deployment complexity
2. Bundle curated open fonts
  • ➕ Provides consistent exact faces on every installation
  • ➕ Requires no operator setup
  • ➖ Increases distribution size and maintenance burden
  • ➖ Requires ongoing license and attribution management
  • ➖ Still cannot cover arbitrary plugin-defined families

Recommendation: Keep the PR's explicit configuration plus PDF-standard category mapping. It is portable, deterministic, licensing-neutral, and failure-tolerant while still allowing exact operator-selected faces; system discovery or bundled fonts could be separate opt-in features if stronger out-of-box fidelity becomes necessary.

Files changed (6) +609 / -17

Bug fix (1) +317 / -17
ExportPdfNative.tsResolve and embed font families during native PDF rendering +317/-17

Resolve and embed font families during native PDF rendering

• Parses CSS and data-attribute family lists, maps known faces to Helvetica, Times, or Courier variants, and preserves inherited defaults. Adds secure configured-font lookup, file caching, PDFKit registration, variant selection, and non-fatal fallback for missing or malformed files.

src/node/utils/ExportPdfNative.ts

Tests (1) +208 / -0
export.tsCover native PDF font selection and fallback behavior +208/-0

Cover native PDF font selection and fallback behavior

• Adds backend assertions for category mappings, style variants, ordered family lists, data attributes, inheritance, prototype-safe lookups, and unchanged defaults. Verifies configured font embedding, variant degradation, built-in overrides, missing files, and malformed-font fallbacks.

src/tests/backend/specs/export.ts

Documentation (2) +39 / -0
CHANGELOG.mdDocument restored font fidelity in native PDF export +2/-0

Document restored font fidelity in native PDF export

• Adds a notable fix describing category-based font preservation, configurable exact faces, graceful fallback behavior, and compatibility for unstyled pads.

CHANGELOG.md

faq.mdExplain native PDF font mapping and custom embedding +37/-0

Explain native PDF font mapping and custom embedding

• Documents standard-font category mapping, the 'exportPdfFonts' schema, family normalization, relative paths, fallback behavior, licensing responsibility, and the LibreOffice bypass.

doc/faq.md

Other (2) +45 / -0
settings.json.templateAdd the exportPdfFonts configuration template +32/-0

Add the exportPdfFonts configuration template

• Introduces an empty 'exportPdfFonts' setting with examples for single-file and per-variant font registration. Explains path resolution, built-in fallbacks, and font licensing expectations.

settings.json.template

Settings.tsType and initialize configurable PDF fonts +13/-0

Type and initialize configurable PDF fonts

• Extends application settings with 'exportPdfFonts', supporting bare paths or per-variant path objects with an optional built-in fallback.

src/node/utils/Settings.ts

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Sep 20, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (1) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Important font styles are lost in PDFs ✓ Resolved 📎 Requirement gap ≡ Correctness
Description
parseFontFamily() uses a non-global match for only the first font-family declaration and retains
any trailing !important token, producing lookup keys such as serif !important instead of
applying CSS declaration order and priority. Repeated or prioritized declarations can therefore
resolve a different family than CSS specifies or miss both built-in and operator-configured
families, causing the native PDF renderer to inherit the surrounding font.
Code

src/node/utils/ExportPdfNative.ts[R146-148]

+  const m = FONT_FAMILY_DECL_RE.exec(style);
+  if (!m) return [];
+  return m[1].split(',').map(normalizeFamilyName).filter((s) => s !== '');
Evidence
The cited parser lines show that a non-global regular expression selects only the first declaration
and that normalization does not strip !important; the resulting value is passed directly to the
native PDF family resolver, which requires an exact configured or built-in key. Together, these
citations show why priority annotations and repeated declarations prevent native PDF export from
preserving the CSS font-family selection.

Native PDF export preserves font families
src/node/utils/ExportPdfNative.ts[144-148]
src/node/utils/ExportPdfNative.ts[247-254]
src/node/utils/ExportPdfNative.ts[129-149]
src/node/utils/ExportPdfNative.ts[483-499]
src/node/handler/ExportHandler.ts[141-146]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The inline-style parser selects only the first `font-family` declaration and retains a trailing `!important` annotation during normalization. As a result, repeated or prioritized declarations can select the wrong PDF font or fail exact lookup for built-in and configured families.
## Fix Focus Areas
- src/node/utils/ExportPdfNative.ts[139-149]
- src/node/utils/ExportPdfNative.ts[483-492]
## Recommended Fix
Parse every `font-family` declaration, remove a trailing case-insensitive `!important` token before splitting and normalizing family candidates, and choose the last declaration at the highest applicable priority. Add coverage for repeated declarations, earlier and later `!important` declarations, and prioritized built-in and configured families.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

2. Exported code ignores inherited fonts 🐞 Bug ≡ Correctness
Description
onopentag retains the tag-assigned Courier family when an explicit font-family declaration
resolves to no known family, rather than restoring the enclosing state's family. This occurs on
code, pre, tt, kbd, and samp elements with an unknown family, so code nested inside a
serif or configured-font span renders as Courier despite the documented inheritance behavior.
Code

src/node/utils/ExportPdfNative.ts[R489-492]

+        const styleFamily = resolveFamilyList(parseFontFamily(attribs.style));
+        if (styleFamily) {
+          next.fontFamily = styleFamily;
+        } else {
Evidence
Code-like tags first overwrite the copied parent state with Courier, while the newly added resolver
only updates that state after successful resolution. The implementation comment and unknown-family
test explicitly establish inheritance as the intended behavior.

src/node/utils/ExportPdfNative.ts[421-434]
src/node/utils/ExportPdfNative.ts[483-499]
src/tests/backend/specs/export.ts[669-677]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Unknown explicit font families on code-like tags retain the tag's Courier default instead of inheriting the enclosing font.
## Fix Focus Areas
- src/node/utils/ExportPdfNative.ts[421-434]
- src/node/utils/ExportPdfNative.ts[483-499]
## Recommended Fix
Distinguish an absent font declaration from a present but unresolved declaration. When an explicit style or data font value is present but no candidate resolves, restore `cur.fontFamily`; otherwise preserve the tag-specific default.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Updated font files stay stale in PDFs ✓ Resolved 🐞 Bug ☼ Reliability
Description
readFontFile caches both font buffers and failed reads forever using only the absolute filename,
without invalidating that cache when settings are reloaded. If an operator replaces a font or fixes
a missing file at the same path through the live settings workflow, later exports keep using the old
bytes or fallback until Etherpad is restarted.
Code

src/node/utils/ExportPdfNative.ts[R217-220]

+const readFontFile = (file: string): Buffer | null => {
+  if (fontFileCache.has(file)) return fontFileCache.get(file)!;
+  let buf: Buffer | null = null;
+  try {
Evidence
Every successful or failed file read is placed in the process-wide map and subsequent reads return
it without consulting the filesystem. The only clearing mechanism is the test helper, while the
admin settings route can reload configuration in a running process without invoking it.

src/node/utils/ExportPdfNative.ts[193-227]
src/node/utils/ExportPdfNative.ts[230-239]
src/node/utils/Settings.ts[1203-1210]
src/node/hooks/express/adminsettings.ts[486-490]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Process-wide font-file caching survives production settings reloads, leaving corrected or replaced files at an unchanged path invisible to PDF exports.
## Fix Focus Areas
- src/node/utils/ExportPdfNative.ts[193-227]
- src/node/utils/Settings.ts[1203-1210]
- src/node/hooks/express/adminsettings.ts[486-490]
## Recommended Fix
Invalidate cached font buffers whenever the font configuration is rebuilt, or key entries by file metadata and retry cached failures after changes. Ensure the production settings-reload path triggers invalidation, and test replacing or creating a font at the same configured path without restarting the process.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Tip of the day
💡 Did you know, you can turn on the rule miner and Qodo learns your standards from review history

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread src/node/utils/ExportPdfNative.ts Outdated
Comment on lines +489 to +492
const styleFamily = resolveFamilyList(parseFontFamily(attribs.style));
if (styleFamily) {
next.fontFamily = styleFamily;
} else {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

2. Exported code ignores inherited fonts 🐞 Bug ≡ Correctness

onopentag retains the tag-assigned Courier family when an explicit font-family declaration
resolves to no known family, rather than restoring the enclosing state's family. This occurs on
code, pre, tt, kbd, and samp elements with an unknown family, so code nested inside a
serif or configured-font span renders as Courier despite the documented inheritance behavior.
Agent Prompt
## Issue description
Unknown explicit font families on code-like tags retain the tag's Courier default instead of inheriting the enclosing font.

## Fix Focus Areas
- src/node/utils/ExportPdfNative.ts[421-434]
- src/node/utils/ExportPdfNative.ts[483-499]

## Recommended Fix
Distinguish an absent font declaration from a present but unresolved declaration. When an explicit style or data font value is present but no candidate resolves, restore `cur.fontFamily`; otherwise preserve the tag-specific default.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread src/node/utils/ExportPdfNative.ts
JohnMcLear and others added 2 commits September 20, 2026 18:31
3.3.6 shipped upstream, so the changelog entry this branch added under the
3.3.6 heading is dropped; release notes are written at release time.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013S4pYSjwUsiZtdtMMpW7bw
Review feedback on #8249.

`parseFontFamily()` read only the first `font-family` declaration in a
style attribute and kept a trailing `!important` in the value, so
`font-family: serif !important` produced the lookup key
`serif !important`, which matches nothing and silently inherited the
enclosing font. It now walks every declaration, strips the `!important`
flag before normalising, and picks the winner the way CSS does: the last
declaration, unless an earlier one is flagged `!important`.

Also documents the other half of the fallback rule, which review read as
a bug: a family that resolves to nothing leaves the element on whatever
font it would otherwise use. For ordinary elements that is the enclosing
font; for `code`/`pre`/`tt`/`kbd`/`samp` it stays Courier, deliberately —
an unusable font name is no reason to render code in a proportional
face. Covered by tests either way.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013S4pYSjwUsiZtdtMMpW7bw
@JohnMcLear

Copy link
Copy Markdown
Member Author

Thanks @qodo-free-for-open-source-projects — both findings actioned in bc6bff4.

1. font-family declaration order and !important (High) — fixed. Correct: parseFontFamily() took only the first declaration and left a trailing !important in the value, so font-family: serif !important produced the lookup key serif !important, matched nothing, and silently inherited. It now walks every declaration, strips the flag before normalising, and picks the winner the way CSS does within one declaration block — the last declaration, unless an earlier one is !important. New test applies CSS declaration order and !important covers repeated declarations, an earlier !important beating a later declaration, a lone !important value, and a preceding unrelated declaration.

2. Code-like tags with an unknown family (Medium) — intended, now documented and tested. The behaviour is deliberate rather than an oversight: when a family resolves to nothing the renderer leaves the element on whatever font it would otherwise use. For ordinary elements that is the enclosing font; for code/pre/tt/kbd/samp it stays Courier. Restoring the parent's family there would render code in a proportional face because of an unusable font name, which is the worse outcome of the two — and it is not what a browser does either: an author declaration naming only unavailable families falls back to the default font, not to the parent's. The imprecise wording that made this read as a contradiction is fixed in the code comment and in doc/faq.md, and keeps code/pre monospace when an explicit family is unknown pins both halves of the rule.

The branch is also merged up to current develop; the CHANGELOG entry was dropped since 3.3.6 shipped in #8248 and release notes are written at release time.

🤖 Generated with Claude Code

https://claude.ai/code/session_013S4pYSjwUsiZtdtMMpW7bw

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Thanks @qodo-free-for-open-source-projects — both findings actioned in bc6bff4.

1. font-family declaration order and !important (High) — fixed. Correct: parseFontFamily() took only the first declaration and left a trailing !important in the value, so font-family: serif !important produced the lookup key serif !important, matched nothing, and silently inherited. It now walks every declaration, strips the flag before normalising, and picks the winner the way CSS does within one declaration block — the last declaration, unless an earlier one is !important. New test applies CSS declaration order and !important covers repeated declarations, an earlier !important beating a later declaration, a lone !important value, and a preceding unrelated declaration.

2. Code-like tags with an unknown family (Medium) — intended, now documented and tested. The behaviour is deliberate rather than an oversight: when a family resolves to nothing the renderer leaves the element on whatever font it would otherwise use. For ordinary elements that is the enclosing font; for code/pre/tt/kbd/samp it stays Courier. Restoring the parent's family there would render code in a proportional face because of an unusable font name, which is the worse outcome of the two — and it is not what a browser does either: an author declaration naming only unavailable families falls back to the default font, not to the parent's. The imprecise wording that made this read as a contradiction is fixed in the code comment and in doc/faq.md, and keeps code/pre monospace when an explicit family is unknown pins both halves of the rule.

The branch is also merged up to current develop; the CHANGELOG entry was dropped since 3.3.6 shipped in #8248 and release notes are written at release time.

🤖 Generated with Claude Code

https://claude.ai/code/session_013S4pYSjwUsiZtdtMMpW7bw

Thanks for the detailed follow-up. The changes in bc6bff4 address both concerns: declaration precedence and !important handling are covered, and the intentional monospace fallback for code-like tags is now documented and tested. The develop merge and release-note rationale also make sense.

Review feedback on #8249.

`readFontFile()` cached both font buffers and read failures forever,
keyed only by path. An operator who corrected a wrong `exportPdfFonts`
path, or replaced a face in place, kept getting the old bytes — or the
fallback — until Etherpad was restarted.

The cache now keys on the file's mtime and size as well as its path, and
failures are not cached at all, so a font that appears or changes at a
configured path is picked up by the next export. One `stat` per font
variant per export.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013S4pYSjwUsiZtdtMMpW7bw
@JohnMcLear

Copy link
Copy Markdown
Member Author

Third finding actioned in 7db606b.

3. Stale font-file cache (Medium) — fixed. Correct: readFontFile() cached buffers and read failures forever, keyed only by path, so correcting a wrong exportPdfFonts path or replacing a face in place kept serving the old bytes (or the fallback) until a restart. The cache now keys on the file's mtime and size as well as its path, and failures are not cached at all — one stat per font variant per export. New test picks up a font file that appears or changes on disk walks the whole sequence in one process: missing path falls back, the file appearing at that path is picked up, and replacing it with a different face stops serving the previous bytes.

🤖 Generated with Claude Code

https://claude.ai/code/session_013S4pYSjwUsiZtdtMMpW7bw

JohnMcLear and others added 2 commits September 20, 2026 19:03
@JohnMcLear
JohnMcLear merged commit 20e0bd4 into develop Sep 20, 2026
40 checks passed
@JohnMcLear
JohnMcLear deleted the fix/8245-pdf-fonts branch September 20, 2026 18:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

PDF export without LibreOffice ignores font-family

1 participant