Skip to content

fix(slides): enhance text overflow and occlusion detection in xml lint - #2152

Open
ethan-zhx wants to merge 9 commits into
mainfrom
fix/text_over_flow
Open

fix(slides): enhance text overflow and occlusion detection in xml lint#2152
ethan-zhx wants to merge 9 commits into
mainfrom
fix/text_over_flow

Conversation

@ethan-zhx

@ethan-zhx ethan-zhx commented Aug 3, 2026

Copy link
Copy Markdown
Collaborator

Summary

增强 Slides XML Lint 工具 xml_text_overlap_lint.py 的文本溢出与遮挡检测能力,新增 6 类检测规则,修复多个漏报场景,并统一 z-order 判断逻辑。

变更文件(2 个文件,+1804 / -88)

文件 变更
skills/lark-slides/scripts/xml_text_overlap_lint.py +1055 / -88
skills/lark-slides/scripts/xml_text_overlap_lint_test.py +837 / -0

新增检测能力

  1. 文本-线条重叠检测 (line_covers_text):使用 Liang-Barsky 线段相交算法检测 <line> 与文本字形的重叠,支持水平、垂直及斜线。通过边缘侵蚀(max(fontSize * 0.12, 2px))规避边框擦过误报,border_alpha < 0.08 的不可见线条自动跳过。

  2. 宽度触发的文本换行检测 (text_may_overflow_shape,新增 overflow_axis="width"):原检测仅覆盖高度溢出,新增宽度轴检测——当单行短标签/指标的估计宽度超出内容框时(Latin 文本 0.85 风险带、纯 CJK 精确匹配、纯数值 1.18 容差),报告潜在的 Skia 渲染换行。

  3. 形状遮挡文本检测 (shape_covers_text):当非文本 <shape>(填充 alpha ≥ 0.08)的填充区域覆盖文本字形时报警,排除细线/分隔线(短边 ≤ 8px)。

  4. 容器溢出检测 (text_overflows_container):检测文本的排版框是否超出其背景容器(卡片/色块),通过 z-order 和最大交集面积自动识别文本归属容器。

  5. 表格/图表遮挡文本检测 (table_covers_text / chart_covers_text):当自由文本与 <table><chart> 的绘制区域重叠时报警,无论 z-order 如何。

  6. Auto-fit 增长碰撞检测 (bbox_overlap):检测 shape-auto-fit 文本因换行向下增长后与下方文本的碰撞。

Bug 修复

  • 移除 image-text 遮挡的顺序豁免:原先 image.order <= text.order 的逻辑允许 XML 中先出现的图片静默遮挡文本,现已移除,任何几何重叠均报警。
  • 修复漏报:CJK 歧义宽度字符估计、百分号字形宽度、全画布背景图豁免、width/height 溢出去重中的严重级别屏蔽。
  • 统一 z-order 判断:将分散的原始绘制顺序比较整合为 is_drawn_behind / is_drawn_in_front_of,单一决策点,附带契约测试。

Commits

ad0651a2 feat(slides): detect text width overflow
e3150d67 fix(slides): detect shape overlap
dee80503 fix(slides): detect container overflow
79e70c42 feat(slides): detect shape text occlusions
af93dcaa fix(slides): treat text overflow as error beyond subpixel tolerance
f8508a6a fix(slides): close text-overlap false negatives and unify z-order checks
e2fd7a41 fix(slides): detect width-induced text wrap in xml_text_overlap_lint
4e61083e fix(slides): remove order exemption in image-text occlusion detection
70dcd0cd fix(slides): detect text-line overlap in xml_text_overlap_lint

@github-actions github-actions Bot added the size/XL Architecture-level or global-impact change label Aug 3, 2026
@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The slide XML overlap linter adds text measurement, wrapping, overflow, occlusion, container, rotation, stacking-order, and issue-deduplication logic. Regression tests cover Unicode sizing, spacing, auto-fit growth, geometry, z-order, and error severity.

Changes

Slide overlap and overflow linting

