diff --git a/crates/pdf/src/exporter.rs b/crates/pdf/src/exporter.rs
--- a/crates/pdf/src/exporter.rs
+++ b/crates/pdf/src/exporter.rs
@@ -1,5 +1,5 @@
 use super::export::Fit;
-use super::glyphs::{baseline, cluster_ends, glyph_outlines, left_to_right, split_clusters, visual_order};
+use super::glyphs::{baseline, cluster_ends, glyph_id, glyph_outlines, left_to_right, split_clusters, visual_order};
 use super::options::{PdfError, PdfOptions};
 use super::paint::{append_path, fill, grown, norm, ok, paint_fill, pdf_line_cap, rect_path, shape_effects, shape_path};
 use super::picture::{Vector, build_vector, load_image, paint_of, vector_picture};
@@ -341,7 +341,7 @@ impl Exporter<'_> {
                 let end = ends.get(i).copied().unwrap_or(text.len());
                 let start = g.cluster.min(end);
                 KrillaGlyph::new(
-                    GlyphId::new(g.gid),
+                    glyph_id(g.gid),
                     g.x_advance as f32 / upem,
                     g.x_offset as f32 / upem,
                     g.y_offset as f32 / upem,
diff --git a/crates/pdf/src/exporter/text.rs b/crates/pdf/src/exporter/text.rs
--- a/crates/pdf/src/exporter/text.rs
+++ b/crates/pdf/src/exporter/text.rs
@@ -120,7 +120,7 @@ impl Exporter<'_> {
                     Some((_, nx, _, _)) => (nx - x) / size,
                     None => face.advance(*g) as f32 / upem,
                 };
-                KrillaGlyph::new(GlyphId::new(*g), adv, 0.0, (y0 - y) / size, 0.0, r.clone(), None)
+                KrillaGlyph::new(glyph_id(*g), adv, 0.0, (y0 - y) / size, 0.0, r.clone(), None)
             })
             .collect();
         let pushed = italic;
@@ -208,7 +208,7 @@ impl Exporter<'_> {
                             None => g.x_advance as f32 / upem,
                         };
                         let dy = lay.glyphs.get(i).map_or(0.0, |p| ((lay.baseline - p.1) / lay.em) as f32);
-                        KrillaGlyph::new(GlyphId::new(g.gid), adv, 0.0, dy, 0.0, start..end, None)
+                        KrillaGlyph::new(glyph_id(g.gid), adv, 0.0, dy, 0.0, start..end, None)
                     })
                     .collect();
                 let x0 = lay.glyphs.first().map_or(0.0, |p| p.0) as f32;
diff --git a/crates/pdf/src/glyphs.rs b/crates/pdf/src/glyphs.rs
--- a/crates/pdf/src/glyphs.rs
+++ b/crates/pdf/src/glyphs.rs
@@ -1,6 +1,14 @@
 use super::paint::append_path;
 use super::*;
 
+/// A shaped glyph id as krilla takes it. Shaping gives 32-bit ids (a format-12 cmap can map to
+/// any), but krilla's font subsetting takes 16-bit ones and panics on a larger id (#3674): one
+/// that doesn't fit is written as .notdef (glyph 0), keeping its place. Its own character doesn't
+/// extract, since krilla maps no text to .notdef.
+pub(crate) fn glyph_id(g: u32) -> GlyphId {
+    GlyphId::new(u16::try_from(g).map_or(0, u32::from))
+}
+
 /// The outlines of `glyphs` (glyph id, pen position) at `size`, as one path in page coordinates.
 pub(crate) fn glyph_outlines(face: &FaceRef, size: f32, glyphs: &[(u32, f32, f32)]) -> Option<Path> {
     let db = wordcraft_fonts::FontDb::global();
diff --git a/crates/pdf/src/tests.rs b/crates/pdf/src/tests.rs
--- a/crates/pdf/src/tests.rs
+++ b/crates/pdf/src/tests.rs
@@ -773,6 +773,32 @@ fn unsubsettable_font_is_drawn_as_outlines() {
     assert!(paths > 0, "its glyphs are drawn as outlines");
 }
 
+/// #3674: shaping gives 32-bit glyph ids (a format-12 cmap can map a character to any of them),
+/// but krilla takes 16-bit ones and panics on a larger id. That failed the export, and on wasm
+/// (`panic = "abort"`) lost the tab. A glyph id above 65535 is now written as .notdef.
+#[test]
+fn glyph_id_above_u16_exports() {
+    let mut d = Document::new();
+    d.body = vec![para_block(Paragraph::with_text("Wide glyph", CharProps { font: Some("Source Sans 3".into()), ..Default::default() }))];
+    let opts = PdfOptions { tagged: false, ..Default::default() };
+    let mut lay = layout(&d, &mut LayoutCache::new(), &LayoutOptions::default());
+    let (want, _) = count_glyphs_and_paths(&export_layout(&d, &lay, &opts).unwrap());
+    assert!(want > 0);
+    let mut wide = 0;
+    for item in lay.pages.iter_mut().flat_map(|p| p.items.iter_mut()) {
+        if let Placed::Lines { para, .. } = item
+            && let Some(g) = Arc::make_mut(para).glyphs.first_mut()
+        {
+            g.gid = 70_000;
+            wide += 1;
+        }
+    }
+    assert_eq!(wide, 1, "one glyph id is out of range");
+    let bytes = export_layout(&d, &lay, &opts).expect("a glyph id above 65535 must not fail the export");
+    let (glyphs, _) = count_glyphs_and_paths(&bytes);
+    assert_eq!(glyphs, want, "every glyph is still drawn, the wide one as .notdef");
+}
+
 #[test]
 fn flat_lines_in_a_group_are_drawn() {
     // A horizontal divider inside a group has no height (`a:ext cy="0"`): it used to be left out
