@worm-vue3-print/core 1.3.6 → 1.3.8

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.
package/dist/index.cjs CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * @worm-vue3-print/core v1.3.6
2
+ * @worm-vue3-print/core v1.3.8
3
3
  * Copyright (c) 2026 worm <liu_long_qqcom@qq.com>
4
4
  * SPDX-FileCopyrightText: 2026 worm <liu_long_qqcom@qq.com>
5
5
  * SPDX-License-Identifier: MIT
@@ -858,55 +858,55 @@ function formatDate(value, fmt) {
858
858
  const pad3 = (n) => String(n).padStart(2, "0");
859
859
  return fmt.replace("YYYY", String(date.getFullYear())).replace("MM", pad3(date.getMonth() + 1)).replace("DD", pad3(date.getDate())).replace("HH", pad3(date.getHours())).replace("mm", pad3(date.getMinutes())).replace("ss", pad3(date.getSeconds()));
860
860
  }
861
+ var CN_DIGITS = ["\u96F6", "\u58F9", "\u8D30", "\u53C1", "\u8086", "\u4F0D", "\u9646", "\u67D2", "\u634C", "\u7396"];
862
+ var CN_UNITS = ["", "\u62FE", "\u4F70", "\u4EDF"];
863
+ var CN_GROUP_UNITS = ["", "\u4E07", "\u4EBF", "\u4E07\u4EBF", "\u4EBF\u4EBF"];
864
+ function fourDigitToCapital(n) {
865
+ let out = "";
866
+ let zeroPending = false;
867
+ for (let pos = 3; pos >= 0; pos--) {
868
+ const d = Math.floor(n / 10 ** pos) % 10;
869
+ if (d === 0) {
870
+ if (out) zeroPending = true;
871
+ continue;
872
+ }
873
+ if (zeroPending) {
874
+ out += "\u96F6";
875
+ zeroPending = false;
876
+ }
877
+ out += CN_DIGITS[d] + CN_UNITS[pos];
878
+ }
879
+ return out;
880
+ }
861
881
  function toUpperCaseAmount(value) {
862
882
  const num = Number(value);
863
883
  if (isNaN(num)) return String(value);
864
884
  if (num === 0) return "\u96F6\u5143\u6574";
865
- const digits = ["\u96F6", "\u58F9", "\u8D30", "\u53C1", "\u8086", "\u4F0D", "\u9646", "\u67D2", "\u634C", "\u7396"];
866
- const units = ["", "\u62FE", "\u4F70", "\u4EDF"];
867
- const bigUnits = ["", "\u4E07", "\u4EBF"];
868
885
  const sign = num < 0 ? "\u8D1F" : "";
869
- const abs = Math.abs(num);
870
- const [intStr, decStr] = abs.toFixed(2).split(".");
871
- const intNum = parseInt(intStr, 10);
886
+ const [intStr, decStr] = Math.abs(num).toFixed(2).split(".");
887
+ const groupCount = Math.ceil(intStr.length / 4);
872
888
  let result = "";
873
- if (intNum > 0) {
874
- const intChars = intStr.split("").reverse();
875
- let groupIdx = 0;
876
- let lastDigit = 0;
877
- let needZero = false;
878
- for (let i = 0; i < intChars.length; i++) {
879
- const d = parseInt(intChars[i], 10);
880
- const unit = units[groupIdx % 4];
881
- const bigUnit = bigUnits[Math.floor(groupIdx / 4)];
882
- if (d === 0) {
883
- if (lastDigit !== 0) needZero = true;
884
- } else {
885
- if (needZero) {
886
- result = "\u96F6" + result;
887
- needZero = false;
888
- }
889
- result = digits[d] + unit + result;
890
- lastDigit = d;
891
- }
892
- if (groupIdx % 4 === 3 && groupIdx > 0) {
893
- result = bigUnit + result;
894
- }
895
- groupIdx++;
889
+ let zeroPending = false;
890
+ for (let groupIdx = groupCount - 1; groupIdx >= 0; groupIdx--) {
891
+ const end = intStr.length - groupIdx * 4;
892
+ const group = parseInt(intStr.slice(Math.max(0, end - 4), end), 10);
893
+ if (group === 0) {
894
+ if (result) zeroPending = true;
895
+ continue;
896
896
  }
897
- result += "\u5143";
897
+ if (result && (zeroPending || group < 1e3)) result += "\u96F6";
898
+ zeroPending = false;
899
+ result += fourDigitToCapital(group) + (CN_GROUP_UNITS[groupIdx] ?? "");
898
900
  }
901
+ result = `${result || "\u96F6"}\u5143`;
899
902
  const jiao = parseInt(decStr[0], 10);
900
903
  const fen = parseInt(decStr[1], 10);
901
904
  if (jiao === 0 && fen === 0) {
902
905
  result += "\u6574";
903
906
  } else {
904
- if (jiao > 0) {
905
- result += digits[jiao] + "\u89D2";
906
- }
907
- if (fen > 0) {
908
- result += digits[fen] + "\u5206";
909
- }
907
+ if (jiao === 0) result += "\u96F6";
908
+ else result += CN_DIGITS[jiao] + "\u89D2";
909
+ if (fen > 0) result += CN_DIGITS[fen] + "\u5206";
910
910
  }
911
911
  return sign + result;
912
912
  }
@@ -1189,9 +1189,12 @@ function makeRenderRow(row, resolve, keepRaw = false) {
1189
1189
  cellType: cell.cellType,
1190
1190
  barcodeType: cell.barcodeType,
1191
1191
  qrCodeLevel: cell.qrCodeLevel,
1192
+ qrQuietZone: cell.qrQuietZone,
1192
1193
  showBarcodeText: cell.showBarcodeText,
1193
1194
  printerDpi: cell.printerDpi,
1194
1195
  fit: cell.fit,
1196
+ objectPosition: cell.objectPosition,
1197
+ fitScale: cell.fitScale,
1195
1198
  maxWidth: cell.maxWidth,
1196
1199
  maxHeight: cell.maxHeight,
1197
1200
  rowspan: cell.rowspan ?? 1,
@@ -1700,6 +1703,9 @@ function parseCellFitKey(key) {
1700
1703
  if (!Number.isFinite(rowIndex) || !Number.isFinite(colIndex)) return void 0;
1701
1704
  return { elementId: key.slice(0, first), kind, rowIndex, colIndex };
1702
1705
  }
1706
+ function cellImageBoxMm(fit, capMm) {
1707
+ return (fit || "contain") === "cover" ? capMm : void 0;
1708
+ }
1703
1709
  function cellFitWidthMm(colWidths, colIndex, cell, defaultPadding = 1) {
1704
1710
  const span = Math.max(cell?.colspan ?? 1, 1);
1705
1711
  let width = 0;
@@ -1751,6 +1757,35 @@ function resolveElementBorder(type, opts) {
1751
1757
  return { all, edges };
1752
1758
  }
1753
1759
 
1760
+ // src/render/crop-style.ts
1761
+ var OBJECT_POSITION_UNSAFE = /[;"'<>\\(){}\[\]@!#]/;
1762
+ var OBJECT_POSITION_MAX_LEN = 64;
1763
+ var CROP_SCALE_MIN = 100;
1764
+ var CROP_SCALE_MAX = 400;
1765
+ function sanitizeObjectPosition(value) {
1766
+ const v = (value ?? "").trim();
1767
+ if (!v || v.length > OBJECT_POSITION_MAX_LEN || OBJECT_POSITION_UNSAFE.test(v)) return "";
1768
+ return v;
1769
+ }
1770
+ function cropScalePct(fit, scale) {
1771
+ if ((fit || "contain") !== "cover") return void 0;
1772
+ if (typeof scale !== "number" || !Number.isFinite(scale)) return void 0;
1773
+ const pct = Math.round(scale);
1774
+ if (pct <= CROP_SCALE_MIN) return void 0;
1775
+ return Math.min(pct, CROP_SCALE_MAX);
1776
+ }
1777
+ function cropStyleDeclarations(fit, scale, objectPosition) {
1778
+ const pos = sanitizeObjectPosition(objectPosition);
1779
+ const out = {};
1780
+ if (pos) out.objectPosition = pos;
1781
+ const pct = cropScalePct(fit, scale);
1782
+ if (pct !== void 0) {
1783
+ out.transform = `scale(${Number((pct / 100).toFixed(2))})`;
1784
+ out.transformOrigin = pos || "50% 50%";
1785
+ }
1786
+ return out;
1787
+ }
1788
+
1754
1789
  // src/render/watermark.ts
1755
1790
  var PX_PER_MM = 96 / 25.4;
1756
1791
  var MM_PER_PX = 25.4 / 96;
@@ -1952,6 +1987,17 @@ function followGroupHeight(el, members, template, _measuredElements) {
1952
1987
  function effectiveHeight(el, measured) {
1953
1988
  return Math.max(el.options?.height ?? 0, measured.get(el.id)?.measuredHeight ?? 0);
1954
1989
  }
1990
+ function xRange(el) {
1991
+ const o = el.options ?? {};
1992
+ const x0 = o.left ?? 0;
1993
+ return [x0, x0 + (o.width ?? 100)];
1994
+ }
1995
+ function xIntersects(a, b) {
1996
+ return a[0] < b[1] && b[0] < a[1];
1997
+ }
1998
+ function xUnion(a, b) {
1999
+ return [Math.min(a[0], b[0]), Math.max(a[1], b[1])];
2000
+ }
1955
2001
  function buildPaginationUnits(sorted, measured, groupMap) {
1956
2002
  const unitOf = /* @__PURE__ */ new Map();
1957
2003
  for (const members of groupMap.values()) {
@@ -1959,7 +2005,8 @@ function buildPaginationUnits(sorted, measured, groupMap) {
1959
2005
  const unit = {
1960
2006
  anchorId: members[0].id,
1961
2007
  ids: members.map((m) => m.id),
1962
- forceFirstPage: members.some((m) => paginationOf(m)?.pageable === false)
2008
+ forceFirstPage: members.some((m) => paginationOf(m)?.pageable === false),
2009
+ band: members.map(xRange).reduce(xUnion)
1963
2010
  };
1964
2011
  for (const m of members) unitOf.set(m.id, unit);
1965
2012
  }
@@ -1991,11 +2038,13 @@ function buildPaginationUnits(sorted, measured, groupMap) {
1991
2038
  if (active[i].bottom <= top) active.splice(i, 1);
1992
2039
  }
1993
2040
  const bottom = top + effectiveHeight(el, measured);
2041
+ const x = xRange(el);
1994
2042
  for (const a of active) {
1995
- if (a.bottom > top) union(el.id, a.id);
2043
+ if (a.bottom > top && xIntersects(a.x, x)) union(el.id, a.id);
1996
2044
  }
1997
- active.push({ id: el.id, top, bottom });
2045
+ active.push({ id: el.id, top, bottom, x });
1998
2046
  }
2047
+ const eligibleById = new Map(eligible.map((e) => [e.id, e]));
1999
2048
  const clusters = /* @__PURE__ */ new Map();
2000
2049
  for (const el of eligible) {
2001
2050
  const root = find(el.id);
@@ -2005,7 +2054,12 @@ function buildPaginationUnits(sorted, measured, groupMap) {
2005
2054
  }
2006
2055
  for (const ids of clusters.values()) {
2007
2056
  if (ids.length < 2) continue;
2008
- const unit = { anchorId: ids[0], ids, forceFirstPage: false };
2057
+ const unit = {
2058
+ anchorId: ids[0],
2059
+ ids,
2060
+ forceFirstPage: false,
2061
+ band: ids.map((id) => xRange(eligibleById.get(id))).reduce(xUnion)
2062
+ };
2009
2063
  for (const id of ids) unitOf.set(id, unit);
2010
2064
  }
2011
2065
  return unitOf;
@@ -2054,17 +2108,30 @@ function paginate(template, measuredElements) {
2054
2108
  const unitOf = buildPaginationUnits(sorted, measuredElements, groupMap);
2055
2109
  const pages = [];
2056
2110
  let currentPage = [];
2057
- let remaining = contentHeight - SAFETY_MARGIN;
2111
+ let occupied = [];
2058
2112
  let pageBroken = false;
2059
- function sectionTop(el) {
2060
- return pageBroken ? pageCursorTop() : el.options?.top ?? 0;
2061
- }
2062
- function pageCursorTop() {
2063
- return Math.max(0, fullPageHeight() - remaining);
2064
- }
2065
2113
  function fullPageHeight() {
2066
2114
  return contentHeight - SAFETY_MARGIN;
2067
2115
  }
2116
+ function depthIn(x) {
2117
+ let used = 0;
2118
+ for (const r of occupied) {
2119
+ if (xIntersects(x, [r.x0, r.x1])) used += r.height;
2120
+ }
2121
+ return Math.max(0, used);
2122
+ }
2123
+ function remainingIn(x) {
2124
+ return fullPageHeight() - depthIn(x);
2125
+ }
2126
+ function occupy(x, height) {
2127
+ occupied.push({ x0: x[0], x1: x[1], height });
2128
+ }
2129
+ function sectionTop(el, x = xRange(el)) {
2130
+ return pageBroken ? pageCursorTop(x) : el.options?.top ?? 0;
2131
+ }
2132
+ function pageCursorTop(x) {
2133
+ return Math.max(0, fullPageHeight() - remainingIn(x));
2134
+ }
2068
2135
  let overflowOnCurrent = false;
2069
2136
  function pushPage() {
2070
2137
  pages.push(
@@ -2081,7 +2148,7 @@ function paginate(template, measuredElements) {
2081
2148
  }
2082
2149
  pushPage();
2083
2150
  currentPage = [];
2084
- remaining = contentHeight - SAFETY_MARGIN;
2151
+ occupied = [];
2085
2152
  pageBroken = true;
2086
2153
  overflowOnCurrent = false;
2087
2154
  }
@@ -2125,31 +2192,32 @@ function paginate(template, measuredElements) {
2125
2192
  return pages;
2126
2193
  function paginateNonTable(el, measured, sortedList, idx) {
2127
2194
  const elHeight = measured.get(el.id)?.measuredHeight ?? el.options?.height ?? 0;
2128
- if (elHeight <= remaining) {
2129
- const top2 = pageBroken ? pageCursorTop() : el.options?.top ?? 0;
2130
- noteOverflow(top2 + elHeight);
2131
- currentPage.push({ elementId: el.id, type: "element", renderTop: sectionTop(el) });
2132
- remaining -= elHeight;
2195
+ const x = xRange(el);
2196
+ const place = () => {
2197
+ const top = sectionTop(el, x);
2198
+ noteOverflow(top + elHeight);
2199
+ currentPage.push({ elementId: el.id, type: "element", renderTop: top });
2200
+ occupy(x, elHeight);
2201
+ };
2202
+ if (elHeight <= remainingIn(x)) {
2203
+ place();
2133
2204
  if (paginationOf(el)?.keepWithNext && idx + 1 < sortedList.length) {
2134
2205
  const nextEl = sortedList[idx + 1];
2135
2206
  const nextHeight = measured.get(nextEl.id)?.measuredHeight ?? nextEl.options?.height ?? 0;
2136
2207
  if (elHeight + nextHeight > fullPageHeight()) {
2137
2208
  return idx + 1;
2138
2209
  }
2139
- if (nextHeight > remaining) {
2210
+ if (nextHeight > remainingIn(x)) {
2140
2211
  currentPage.pop();
2212
+ occupied.pop();
2141
2213
  finishPage();
2142
- currentPage.push({ elementId: el.id, type: "element", renderTop: sectionTop(el) });
2143
- remaining -= elHeight;
2214
+ place();
2144
2215
  }
2145
2216
  }
2146
2217
  return idx + 1;
2147
2218
  }
2148
2219
  finishPage();
2149
- const top = pageBroken ? pageCursorTop() : el.options?.top ?? 0;
2150
- if (top + elHeight > contentHeight) overflowOnCurrent = true;
2151
- currentPage.push({ elementId: el.id, type: "element", renderTop: sectionTop(el) });
2152
- remaining -= elHeight;
2220
+ place();
2153
2221
  return idx + 1;
2154
2222
  }
2155
2223
  function paginateUnit(unit, idx) {
@@ -2162,8 +2230,9 @@ function paginate(template, measuredElements) {
2162
2230
  maxBottom = Math.max(maxBottom, top2 + effectiveHeight(m, measuredElements));
2163
2231
  }
2164
2232
  const unitHeight = Math.max(maxBottom - minTop, 0);
2233
+ const band = unit.band;
2165
2234
  const place = () => {
2166
- const offset = pageBroken ? pageCursorTop() - minTop : 0;
2235
+ const offset = pageBroken ? pageCursorTop(band) - minTop : 0;
2167
2236
  noteOverflow(minTop + offset + unitHeight);
2168
2237
  for (const m of members) {
2169
2238
  currentPage.push({
@@ -2172,9 +2241,9 @@ function paginate(template, measuredElements) {
2172
2241
  renderTop: (m.options?.top ?? 0) + offset
2173
2242
  });
2174
2243
  }
2175
- remaining -= unitHeight;
2244
+ occupy(band, unitHeight);
2176
2245
  };
2177
- if (unitHeight <= remaining) {
2246
+ if (unitHeight <= remainingIn(band)) {
2178
2247
  place();
2179
2248
  return idx + 1;
2180
2249
  }
@@ -2198,7 +2267,8 @@ function paginate(template, measuredElements) {
2198
2267
  const rowCount = Math.min(bodyRows.length, bodyHeights.length);
2199
2268
  const repeatCount = opts._repeatHeaderCount ?? 0;
2200
2269
  const repeatH = measured.get(el.id)?.repeatHeaderHeight ?? rowHeights.slice(0, repeatCount).reduce((s, h) => s + h, 0);
2201
- const freshAvail = contentHeight - SAFETY_MARGIN;
2270
+ const freshAvail = fullPageHeight();
2271
+ const tb = xRange(el);
2202
2272
  if (repeatH > freshAvail) {
2203
2273
  throw new Error(
2204
2274
  `\u8868\u683C ${el.id} \u7684\u91CD\u590D\u8868\u5934\u9AD8\u5EA6 (${repeatH.toFixed(1)}mm) \u8D85\u8FC7\u9875\u9762\u53EF\u7528\u9AD8\u5EA6 (${freshAvail.toFixed(1)}mm)\uFF0C\u8BF7\u51CF\u5C11\u91CD\u590D\u8868\u5934\u884C`
@@ -2211,16 +2281,16 @@ function paginate(template, measuredElements) {
2211
2281
  startRow: 0,
2212
2282
  endRow: 0,
2213
2283
  summary: summaryH > 0,
2214
- renderTop: sectionTop(el)
2284
+ renderTop: sectionTop(el, tb)
2215
2285
  });
2216
- remaining -= summaryH;
2217
- applyFollowGroup(el, measured);
2286
+ occupy(tb, summaryH);
2287
+ applyFollowGroup(el, measured, tb);
2218
2288
  return idx + 1;
2219
2289
  }
2220
2290
  const totalH = bodyHeights.slice(0, rowCount).reduce((s, h) => s + h, 0);
2221
2291
  const totalNeed = totalH + subtotalH + summaryH;
2222
2292
  if (tablePaginationOf(el)?.enabled === false) {
2223
- if (totalNeed > remaining && currentPage.length > 0) finishPage();
2293
+ if (totalNeed > remainingIn(tb) && currentPage.length > 0) finishPage();
2224
2294
  currentPage.push({
2225
2295
  elementId: el.id,
2226
2296
  type: "table-slice",
@@ -2228,78 +2298,78 @@ function paginate(template, measuredElements) {
2228
2298
  endRow: rowCount,
2229
2299
  subtotal: subtotalH > 0,
2230
2300
  summary: summaryH > 0,
2231
- renderTop: sectionTop(el)
2301
+ renderTop: sectionTop(el, tb)
2232
2302
  });
2233
- remaining -= totalNeed;
2234
- applyFollowGroup(el, measured);
2303
+ occupy(tb, totalNeed);
2304
+ applyFollowGroup(el, measured, tb);
2235
2305
  return idx + 1;
2236
2306
  }
2237
2307
  const groups = buildRowGroups(bodyRows, rowCount);
2238
2308
  let sliceStart = 0;
2239
2309
  let firstSlice = true;
2240
- let sliceTop = sectionTop(el);
2310
+ let sliceTop = sectionTop(el, tb);
2311
+ let sliceUsed = 0;
2312
+ const avail = () => remainingIn(tb) - sliceUsed;
2313
+ const pushSlice = (endRow, repeatHeader) => {
2314
+ currentPage.push({
2315
+ elementId: el.id,
2316
+ type: "table-slice",
2317
+ startRow: sliceStart,
2318
+ endRow,
2319
+ subtotal: subtotalH > 0,
2320
+ ...!firstSlice && repeatHeader ? { repeatHeader: true } : {},
2321
+ renderTop: sliceTop
2322
+ });
2323
+ occupy(tb, sliceUsed);
2324
+ sliceUsed = 0;
2325
+ };
2241
2326
  for (const g of groups) {
2242
2327
  const gh = bodyHeights.slice(g.start, g.end).reduce((s, h) => s + h, 0);
2243
- if (gh + subtotalH <= remaining) {
2244
- remaining -= gh;
2328
+ if (gh + subtotalH <= avail()) {
2329
+ sliceUsed += gh;
2245
2330
  continue;
2246
2331
  }
2247
2332
  if (g.start > sliceStart) {
2248
- currentPage.push({
2249
- elementId: el.id,
2250
- type: "table-slice",
2251
- startRow: sliceStart,
2252
- endRow: g.start,
2253
- subtotal: subtotalH > 0,
2254
- ...!firstSlice && repeatCount > 0 ? { repeatHeader: true } : {},
2255
- renderTop: sliceTop
2256
- });
2333
+ pushSlice(g.start, repeatCount > 0);
2257
2334
  firstSlice = false;
2258
2335
  sliceStart = g.start;
2259
2336
  }
2260
2337
  finishPage();
2261
- sliceTop = sectionTop(el);
2262
- if (!firstSlice) remaining -= repeatH;
2263
- remaining -= gh;
2338
+ sliceTop = sectionTop(el, tb);
2339
+ sliceUsed = 0;
2340
+ if (!firstSlice) sliceUsed += repeatH;
2341
+ sliceUsed += gh;
2264
2342
  }
2265
2343
  if (sliceStart < rowCount) {
2266
- currentPage.push({
2267
- elementId: el.id,
2268
- type: "table-slice",
2269
- startRow: sliceStart,
2270
- endRow: rowCount,
2271
- subtotal: subtotalH > 0,
2272
- ...!firstSlice && repeatCount > 0 ? { repeatHeader: true } : {},
2273
- renderTop: sliceTop
2274
- });
2344
+ pushSlice(rowCount, repeatCount > 0);
2275
2345
  }
2276
2346
  if (summaryH > 0) {
2277
- if (summaryH <= remaining) {
2347
+ if (summaryH <= avail()) {
2278
2348
  const lastIdx = currentPage.length - 1;
2279
2349
  const last = currentPage[lastIdx];
2280
2350
  if (last && last.type === "table-slice" && last.elementId === el.id) {
2281
2351
  currentPage[lastIdx] = { ...last, summary: true };
2282
- remaining -= summaryH;
2352
+ occupy(tb, summaryH);
2283
2353
  } else {
2284
2354
  finishPage();
2285
2355
  currentPage.push({ elementId: el.id, type: "table-slice", startRow: rowCount, endRow: rowCount, summary: true, renderTop: 0 });
2286
- remaining -= summaryH;
2356
+ occupy(tb, summaryH);
2287
2357
  }
2288
2358
  } else {
2289
2359
  finishPage();
2290
2360
  currentPage.push({ elementId: el.id, type: "table-slice", startRow: rowCount, endRow: rowCount, summary: true, renderTop: 0 });
2291
- remaining -= summaryH;
2361
+ occupy(tb, summaryH);
2292
2362
  }
2293
2363
  }
2294
- applyFollowGroup(el, measured);
2364
+ applyFollowGroup(el, measured, tb);
2295
2365
  return idx + 1;
2296
2366
  }
2297
- function applyFollowGroup(el, measured) {
2367
+ function applyFollowGroup(el, measured, band) {
2298
2368
  const members = followMap.get(el.id) ?? [];
2299
2369
  if (members.length === 0) return;
2300
2370
  const followH = followGroupHeight(el, members, template, measured);
2301
2371
  const opts = el.options ?? {};
2302
- if (followH <= remaining) {
2372
+ if (followH <= remainingIn(band)) {
2303
2373
  const lastIdx = currentPage.length - 1;
2304
2374
  const last = currentPage[lastIdx];
2305
2375
  if (last && last.type === "table-slice" && last.elementId === el.id) {
@@ -2310,7 +2380,7 @@ function paginate(template, measuredElements) {
2310
2380
  // 沿用该切片在本页的定位 top(续片已 stamp 为 0)
2311
2381
  groupTop: last.renderTop ?? opts.top ?? 0
2312
2382
  };
2313
- remaining -= followH;
2383
+ occupy(band, followH);
2314
2384
  return;
2315
2385
  }
2316
2386
  }
@@ -2323,7 +2393,7 @@ function paginate(template, measuredElements) {
2323
2393
  followElementIds: members,
2324
2394
  groupTop: 0
2325
2395
  });
2326
- remaining -= followH;
2396
+ occupy(band, followH);
2327
2397
  }
2328
2398
  }
2329
2399
  function buildRowGroups(renderRows, rowCount) {
@@ -2537,12 +2607,23 @@ function textStyle(opts) {
2537
2607
  if (opts.lineHeight) parts.push(`line-height:${opts.lineHeight}pt`);
2538
2608
  if (opts.letterSpacing) parts.push(`letter-spacing:${opts.letterSpacing}pt`);
2539
2609
  if (opts.verticalAlign) {
2540
- parts.push(`display:flex;align-items:${V_ALIGN_FLEX[opts.verticalAlign] ?? "flex-start"}`);
2541
- parts.push(`justify-content:${H_ALIGN_FLEX[opts.textAlign ?? "left"] ?? "flex-start"}`);
2610
+ if (opts.textAlign === "justify") {
2611
+ parts.push(`display:flex;flex-direction:column;justify-content:${V_ALIGN_FLEX[opts.verticalAlign] ?? "flex-start"}`);
2612
+ } else {
2613
+ parts.push(`display:flex;align-items:${V_ALIGN_FLEX[opts.verticalAlign] ?? "flex-start"}`);
2614
+ parts.push(`justify-content:${H_ALIGN_FLEX[opts.textAlign ?? "left"] ?? "flex-start"}`);
2615
+ }
2542
2616
  }
2543
2617
  if (opts.textAlign) parts.push(`text-align:${opts.textAlign}`);
2618
+ if (opts.textAlign === "justify") parts.push("text-align-last:justify");
2544
2619
  return parts.length ? parts.join(";") + ";" : "";
2545
2620
  }
2621
+ function indentStyle(opts) {
2622
+ return opts.longTextIndent ? `text-indent:${opts.longTextIndent}em;` : "";
2623
+ }
2624
+ function cropStyle(fit, fitScale, objectPosition) {
2625
+ return Object.entries(cropStyleDeclarations(fit, fitScale, objectPosition)).map(([key, value]) => `${key.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`)}:${escapeInlineStyleValue(value)};`).join("");
2626
+ }
2546
2627
  function elementFitStyle(fit, opts) {
2547
2628
  if (fit === "autoHeight") return "overflow:visible;";
2548
2629
  if (opts.wordWrap === false) return "white-space:nowrap;text-overflow:ellipsis;";
@@ -2581,7 +2662,7 @@ function renderElement(el, isMeasure, containerStyle, overrideTop, ctx) {
2581
2662
  return renderTableElement(el, isMeasure, measureAttr, ctx);
2582
2663
  case "image":
2583
2664
  return `<div class="print-element" style="${style}"${measureAttr}>
2584
- <img src="${esc(opts.src ?? "")}" style="width:100%;height:100%;object-fit:contain;" />
2665
+ <img src="${esc(opts.src ?? "")}" style="width:100%;height:100%;object-fit:${opts.fit || "contain"};${cropStyle(opts.fit, opts.fitScale, opts.objectPosition)}" />
2585
2666
  </div>`;
2586
2667
  case "barcode":
2587
2668
  case "qrcode": {
@@ -2608,7 +2689,7 @@ function renderElement(el, isMeasure, containerStyle, overrideTop, ctx) {
2608
2689
  case "oval":
2609
2690
  return `<div class="print-element" style="${style};border:${opts.borderWidth || 1}pt ${opts.borderStyle || "solid"} ${opts.borderColor || "#333"};border-radius:50%;"${measureAttr}></div>`;
2610
2691
  case "longText":
2611
- return `<div class="print-element" style="${style}${textStyle(opts)}${elementFitStyle(fit, opts)}"${measureAttr}${fitAttr}>${esc(opts.formatter ?? opts.testData ?? "")}</div>`;
2692
+ return `<div class="print-element" style="${style}${textStyle(opts)}${indentStyle(opts)}${elementFitStyle(fit, opts)}"${measureAttr}${fitAttr}>${esc(opts.formatter ?? opts.testData ?? "")}</div>`;
2612
2693
  case "html":
2613
2694
  return `<div class="print-element" style="${style}"${measureAttr}>${opts.testData ?? opts.title ?? ""}</div>`;
2614
2695
  default:
@@ -2623,6 +2704,8 @@ function codeImgHtml(value, cellType, opts, io) {
2623
2704
  const svg = codeRenderer.render(value, cellType, {
2624
2705
  barcodeType: opts.barcodeType,
2625
2706
  qrCodeLevel: opts.qrCodeLevel != null ? String(opts.qrCodeLevel) : void 0,
2707
+ // 0 是合法档(无白边),必须用 typeof 判定而非 or/?? 之类假值判断
2708
+ qrQuietZone: typeof opts.qrQuietZone === "number" ? opts.qrQuietZone : void 0,
2626
2709
  showText: opts.hideTitle !== void 0 ? !opts.hideTitle : opts.showBarcodeText,
2627
2710
  barWidth: typeof opts.barWidth === "number" ? opts.barWidth : void 0,
2628
2711
  fontSize: typeof opts.fontSize === "number" ? opts.fontSize : void 0,
@@ -2633,10 +2716,8 @@ function codeImgHtml(value, cellType, opts, io) {
2633
2716
  if (isBarcode && /^<svg[\s>]/i.test(svg)) return inlineCodeSvgHtml(svg);
2634
2717
  const src = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`;
2635
2718
  const styleParts = [];
2636
- styleParts.push("max-width:100%", "max-height:100%", "display:block", "margin:auto");
2637
- if (fit) {
2638
- styleParts.push(`object-fit:${fit}`);
2639
- }
2719
+ styleParts.push("width:100%", "height:100%", "max-width:100%", "max-height:100%", "display:block", "margin:auto");
2720
+ styleParts.push(`object-fit:${fit || "contain"}`);
2640
2721
  if (maxWidth) {
2641
2722
  styleParts.push(`max-width:${maxWidth}mm`);
2642
2723
  }
@@ -2664,6 +2745,7 @@ function matrixCellStyle(cell, opts) {
2664
2745
  if (color) parts.push(`color:${color}`);
2665
2746
  if (cell.backgroundColor) parts.push(`background-color:${cell.backgroundColor}`);
2666
2747
  parts.push(`text-align:${cell.align ?? "left"}`);
2748
+ if (cell.align === "justify") parts.push("text-align-last:justify");
2667
2749
  parts.push(`vertical-align:${cell.valign ?? "middle"}`);
2668
2750
  parts.push(`padding:${padding}mm`);
2669
2751
  parts.push(cell.wordWrap === false ? "white-space:nowrap;overflow:hidden" : "word-break:break-all");
@@ -2703,7 +2785,10 @@ function renderMatrixRows(renderRows, start, end, opts, withRowIndex, ctx, fitOw
2703
2785
  const fit = cell.fit || "contain";
2704
2786
  const maxWidth = cell.maxWidth ? `max-width:${cell.maxWidth}mm;` : "max-width:100%;";
2705
2787
  const maxHeight = cell.maxHeight ? `max-height:${cell.maxHeight}mm;` : "max-height:100%;";
2706
- inner = `<div style="width:100%;height:100%;display:flex;align-items:center;justify-content:center;overflow:hidden;"><img src="${esc(resolveCellContent(cell, ctx))}" style="object-fit:${fit};${maxWidth}${maxHeight}display:block;" /></div>`;
2788
+ const boxMm = cellImageBoxMm(fit, cellFitCapMm(renderRows, r, cell, defaultPadding));
2789
+ const boxHeight = boxMm === void 0 ? "height:100%" : `height:${boxMm}mm`;
2790
+ const imgBox = boxMm === void 0 ? "" : "width:100%;height:100%;";
2791
+ inner = `<div style="width:100%;${boxHeight};display:flex;align-items:center;justify-content:center;overflow:hidden;"><img src="${esc(resolveCellContent(cell, ctx))}" style="${imgBox}object-fit:${fit};${cropStyle(fit, cell.fitScale, cell.objectPosition)}${maxWidth}${maxHeight}display:block;" /></div>`;
2707
2792
  } else {
2708
2793
  inner = renderTextCellInner(renderRows, r, colIndex, cell, opts, fitOwner, defaultPadding, ctx);
2709
2794
  }
@@ -3046,7 +3131,8 @@ function codeSpecKey(value, cellType, opts = {}) {
3046
3131
  opts.qrCodeLevel ?? null,
3047
3132
  opts.showText ?? null,
3048
3133
  opts.barWidth ?? null,
3049
- opts.fontSize ?? null
3134
+ opts.fontSize ?? null,
3135
+ opts.qrQuietZone ?? null
3050
3136
  ]);
3051
3137
  }
3052
3138
  function createMapCodeRenderer(map) {
package/dist/index.d.cts CHANGED
@@ -1,13 +1,13 @@
1
1
  /*!
2
- * @worm-vue3-print/core v1.3.6
2
+ * @worm-vue3-print/core v1.3.8
3
3
  * Copyright (c) 2026 worm <liu_long_qqcom@qq.com>
4
4
  * SPDX-FileCopyrightText: 2026 worm <liu_long_qqcom@qq.com>
5
5
  * SPDX-License-Identifier: MIT
6
6
  */
7
- import { T as TemplateData, C as CodeRenderer, P as PageLayout, J as MeasuredElement, K as TemplateElement, W as WatermarkOptions, L as PaperMm, N as ViewportPx, O as HeightSource, Q as MarginsMm, S as PdfTargetSpec, R as RawMeasurement, a as CodeSpec, X as CodeRenderOptions, D as DriverFactory, Y as ExecutorBundle, Z as TileLayout, _ as PrintJob, $ as PreparedDocument, a0 as RenderPdfResult, M as MultiPageTemplateData, a1 as PrintFontDeclaration } from './driver-CFm0C_yX.cjs';
8
- export { a2 as ContinuousPaperSize, a3 as EXECUTOR_TARGETS, a4 as ExecutorMethod, a5 as ExecutorTargetKind, a6 as FALLBACK_FONT_STACK, a7 as MAX_BATCH_COPIES, a8 as NormalizedPrintData, a9 as PAPER_DIMENSIONS, aa as PageDriver, ab as PageSection, ac as PaperSize, ad as PrintDataInput, ae as PrintFontFile, af as RenderCell, ag as RenderRequest, ah as RenderRow, ai as SheetPaperSize, aj as TILE_DEFAULTS, H as TextFit, ak as TilingError, al as TilingIssue, am as TilingIssueCode, an as TilingOptions, ao as TilingResolveOptions, ap as TilingTemplateInput, aq as buildFontFaceCss, ar as computeMaxColumns, as as computeTileLayout, at as escapeInlineStyleValue, au as getPaperDimensions, av as isContinuousPaper, aw as isContinuousPaperSize, ax as normalizePrintData, ay as normalizeTilingOptions, az as resolveSheetMm, aA as roundMm, aB as tilePosition, aC as toFontFamilyStack, aD as validateTiling } from './driver-CFm0C_yX.cjs';
9
- import { F as FitFontSize, P as PrintRuntime } from './ports-BXYcHExa.cjs';
10
- export { C as CellFitRowKind, h as DEFAULT_READINESS_MS, D as DEFAULT_SHRINK_MIN_FONT_SIZE_PT, i as DEFAULT_TIMEOUT_MS, M as MIN_SHRINK_FONT_SIZE_PT, j as MeasureResult, k as PrintSession, S as SessionBudget, c as cellFitCapMm, a as cellFitKey, f as floorFontSize, p as parseCellFitKey, r as resolveCellTextFit, d as resolveElementTextFit, e as resolveShrinkMinFontSize, g as roundFontSize } from './ports-BXYcHExa.cjs';
7
+ import { T as TemplateData, C as CodeRenderer, P as PageLayout, J as MeasuredElement, K as TemplateElement, W as WatermarkOptions, L as PaperMm, N as ViewportPx, O as HeightSource, Q as MarginsMm, S as PdfTargetSpec, R as RawMeasurement, a as CodeSpec, X as CodeRenderOptions, D as DriverFactory, Y as ExecutorBundle, Z as TileLayout, _ as PrintJob, $ as PreparedDocument, a0 as RenderPdfResult, M as MultiPageTemplateData, a1 as PrintFontDeclaration } from './driver-hJxBl2_n.cjs';
8
+ export { a2 as ContinuousPaperSize, a3 as EXECUTOR_TARGETS, a4 as ExecutorMethod, a5 as ExecutorTargetKind, a6 as FALLBACK_FONT_STACK, a7 as MAX_BATCH_COPIES, a8 as NormalizedPrintData, a9 as PAPER_DIMENSIONS, aa as PageDriver, ab as PageSection, ac as PaperSize, ad as PrintDataInput, ae as PrintFontFile, af as RenderCell, ag as RenderRequest, ah as RenderRow, ai as SheetPaperSize, aj as TILE_DEFAULTS, H as TextFit, ak as TilingError, al as TilingIssue, am as TilingIssueCode, an as TilingOptions, ao as TilingResolveOptions, ap as TilingTemplateInput, aq as buildFontFaceCss, ar as computeMaxColumns, as as computeTileLayout, at as escapeInlineStyleValue, au as getPaperDimensions, av as isContinuousPaper, aw as isContinuousPaperSize, ax as normalizePrintData, ay as normalizeTilingOptions, az as resolveSheetMm, aA as roundMm, aB as tilePosition, aC as toFontFamilyStack, aD as validateTiling } from './driver-hJxBl2_n.cjs';
9
+ import { F as FitFontSize, P as PrintRuntime } from './ports-B-Jb2XwT.cjs';
10
+ export { C as CellFitRowKind, i as DEFAULT_READINESS_MS, D as DEFAULT_SHRINK_MIN_FONT_SIZE_PT, j as DEFAULT_TIMEOUT_MS, M as MIN_SHRINK_FONT_SIZE_PT, k as MeasureResult, l as PrintSession, S as SessionBudget, c as cellFitCapMm, a as cellFitKey, f as floorFontSize, p as parseCellFitKey, r as resolveCellTextFit, e as resolveElementTextFit, g as resolveShrinkMinFontSize, h as roundFontSize } from './ports-B-Jb2XwT.cjs';
11
11
 
12
12
  type TokenType = 'num' | 'str' | 'bool' | 'ident' | 'punc';
13
13
  interface Token {
@@ -113,9 +113,7 @@ declare function formatMoney(value: any): string;
113
113
  * 支持: YYYY, MM, DD, HH, mm, ss
114
114
  */
115
115
  declare function formatDate(value: any, fmt: string): string;
116
- /**
117
- * 数字转大写金额
118
- */
116
+ /** 数字转大写金额(人民币票据规范:四位一节,万/亿 节单位不可缺) */
119
117
  declare function toUpperCaseAmount(value: any): string;
120
118
  /**
121
119
  * 条件判断函数
package/dist/index.d.ts CHANGED
@@ -1,13 +1,13 @@
1
1
  /*!
2
- * @worm-vue3-print/core v1.3.6
2
+ * @worm-vue3-print/core v1.3.8
3
3
  * Copyright (c) 2026 worm <liu_long_qqcom@qq.com>
4
4
  * SPDX-FileCopyrightText: 2026 worm <liu_long_qqcom@qq.com>
5
5
  * SPDX-License-Identifier: MIT
6
6
  */
7
- import { T as TemplateData, C as CodeRenderer, P as PageLayout, J as MeasuredElement, K as TemplateElement, W as WatermarkOptions, L as PaperMm, N as ViewportPx, O as HeightSource, Q as MarginsMm, S as PdfTargetSpec, R as RawMeasurement, a as CodeSpec, X as CodeRenderOptions, D as DriverFactory, Y as ExecutorBundle, Z as TileLayout, _ as PrintJob, $ as PreparedDocument, a0 as RenderPdfResult, M as MultiPageTemplateData, a1 as PrintFontDeclaration } from './driver-CFm0C_yX.js';
8
- export { a2 as ContinuousPaperSize, a3 as EXECUTOR_TARGETS, a4 as ExecutorMethod, a5 as ExecutorTargetKind, a6 as FALLBACK_FONT_STACK, a7 as MAX_BATCH_COPIES, a8 as NormalizedPrintData, a9 as PAPER_DIMENSIONS, aa as PageDriver, ab as PageSection, ac as PaperSize, ad as PrintDataInput, ae as PrintFontFile, af as RenderCell, ag as RenderRequest, ah as RenderRow, ai as SheetPaperSize, aj as TILE_DEFAULTS, H as TextFit, ak as TilingError, al as TilingIssue, am as TilingIssueCode, an as TilingOptions, ao as TilingResolveOptions, ap as TilingTemplateInput, aq as buildFontFaceCss, ar as computeMaxColumns, as as computeTileLayout, at as escapeInlineStyleValue, au as getPaperDimensions, av as isContinuousPaper, aw as isContinuousPaperSize, ax as normalizePrintData, ay as normalizeTilingOptions, az as resolveSheetMm, aA as roundMm, aB as tilePosition, aC as toFontFamilyStack, aD as validateTiling } from './driver-CFm0C_yX.js';
9
- import { F as FitFontSize, P as PrintRuntime } from './ports-Bdst9MWJ.js';
10
- export { C as CellFitRowKind, h as DEFAULT_READINESS_MS, D as DEFAULT_SHRINK_MIN_FONT_SIZE_PT, i as DEFAULT_TIMEOUT_MS, M as MIN_SHRINK_FONT_SIZE_PT, j as MeasureResult, k as PrintSession, S as SessionBudget, c as cellFitCapMm, a as cellFitKey, f as floorFontSize, p as parseCellFitKey, r as resolveCellTextFit, d as resolveElementTextFit, e as resolveShrinkMinFontSize, g as roundFontSize } from './ports-Bdst9MWJ.js';
7
+ import { T as TemplateData, C as CodeRenderer, P as PageLayout, J as MeasuredElement, K as TemplateElement, W as WatermarkOptions, L as PaperMm, N as ViewportPx, O as HeightSource, Q as MarginsMm, S as PdfTargetSpec, R as RawMeasurement, a as CodeSpec, X as CodeRenderOptions, D as DriverFactory, Y as ExecutorBundle, Z as TileLayout, _ as PrintJob, $ as PreparedDocument, a0 as RenderPdfResult, M as MultiPageTemplateData, a1 as PrintFontDeclaration } from './driver-hJxBl2_n.js';
8
+ export { a2 as ContinuousPaperSize, a3 as EXECUTOR_TARGETS, a4 as ExecutorMethod, a5 as ExecutorTargetKind, a6 as FALLBACK_FONT_STACK, a7 as MAX_BATCH_COPIES, a8 as NormalizedPrintData, a9 as PAPER_DIMENSIONS, aa as PageDriver, ab as PageSection, ac as PaperSize, ad as PrintDataInput, ae as PrintFontFile, af as RenderCell, ag as RenderRequest, ah as RenderRow, ai as SheetPaperSize, aj as TILE_DEFAULTS, H as TextFit, ak as TilingError, al as TilingIssue, am as TilingIssueCode, an as TilingOptions, ao as TilingResolveOptions, ap as TilingTemplateInput, aq as buildFontFaceCss, ar as computeMaxColumns, as as computeTileLayout, at as escapeInlineStyleValue, au as getPaperDimensions, av as isContinuousPaper, aw as isContinuousPaperSize, ax as normalizePrintData, ay as normalizeTilingOptions, az as resolveSheetMm, aA as roundMm, aB as tilePosition, aC as toFontFamilyStack, aD as validateTiling } from './driver-hJxBl2_n.js';
9
+ import { F as FitFontSize, P as PrintRuntime } from './ports-CJ0SUYFt.js';
10
+ export { C as CellFitRowKind, i as DEFAULT_READINESS_MS, D as DEFAULT_SHRINK_MIN_FONT_SIZE_PT, j as DEFAULT_TIMEOUT_MS, M as MIN_SHRINK_FONT_SIZE_PT, k as MeasureResult, l as PrintSession, S as SessionBudget, c as cellFitCapMm, a as cellFitKey, f as floorFontSize, p as parseCellFitKey, r as resolveCellTextFit, e as resolveElementTextFit, g as resolveShrinkMinFontSize, h as roundFontSize } from './ports-CJ0SUYFt.js';
11
11
 
12
12
  type TokenType = 'num' | 'str' | 'bool' | 'ident' | 'punc';
13
13
  interface Token {
@@ -113,9 +113,7 @@ declare function formatMoney(value: any): string;
113
113
  * 支持: YYYY, MM, DD, HH, mm, ss
114
114
  */
115
115
  declare function formatDate(value: any, fmt: string): string;
116
- /**
117
- * 数字转大写金额
118
- */
116
+ /** 数字转大写金额(人民币票据规范:四位一节,万/亿 节单位不可缺) */
119
117
  declare function toUpperCaseAmount(value: any): string;
120
118
  /**
121
119
  * 条件判断函数