Layer / File(s) Summary
Text measurement and overflow
skills/lark-slides/scripts/xml_text_overlap_lint.py, skills/lark-slides/scripts/xml_text_overlap_lint_test.py
Text extraction preserves spacing. Width estimation handles CJK glyphs, percentages, labels, wrapping, and trailing spaces. Paragraph-aware height estimation reports width and height overflow as errors.
Occlusion and container checks
skills/lark-slides/scripts/xml_text_overlap_lint.py, skills/lark-slides/scripts/xml_text_overlap_lint_test.py
The linter detects table, chart, image, filled-shape, container, and auto-fit collisions. Full-canvas image handling uses visibility and stacking order.
Rotation-aware geometry and result normalization
skills/lark-slides/scripts/xml_text_overlap_lint.py, skills/lark-slides/scripts/xml_text_overlap_lint_test.py
Rotated glyph bounds and spacing-aware line crossings update overlap checks. Slide linting wires in the new detectors and removes duplicate issues while retaining the highest-severity overflow result.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related PRs

Suggested reviewers: liangshuo-1

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly summarizes the main change: improved text overflow and occlusion detection in the Slides XML lint tool.
Description check ✅ Passed The description explains the motivation and changes in detail and documents test coverage, but it omits the template's explicit Test Plan and Related Issues sections.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/text_over_flow

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown

🚀 PR Preview Install Guide

🧰 CLI update

npm i -g https://pkg.pr.new/larksuite/cli/@larksuite/cli@ad0651a24b4d52b4adf0ad6b9cb0f025465e3aa7

🧩 Skill update

npx skills add larksuite/cli#fix/text_over_flow -y -g

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
skills/lark-slides/scripts/xml_text_overlap_lint.py (1)

1954-1972: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Anchor the unclamped auto-fit box to the authored top.

shape-auto-fit now keeps raw_height even when it exceeds content_height. The vertical anchoring below still runs. With the default verticalAlign of "middle" (set in extract_elements), (content_height - visual_height) / 2 is negative, so the box is shifted up by half the excess. With "bottom" it is shifted up by the full excess.

The renderer grows a shape-auto-fit box downward from the authored top, as the docstring on detect_auto_fit_growth_collisions states. The current geometry therefore places the grown region too high:

  • detect_auto_fit_growth_collisions measures glyph["y"] + glyph["height"] - authored_bottom, which reports about half of the real downward growth for middle-aligned runs.
  • The box also extends above the authored top, which can create overlap reports against content sitting above the run.

Skip the vertical re-anchoring when the estimated height exceeds the content box.

🐛 Suggested fix
     y = element["y"] + padding_top
-    if element.get("verticalAlign") == "middle":
-        y += (content_height - visual_height) / 2
-    elif element.get("verticalAlign") == "bottom":
-        y += content_height - visual_height
+    # A grown shape-auto-fit box extends downward from the authored top, so alignment
+    # offsets only apply while the estimated block still fits the content box.
+    if visual_height <= content_height:
+        if element.get("verticalAlign") == "middle":
+            y += (content_height - visual_height) / 2
+        elif element.get("verticalAlign") == "bottom":
+            y += content_height - visual_height
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@skills/lark-slides/scripts/xml_text_overlap_lint.py` around lines 1954 -
1972, Update the vertical alignment logic in the text geometry calculation to
skip middle/bottom re-anchoring when shape-auto-fit uses a visual_height larger
than content_height. Keep the authored top as the y origin for the grown box,
while preserving existing vertical alignment behavior for non-grown boxes and
auto-fit cases that do not exceed the content height.
🧹 Nitpick comments (3)
skills/lark-slides/scripts/xml_text_overlap_lint.py (3)

1204-1213: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Precompute the glyph boxes once per text element.

estimate_text_visual_bbox runs per-character width estimation and wrapped line counting. This loop calls it once for every (shape, text) pair, and detect_auto_fit_growth_collisions repeats the same pattern for other_bbox. Build the glyph boxes once before the shape loop.

♻️ Suggested change
+    glyph_boxes = {
+        element["id"]: estimate_text_visual_bbox(element) for element in text_elements
+    }
     for shape in covering_shapes:
         for text_element in text_elements:
             if not is_drawn_in_front_of(shape, text_element):
                 continue
-            glyph = estimate_text_visual_bbox(text_element)
+            glyph = glyph_boxes[text_element["id"]]
             if glyph is None:
                 continue
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@skills/lark-slides/scripts/xml_text_overlap_lint.py` around lines 1204 -
1213, Precompute each text element’s glyph bounding box once before iterating
through covering_shapes, then reuse the cached result when checking shape
overlap. Apply the same caching approach to the repeated other_bbox estimation
in detect_auto_fit_growth_collisions, while preserving the existing handling for
None boxes and overlap thresholds.

1102-1102: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use an area constant for the area gate.

