diff --git a/internal/shared/pdf/database_table_reader.go b/internal/shared/pdf/database_table_reader.go index f5da128..0daead6 100644 --- a/internal/shared/pdf/database_table_reader.go +++ b/internal/shared/pdf/database_table_reader.go @@ -552,6 +552,9 @@ func (r *DatabaseTableReader) getContentPreview(content string, maxLen int) stri if len(content) <= maxLen { return content } + if maxLen > len(content) { + maxLen = len(content) + } return content[:maxLen] + "..." } diff --git a/internal/shared/pdf/page_builder.go b/internal/shared/pdf/page_builder.go index 11db7db..88a8f40 100644 --- a/internal/shared/pdf/page_builder.go +++ b/internal/shared/pdf/page_builder.go @@ -1186,10 +1186,14 @@ func (pb *PageBuilder) drawRichTextBlock(pdf *gofpdf.Fpdf, text string, contentW // getContentPreview 获取内容预览(用于日志记录) func (pb *PageBuilder) getContentPreview(content string, maxLen int) string { content = strings.TrimSpace(content) - if len(content) <= maxLen { + if maxLen <= 0 || len(content) <= maxLen { return content } - return content[:maxLen] + "..." + n := maxLen + if n > len(content) { + n = len(content) + } + return content[:n] + "..." } // wrapJSONLinesToWidth 将 JSON 文本按宽度换行,返回用于绘制的行列表(兼容中文等) diff --git a/internal/shared/pdf/text_processor.go b/internal/shared/pdf/text_processor.go index 5dfdb32..93746e7 100644 --- a/internal/shared/pdf/text_processor.go +++ b/internal/shared/pdf/text_processor.go @@ -175,10 +175,11 @@ func (tp *TextProcessor) parseInlineSegments(block string) []inlineSeg { if plain == "" { return segs } - // 在原始 block 上找加粗区间,再映射到 plain(去掉标签后的位置) + // 在 block 上找加粗区间,再映射到 plain(去掉标签后的位置) + // 注意:work 每次循环被截断,必须用相对 work 的索引切片,避免 work[:endInWork] 越界 work := block var boldRanges [][2]int - offset := 0 + plainOffset := 0 for { idxOpen := reBoldOpen.FindStringIndex(work) if idxOpen == nil { @@ -189,34 +190,45 @@ func (tp *TextProcessor) parseInlineSegments(block string) []inlineSeg { if idxClose == nil { break } - startInWork := offset + idxOpen[1] - endInWork := offset + idxOpen[1] + idxClose[0] - // 将 work 坐标映射到 plain:需要数 plain 中对应字符 - workBefore := work[:startInWork] + closeLen := len(reBoldClose.FindString(afterOpen)) + // 使用相对当前 work 的字节偏移,保证 work[:endInWork] 不越界 + endInWork := idxOpen[1] + idxClose[0] + workBefore := work[:idxOpen[1]] plainBefore := regexp.MustCompile(`<[^>]+>`).ReplaceAllString(workBefore, "") plainBefore = regexp.MustCompile(`[ \t]+`).ReplaceAllString(plainBefore, " ") - startPlain := len([]rune(plainBefore)) + startPlain := plainOffset + len([]rune(plainBefore)) workUntil := work[:endInWork] plainUntil := regexp.MustCompile(`<[^>]+>`).ReplaceAllString(workUntil, "") plainUntil = regexp.MustCompile(`[ \t]+`).ReplaceAllString(plainUntil, " ") - endPlain := len([]rune(plainUntil)) + endPlain := plainOffset + len([]rune(plainUntil)) boldRanges = append(boldRanges, [2]int{startPlain, endPlain}) - work = work[endInWork+len(reBoldClose.FindString(afterOpen)):] - offset = endInWork + len(reBoldClose.FindString(afterOpen)) + consumed := work[:endInWork+closeLen] + strippedConsumed := regexp.MustCompile(`<[^>]+>`).ReplaceAllString(consumed, "") + strippedConsumed = regexp.MustCompile(`[ \t]+`).ReplaceAllString(strippedConsumed, " ") + plainOffset += len([]rune(strippedConsumed)) + work = work[endInWork+closeLen:] } - // 按 boldRanges 切分 plain + // 按 boldRanges 切分 plain(限制区间在 [0,len(runes)] 内,防止越界) runes := []rune(plain) + nr := len(runes) inBold := false var start int - for i := 0; i <= len(runes); i++ { + for i := 0; i <= nr; i++ { nowBold := false for _, r := range boldRanges { - if i >= r[0] && i < r[1] { + r0, r1 := r[0], r[1] + if r0 < 0 { + r0 = 0 + } + if r1 > nr { + r1 = nr + } + if r0 < r1 && i >= r0 && i < r1 { nowBold = true break } } - if nowBold != inBold || i == len(runes) { + if nowBold != inBold || i == nr { if i > start { segs = append(segs, inlineSeg{Text: string(runes[start:i]), Bold: inBold}) }