js.documents 6.1.1 → 6.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -74,28 +74,21 @@ function classifyAxisLine(seg) {
74
74
  };
75
75
  }
76
76
  const POSITION_DEDUPE_TOLERANCE_PT = .5;
77
- function longestContiguousSpan(segments) {
77
+ function mergedRanges(segments) {
78
78
  const sorted = [...segments].sort((a, b) => a.startPt - b.startPt);
79
- let bestStart = sorted[0].startPt;
80
- let bestEnd = sorted[0].endPt;
81
- let runStart = sorted[0].startPt;
82
- let runEnd = sorted[0].endPt;
79
+ const ranges = [{
80
+ startPt: sorted[0].startPt,
81
+ endPt: sorted[0].endPt
82
+ }];
83
83
  for (const segment of sorted.slice(1)) {
84
- if (segment.startPt <= runEnd + POSITION_DEDUPE_TOLERANCE_PT) runEnd = Math.max(runEnd, segment.endPt);
85
- else {
86
- runStart = segment.startPt;
87
- runEnd = segment.endPt;
88
- }
89
- if (runEnd - runStart > bestEnd - bestStart) {
90
- bestStart = runStart;
91
- bestEnd = runEnd;
92
- }
84
+ const last = ranges[ranges.length - 1];
85
+ if (segment.startPt <= last.endPt + POSITION_DEDUPE_TOLERANCE_PT) last.endPt = Math.max(last.endPt, segment.endPt);
86
+ else ranges.push({
87
+ startPt: segment.startPt,
88
+ endPt: segment.endPt
89
+ });
93
90
  }
94
- return {
95
- spanPt: bestEnd - bestStart,
96
- startPt: bestStart,
97
- endPt: bestEnd
98
- };
91
+ return ranges;
99
92
  }
100
93
  function dedupeAxisLines(segments, descending) {
101
94
  const sorted = [...segments].sort((a, b) => descending ? b.position - a.position : a.position - b.position);
@@ -105,24 +98,103 @@ function dedupeAxisLines(segments, descending) {
105
98
  if (last !== void 0 && Math.abs(candidate.position - last[0].position) <= POSITION_DEDUPE_TOLERANCE_PT) last.push(candidate);
106
99
  else groups.push([candidate]);
107
100
  }
108
- return groups.map((group) => {
109
- const { spanPt, startPt, endPt } = longestContiguousSpan(group);
110
- return {
111
- position: group[0].position,
112
- spanPt,
113
- startPt,
114
- endPt,
115
- items: group.map((segment) => segment.item)
116
- };
117
- });
101
+ return groups.map((group) => ({
102
+ position: group[0].position,
103
+ ranges: mergedRanges(group),
104
+ items: group.map((segment) => segment.item)
105
+ }));
118
106
  }
119
107
  const MIN_GRIDLINE_COUNT_PER_AXIS = 3;
120
- const GRID_SPAN_CONSISTENCY_RATIO = .9;
121
- function consistentLines(lines) {
122
- if (lines.length < MIN_GRIDLINE_COUNT_PER_AXIS) return [];
123
- const maxSpanPt = Math.max(...lines.map((l) => l.spanPt));
124
- const consistent = lines.filter((l) => l.spanPt >= maxSpanPt * GRID_SPAN_CONSISTENCY_RATIO);
125
- return consistent.length >= MIN_GRIDLINE_COUNT_PER_AXIS ? consistent : [];
108
+ const EDGE_COVERAGE_RATIO = .9;
109
+ function bestRunCoverageRatio(ranges, aPt, bPt) {
110
+ const lo = Math.min(aPt, bPt);
111
+ const hi = Math.max(aPt, bPt);
112
+ if (hi <= lo) return 1;
113
+ let bestPt = 0;
114
+ for (const range of ranges) {
115
+ const overlapLo = Math.max(range.startPt, lo);
116
+ const overlapHi = Math.min(range.endPt, hi);
117
+ if (overlapHi > overlapLo) bestPt = Math.max(bestPt, overlapHi - overlapLo);
118
+ }
119
+ return bestPt / (hi - lo);
120
+ }
121
+ function hasClosedOuterRectangle(rowLines, columnLines) {
122
+ const leftXPt = columnLines[0].position;
123
+ const rightXPt = columnLines[columnLines.length - 1].position;
124
+ const topYPt = rowLines[0].position;
125
+ const bottomYPt = rowLines[rowLines.length - 1].position;
126
+ return bestRunCoverageRatio(rowLines[0].ranges, leftXPt, rightXPt) >= EDGE_COVERAGE_RATIO && bestRunCoverageRatio(rowLines[rowLines.length - 1].ranges, leftXPt, rightXPt) >= EDGE_COVERAGE_RATIO && bestRunCoverageRatio(columnLines[0].ranges, bottomYPt, topYPt) >= EDGE_COVERAGE_RATIO && bestRunCoverageRatio(columnLines[columnLines.length - 1].ranges, bottomYPt, topYPt) >= EDGE_COVERAGE_RATIO;
127
+ }
128
+ function unionFind(size) {
129
+ return Array.from({ length: size }, (_, i) => i);
130
+ }
131
+ function findRoot(parents, index) {
132
+ let root = index;
133
+ while (parents[root] !== root) root = parents[root];
134
+ let current = index;
135
+ while (parents[current] !== root) {
136
+ const next = parents[current];
137
+ parents[current] = root;
138
+ current = next;
139
+ }
140
+ return root;
141
+ }
142
+ function union(parents, a, b) {
143
+ const rootA = findRoot(parents, a);
144
+ const rootB = findRoot(parents, b);
145
+ if (rootA !== rootB) parents[rootA] = rootB;
146
+ }
147
+ function findCellRegions(rowLines, columnLines) {
148
+ const rowCount = rowLines.length - 1;
149
+ const colCount = columnLines.length - 1;
150
+ const indexOf = (row, col) => row * colCount + col;
151
+ const parents = unionFind(rowCount * colCount);
152
+ for (let i = 0; i < rowCount; i++) {
153
+ const rowTopPt = rowLines[i].position;
154
+ const rowBottomPt = rowLines[i + 1].position;
155
+ for (let j = 0; j < colCount - 1; j++) {
156
+ const divider = columnLines[j + 1];
157
+ if (bestRunCoverageRatio(divider.ranges, rowBottomPt, rowTopPt) < EDGE_COVERAGE_RATIO) union(parents, indexOf(i, j), indexOf(i, j + 1));
158
+ }
159
+ }
160
+ for (let j = 0; j < colCount; j++) {
161
+ const colLeftPt = columnLines[j].position;
162
+ const colRightPt = columnLines[j + 1].position;
163
+ for (let i = 0; i < rowCount - 1; i++) {
164
+ const divider = rowLines[i + 1];
165
+ if (bestRunCoverageRatio(divider.ranges, colLeftPt, colRightPt) < EDGE_COVERAGE_RATIO) union(parents, indexOf(i, j), indexOf(i + 1, j));
166
+ }
167
+ }
168
+ const cellsByRoot = /* @__PURE__ */ new Map();
169
+ for (let i = 0; i < rowCount; i++) for (let j = 0; j < colCount; j++) {
170
+ const root = findRoot(parents, indexOf(i, j));
171
+ const cells = cellsByRoot.get(root);
172
+ if (cells === void 0) cellsByRoot.set(root, [{
173
+ row: i,
174
+ col: j
175
+ }]);
176
+ else cells.push({
177
+ row: i,
178
+ col: j
179
+ });
180
+ }
181
+ if (cellsByRoot.size <= 1) return;
182
+ const regions = [];
183
+ for (const cells of cellsByRoot.values()) {
184
+ const rowStart = Math.min(...cells.map((c) => c.row));
185
+ const rowEnd = Math.max(...cells.map((c) => c.row)) + 1;
186
+ const colStart = Math.min(...cells.map((c) => c.col));
187
+ const colEnd = Math.max(...cells.map((c) => c.col)) + 1;
188
+ if (cells.length !== (rowEnd - rowStart) * (colEnd - colStart)) return;
189
+ regions.push({
190
+ rowStart,
191
+ rowEnd,
192
+ colStart,
193
+ colEnd
194
+ });
195
+ }
196
+ regions.sort((a, b) => a.rowStart - b.rowStart || a.colStart - b.colStart);
197
+ return regions;
126
198
  }
127
199
  function detectGridLattice(items) {
128
200
  const horizontal = [];
@@ -132,14 +204,18 @@ function detectGridLattice(items) {
132
204
  if (classified === void 0) continue;
133
205
  (classified.axis === "horizontal" ? horizontal : vertical).push(classified);
134
206
  }
135
- const rowBoundaries = consistentLines(dedupeAxisLines(horizontal, true));
136
- const columnBoundaries = consistentLines(dedupeAxisLines(vertical, false));
137
- if (rowBoundaries.length === 0 || columnBoundaries.length === 0) return;
207
+ const rowLines = dedupeAxisLines(horizontal, true);
208
+ const columnLines = dedupeAxisLines(vertical, false);
209
+ if (rowLines.length < MIN_GRIDLINE_COUNT_PER_AXIS || columnLines.length < MIN_GRIDLINE_COUNT_PER_AXIS) return;
210
+ if (!hasClosedOuterRectangle(rowLines, columnLines)) return;
211
+ const regions = findCellRegions(rowLines, columnLines);
212
+ if (regions === void 0) return;
138
213
  const sourceItems = /* @__PURE__ */ new Set();
139
- for (const line of [...rowBoundaries, ...columnBoundaries]) for (const item of line.items) sourceItems.add(item);
214
+ for (const line of [...rowLines, ...columnLines]) for (const item of line.items) sourceItems.add(item);
140
215
  return {
141
- rowBoundariesDescPt: rowBoundaries.map((l) => l.position),
142
- columnBoundariesAscPt: columnBoundaries.map((l) => l.position),
216
+ rowBoundariesDescPt: rowLines.map((l) => l.position),
217
+ columnBoundariesAscPt: columnLines.map((l) => l.position),
218
+ regions,
143
219
  sourceItems
144
220
  };
145
221
  }
@@ -8,13 +8,20 @@ interface LineSegment {
8
8
  readonly y2Pt: number;
9
9
  }
10
10
  declare function extractLineCandidates(items: readonly LayoutItem[]): LineSegment[];
11
+ interface TableRegion {
12
+ readonly rowStart: number;
13
+ readonly rowEnd: number;
14
+ readonly colStart: number;
15
+ readonly colEnd: number;
16
+ }
11
17
  interface GridLattice {
12
18
  readonly rowBoundariesDescPt: readonly number[];
13
19
  readonly columnBoundariesAscPt: readonly number[];
20
+ readonly regions: readonly TableRegion[];
14
21
  readonly sourceItems: ReadonlySet<LayoutItem>;
15
22
  }
16
23
  declare function detectGridLattice(items: readonly LayoutItem[]): GridLattice | undefined;
17
24
  declare function findRowIndex(rowBoundariesDescPt: readonly number[], yPt: number): number | undefined;
18
25
  declare function findColumnIndex(columnBoundariesAscPt: readonly number[], xPt: number): number | undefined;
19
26
  //#endregion
20
- export { GridLattice, LineSegment, detectGridLattice, extractLineCandidates, findColumnIndex, findRowIndex };
27
+ export { GridLattice, LineSegment, TableRegion, detectGridLattice, extractLineCandidates, findColumnIndex, findRowIndex };
@@ -8,13 +8,20 @@ interface LineSegment {
8
8
  readonly y2Pt: number;
9
9
  }
10
10
  declare function extractLineCandidates(items: readonly LayoutItem[]): LineSegment[];
11
+ interface TableRegion {
12
+ readonly rowStart: number;
13
+ readonly rowEnd: number;
14
+ readonly colStart: number;
15
+ readonly colEnd: number;
16
+ }
11
17
  interface GridLattice {
12
18
  readonly rowBoundariesDescPt: readonly number[];
13
19
  readonly columnBoundariesAscPt: readonly number[];
20
+ readonly regions: readonly TableRegion[];
14
21
  readonly sourceItems: ReadonlySet<LayoutItem>;
15
22
  }
16
23
  declare function detectGridLattice(items: readonly LayoutItem[]): GridLattice | undefined;
17
24
  declare function findRowIndex(rowBoundariesDescPt: readonly number[], yPt: number): number | undefined;
18
25
  declare function findColumnIndex(columnBoundariesAscPt: readonly number[], xPt: number): number | undefined;
19
26
  //#endregion
20
- export { GridLattice, LineSegment, detectGridLattice, extractLineCandidates, findColumnIndex, findRowIndex };
27
+ export { GridLattice, LineSegment, TableRegion, detectGridLattice, extractLineCandidates, findColumnIndex, findRowIndex };
@@ -73,28 +73,21 @@ function classifyAxisLine(seg) {
73
73
  };
74
74
  }
75
75
  const POSITION_DEDUPE_TOLERANCE_PT = .5;
76
- function longestContiguousSpan(segments) {
76
+ function mergedRanges(segments) {
77
77
  const sorted = [...segments].sort((a, b) => a.startPt - b.startPt);
78
- let bestStart = sorted[0].startPt;
79
- let bestEnd = sorted[0].endPt;
80
- let runStart = sorted[0].startPt;
81
- let runEnd = sorted[0].endPt;
78
+ const ranges = [{
79
+ startPt: sorted[0].startPt,
80
+ endPt: sorted[0].endPt
81
+ }];
82
82
  for (const segment of sorted.slice(1)) {
83
- if (segment.startPt <= runEnd + POSITION_DEDUPE_TOLERANCE_PT) runEnd = Math.max(runEnd, segment.endPt);
84
- else {
85
- runStart = segment.startPt;
86
- runEnd = segment.endPt;
87
- }
88
- if (runEnd - runStart > bestEnd - bestStart) {
89
- bestStart = runStart;
90
- bestEnd = runEnd;
91
- }
83
+ const last = ranges[ranges.length - 1];
84
+ if (segment.startPt <= last.endPt + POSITION_DEDUPE_TOLERANCE_PT) last.endPt = Math.max(last.endPt, segment.endPt);
85
+ else ranges.push({
86
+ startPt: segment.startPt,
87
+ endPt: segment.endPt
88
+ });
92
89
  }
93
- return {
94
- spanPt: bestEnd - bestStart,
95
- startPt: bestStart,
96
- endPt: bestEnd
97
- };
90
+ return ranges;
98
91
  }
99
92
  function dedupeAxisLines(segments, descending) {
100
93
  const sorted = [...segments].sort((a, b) => descending ? b.position - a.position : a.position - b.position);
@@ -104,24 +97,103 @@ function dedupeAxisLines(segments, descending) {
104
97
  if (last !== void 0 && Math.abs(candidate.position - last[0].position) <= POSITION_DEDUPE_TOLERANCE_PT) last.push(candidate);
105
98
  else groups.push([candidate]);
106
99
  }
107
- return groups.map((group) => {
108
- const { spanPt, startPt, endPt } = longestContiguousSpan(group);
109
- return {
110
- position: group[0].position,
111
- spanPt,
112
- startPt,
113
- endPt,
114
- items: group.map((segment) => segment.item)
115
- };
116
- });
100
+ return groups.map((group) => ({
101
+ position: group[0].position,
102
+ ranges: mergedRanges(group),
103
+ items: group.map((segment) => segment.item)
104
+ }));
117
105
  }
118
106
  const MIN_GRIDLINE_COUNT_PER_AXIS = 3;
119
- const GRID_SPAN_CONSISTENCY_RATIO = .9;
120
- function consistentLines(lines) {
121
- if (lines.length < MIN_GRIDLINE_COUNT_PER_AXIS) return [];
122
- const maxSpanPt = Math.max(...lines.map((l) => l.spanPt));
123
- const consistent = lines.filter((l) => l.spanPt >= maxSpanPt * GRID_SPAN_CONSISTENCY_RATIO);
124
- return consistent.length >= MIN_GRIDLINE_COUNT_PER_AXIS ? consistent : [];
107
+ const EDGE_COVERAGE_RATIO = .9;
108
+ function bestRunCoverageRatio(ranges, aPt, bPt) {
109
+ const lo = Math.min(aPt, bPt);
110
+ const hi = Math.max(aPt, bPt);
111
+ if (hi <= lo) return 1;
112
+ let bestPt = 0;
113
+ for (const range of ranges) {
114
+ const overlapLo = Math.max(range.startPt, lo);
115
+ const overlapHi = Math.min(range.endPt, hi);
116
+ if (overlapHi > overlapLo) bestPt = Math.max(bestPt, overlapHi - overlapLo);
117
+ }
118
+ return bestPt / (hi - lo);
119
+ }
120
+ function hasClosedOuterRectangle(rowLines, columnLines) {
121
+ const leftXPt = columnLines[0].position;
122
+ const rightXPt = columnLines[columnLines.length - 1].position;
123
+ const topYPt = rowLines[0].position;
124
+ const bottomYPt = rowLines[rowLines.length - 1].position;
125
+ return bestRunCoverageRatio(rowLines[0].ranges, leftXPt, rightXPt) >= EDGE_COVERAGE_RATIO && bestRunCoverageRatio(rowLines[rowLines.length - 1].ranges, leftXPt, rightXPt) >= EDGE_COVERAGE_RATIO && bestRunCoverageRatio(columnLines[0].ranges, bottomYPt, topYPt) >= EDGE_COVERAGE_RATIO && bestRunCoverageRatio(columnLines[columnLines.length - 1].ranges, bottomYPt, topYPt) >= EDGE_COVERAGE_RATIO;
126
+ }
127
+ function unionFind(size) {
128
+ return Array.from({ length: size }, (_, i) => i);
129
+ }
130
+ function findRoot(parents, index) {
131
+ let root = index;
132
+ while (parents[root] !== root) root = parents[root];
133
+ let current = index;
134
+ while (parents[current] !== root) {
135
+ const next = parents[current];
136
+ parents[current] = root;
137
+ current = next;
138
+ }
139
+ return root;
140
+ }
141
+ function union(parents, a, b) {
142
+ const rootA = findRoot(parents, a);
143
+ const rootB = findRoot(parents, b);
144
+ if (rootA !== rootB) parents[rootA] = rootB;
145
+ }
146
+ function findCellRegions(rowLines, columnLines) {
147
+ const rowCount = rowLines.length - 1;
148
+ const colCount = columnLines.length - 1;
149
+ const indexOf = (row, col) => row * colCount + col;
150
+ const parents = unionFind(rowCount * colCount);
151
+ for (let i = 0; i < rowCount; i++) {
152
+ const rowTopPt = rowLines[i].position;
153
+ const rowBottomPt = rowLines[i + 1].position;
154
+ for (let j = 0; j < colCount - 1; j++) {
155
+ const divider = columnLines[j + 1];
156
+ if (bestRunCoverageRatio(divider.ranges, rowBottomPt, rowTopPt) < EDGE_COVERAGE_RATIO) union(parents, indexOf(i, j), indexOf(i, j + 1));
157
+ }
158
+ }
159
+ for (let j = 0; j < colCount; j++) {
160
+ const colLeftPt = columnLines[j].position;
161
+ const colRightPt = columnLines[j + 1].position;
162
+ for (let i = 0; i < rowCount - 1; i++) {
163
+ const divider = rowLines[i + 1];
164
+ if (bestRunCoverageRatio(divider.ranges, colLeftPt, colRightPt) < EDGE_COVERAGE_RATIO) union(parents, indexOf(i, j), indexOf(i + 1, j));
165
+ }
166
+ }
167
+ const cellsByRoot = /* @__PURE__ */ new Map();
168
+ for (let i = 0; i < rowCount; i++) for (let j = 0; j < colCount; j++) {
169
+ const root = findRoot(parents, indexOf(i, j));
170
+ const cells = cellsByRoot.get(root);
171
+ if (cells === void 0) cellsByRoot.set(root, [{
172
+ row: i,
173
+ col: j
174
+ }]);
175
+ else cells.push({
176
+ row: i,
177
+ col: j
178
+ });
179
+ }
180
+ if (cellsByRoot.size <= 1) return;
181
+ const regions = [];
182
+ for (const cells of cellsByRoot.values()) {
183
+ const rowStart = Math.min(...cells.map((c) => c.row));
184
+ const rowEnd = Math.max(...cells.map((c) => c.row)) + 1;
185
+ const colStart = Math.min(...cells.map((c) => c.col));
186
+ const colEnd = Math.max(...cells.map((c) => c.col)) + 1;
187
+ if (cells.length !== (rowEnd - rowStart) * (colEnd - colStart)) return;
188
+ regions.push({
189
+ rowStart,
190
+ rowEnd,
191
+ colStart,
192
+ colEnd
193
+ });
194
+ }
195
+ regions.sort((a, b) => a.rowStart - b.rowStart || a.colStart - b.colStart);
196
+ return regions;
125
197
  }
126
198
  function detectGridLattice(items) {
127
199
  const horizontal = [];
@@ -131,14 +203,18 @@ function detectGridLattice(items) {
131
203
  if (classified === void 0) continue;
132
204
  (classified.axis === "horizontal" ? horizontal : vertical).push(classified);
133
205
  }
134
- const rowBoundaries = consistentLines(dedupeAxisLines(horizontal, true));
135
- const columnBoundaries = consistentLines(dedupeAxisLines(vertical, false));
136
- if (rowBoundaries.length === 0 || columnBoundaries.length === 0) return;
206
+ const rowLines = dedupeAxisLines(horizontal, true);
207
+ const columnLines = dedupeAxisLines(vertical, false);
208
+ if (rowLines.length < MIN_GRIDLINE_COUNT_PER_AXIS || columnLines.length < MIN_GRIDLINE_COUNT_PER_AXIS) return;
209
+ if (!hasClosedOuterRectangle(rowLines, columnLines)) return;
210
+ const regions = findCellRegions(rowLines, columnLines);
211
+ if (regions === void 0) return;
137
212
  const sourceItems = /* @__PURE__ */ new Set();
138
- for (const line of [...rowBoundaries, ...columnBoundaries]) for (const item of line.items) sourceItems.add(item);
213
+ for (const line of [...rowLines, ...columnLines]) for (const item of line.items) sourceItems.add(item);
139
214
  return {
140
- rowBoundariesDescPt: rowBoundaries.map((l) => l.position),
141
- columnBoundariesAscPt: columnBoundaries.map((l) => l.position),
215
+ rowBoundariesDescPt: rowLines.map((l) => l.position),
216
+ columnBoundariesAscPt: columnLines.map((l) => l.position),
217
+ regions,
142
218
  sourceItems
143
219
  };
144
220
  }
@@ -1129,19 +1129,28 @@ function textItemsPdfBox(items) {
1129
1129
  function cellBlocksFromItems(items, pageIndex) {
1130
1130
  return clusterIntoLines(items).map((line) => lineToParagraph(line, pageIndex));
1131
1131
  }
1132
+ function indexRegions(lattice, rowCount, columnCount) {
1133
+ const regionAt = Array.from({ length: rowCount }, () => new Array(columnCount));
1134
+ for (const region of lattice.regions) for (let i = region.rowStart; i < region.rowEnd; i++) for (let j = region.colStart; j < region.colEnd; j++) regionAt[i][j] = region;
1135
+ return regionAt;
1136
+ }
1132
1137
  function recoverTable(page, pageIndex) {
1133
1138
  const lattice = require_layout_lattice.detectGridLattice(page.items);
1134
1139
  if (lattice === void 0) return;
1135
1140
  const rowCount = lattice.rowBoundariesDescPt.length - 1;
1136
1141
  const columnCount = lattice.columnBoundariesAscPt.length - 1;
1137
- const groups = /* @__PURE__ */ new Map();
1142
+ const regionAt = indexRegions(lattice, rowCount, columnCount);
1143
+ const textByRegion = /* @__PURE__ */ new Map();
1138
1144
  const consumedText = /* @__PURE__ */ new Set();
1139
1145
  for (const item of page.items) {
1140
1146
  if (item.kind !== "text") continue;
1141
1147
  const row = require_layout_lattice.findRowIndex(lattice.rowBoundariesDescPt, item.yPt);
1142
1148
  const column = require_layout_lattice.findColumnIndex(lattice.columnBoundariesAscPt, item.xPt);
1143
1149
  if (row === void 0 || column === void 0) continue;
1144
- addToGroup(groups, row, column, item);
1150
+ const region = regionAt[row][column];
1151
+ const existing = textByRegion.get(region);
1152
+ if (existing === void 0) textByRegion.set(region, [item]);
1153
+ else existing.push(item);
1145
1154
  consumedText.add(item);
1146
1155
  }
1147
1156
  if (consumedText.size === 0) return;
@@ -1150,12 +1159,25 @@ function recoverTable(page, pageIndex) {
1150
1159
  const rows = [];
1151
1160
  for (let i = 0; i < rowCount; i++) {
1152
1161
  const cells = [];
1153
- for (let j = 0; j < columnCount; j++) {
1154
- const cell = { blocks: cellBlocksFromItems(groups.get(groupKey(i, j)) ?? [], pageIndex) };
1155
- const cellLeftXPt = lattice.columnBoundariesAscPt[j];
1156
- const cellRightXPt = lattice.columnBoundariesAscPt[j + 1];
1157
- const cellTopYPt = lattice.rowBoundariesDescPt[i];
1158
- const cellBottomYPt = lattice.rowBoundariesDescPt[i + 1];
1162
+ let j = 0;
1163
+ while (j < columnCount) {
1164
+ const region = regionAt[i][j];
1165
+ if (region.rowStart !== i) {
1166
+ cells.push({ blocks: [] });
1167
+ j = region.colEnd;
1168
+ continue;
1169
+ }
1170
+ const colSpan = region.colEnd - region.colStart;
1171
+ const rowSpan = region.rowEnd - region.rowStart;
1172
+ const cell = {
1173
+ blocks: cellBlocksFromItems(textByRegion.get(region) ?? [], pageIndex),
1174
+ colSpan: colSpan > 1 ? colSpan : void 0,
1175
+ rowSpan: rowSpan > 1 ? rowSpan : void 0
1176
+ };
1177
+ const cellLeftXPt = lattice.columnBoundariesAscPt[region.colStart];
1178
+ const cellRightXPt = lattice.columnBoundariesAscPt[region.colEnd];
1179
+ const cellTopYPt = lattice.rowBoundariesDescPt[region.rowStart];
1180
+ const cellBottomYPt = lattice.rowBoundariesDescPt[region.rowEnd];
1159
1181
  require_layout_shared.stampFrame(cell, pageIndex, {
1160
1182
  xPt: cellLeftXPt,
1161
1183
  yPt: cellBottomYPt,
@@ -1163,6 +1185,7 @@ function recoverTable(page, pageIndex) {
1163
1185
  heightPt: cellTopYPt - cellBottomYPt
1164
1186
  });
1165
1187
  cells.push(cell);
1188
+ j = region.colEnd;
1166
1189
  }
1167
1190
  rows.push({
1168
1191
  cells,
@@ -1128,19 +1128,28 @@ function textItemsPdfBox(items) {
1128
1128
  function cellBlocksFromItems(items, pageIndex) {
1129
1129
  return clusterIntoLines(items).map((line) => lineToParagraph(line, pageIndex));
1130
1130
  }
1131
+ function indexRegions(lattice, rowCount, columnCount) {
1132
+ const regionAt = Array.from({ length: rowCount }, () => new Array(columnCount));
1133
+ for (const region of lattice.regions) for (let i = region.rowStart; i < region.rowEnd; i++) for (let j = region.colStart; j < region.colEnd; j++) regionAt[i][j] = region;
1134
+ return regionAt;
1135
+ }
1131
1136
  function recoverTable(page, pageIndex) {
1132
1137
  const lattice = detectGridLattice(page.items);
1133
1138
  if (lattice === void 0) return;
1134
1139
  const rowCount = lattice.rowBoundariesDescPt.length - 1;
1135
1140
  const columnCount = lattice.columnBoundariesAscPt.length - 1;
1136
- const groups = /* @__PURE__ */ new Map();
1141
+ const regionAt = indexRegions(lattice, rowCount, columnCount);
1142
+ const textByRegion = /* @__PURE__ */ new Map();
1137
1143
  const consumedText = /* @__PURE__ */ new Set();
1138
1144
  for (const item of page.items) {
1139
1145
  if (item.kind !== "text") continue;
1140
1146
  const row = findRowIndex(lattice.rowBoundariesDescPt, item.yPt);
1141
1147
  const column = findColumnIndex(lattice.columnBoundariesAscPt, item.xPt);
1142
1148
  if (row === void 0 || column === void 0) continue;
1143
- addToGroup(groups, row, column, item);
1149
+ const region = regionAt[row][column];
1150
+ const existing = textByRegion.get(region);
1151
+ if (existing === void 0) textByRegion.set(region, [item]);
1152
+ else existing.push(item);
1144
1153
  consumedText.add(item);
1145
1154
  }
1146
1155
  if (consumedText.size === 0) return;
@@ -1149,12 +1158,25 @@ function recoverTable(page, pageIndex) {
1149
1158
  const rows = [];
1150
1159
  for (let i = 0; i < rowCount; i++) {
1151
1160
  const cells = [];
1152
- for (let j = 0; j < columnCount; j++) {
1153
- const cell = { blocks: cellBlocksFromItems(groups.get(groupKey(i, j)) ?? [], pageIndex) };
1154
- const cellLeftXPt = lattice.columnBoundariesAscPt[j];
1155
- const cellRightXPt = lattice.columnBoundariesAscPt[j + 1];
1156
- const cellTopYPt = lattice.rowBoundariesDescPt[i];
1157
- const cellBottomYPt = lattice.rowBoundariesDescPt[i + 1];
1161
+ let j = 0;
1162
+ while (j < columnCount) {
1163
+ const region = regionAt[i][j];
1164
+ if (region.rowStart !== i) {
1165
+ cells.push({ blocks: [] });
1166
+ j = region.colEnd;
1167
+ continue;
1168
+ }
1169
+ const colSpan = region.colEnd - region.colStart;
1170
+ const rowSpan = region.rowEnd - region.rowStart;
1171
+ const cell = {
1172
+ blocks: cellBlocksFromItems(textByRegion.get(region) ?? [], pageIndex),
1173
+ colSpan: colSpan > 1 ? colSpan : void 0,
1174
+ rowSpan: rowSpan > 1 ? rowSpan : void 0
1175
+ };
1176
+ const cellLeftXPt = lattice.columnBoundariesAscPt[region.colStart];
1177
+ const cellRightXPt = lattice.columnBoundariesAscPt[region.colEnd];
1178
+ const cellTopYPt = lattice.rowBoundariesDescPt[region.rowStart];
1179
+ const cellBottomYPt = lattice.rowBoundariesDescPt[region.rowEnd];
1158
1180
  stampFrame(cell, pageIndex, {
1159
1181
  xPt: cellLeftXPt,
1160
1182
  yPt: cellBottomYPt,
@@ -1162,6 +1184,7 @@ function recoverTable(page, pageIndex) {
1162
1184
  heightPt: cellTopYPt - cellBottomYPt
1163
1185
  });
1164
1186
  cells.push(cell);
1187
+ j = region.colEnd;
1165
1188
  }
1166
1189
  rows.push({
1167
1190
  cells,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "js.documents",
3
- "version": "6.1.1",
3
+ "version": "6.1.3",
4
4
  "description": "Bidirectional docx/pptx <-> PDF conversion and a read+write editable OOXML document model, built on ooxml.js and Zod 4 codecs.",
5
5
  "type": "module",
6
6
  "repository": {
@@ -78,12 +78,12 @@
78
78
  "license": "MIT",
79
79
  "dependencies": {
80
80
  "byte-codec": "^1.2.0",
81
- "document-schema.js": "^5.1.0",
81
+ "document-schema.js": "^5.2.0",
82
82
  "fflate": "^0.8.3",
83
- "markdown-codec": "^6.1.0",
84
- "odf.js": "^6.1.0",
85
- "ooxml.js": "^6.1.0",
86
- "pdf-codec": "^3.5.0",
83
+ "markdown-codec": "^6.1.1",
84
+ "odf.js": "^6.1.1",
85
+ "ooxml.js": "^6.2.0",
86
+ "pdf-codec": "^3.5.1",
87
87
  "temml": "0.13.4",
88
88
  "zod": "^4.4.3"
89
89
  },