Line 1089 compares growth (pixels) against CONTAINER_OVERFLOW_MIN_PX, which is correct. Line 1102 compares intersection_area(...) (square pixels) against the same constant. The units differ. Both constants are 4.0 today, so behavior is unchanged, but a future tuning of the linear slack would silently move the area gate.

♻️ Suggested change
-            if intersection_area(grown_region, other_bbox) <= CONTAINER_OVERFLOW_MIN_PX:
+            if intersection_area(grown_region, other_bbox) <= SHAPE_TEXT_OCCLUSION_MIN_AREA:
                 continue
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@skills/lark-slides/scripts/xml_text_overlap_lint.py` at line 1102, Update the
area comparison in the overlap-checking logic around intersection_area to use a
dedicated area-threshold constant rather than CONTAINER_OVERFLOW_MIN_PX. Keep
CONTAINER_OVERFLOW_MIN_PX for the growth pixel comparison and initialize the new
area constant to preserve the current behavior.

1016-1050: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider extracting the shared occluder-scan helper.

detect_chart_text_occlusions duplicates detect_table_text_occlusions except for the element kind, issue code, message, and hint. A single parameterized helper keeps the two detectors in sync when the text filter changes.

♻️ Suggested consolidation
def detect_element_text_occlusions(
    elements: list[dict[str, Any]], kind: str, code: str, noun: str, hint: str
) -> list[dict[str, Any]]:
    issues: list[dict[str, Any]] = []
    text_elements = [
        element
        for element in elements
        if is_text_element(element) and has_text_content(element) and not is_ghost_text(element)
    ]
    occluders = [element for element in elements if element["kind"] == kind and element["alpha"] > 0]
    for text_element in text_elements:
        if is_decorative_text(text_element):
            continue
        glyph_bbox = estimate_text_visual_bbox(text_element)
        if glyph_bbox is None:
            continue
        for occluder in occluders:
            if not intersects(occluder, glyph_bbox):
                continue
            issues.append({
                "level": "error",
                "code": code,
                "elements": [occluder["id"], text_element["id"]],
                "message": f'text shape {text_element["id"]} overlaps {noun} {occluder["id"]}',
                "hint": hint,
            })
    return issues
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@skills/lark-slides/scripts/xml_text_overlap_lint.py` around lines 1016 -
1050, Extract the shared scanning logic from detect_table_text_occlusions and
detect_chart_text_occlusions into a parameterized detect_element_text_occlusions
helper. Pass the occluder kind, issue code, noun, and hint for each detector,
while preserving the existing filtering, intersection checks, and issue output.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@skills/lark-slides/scripts/xml_text_overlap_lint.py`:
- Around line 1152-1163: Update the container-overflow handling around the
overflow gate to align with the documented scope of CONTAINER_OVERFLOW_MIN_PX:
either apply that tolerance to this authored text-frame check, or revise the
constant’s comment to explicitly state that it only gates
detect_auto_fit_growth_collisions. Preserve the existing tests’ behavior that
reports a 4px frame overhang.
- Around line 2602-2611: Introduce one element-id-keyed glyph-bbox cache scoped
to lint_slide and pass it through the detectors, so each element’s
estimate_text_visual_bbox result is reused. In
skills/lark-slides/scripts/xml_text_overlap_lint.py#L2602-L2611, resolve left
and right glyph boxes from the shared cache before should_flag_overlap instead
of re-estimating per pair. In
skills/lark-slides/scripts/xml_text_overlap_lint.py#L1204-L1213, update
detect_shape_text_occlusions and detect_auto_fit_growth_collisions to read
cached boxes for each shape/text pair and other_bbox, preserving existing
detector behavior.
- Around line 1836-1850: Update the width-wrap filtering loop around the
existing single-line and short-label checks to skip any element recognized by
is_vertical_text, including all accepted vertical-run values. Keep vertical text
out of the width-based overflow calculation while preserving existing filtering
for horizontal text.
- Around line 2516-2525: Update the crossing-box adjustment around the
glyph_bbox handling to skip the vertical re-anchoring when the text run is
rotated, preserving the rotated bounds returned by estimate_text_visual_bbox.
Keep the existing unrotated vertical-align calculations for non-rotated text,
and ensure rotated runs do not mix rotated x/width with recomputed unrotated
y/height.

---

Outside diff comments:
In `@skills/lark-slides/scripts/xml_text_overlap_lint.py`:
- Around line 1954-1972: Update the vertical alignment logic in the text
geometry calculation to skip middle/bottom re-anchoring when shape-auto-fit uses
a visual_height larger than content_height. Keep the authored top as the y
origin for the grown box, while preserving existing vertical alignment behavior
for non-grown boxes and auto-fit cases that do not exceed the content height.

---

Nitpick comments:
In `@skills/lark-slides/scripts/xml_text_overlap_lint.py`:
- Around line 1204-1213: Precompute each text element’s glyph bounding box once
before iterating through covering_shapes, then reuse the cached result when
checking shape overlap. Apply the same caching approach to the repeated
other_bbox estimation in detect_auto_fit_growth_collisions, while preserving the
existing handling for None boxes and overlap thresholds.
- Line 1102: Update the area comparison in the overlap-checking logic around
intersection_area to use a dedicated area-threshold constant rather than
CONTAINER_OVERFLOW_MIN_PX. Keep CONTAINER_OVERFLOW_MIN_PX for the growth pixel
comparison and initialize the new area constant to preserve the current
behavior.
- Around line 1016-1050: Extract the shared scanning logic from
detect_table_text_occlusions and detect_chart_text_occlusions into a
parameterized detect_element_text_occlusions helper. Pass the occluder kind,
issue code, noun, and hint for each detector, while preserving the existing
filtering, intersection checks, and issue output.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 239a59f1-92ef-45f9-980b-00a67f3d8699

📥 Commits

Reviewing files that changed from the base of the PR and between 427cbd6 and e7cd03b.

📒 Files selected for processing (2)
  • skills/lark-slides/scripts/xml_text_overlap_lint.py
  • skills/lark-slides/scripts/xml_text_overlap_lint_test.py

Comment thread skills/lark-slides/scripts/xml_text_overlap_lint.py
Comment on lines +1836 to +1850
for element in elements:
if not is_text_element(element) or not has_text_content(element):
continue
if element["id"] in already_flagged_ids:
continue
if element.get("wrap") in {"false", "0"}:
continue
# Multi-hard-line text already wraps by author intent; only single logical lines
# that were meant to stay on one line are at risk of an unexpected wrap.
if "\n" in element["text"]:
continue
# Long prose is expected to wrap; this rule targets short labels/metrics
# (e.g. "Slides 87%", "autofix 87%") that were meant to stay on one line.
if len(re.sub(r"\s+", "", element["text"])) > SHORT_LABEL_WRAP_MAX_CHARS:
continue

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Skip vertical text in the width-wrap check.

Vertical runs (vert="vert" and the other values that is_vertical_text accepts) lay out along the height axis. This rule measures the estimated line width against element["width"], so a narrow vertical label can be reported as a width overflow that the renderer cannot produce. detect_image_text_occlusions already treats vertical text as not statically modeled.

🛡️ Suggested guard
         if element.get("wrap") in {"false", "0"}:
             continue
+        # Vertical runs wrap along the height axis, so a width measurement does not apply.
+        if is_vertical_text(element):
+            continue
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for element in elements:
if not is_text_element(element) or not has_text_content(element):
continue
if element["id"] in already_flagged_ids:
continue
if element.get("wrap") in {"false", "0"}:
continue
# Multi-hard-line text already wraps by author intent; only single logical lines
# that were meant to stay on one line are at risk of an unexpected wrap.
if "\n" in element["text"]:
continue
# Long prose is expected to wrap; this rule targets short labels/metrics
# (e.g. "Slides 87%", "autofix 87%") that were meant to stay on one line.
if len(re.sub(r"\s+", "", element["text"])) > SHORT_LABEL_WRAP_MAX_CHARS:
continue
for element in elements:
if not is_text_element(element) or not has_text_content(element):
continue
if element["id"] in already_flagged_ids:
continue
if element.get("wrap") in {"false", "0"}:
continue
# Vertical runs wrap along the height axis, so a width measurement does not apply.
if is_vertical_text(element):
continue
# Multi-hard-line text already wraps by author intent; only single logical lines
# that were meant to stay on one line are at risk of an unexpected wrap.
if "\n" in element["text"]:
continue
# Long prose is expected to wrap; this rule targets short labels/metrics
# (e.g. "Slides 87%", "autofix 87%") that were meant to stay on one line.
if len(re.sub(r"\s+", "", element["text"])) > SHORT_LABEL_WRAP_MAX_CHARS:
continue
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@skills/lark-slides/scripts/xml_text_overlap_lint.py` around lines 1836 -
1850, Update the width-wrap filtering loop around the existing single-line and
short-label checks to skip any element recognized by is_vertical_text, including
all accepted vertical-run values. Keep vertical text out of the width-based
overflow calculation while preserving existing filtering for horizontal text.

