diff --git a/agents.md b/agents.md
index 735b324..285679b 100644
--- a/agents.md
+++ b/agents.md
@@ -263,3 +263,4 @@ cargo run -p ely-doc 启动一个应用(ElyDoc 先 cargo build -p ely
- 2026-10-09(用户定:“打开方式”里不得出现 LibreOffice):系统会登记磁盘上能找到的 .app、挂载镜像里的 .app 与运行过的包,构建目录里的 `LibreOffice.app` 与测试挂载的官方镜像曾因此成为 xlsx 等的默认程序。现构建后由 `scripts/inert-lo.sh` 去掉其全部文件与 URL 声明、快速查看与聚焦插件,注销它,并在 `engines/` 放 `.metadata_never_index`;Ely 只取其 Frameworks 与 Resources。任何脚本与测试都不挂载 LibreOffice 镜像:LOK 探测用本构建的内核,单测所需的官方内核由 `scripts/extract-official-lo.sh` 以 7zz 解成无包的文件夹。`tests/scripts/lo-unregistered.sh` 以 NSWorkspace 核对 docx、xlsx、pptx、odt、csv 的“打开方式”里没有 LibreOffice。
- 2026-10-09(用户报:ElyTable 点格跳到别处、拖不出选区):Calc 在 `sc_print_twips_msgs` 下以打印 twips(行高原值)报光标、选区与 `.uno:SheetGeometryData`,图块与鼠标却在图块 twips(每行按 `ScViewData::ToPixel` 截成整像素),两者随行号漂移(5000 行处约 140 行)。视图对外一律打印 twips;`LokView::set_geometry` 收下工作表几何后,`sheet::Axis::tile`、`print` 照内核的截断换算(每像素 twips 取图块的像素与 twips 之比),绘制、显示、跳转与冻结都先换成图块 twips,鼠标以图块 twips 发出(`tile_point`),拖动句柄仍为打印 twips;`.uno:` 读回与应用(表头、填充柄)照旧用打印 twips,表头的起点取 `LokView::scrolled_start`。内核读鼠标用的比例原取最后一块图块,worker 现于每次 `setClientVisibleArea` 前以 `setClientZoom` 告知图块比例,点击不再依赖先画过图块。
- 2026-10-09(用户报:ElyPDF 改字报“这种字体画不出 V”):文档自带的子集字体常无可用的 Unicode 表(LibreOffice 导出的子集表为占位,0x00 起逐一对应字形号),按 Unicode 找字形一律失败。现以“文档自带字体”写字时,从该页字体资源里找到同一字体(`pdf_load_font` 按对象缓存,与文字层的字体同一对象),按文字提取的读法(ToUnicode 或 cid_to_ucs、`pdf_font_cid_to_gid`)倒出 Unicode 到字形号的表,写字只用页面上已有的字形;新字体的 `/ToUnicode` 以这张表重写(MuPDF 按子集的占位表生成的那份会把新字读成乱码)。页面上没有的字仍报缺字。
+- 2026-10-09:ElyTable 的自动筛选(R2.9)。内核每次打开筛选列表都把未命名的筛选区向下扩到紧邻的数据行(为接上后来追加的行),分不出选区时就在下方的合计行,合计因此进了列表、随数据一起被隐藏。补丁 `0040-lok-calc-autofilter-keeps-a-marked-block.patch`:LOK 下对跨行的选区开自动筛选时,筛选区记下“选定的行”(不存入文件),此后不再扩展;单格开筛选照旧取整个数据区。已知限制:存后重开的文件不带此记号,其筛选区照内核原样扩展。
diff --git a/crates/ely-lok/src/client/tests.rs b/crates/ely-lok/src/client/tests.rs
index 6358fa4..48c989a 100644
--- a/crates/ely-lok/src/client/tests.rs
+++ b/crates/ely-lok/src/client/tests.rs
@@ -6,6 +6,7 @@ mod analysis;
mod charts;
mod check;
mod commands;
+mod filter;
mod format;
mod formula;
mod handouts;
diff --git a/crates/ely-lok/src/client/tests/filter.rs b/crates/ely-lok/src/client/tests/filter.rs
new file mode 100644
index 0000000..c7cb3cf
--- /dev/null
+++ b/crates/ely-lok/src/client/tests/filter.rs
@@ -0,0 +1,47 @@
+//! An AutoFilter keeps to the marked block (TABLE-08).
+
+use super::sheet::open_book;
+use super::*;
+use crate::sheet;
+
+/// A header, three numbers and their total right under them.
+const BLOCK: &str = r#"
+
+
+ Name
+ 1
+ 2
+ 3
+ 6
+
+"#;
+
+/// The filter entries after an AutoFilter on `marked`.
+fn entries_after(marked: &str) -> Vec {
+ let (_dir, mut worker, doc) = open_book(BLOCK);
+ worker.run(&doc, "cell.goto", &[marked]).expect("marks the cells");
+ worker.run(&doc, "data.autofilter", &[]).expect("filters");
+ let query = sheet::cell_query(sheet::FILTER, 0, 0);
+ let json = worker.values(&doc, &query).expect("the entries");
+ sheet::filter_values(&json).expect("parses").entries.into_iter().map(|e| e.text).collect()
+}
+
+#[test]
+fn an_autofilter_on_a_marked_block_leaves_the_row_below_out() {
+ if install().is_none() {
+ eprintln!("SKIP: no pinned LOK build");
+ return;
+ }
+ assert_eq!(entries_after("A1:A4"), ["1", "2", "3"], "the total below stays out");
+}
+
+#[test]
+fn an_autofilter_on_one_cell_takes_the_data_area() {
+ if install().is_none() {
+ eprintln!("SKIP: no pinned LOK build");
+ return;
+ }
+ assert_eq!(entries_after("A1"), ["1", "2", "3", "6"], "the whole column of data");
+}
diff --git a/patches/libreoffice/0040-lok-calc-autofilter-keeps-a-marked-block.patch b/patches/libreoffice/0040-lok-calc-autofilter-keeps-a-marked-block.patch
new file mode 100644
index 0000000..8c78c3c
--- /dev/null
+++ b/patches/libreoffice/0040-lok-calc-autofilter-keeps-a-marked-block.patch
@@ -0,0 +1,110 @@
+LOK Calc: an AutoFilter on marked rows keeps to those rows
+
+Each time the filter list opens, GetFilterEntries grows an unnamed
+filter range to the data rows right under it (ExtendDataArea and
+ExtendBackColorArea), so appended rows join. It cannot tell them from
+a totals row that was there when the user marked the block: the
+total joined the filter, showed in its list and hid with the data.
+Under LOK, an AutoFilter set on a marked block that spans rows keeps
+that block: ScDBData remembers it (not saved) and never extends.
+
+diff --git a/sc/inc/dbdata.hxx b/sc/inc/dbdata.hxx
+index 6d80216b4..54515b138 100644
+--- a/sc/inc/dbdata.hxx
++++ b/sc/inc/dbdata.hxx
+@@ -119,6 +119,7 @@ private:
+
+ sal_uInt16 nIndex; ///< unique index formulas
+ bool bAutoFilter; ///< AutoFilter? (not saved)
++ bool bMarkedRows; ///< rows were marked; never extend (not saved)
+ bool bModified; ///< is set/cleared for/by(?) UpdateReference
+
+ ::std::vector< OUString > maTableColumnNames; ///< names of table columns
+@@ -244,6 +245,9 @@ public:
+ bool HasAutoFilter() const { return bAutoFilter; }
+ void SetAutoFilter(bool bSet) { bAutoFilter = bSet; }
+
++ bool HasMarkedRows() const { return bMarkedRows; }
++ void SetMarkedRows(bool bSet) { bMarkedRows = bSet; }
++
+ bool IsModified() const { return bModified; }
+ void SetModified(bool bMod) { bModified = bMod; }
+
+diff --git a/sc/source/core/tool/dbdata.cxx b/sc/source/core/tool/dbdata.cxx
+index b1d714c55..e7c5a16c1 100644
+--- a/sc/source/core/tool/dbdata.cxx
++++ b/sc/source/core/tool/dbdata.cxx
+@@ -75,6 +75,7 @@ ScDBData::ScDBData( const OUString& rName,
+ bDBSelection(false),
+ nIndex (0),
+ bAutoFilter (false),
++ bMarkedRows (false),
+ bModified (false),
+ mbTableColumnNamesDirty(true),
+ nFilteredRowCount(SCSIZE_MAX)
+@@ -110,6 +111,7 @@ ScDBData::ScDBData( const ScDBData& rData ) :
+ bDBSelection (rData.bDBSelection),
+ nIndex (rData.nIndex),
+ bAutoFilter (rData.bAutoFilter),
++ bMarkedRows (rData.bMarkedRows),
+ bModified (rData.bModified),
+ maTableColumnNames (rData.maTableColumnNames),
+ maTableColumnAttributes(rData.maTableColumnAttributes),
+@@ -146,6 +148,7 @@ ScDBData::ScDBData( const OUString& rName, const ScDBData& rData ) :
+ bDBSelection (rData.bDBSelection),
+ nIndex (rData.nIndex),
+ bAutoFilter (rData.bAutoFilter),
++ bMarkedRows (rData.bMarkedRows),
+ bModified (rData.bModified),
+ maTableColumnNames (rData.maTableColumnNames),
+ maTableColumnAttributes(rData.maTableColumnAttributes),
+@@ -192,6 +195,7 @@ ScDBData& ScDBData::operator= (const ScDBData& rData)
+ bDBSelection = rData.bDBSelection;
+ nIndex = rData.nIndex;
+ bAutoFilter = rData.bAutoFilter;
++ bMarkedRows = rData.bMarkedRows;
+ nFilteredRowCount = rData.nFilteredRowCount;
+
+ if (bHeaderRangeDiffers)
+@@ -646,6 +650,8 @@ bool ScDBData::UpdateReference(const ScDocument& rDoc, UpdateRefMode eUpdateRefM
+
+ void ScDBData::ExtendDataArea(const ScDocument& rDoc)
+ {
++ if (bMarkedRows)
++ return;
+ // Extend the DB area to include data rows immediately below.
+ SCCOL nOldCol1 = nStartCol, nOldCol2 = nEndCol;
+ SCROW nOldEndRow = nEndRow;
+@@ -665,6 +671,8 @@ void ScDBData::ExtendDataArea(const ScDocument& rDoc)
+
+ void ScDBData::ExtendBackColorArea(const ScDocument& rDoc)
+ {
++ if (bMarkedRows)
++ return;
+ // Extend the DB area to include data rows immediately below.
+ SCCOL nOldCol1 = nStartCol, nOldCol2 = nEndCol;
+ SCROW nOldEndRow = nEndRow;
+diff --git a/sc/source/ui/view/dbfunc.cxx b/sc/source/ui/view/dbfunc.cxx
+index db3d23d86..8140ce4d0 100644
+--- a/sc/source/ui/view/dbfunc.cxx
++++ b/sc/source/ui/view/dbfunc.cxx
+@@ -18,6 +18,7 @@
+ */
+
+ #include
++#include
+ #include
+ #include
+ #include
+@@ -286,6 +287,11 @@ void ScDBFunc::ToggleAutoFilter()
+ return;
+
+ pDBData->SetByRow( true ); //! undo, retrieve beforehand ??
++ // LOK: marked rows are the range, as in Excel.
++ ScRange aMarked;
++ pDBData->SetMarkedRows(comphelper::LibreOfficeKit::isActive()
++ && rViewData.GetSimpleArea(aMarked) == SC_MARK_SIMPLE
++ && aMarked.aStart.Row() != aMarked.aEnd.Row());
+ pDBData->GetQueryParam( aParam );
+
+ SCCOL nCol;