From 7cd8eb8b15ef9048d9505194dc72d23e90db548c Mon Sep 17 00:00:00 2001 From: RyuJongJin <266630912+RyuJongJin@users.noreply.github.com> Date: Mon, 19 Oct 2026 22:30:46 +0900 Subject: [PATCH] =?UTF-8?q?WIP=20=EA=B8=80=20=EB=B7=B0=EC=96=B4:=20?= =?UTF-8?q?=EA=B8=B4=20=EC=A4=84=EC=9D=84=20=EC=9E=AC=EB=8A=94=20=EC=A1=B0?= =?UTF-8?q?=EA=B0=81=EC=9D=B4=20=EC=9D=B4=EB=AA=A8=EC=A7=80=20(=EB=91=90?= =?UTF-8?q?=20=EB=8B=A8=EC=9C=84=20=EA=B8=80=EC=9E=90)=20=EA=B0=80?= =?UTF-8?q?=EC=9A=B4=EB=8D=B0=EC=84=9C=20=EB=81=8A=EA=B2=A8=20"not=20well-?= =?UTF-8?q?formed=20UTF-16"=20=EC=9D=B4=20=EB=82=98=EB=8D=98=20=EA=B2=83?= =?UTF-8?q?=20(=EA=B2=80=ED=86=A0=20=EB=8C=80=EA=B8=B0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 28 지적 (10/19, 4716537 의 회귀): 이모지가 든 긴 줄에서 cut 콜백의 재는 조각 끝 (from + size) 이 서로게이트 쌍 가운데 (0xD800 다음) 에 떨어지면 반쪽 글자로 TextSpan 을 만들어 painting 오류 "string is not well-formed UTF-16" 이 화면마다 30번 나고, 그 재기를 버리고 더 큰 조각을 다시 쟀다 (화면에 빠진 글은 없었음). → 조각 끝이 두 단위 글자의 뒷쪽 (0xDC00~0xDFFF) 이면 한 칸 앞에서 자른다 (viewerRows 뒤쪽과 같은 처리). 같이: 4716537 에서 l10n 검사에 걸린 폭 재기 글자 '가' 에 // l10n-skip (화면 글이 아님). 확인: integration_test/windows_textview_shot_test.dart 에 이모지 줄 ('abc😀' × 3000, 412 · 960) 과 "그리는 중 오류 없음" 을 더함 - 고치기 전 코드로는 Windows 에서 UTF-16 오류 10건으로 실패, 고친 뒤 8 통과 · 오류 0 (release\test\shots_textview_emoji). xplore_tools_test (새 1: 이모지 긴 줄 · 조각이 반쪽 이모지로 끝나지 않음 - flutter_tester 엔진은 반쪽 글자를 받아들여 회귀는 Windows 캡처로 잡음) 8 통과, analyze 문제 없음, l10n 빠짐 0. Co-Authored-By: Claude Opus 5.5 --- .../windows_textview_shot_test.dart | 3 +++ lib/ui/text_viewer.dart | 6 +++++- test/xplore_tools_test.dart | 21 +++++++++++++++++++ 3 files changed, 29 insertions(+), 1 deletion(-) diff --git a/integration_test/windows_textview_shot_test.dart b/integration_test/windows_textview_shot_test.dart index d1f7638..b7b55f9 100644 --- a/integration_test/windows_textview_shot_test.dart +++ b/integration_test/windows_textview_shot_test.dart @@ -21,6 +21,8 @@ void main() { ('ascii', List.generate(3000, (i) => String.fromCharCode(0x61 + i % 26)).join()), ('mixed', List.generate(3000, (i) => i.isEven ? '한' : 'x').join()), ('korean', '가나다라마바사아자차카타파하' * 200), + // 28 지적 (10/19): 이모지 (두 단위 글자) 가 든 긴 줄 - 재는 조각이 글자 가운데서 끊기면 UTF-16 오류 + ('emoji', List.generate(3000, (i) => 'abc😀').join()), ]) { testWidgets('글 뷰어 긴 줄 $name ${w}x$h', (t) async { t.view.physicalSize = Size(w.toDouble(), h.toDouble()); @@ -44,6 +46,7 @@ void main() { final bytes = await t.runAsync(() => img!.toByteData(format: ui.ImageByteFormat.png)); Directory(out).createSync(recursive: true); File(p.join(out, 'textview_${name}_$w.png')).writeAsBytesSync(bytes!.buffer.asUint8List()); + expect(t.takeException(), isNull, reason: '그리는 중 오류 없음 ($name $w)'); await t.pumpWidget(const SizedBox()); }); } diff --git a/lib/ui/text_viewer.dart b/lib/ui/text_viewer.dart index c3c6d98..df1e1b0 100644 --- a/lib/ui/text_viewer.dart +++ b/lib/ui/text_viewer.dart @@ -122,6 +122,7 @@ class _TextViewerPageState extends State { /// 넓은 글자 (한글 · 한자 · 가나 · 전각) 하나가 고정폭 글자 몇 개 폭인지 (관리자 지시 10/19: 넓은 글자가 섞인 줄도 줄 끝이 비지 않게) double _wideRatio(BuildContext context) { + // l10n-skip: 폭을 재는 넓은 글자 (화면 글이 아님) final m = _measure(context, 'M'), w = _measure(context, '가'); return m == null || w == null ? 2 : w / m; } @@ -158,7 +159,10 @@ class _TextViewerPageState extends State { _rows = viewerRows(_text, perRow, wide: _wideRatio(context), cut: (line, from) { var size = perRow * 8; while (true) { - final end = (from + size).clamp(0, line.length); + var end = (from + size).clamp(0, line.length); + // 28 지적 (10/19): 재는 조각이 이모지 같은 두 단위 글자의 가운데에서 끝나면 반쪽 글자라 TextSpan 이 + // "not well-formed UTF-16" 으로 실패했다 (화면은 정상이나 오류가 쏟아지고 더 큰 조각을 다시 잼) - 글자 앞에서 자른다 + if (end < line.length && end > from + 1 && (line.codeUnitAt(end) & 0xFC00) == 0xDC00) end--; tp.text = TextSpan(text: line.substring(from, end), style: style); tp.layout(maxWidth: room); final m = tp.computeLineMetrics(); diff --git a/test/xplore_tools_test.dart b/test/xplore_tools_test.dart index ee0ee57..3cefb02 100644 --- a/test/xplore_tools_test.dart +++ b/test/xplore_tools_test.dart @@ -143,6 +143,27 @@ void main() { expect(lines, 4, reason: '네 줄 분량으로 나눔 (다섯째 반쪽 줄 없음)'); }); + testWidgets('h 글 뷰어 (28 지적 10/19): 이모지가 든 긴 줄 - 재는 조각이 두 단위 글자 가운데서 끊기지 않음 (UTF-16 오류 없음)', (t) async { + final line = List.generate(3000, (i) => 'abc😀').join(); + final f = File(p.join(tmp.path, 'emoji.txt'))..writeAsStringSync('$line\nend'); + for (final w in [412.0, 960.0]) { + t.view.physicalSize = Size(w, 900); + t.view.devicePixelRatio = 1; + await t.pumpWidget(MaterialApp(home: TextViewerPage(key: ValueKey(w), path: f.path))); + for (var i = 0; i < 40 && find.textContaining('abc😀abc').evaluate().isEmpty; i++) { + await t.runAsync(() => Future.delayed(const Duration(milliseconds: 30))); + await t.pump(); + } + expect(t.takeException(), isNull, reason: '폭 $w'); + final texts = t.widgetList(find.textContaining('abc😀')).map((x) => x.data!).toList(); + expect(texts, isNotEmpty); + for (final s in texts) { + expect((s.codeUnitAt(s.length - 1) & 0xFC00) == 0xD800, isFalse, reason: '조각이 반쪽 이모지로 끝나지 않음'); + } + } + t.view.reset(); + }); + testWidgets('g 격자 보기: 그림 · 동영상만, 누르면 열기', (t) async { final png = File(p.join(tmp.path, 'a.png'))..writeAsBytesSync([0x89, 0x50]); final opened = [];