Comment on lines +2516 to +2525
updated = dict(glyph_bbox)
# Re-anchor vertically the same way estimate_text_visual_bbox does, using the taller span.
y = element["y"] + padding_top
if element.get("verticalAlign") == "middle":
y += (content_height - spacing_height) / 2
elif element.get("verticalAlign") == "bottom":
y += content_height - spacing_height
updated["y"] = y
updated["height"] = spacing_height
return updated

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Do not re-anchor the crossing box for rotated text.

glyph_bbox comes from estimate_text_visual_bbox, which applies rotate_bbox_around_element_center. The re-anchoring below recomputes y and height in the unrotated element frame. For a rotated run the result mixes rotated x/width with unrotated y/height.

The early return at line 2514 hides this for most 90/270 runs, because the rotated height is the unrotated width and usually exceeds spacing_height. It is not guaranteed for a tall narrow rotated run.

🛡️ Suggested guard
     spacing_height = min(content_height, max(1, block["estimated_height"]))
     if spacing_height <= glyph_bbox["height"]:
         return glyph_bbox
+    rotation = element.get("rotation", 0)
+    # Re-anchoring works in the unrotated frame, so keep the rotated box as-is.
+    if isinstance(rotation, (int, float)) and math.isfinite(rotation) and rotation % 360 != 0:
+        return glyph_bbox
     updated = dict(glyph_bbox)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
