feat(pdf): typed-text tool (Windows-Ink friendly)
Some checks failed
CI / Windows build (push) Has been cancelled
Some checks failed
CI / Windows build (push) Has been cancelled
Add a text-annotation tool to the PDF editor. With the text tool a
pen-tap, or a mouse double-click, drops a text box at that normalized
page point and focuses a real Flutter TextField — so the OS IME and the
Windows-Ink handwriting panel feed it (device-validated). Tapping an
existing box re-opens it; clearing it deletes it.
- SidecarText {nx, ny, text, fontSize (page-relative), color} per page,
glued under zoom; stored in the sidecar `texts` field (back-compat
missing -> none), saved via scheduleTextsSave and loaded on open.
- Rendered in pageOverlaysBuilder at the scaled position.
PDF editor only for now (note text later). analyze clean, 397 tests.
This commit is contained in:
@@ -76,6 +76,21 @@ BadnoteSidecar _fullSidecar() => BadnoteSidecar(
|
||||
SidecarHighlight(l: 0.12, t: 0.20, r: 0.88, b: 0.235, color: 0xFFFFEB3B),
|
||||
],
|
||||
},
|
||||
texts: {
|
||||
0: const [
|
||||
SidecarText(
|
||||
id: 't0',
|
||||
nx: 0.25,
|
||||
ny: 0.33,
|
||||
text: 'Hello 手写',
|
||||
fontSize: 0.04,
|
||||
color: 0xFF112233,
|
||||
),
|
||||
],
|
||||
3: const [
|
||||
SidecarText(id: 't1', nx: 0.5, ny: 0.6, text: 'second'),
|
||||
],
|
||||
},
|
||||
bookmarks: [
|
||||
Bookmark(
|
||||
id: 'bm1',
|
||||
@@ -131,6 +146,11 @@ void main() {
|
||||
// Highlights.
|
||||
expect(reparsed.highlights[0], original.highlights[0]);
|
||||
|
||||
// Typed-text annotations (SidecarText value equality).
|
||||
expect(reparsed.texts.keys.toSet(), {0, 3});
|
||||
expect(reparsed.texts[0], original.texts[0]);
|
||||
expect(reparsed.texts[3], original.texts[3]);
|
||||
|
||||
// Bookmarks (Bookmark value equality via freezed).
|
||||
expect(reparsed.bookmarks, original.bookmarks);
|
||||
|
||||
@@ -214,11 +234,64 @@ void main() {
|
||||
final reparsed = _roundTrip(BadnoteSidecar());
|
||||
expect(reparsed.strokes, isEmpty);
|
||||
expect(reparsed.highlights, isEmpty);
|
||||
expect(reparsed.texts, isEmpty);
|
||||
expect(reparsed.bookmarks, isEmpty);
|
||||
expect(reparsed.scratchLinks, isEmpty);
|
||||
expect(reparsed.version, kBadnoteSidecarVersion);
|
||||
});
|
||||
|
||||
test('text annotation round-trips (page, nx/ny, text, fontSize, color)', () {
|
||||
final original = BadnoteSidecar(texts: {
|
||||
2: const [
|
||||
SidecarText(
|
||||
id: 'tx1',
|
||||
nx: 0.18,
|
||||
ny: 0.42,
|
||||
text: 'A typed note 手写测试',
|
||||
fontSize: 0.035,
|
||||
color: 0xFFAB12CD,
|
||||
),
|
||||
],
|
||||
});
|
||||
final reparsed = _roundTrip(original);
|
||||
expect(reparsed.texts.keys.toSet(), {2});
|
||||
final t = reparsed.texts[2]!.single;
|
||||
expect(t.id, 'tx1');
|
||||
expect(t.nx, 0.18);
|
||||
expect(t.ny, 0.42);
|
||||
expect(t.text, 'A typed note 手写测试');
|
||||
expect(t.fontSize, 0.035);
|
||||
expect(t.color, 0xFFAB12CD);
|
||||
// Full SidecarText value equality.
|
||||
expect(reparsed.texts[2], original.texts[2]);
|
||||
});
|
||||
|
||||
test('text JSON is byte-compatible with SidecarText.toJson', () {
|
||||
const tx = SidecarText(id: 't0', nx: 0.25, ny: 0.33, text: 'hi');
|
||||
final sidecar = BadnoteSidecar(texts: {1: const [tx]});
|
||||
final json = sidecar.toJson();
|
||||
final pageList = (json['texts'] as Map)['1'] as List;
|
||||
expect(pageList.single, tx.toJson());
|
||||
});
|
||||
|
||||
test('missing texts decodes to empty (back-compat)', () {
|
||||
// A sidecar authored before the texts field existed (no `texts` key).
|
||||
final json =
|
||||
jsonDecode(jsonEncode(BadnoteSidecar().toJson())) as Map<String, dynamic>;
|
||||
expect(json.containsKey('texts'), isFalse,
|
||||
reason: 'empty texts omitted (no key bloat for legacy sidecars)');
|
||||
final reparsed = BadnoteSidecar.fromJson(json);
|
||||
expect(reparsed.texts, isEmpty);
|
||||
});
|
||||
|
||||
test('SidecarText defaults fill in for a minimal JSON blob', () {
|
||||
// Only the required fields present; fontSize/color/text fall to defaults.
|
||||
final t = SidecarText.fromJson(const {'id': 'a', 'nx': 0.1, 'ny': 0.2});
|
||||
expect(t.text, '');
|
||||
expect(t.fontSize, 0.03);
|
||||
expect(t.color, 0xFF000000);
|
||||
});
|
||||
|
||||
test('paragraph-anchored bookmark round-trips its anchor + char index', () {
|
||||
final original = BadnoteSidecar(bookmarks: [
|
||||
Bookmark(
|
||||
|
||||
Reference in New Issue
Block a user