尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

Readest 高亮绘制跨页泄漏修复实战:foliate-js Overlayer 按页裁剪与 issue 6128 全解析

发布时间:2026/9/20 11:47:59

资讯中心
01
ARTICLE

Readest 高亮绘制跨页泄漏修复实战:foliate-js Overlayer 按页裁剪与 issue 6128 全解析

Readest 高亮绘制跨页泄漏修复实战:foliate-js Overlayer 按页裁剪与 issue 6128 全解析
Readest 高亮绘制跨页泄漏修复实战foliate-js Overlayer 按页裁剪与 issue #6128 全解析【免费下载链接】readestReadest is a modern, feature-rich ebook reader designed for avid readers offering seamless cross-platform access, powerful tools, and an intuitive interface to elevate your reading experience.项目地址: https://gitcode.com/gh_mirrors/re/readest本篇技术指南围绕 Readest 阅读器中一个真实且隐蔽的渲染缺陷展开当页面边距与栏间距被压缩到零时高亮选区末尾的圆角补齐cap会把 2px 的色条画到下一页上形成一条沿着页边延伸的绿色细线issue #6128。文章完整还原该缺陷的复现条件、根因定位、修复方案在 foliate-js 的Overlayer.highlight中按页裁剪绘制矩形以及配套的浏览器回归测试帮助读者掌握分页渲染下 overlay 绘制越界的通用排查与修复方法。问题现象下一页边缘出现一条 1~2px 的条纹在 Android 14、Readest 0.12.7 上用户反馈对一段从文本一直选中到一张“整列宽”图片的内容添加高亮后翻到下一页时页面左边缘会出现一条 1~2px 高的绿色竖条纹其纵向范围恰好等于上一页那张图片的高度。由于翻页采用交叉淡化cross-fade过渡这条缝隙在视频中几乎不可见极易被误认为是截图拼接或渲染抖动。该缺陷的复现条件非常具体三点缺一不可分页paginated布局而非滚动scrolled模式高亮选区的最后一个矩形rect落在一张占满整列的图片上或者选区起始/结束处是两端对齐的文本行页面边距与栏间距被调到最小值即margin与gap同时为零相邻页面之间没有间隙、直接相接。在 Readest 中这一条件通过移动端底部工具栏的“Page Margin”滑块即可触发将滑块拖到最小值后前一页最后一个字符的绘制矩形和下一页的内容矩形在几何上共边任何越界的绘制都会“溢出”到下一页表面。根因定位圆角补齐撞上零间距布局高亮的首尾补丁radiusPadding 2Readest 的高亮渲染由 foliate-js 包中的 Overlayer.highlight() 实现。为了让高亮块首尾的 4px 圆角完全覆盖字形、不露出底色绘制函数会对选区中的第一个和最后一个矩形额外扩展radiusPadding 2像素// packages/foliate-js/overlayer.js修复前逻辑要点 const isFirst index 0 const isLast index rects.length - 1 // 横向书写 x left - padding - (isFirst ? radiusPadding : 0) w width padding * 2 (isFirst ? radiusPadding : 0) (isLast ? radiusPadding : 0) // 只有首/末矩形被向外多画 2px中间矩形保持原尺寸这段补齐本身是合理的装饰性处理——圆角半径为 4px若首末矩形不向外扩圆角圆弧会切进字形内部。问题出在它无条件向外扩而没有考虑目标矩形是否已经贴在页面边缘。零间距布局分页器的栏间距公式分页器在 paginator.js 的 columnize() 中计算栏间距const horizontalColumnGap columnCount 1 ? (marginLeft marginRight) / 4 gap / 2 : (marginLeft marginRight) / 2 gap当marginLeft marginRight 0且gap 0时栏间距为 0——相邻页面在几何上贴合下一页的起点就是上一页的终点。一个滑块同时驱动两个量handleMarginChangeReadest 的移动端字体/布局面板 FontLayoutPanel.tsx 中handleMarginChange用同一个滑块值同时换算边距和栏间距const MARGIN_CONSTANTS { MAX_MARGIN_PX: 88, // 边距上限 88px MAX_GAP_PERCENT: 10, // 栏间距上限 10% } as const; const handleMarginChange useCallback((value: number) { const marginPx Math.round((value / 100) * MAX_MARGIN_PX); const gapPercent Math.round((value / 100) * MAX_GAP_PERCENT); currentViewSettings.marginTopPx marginPx; currentViewSettings.marginBottomPx marginPx / 2; currentViewSettings.marginLeftPx marginPx / 2; currentViewSettings.marginRightPx marginPx / 2; saveViewSettings(envConfig, bookKey, gapPercent, gapPercent, false, false); view?.renderer.setAttribute(margin, ${marginPx}px); view?.renderer.setAttribute(gap, ${gapPercent}%); }, [envConfig, bookKey, view, getViewSettings]);滑块范围为 0~100、步进 10面板中step{10}。滑块拖到 0 时marginPx 0、gapPercent 0分页器随即收到margin0px、gap0%两页完全贴合。这正是“最小页边距”场景下 bug 必然出现的原因——边距和栏间距被耦合到一个值里最小值时没有任何隔离带。泄漏路径最后一个矩形的 2px 补齐画到了下一页选区中的图片作为替换元素是最后一个矩形其右边缘贴住页面右边界。highlight()对它做isLast补齐时x w越出本页 2px。在 800px 宽的页面上探针probe实测图片路径被绘制到x800..1602——已经落在下一页的画布上同理选区第一个文本矩形在x-2处画到了前一页。竖直书写vertical-rl时对称地出现纵向泄漏页面顶部的首行文字会把 2px cap 画到上一页底部。两端对齐justified的文本行同理只是行高仅 18px条纹更细、更难察觉。该缺陷与#splitRange0.12.7 版本内无关也与图片缩放、文档分段fragmentation无关——纯粹的几何越界。修复方案为每个矩形绑定所属页面并裁剪修复落点在 foliate-js 的 overlayer.js核心思路是**“先知道矩形属于哪一页再把它画在该页边界之内”**整个改动约 30 行。第一步pageOf根据根盒计算矩形所属页面// packages/foliate-js/overlayer.js const pageOf (root, { left, top, right, bottom }) { if (!(root.width 0 root.height 0)) return null const x root.left Math.floor(((left right) / 2 - root.left) / root.width) * root.width const y root.top Math.floor(((top bottom) / 2 - root.top) / root.height) * root.height return { left: x, top: y, right: x root.width, bottom: y root.height } }pageOf以矩形的中心点为基准把它归入所在的“页面瓦片”tiledocumentElement的根盒被分页器展开为N × 单页尺寸的平铺网格Math.floor计算出该矩形落在第几块瓦片上返回该瓦片的{ left, top, right, bottom }边界。第二步#getRects为每个矩形打上页面标记选区矩形收集函数 #getRects 在缩放并规范化每个getClientRects()结果后立即附加页面信息const scaled { left: rect.left * zoom, top: rect.top * zoom, right: rect.right * zoom, bottom: rect.bottom * zoom, width: rect.width * zoom, height: rect.height * zoom, } scaled.page pageOf(root, scaled) // 关键绑定所属页面瓦片 rects.push(scaled)#zoom处理的是 Safari 对 client rect 不做页面缩放、而 Chrome/Edge/Firefox 会缩放这一差异确保坐标空间一致后再绑定页面。第三步highlight()将绘制矩形裁剪到所属页面内在 highlight() 中完成首末补齐后立即执行裁剪// 补齐后仍可能越过页面边界裁剪到本页内issue readest/readest#6128 if (page) { const right Math.min(x w, page.right) const bottom Math.min(y h, page.bottom) x Math.max(x, page.left) y Math.max(y, page.top) w right - x h bottom - y if (w 0 || h 0) continue // 完全越界则跳过该矩形不绘制 }裁剪后若宽或高非正说明该矩形整体位于页外直接跳过。圆角半径随后仍会按Math.min(radius, w / 2, h / 2)自适应收缩不会因为矩形被裁小而出现非法路径。为什么这个方案是安全的平铺机制保证pageOf始终有效分页器的 expand() 会把documentElement.style[width|height]设为单页尺寸columnSize让正文溢出到相邻的瓦片中从而让根盒沿轴向、沿任意方向RTL 时floor得到负的瓦片序号平铺页面。修复前的代码注释也印证了这一设计// packages/foliate-js/paginator.js const columnSize this.#size / this.#columnCount const pageCount Math.ceil(contentSize / columnSize) const expandedSize pageCount * columnSize // One column per page — overflow columns extend into adjacent pages documentElement.style[side] ${columnSize}px滚动模式是天然的 no-opscrolled 模式下根盒等于整个文档任何矩形都落在同一块“瓦片”里pageOf返回整篇文档范围裁剪不起任何作用视觉与修复前完全一致。真实页边距不受影响裁剪边界是“页面瓦片”tile即包含边距的整页盒子而非内容盒。因此首末矩形的 2px 补齐仍然能正常延伸进真实的页边距内——它只是不再越过页面瓦片边界正常布局下的外观没有任何变化。对所有方向与书写模式生效横排、竖排、RTLfloor负索引、图片矩形、两端对齐文本行统一由中心点归属 边界裁剪覆盖无需逐个特判。通用性原则任何越界绘制的 overlay 都必须裁剪文档中给出的通用结论值得所有在分页渲染中做 overlay 绘制的开发者牢记任何从getClientRects()结果向外扩展的 overlay 绘制器都必须把最终绘制矩形裁剪到所属页面瓦片rect.page内。foliate-js 的 Overlayer 家族中不止highlight()一种绘制器——还有underline、strikethrough、squiggly、outline、bubble、copyImage。其中underline/strikethrough/squiggly会按padding或strokeWidth向外延伸同样存在越界风险。在 Readest 中高亮绘制经由 annotatorUtil.ts 调用draw(Overlayer.highlight, { color, vertical })瞬时选区高亮transient highlight则通过 transientHighlight.ts 调用overlayer.add(HIGHLIGHT_KEY, range, Overlayer.highlight, { color })——两者最终都汇入Overlayer.highlight因此这一处裁剪修复覆盖了阅读器内所有高亮场景。Overlay 的视觉样式还支持通过 CSS 变量定制如 FoliateViewer.tsx 中的--overlayer-highlight-blend-mode这些样式层不参与几何计算与本次修复互不影响。回归测试极端布局下的几何不变量修复随附的浏览器回归测试位于 overlayer-highlight-page-bounds.browser.test.ts在 Chromium 中运行刻意构造最恶劣的边界条件800×600 视口、单栏、四边 margin 均为 0px、gap0%让相邻页面完全共边。测试用两个固定 EPub 夹具复现两种泄漏方向图片结尾横排repro-6128-image-highlight.epub第 1 节选区从selection-start延伸到target-image之后。先断言图片确实占满整列imgRect.width ≈ PAGE_WIDTH且imgRect.right % PAGE_WIDTH 0再断言每个绘制矩形的四个边都落在“持有其中心点”的那一页内。同时校验裁剪没有吃掉图片本身——图片矩形仍完整覆盖图片范围。首行起始竖排sample-vertical-rl.epub的h1前 3 个字符竖排首行字形贴住页面顶部range.getBoundingClientRect().top 1若不裁剪首部 cap 会画到上一页底部修复前实测y -2。测试中的核心不变量expectWithinOwnPage与生产代码的pageOf采用同一套瓦片归属逻辑const expectWithinOwnPage (box: Box) { const pageLeft Math.floor((box.left box.right) / 2 / PAGE_WIDTH) * PAGE_WIDTH; const pageTop Math.floor((box.top box.bottom) / 2 / PAGE_HEIGHT) * PAGE_HEIGHT; expect(box.left).toBeGreaterThanOrEqual(pageLeft - 0.01); expect(box.right).toBeLessThanOrEqual(pageLeft PAGE_WIDTH 0.01); expect(box.top).toBeGreaterThanOrEqual(pageTop - 0.01); expect(box.bottom).toBeLessThanOrEqual(pageTop PAGE_HEIGHT 0.01); };修复前两条用例均失败报错形如expected -2 to be -0.01修复后全部通过。测试通过document.createElement(foliate-paginator)真实渲染分页器、等待stabilized事件后再叠加Overlayer属于端到端的浏览器级验证而非 mock。另有一条来自实测的测试技巧值得记录浏览器测试的onConsoleLog会同时吞掉 stdout 与 stderr调试时不要把关键输出直接console.log而应把内容拼进断言消息如expect(out).toBe()这样失败时输出会随断言一起出现在报告里。修复状态与验证流程截至 memory 记录日期2026-09-11该修复的演进状态如下foliate-js 侧PR #94 以 squash 方式合并为9e05bf2内容与分支58af29d一致main 上无其他变更。readest 侧PR #6182 以d61366835合并进 main未发布即尚未进入任何正式版本分支上依次包含70100af75修复 回归测试 夹具并 pin 了 foliate-js 分支提交与e069f9b77re-pin 到9e05bf2回归测试复跑全绿随后与 main1b681939d合并。由于 foliate-js 在 Readest 仓库中以submodule方式引入修复必须先在 foliate-js 独立开 PR 合并再在 readest 侧 re-pin 子模块提交——这是两个仓库间修复联动的基本流程。推送前验证pnpm lint、pnpm format:check、foliate 的 eslint、pnpm test919 个文件、pnpm test:browser58 个文件全部通过。已知限制尚未在真实设备上验证review 环节因 gstack 子 agent 看门狗超时600s而改为人工审查。总结issue #6128 是一次典型的“装饰性绘制遇上极端布局”的边界缺陷radiusPadding 2的圆角补齐本身没有问题但 Readest 移动端用一个滑块耦合驱动边距与栏间距的设计使其最小值把页间距压缩到零任何跨页绘制都无所遁形。修复没有在 UI 层打补丁而是回到几何本质——用pageOf建立“矩形 → 页面瓦片”的映射再在highlight()中按页裁剪并跳过空盒30 行代码同时覆盖横排、竖排、RTL、图片与两端对齐文本。配套的浏览器回归测试用 800×600 零边距布局把回归风险钉死也为所有“从getClientRects()向外扩展的 overlay 绘制器必须 clamp 到rect.page”的同类问题提供了可直接复用的范式。【免费下载链接】readestReadest is a modern, feature-rich ebook reader designed for avid readers offering seamless cross-platform access, powerful tools, and an intuitive interface to elevate your reading experience.项目地址: https://gitcode.com/gh_mirrors/re/readest创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。