updated = dict(glyph_bbox)
# Re-anchor vertically the same way estimate_text_visual_bbox does, using the taller span.
y = element["y"] + padding_top
if element.get("verticalAlign") == "middle":
y += (content_height - spacing_height) / 2
elif element.get("verticalAlign") == "bottom":
y += content_height - spacing_height
updated["y"] = y
updated["height"] = spacing_height
return updated
spacing_height = min(content_height, max(1, block["estimated_height"]))
if spacing_height <= glyph_bbox["height"]:
return glyph_bbox
rotation = element.get("rotation", 0)
# Re-anchoring works in the unrotated frame, so keep the rotated box as-is.
if isinstance(rotation, (int, float)) and math.isfinite(rotation) and rotation % 360 != 0:
return glyph_bbox
updated = dict(glyph_bbox)
# Re-anchor vertically the same way estimate_text_visual_bbox does, using the taller span.
y = element["y"] + padding_top
if element.get("verticalAlign") == "middle":
y += (content_height - spacing_height) / 2
elif element.get("verticalAlign") == "bottom":
y += content_height - spacing_height
updated["y"] = y
updated["height"] = spacing_height
return updated
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@skills/lark-slides/scripts/xml_text_overlap_lint.py` around lines 2516 -
2525, Update the crossing-box adjustment around the glyph_bbox handling to skip
the vertical re-anchoring when the text run is rotated, preserving the rotated
bounds returned by estimate_text_visual_bbox. Keep the existing unrotated
vertical-align calculations for non-rotated text, and ensure rotated runs do not
mix rotated x/width with recomputed unrotated y/height.

Comment on lines 2602 to 2611
for index, left in enumerate(elements):
for right in elements[index + 1 :]:
horizontal_overflow = should_flag_horizontal_text_overflow(left, right)
if not horizontal_overflow and (not intersects(left, right) or not should_flag_overlap(left, right)):
# should_flag_overlap already returns False for non-text pairs and for glyph boxes that do
# not overlap, so the earlier raw-bbox intersects() guard was redundant for axis-aligned
# text (the glyph box is inside the element box) and wrong for rotated text (rotation
# expands the glyph box past a raw box that no longer intersects -- slides p6). Rely on
# should_flag_overlap's rotation-aware glyph geometry as the sole authority.
if not horizontal_overflow and not should_flag_overlap(left, right):
continue

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win

Repeated estimate_text_visual_bbox calls across detectors. The estimator has no per-slide cache, so every detector re-derives the same glyph geometry for the same element. Each call re-walks the run character by character for width estimation and wrapped line counting. Introduce one cache keyed by element id for the duration of lint_slide and pass it to the detectors.

  • skills/lark-slides/scripts/xml_text_overlap_lint.py#L2602-L2611: the pairwise loop lost its intersects() short-circuit, so should_flag_overlap now runs for every pair and calls the estimator up to four times per pair, twice of them inside is_similar_text_overlay. Resolve the glyph boxes from the shared cache.
  • skills/lark-slides/scripts/xml_text_overlap_lint.py#L1204-L1213: detect_shape_text_occlusions calls the estimator once per (shape, text) pair. Read the glyph box from the shared cache instead. detect_auto_fit_growth_collisions repeats the same pattern for other_bbox.
📍 Affects 1 file
  • skills/lark-slides/scripts/xml_text_overlap_lint.py#L2602-L2611 (this comment)
  • skills/lark-slides/scripts/xml_text_overlap_lint.py#L1204-L1213
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@skills/lark-slides/scripts/xml_text_overlap_lint.py` around lines 2602 -
2611, Introduce one element-id-keyed glyph-bbox cache scoped to lint_slide and
pass it through the detectors, so each element’s estimate_text_visual_bbox
result is reused. In
skills/lark-slides/scripts/xml_text_overlap_lint.py#L2602-L2611, resolve left
and right glyph boxes from the shared cache before should_flag_overlap instead
of re-estimating per pair. In
skills/lark-slides/scripts/xml_text_overlap_lint.py#L1204-L1213, update
detect_shape_text_occlusions and detect_auto_fit_growth_collisions to read
cached boxes for each shape/text pair and other_bbox, preserving existing
detector behavior.

@codecov

codecov Bot commented Aug 3, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 75.55%. Comparing base (40a0a9d) to head (e7cd03b).
⚠️ Report is 1 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2152      +/-   ##
==========================================
- Coverage   75.57%   75.55%   -0.02%     
==========================================
  Files         931      931              
  Lines       99162    99362     +200     
==========================================
+ Hits        74937    75077     +140     
- Misses      18501    18549      +48     
- Partials     5724     5736      +12     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

The order-based skip (`image.order <= text.order`) allowed images that
appear before text in XML to silently cover text glyphs. Remove it so
any geometric overlap is reported regardless of XML element order.

Also update the error hint to no longer suggest reordering XML as a
fix, since that no longer works.

Add a regression test verifying the new behavior.
The height-only check (text_may_overflow_shape) misses shapes that wrap
because their box is too narrow, not too short. Added a new width-axis
detector that:

- Flags single-line short labels/metrics whose estimated width exceeds
  the content box (0.85 risk band for latin runs, 1.18 tolerance for
  plain metrics, exact fit for pure CJK).
- Works independently of autoFit (shape-auto-fit only grows height).
- Preserves internal whitespace (e.g. "autofix      87%") so spaces
  are not collapsed away.
- Deduplicates with the height check so the same shape is not
  double-reported under the shared text_may_overflow_shape code.

The two axes share code="text_may_overflow_shape" and are distinguished
by overflow_axis="height"|"width". Regression test covers all three
real false-negative cases (bMP/bMp/bMm) plus negative controls.
Fix missed overflow and occlusion cases in xml_text_overlap_lint: CJK
ambiguous-width and percent glyph width estimation, chart-vs-text
occlusion, full-canvas background-image exemption, and severity masking
in the width/height overflow dedupe. Consolidate the scattered raw
paint-order comparisons into is_drawn_behind / is_drawn_in_front_of so
stacking direction is decided in one place, with contract tests that
turn red if the fixes are reverted.
@ethan-zhx
ethan-zhx force-pushed the fix/text_over_flow branch from fec80e5 to ad0651a Compare August 3, 2026 12:19
@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown

PR Quality Summary

CI did not complete successfully. Use the failed check links below to decide whether this PR needs a code change or a rerun.

Failed checks

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
skills/lark-slides/scripts/xml_text_overlap_lint.py (2)

980-1050: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Extract a shared occluder-versus-text detector.

detect_table_text_occlusions and detect_chart_text_occlusions differ only in the element kind, the issue code, the message, and the hint. The filtering, glyph-box estimation, and intersection logic are identical. A future change to the shared logic must be applied twice.

♻️ Proposed consolidation
+def detect_element_text_occlusions(
+    elements: list[dict[str, Any]], kind: str, code: str, hint: str
+) -> list[dict[str, Any]]:
+    issues: list[dict[str, Any]] = []
+    text_elements = [
+        element
+        for element in elements
+        if is_text_element(element) and has_text_content(element) and not is_ghost_text(element)
+    ]
+    occluders = [element for element in elements if element["kind"] == kind and element["alpha"] > 0]
+    for text_element in text_elements:
+        if is_decorative_text(text_element):
+            continue
+        glyph_bbox = estimate_text_visual_bbox(text_element)
+        if glyph_bbox is None:
+            continue
+        for occluder in occluders:
+            if not intersects(occluder, glyph_bbox):
+                continue
+            issues.append({
+                "level": "error",
+                "code": code,
+                "elements": [occluder["id"], text_element["id"]],
+                "message": f'text shape {text_element["id"]} overlaps {kind} {occluder["id"]}',
+                "hint": hint,
+            })
+    return issues

detect_table_text_occlusions and detect_chart_text_occlusions then become thin wrappers that keep their docstrings and pass the kind, code, and hint.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@skills/lark-slides/scripts/xml_text_overlap_lint.py` around lines 980 - 1050,
Extract the duplicated filtering, glyph-bounding-box estimation, and
intersection logic from detect_table_text_occlusions and
detect_chart_text_occlusions into a shared helper that accepts the occluder
kind, issue code, message context, and hint. Convert both existing functions
into thin wrappers that preserve their docstrings and pass their table- or
chart-specific values while retaining the current issue structure and behavior.

1102-1103: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use an area constant for the area gate.

Line 1102 compares an intersection area in px² against CONTAINER_OVERFLOW_MIN_PX, which lines 76-79 document as linear pixels of slack. The neighbouring detector already has an area-scoped constant with the same value, SHAPE_TEXT_OCCLUSION_MIN_AREA. Reuse it here so the units match the comparison.

♻️ Proposed change
-            if intersection_area(grown_region, other_bbox) <= CONTAINER_OVERFLOW_MIN_PX:
+            if intersection_area(grown_region, other_bbox) <= SHAPE_TEXT_OCCLUSION_MIN_AREA:
                 continue
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@skills/lark-slides/scripts/xml_text_overlap_lint.py` around lines 1102 -
1103, Update the area threshold in the intersection check around
intersection_area(grown_region, other_bbox) to use SHAPE_TEXT_OCCLUSION_MIN_AREA
instead of CONTAINER_OVERFLOW_MIN_PX, preserving the existing comparison and
control flow.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@skills/lark-slides/scripts/xml_text_overlap_lint_test.py`:
- Around line 1177-1179: Update the comment above the assertions in
test_lint_xml_reports_wrap_false_text_wider_than_box to remove the claim that
wrap="false" opts a run out; state instead that no-wrap-label is not flagged
because its estimated width exceeds the heuristic risk band but remains within
the exact available-width tolerance, while comfortable fits normally.

---

Nitpick comments:
In `@skills/lark-slides/scripts/xml_text_overlap_lint.py`:
- Around line 980-1050: Extract the duplicated filtering, glyph-bounding-box
estimation, and intersection logic from detect_table_text_occlusions and
detect_chart_text_occlusions into a shared helper that accepts the occluder
kind, issue code, message context, and hint. Convert both existing functions
into thin wrappers that preserve their docstrings and pass their table- or
chart-specific values while retaining the current issue structure and behavior.
- Around line 1102-1103: Update the area threshold in the intersection check
around intersection_area(grown_region, other_bbox) to use
SHAPE_TEXT_OCCLUSION_MIN_AREA instead of CONTAINER_OVERFLOW_MIN_PX, preserving
the existing comparison and control flow.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: d41aff56-d416-4ade-a24b-28ac38aa7fc5

📥 Commits

Reviewing files that changed from the base of the PR and between e7cd03b and ad0651a.

📒 Files selected for processing (2)
  • skills/lark-slides/scripts/xml_text_overlap_lint.py
  • skills/lark-slides/scripts/xml_text_overlap_lint_test.py

Comment on lines +1177 to +1179
# wrap="false" opts a run out; a label that comfortably fits is not flagged.
self.assertNotIn("no-wrap-label", wrap_ids)
self.assertNotIn("comfortable", wrap_ids)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Correct the wrap="false" comment; it no longer opts a run out.

detect_text_may_wrap_shapes now forwards wrap="false" runs to detect_wrap_false_width_overflow, and test_lint_xml_reports_wrap_false_text_wider_than_box asserts that such runs are reported. no-wrap-label stays unreported only because its estimated width exceeds the heuristic band (available_width * TEXT_WIDTH_WRAP_RISK_RATIO) but not the exact gate (available_width + tolerance). State that reason so the assertion cannot be read as a wrap="false" exemption.

📝 Proposed comment change
-        # wrap="false" opts a run out; a label that comfortably fits is not flagged.
+        # wrap="false" switches the run to the exact-width path: "Docs 99%" clears the heuristic
+        # risk band but not the box width itself, so it is not reported. A label that comfortably
+        # fits is not flagged either.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# wrap="false" opts a run out; a label that comfortably fits is not flagged.
self.assertNotIn("no-wrap-label", wrap_ids)
self.assertNotIn("comfortable", wrap_ids)
# wrap="false" switches the run to the exact-width path: "Docs 99%" clears the heuristic
# risk band but not the box width itself, so it is not reported. A label that comfortably
# fits is not flagged either.
self.assertNotIn("no-wrap-label", wrap_ids)
self.assertNotIn("comfortable", wrap_ids)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@skills/lark-slides/scripts/xml_text_overlap_lint_test.py` around lines 1177 -
1179, Update the comment above the assertions in
test_lint_xml_reports_wrap_false_text_wider_than_box to remove the claim that
wrap="false" opts a run out; state instead that no-wrap-label is not flagged
because its estimated width exceeds the heuristic risk band but remains within
the exact available-width tolerance, while comfortable fits normally.

@ethan-zhx ethan-zhx changed the title fix(slides): detect text overflow and occlusions fix(slides): enhance text overflow and occlusion detection in xml lint Aug 3, 2026
# A full-canvas image drawn behind the text is the slide backdrop, not an occluder; text on
# top of it renders cleanly (slides p9). An image that fills the canvas but sits *above* the
# text still occludes, so this only exempts the behind-in-z-order case.
if is_drawn_behind(image_element, text_element) and is_full_canvas_background_image(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[P1] Keep images behind text out of occlusion errors

is_drawn_behind defines a lower-order image as rendered underneath the text, but this condition exempts it only when the image covers at least 95% of the canvas. A normal title or caption over a cropped photo therefore fails the mandatory lint gate even though the image cannot occlude the glyphs. Please preserve the z-order exemption for every image painted behind the text; contrast or readability concerns should be handled by a separate rule if needed.

def extract_fill_alpha(value: str) -> int | float | None:
fill_color_attrs = extract_tag_attributes(value, "fillColor")
color = extract_attribute(fill_color_attrs, "color")
if color is None:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[P2] Preserve default and non-color shape fills

Returning None whenever <fillColor color> is absent treats <fill/>, <fillImg>, and <fillPattern> as unfilled. The Slides schema makes an empty <fill/> opaque by default for non-text shapes, and image or pattern fills can also be visible. Since detect_shape_text_occlusions excludes shapes whose fillAlpha is not numeric, a default-filled rectangle painted over text currently produces no error. Please derive visibility and alpha for every FillType variant and its defaults, while keeping a genuinely absent <fill> unfilled.

# sits at the bottom of a card, the glyph box can fall entirely outside the card even though the
# frame still crosses the card border.
candidates = [c for c in containers if is_drawn_behind(c, text_element)]
owner = max(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Problem

Selecting the owner by maximum intersection area can choose the full-slide background instead of the actual card, causing container overflow to be missed.

Reproduction

  1. Add a canvas-sized background.
  2. Add a smaller card above it.
  3. Place text inside the card with its bottom extending beyond the card.
  4. Run the lint.

The background wins because it has the largest intersection, so the text is incorrectly checked against the canvas.

Suggested fix

Ignore canvas-sized backgrounds when a more specific container exists, or rank candidates by proximity and specificity instead of raw intersection area. Please add a regression test for this shape hierarchy.

if character == "%":
return font_size * PERCENT_SIGN_WIDTH_RATIO * bold_multiplier
category = classify_font_family(font_family)
coeffs = _FONT_CATEGORY_MULTIPLIERS[category]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Problem

The fixed digit and punctuation coefficients produce the same estimate across font families, leading to false negatives near wrapping boundaries.

Reproduction

  1. Create a text box containing 60%+.
  2. Set the font size to 44pt and the width to approximately 113px.
  3. Render it with Arial, Verdana, Garamond, or Cambria.
  4. Compare the rendered result with the lint output.

The + can wrap onto a second line, while the estimator consistently returns 110.44px and reports no issue.

Suggested fix

Use font-aware width estimates or conservatively handle numeric-symbol text near the available-width boundary. Changing only the metric regex is insufficient because its 1.18 tolerance is more permissive. Please add boundary-width coverage across multiple fonts.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/XL Architecture-level or global-impact change

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants