@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.
@@ -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
@@ -292,6 +292,10 @@ function renderBarcodeSvg(value, opts) {
292
292
  svg.setAttribute("shape-rendering", "crispEdges");
293
293
  return svg.outerHTML;
294
294
  }
295
+ var QR_DEFAULT_QUIET_MODULES = 0;
296
+ function qrQuietModules(value) {
297
+ return typeof value === "number" && Number.isFinite(value) ? Math.max(0, Math.floor(value)) : QR_DEFAULT_QUIET_MODULES;
298
+ }
295
299
  function renderQrSvg(value, opts) {
296
300
  const level = (opts.qrCodeLevel ?? "M").toUpperCase();
297
301
  const qr = import_qrcode.default.create(value, {
@@ -300,7 +304,7 @@ function renderQrSvg(value, opts) {
300
304
  const modules = qr.modules;
301
305
  const size = modules.size;
302
306
  const dot = 4;
303
- const quiet = 4 * dot;
307
+ const quiet = qrQuietModules(opts.qrQuietZone) * dot;
304
308
  const dim = size * dot + quiet * 2;
305
309
  let rects = "";
306
310
  for (let y = 0; y < size; y++) {
@@ -370,6 +374,9 @@ function parseCellFitKey(key) {
370
374
  if (!Number.isFinite(rowIndex) || !Number.isFinite(colIndex)) return void 0;
371
375
  return { elementId: key.slice(0, first), kind, rowIndex, colIndex };
372
376
  }
377
+ function cellImageBoxMm(fit, capMm) {
378
+ return (fit || "contain") === "cover" ? capMm : void 0;
379
+ }
373
380
  function cellFitWidthMm(colWidths, colIndex, cell, defaultPadding = 1) {
374
381
  const span = Math.max(cell?.colspan ?? 1, 1);
375
382
  let width = 0;
@@ -1299,55 +1306,55 @@ function formatDate(value, fmt) {
1299
1306
  const pad3 = (n) => String(n).padStart(2, "0");
1300
1307
  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()));
1301
1308
  }
1309
+ var CN_DIGITS = ["\u96F6", "\u58F9", "\u8D30", "\u53C1", "\u8086", "\u4F0D", "\u9646", "\u67D2", "\u634C", "\u7396"];
1310
+ var CN_UNITS = ["", "\u62FE", "\u4F70", "\u4EDF"];
1311
+ var CN_GROUP_UNITS = ["", "\u4E07", "\u4EBF", "\u4E07\u4EBF", "\u4EBF\u4EBF"];
1312
+ function fourDigitToCapital(n) {
1313
+ let out = "";
1314
+ let zeroPending = false;
1315
+ for (let pos = 3; pos >= 0; pos--) {
1316
+ const d = Math.floor(n / 10 ** pos) % 10;
1317
+ if (d === 0) {
1318
+ if (out) zeroPending = true;
1319
+ continue;
1320
+ }
1321
+ if (zeroPending) {
1322
+ out += "\u96F6";
1323
+ zeroPending = false;
1324
+ }
1325
+ out += CN_DIGITS[d] + CN_UNITS[pos];
1326
+ }
1327
+ return out;
1328
+ }
1302
1329
  function toUpperCaseAmount(value) {
1303
1330
  const num = Number(value);
1304
1331
  if (isNaN(num)) return String(value);
1305
1332
  if (num === 0) return "\u96F6\u5143\u6574";
1306
- const digits = ["\u96F6", "\u58F9", "\u8D30", "\u53C1", "\u8086", "\u4F0D", "\u9646", "\u67D2", "\u634C", "\u7396"];
1307
- const units = ["", "\u62FE", "\u4F70", "\u4EDF"];
1308
- const bigUnits = ["", "\u4E07", "\u4EBF"];
1309
1333
  const sign = num < 0 ? "\u8D1F" : "";
1310
- const abs = Math.abs(num);
1311
- const [intStr, decStr] = abs.toFixed(2).split(".");
1312
- const intNum = parseInt(intStr, 10);
1334
+ const [intStr, decStr] = Math.abs(num).toFixed(2).split(".");
1335
+ const groupCount = Math.ceil(intStr.length / 4);
1313
1336
  let result = "";
1314
- if (intNum > 0) {
1315
- const intChars = intStr.split("").reverse();
1316
- let groupIdx = 0;
1317
- let lastDigit = 0;
1318
- let needZero = false;
1319
- for (let i = 0; i < intChars.length; i++) {
1320
- const d = parseInt(intChars[i], 10);
1321
- const unit = units[groupIdx % 4];
1322
- const bigUnit = bigUnits[Math.floor(groupIdx / 4)];
1323
- if (d === 0) {
1324
- if (lastDigit !== 0) needZero = true;
1325
- } else {
1326
- if (needZero) {
1327
- result = "\u96F6" + result;
1328
- needZero = false;
1329
- }
1330
- result = digits[d] + unit + result;
1331
- lastDigit = d;
1332
- }
1333
- if (groupIdx % 4 === 3 && groupIdx > 0) {
1334
- result = bigUnit + result;
1335
- }
1336
- groupIdx++;
1337
+ let zeroPending = false;
1338
+ for (let groupIdx = groupCount - 1; groupIdx >= 0; groupIdx--) {
1339
+ const end = intStr.length - groupIdx * 4;
1340
+ const group = parseInt(intStr.slice(Math.max(0, end - 4), end), 10);
1341
+ if (group === 0) {
1342
+ if (result) zeroPending = true;
1343
+ continue;
1337
1344
  }
1338
- result += "\u5143";
1345
+ if (result && (zeroPending || group < 1e3)) result += "\u96F6";
1346
+ zeroPending = false;
1347
+ result += fourDigitToCapital(group) + (CN_GROUP_UNITS[groupIdx] ?? "");
1339
1348
  }
1349
+ result = `${result || "\u96F6"}\u5143`;
1340
1350
  const jiao = parseInt(decStr[0], 10);
1341
1351
  const fen = parseInt(decStr[1], 10);
1342
1352
  if (jiao === 0 && fen === 0) {
1343
1353
  result += "\u6574";
1344
1354
  } else {
1345
- if (jiao > 0) {
1346
- result += digits[jiao] + "\u89D2";
1347
- }
1348
- if (fen > 0) {
1349
- result += digits[fen] + "\u5206";
1350
- }
1355
+ if (jiao === 0) result += "\u96F6";
1356
+ else result += CN_DIGITS[jiao] + "\u89D2";
1357
+ if (fen > 0) result += CN_DIGITS[fen] + "\u5206";
1351
1358
  }
1352
1359
  return sign + result;
1353
1360
  }
@@ -1621,9 +1628,12 @@ function makeRenderRow(row, resolve, keepRaw = false) {
1621
1628
  cellType: cell.cellType,
1622
1629
  barcodeType: cell.barcodeType,
1623
1630
  qrCodeLevel: cell.qrCodeLevel,
1631
+ qrQuietZone: cell.qrQuietZone,
1624
1632
  showBarcodeText: cell.showBarcodeText,
1625
1633
  printerDpi: cell.printerDpi,
1626
1634
  fit: cell.fit,
1635
+ objectPosition: cell.objectPosition,
1636
+ fitScale: cell.fitScale,
1627
1637
  maxWidth: cell.maxWidth,
1628
1638
  maxHeight: cell.maxHeight,
1629
1639
  rowspan: cell.rowspan ?? 1,
@@ -2116,6 +2126,35 @@ function resolveElementBorder(type, opts) {
2116
2126
  return { all, edges };
2117
2127
  }
2118
2128
 
2129
+ // src/render/crop-style.ts
2130
+ var OBJECT_POSITION_UNSAFE = /[;"'<>\\(){}\[\]@!#]/;
2131
+ var OBJECT_POSITION_MAX_LEN = 64;
2132
+ var CROP_SCALE_MIN = 100;
2133
+ var CROP_SCALE_MAX = 400;
2134
+ function sanitizeObjectPosition(value) {
2135
+ const v = (value ?? "").trim();
2136
+ if (!v || v.length > OBJECT_POSITION_MAX_LEN || OBJECT_POSITION_UNSAFE.test(v)) return "";
2137
+ return v;
2138
+ }
2139
+ function cropScalePct(fit, scale) {
2140
+ if ((fit || "contain") !== "cover") return void 0;
2141
+ if (typeof scale !== "number" || !Number.isFinite(scale)) return void 0;
2142
+ const pct = Math.round(scale);
2143
+ if (pct <= CROP_SCALE_MIN) return void 0;
2144
+ return Math.min(pct, CROP_SCALE_MAX);
2145
+ }
2146
+ function cropStyleDeclarations(fit, scale, objectPosition) {
2147
+ const pos = sanitizeObjectPosition(objectPosition);
2148
+ const out = {};
2149
+ if (pos) out.objectPosition = pos;
2150
+ const pct = cropScalePct(fit, scale);
2151
+ if (pct !== void 0) {
2152
+ out.transform = `scale(${Number((pct / 100).toFixed(2))})`;
2153
+ out.transformOrigin = pos || "50% 50%";
2154
+ }
2155
+ return out;
2156
+ }
2157
+
2119
2158
  // src/render/watermark.ts
2120
2159
  var PX_PER_MM = 96 / 25.4;
2121
2160
  var MM_PER_PX = 25.4 / 96;
@@ -2312,6 +2351,17 @@ function followGroupHeight(el, members, template, _measuredElements) {
2312
2351
  function effectiveHeight(el, measured) {
2313
2352
  return Math.max(el.options?.height ?? 0, measured.get(el.id)?.measuredHeight ?? 0);
2314
2353
  }
2354
+ function xRange(el) {
2355
+ const o = el.options ?? {};
2356
+ const x0 = o.left ?? 0;
2357
+ return [x0, x0 + (o.width ?? 100)];
2358
+ }
2359
+ function xIntersects(a, b) {
2360
+ return a[0] < b[1] && b[0] < a[1];
2361
+ }
2362
+ function xUnion(a, b) {
2363
+ return [Math.min(a[0], b[0]), Math.max(a[1], b[1])];
2364
+ }
2315
2365
  function buildPaginationUnits(sorted, measured, groupMap) {
2316
2366
  const unitOf = /* @__PURE__ */ new Map();
2317
2367
  for (const members of groupMap.values()) {
@@ -2319,7 +2369,8 @@ function buildPaginationUnits(sorted, measured, groupMap) {
2319
2369
  const unit = {
2320
2370
  anchorId: members[0].id,
2321
2371
  ids: members.map((m) => m.id),
2322
- forceFirstPage: members.some((m) => paginationOf(m)?.pageable === false)
2372
+ forceFirstPage: members.some((m) => paginationOf(m)?.pageable === false),
2373
+ band: members.map(xRange).reduce(xUnion)
2323
2374
  };
2324
2375
  for (const m of members) unitOf.set(m.id, unit);
2325
2376
  }
@@ -2351,11 +2402,13 @@ function buildPaginationUnits(sorted, measured, groupMap) {
2351
2402
  if (active[i].bottom <= top) active.splice(i, 1);
2352
2403
  }
2353
2404
  const bottom = top + effectiveHeight(el, measured);
2405
+ const x = xRange(el);
2354
2406
  for (const a of active) {
2355
- if (a.bottom > top) union(el.id, a.id);
2407
+ if (a.bottom > top && xIntersects(a.x, x)) union(el.id, a.id);
2356
2408
  }
2357
- active.push({ id: el.id, top, bottom });
2409
+ active.push({ id: el.id, top, bottom, x });
2358
2410
  }
2411
+ const eligibleById = new Map(eligible.map((e) => [e.id, e]));
2359
2412
  const clusters = /* @__PURE__ */ new Map();
2360
2413
  for (const el of eligible) {
2361
2414
  const root = find(el.id);
@@ -2365,7 +2418,12 @@ function buildPaginationUnits(sorted, measured, groupMap) {
2365
2418
  }
2366
2419
  for (const ids of clusters.values()) {
2367
2420
  if (ids.length < 2) continue;
2368
- const unit = { anchorId: ids[0], ids, forceFirstPage: false };
2421
+ const unit = {
2422
+ anchorId: ids[0],
2423
+ ids,
2424
+ forceFirstPage: false,
2425
+ band: ids.map((id) => xRange(eligibleById.get(id))).reduce(xUnion)
2426
+ };
2369
2427
  for (const id of ids) unitOf.set(id, unit);
2370
2428
  }
2371
2429
  return unitOf;
@@ -2414,17 +2472,30 @@ function paginate(template, measuredElements) {
2414
2472
  const unitOf = buildPaginationUnits(sorted, measuredElements, groupMap);
2415
2473
  const pages = [];
2416
2474
  let currentPage = [];
2417
- let remaining = contentHeight - SAFETY_MARGIN;
2475
+ let occupied = [];
2418
2476
  let pageBroken = false;
2419
- function sectionTop(el) {
2420
- return pageBroken ? pageCursorTop() : el.options?.top ?? 0;
2421
- }
2422
- function pageCursorTop() {
2423
- return Math.max(0, fullPageHeight() - remaining);
2424
- }
2425
2477
  function fullPageHeight() {
2426
2478
  return contentHeight - SAFETY_MARGIN;
2427
2479
  }
2480
+ function depthIn(x) {
2481
+ let used = 0;
2482
+ for (const r of occupied) {
2483
+ if (xIntersects(x, [r.x0, r.x1])) used += r.height;
2484
+ }
2485
+ return Math.max(0, used);
2486
+ }
2487
+ function remainingIn(x) {
2488
+ return fullPageHeight() - depthIn(x);
2489
+ }
2490
+ function occupy(x, height) {
2491
+ occupied.push({ x0: x[0], x1: x[1], height });
2492
+ }
2493
+ function sectionTop(el, x = xRange(el)) {
2494
+ return pageBroken ? pageCursorTop(x) : el.options?.top ?? 0;
2495
+ }
2496
+ function pageCursorTop(x) {
2497
+ return Math.max(0, fullPageHeight() - remainingIn(x));
2498
+ }
2428
2499
  let overflowOnCurrent = false;
2429
2500
  function pushPage() {
2430
2501
  pages.push(
@@ -2441,7 +2512,7 @@ function paginate(template, measuredElements) {
2441
2512
  }
2442
2513
  pushPage();
2443
2514
  currentPage = [];
2444
- remaining = contentHeight - SAFETY_MARGIN;
2515
+ occupied = [];
2445
2516
  pageBroken = true;
2446
2517
  overflowOnCurrent = false;
2447
2518
  }
@@ -2485,31 +2556,32 @@ function paginate(template, measuredElements) {
2485
2556
  return pages;
2486
2557
  function paginateNonTable(el, measured, sortedList, idx) {
2487
2558
  const elHeight = measured.get(el.id)?.measuredHeight ?? el.options?.height ?? 0;
2488
- if (elHeight <= remaining) {
2489
- const top2 = pageBroken ? pageCursorTop() : el.options?.top ?? 0;
2490
- noteOverflow(top2 + elHeight);
2491
- currentPage.push({ elementId: el.id, type: "element", renderTop: sectionTop(el) });
2492
- remaining -= elHeight;
2559
+ const x = xRange(el);
2560
+ const place = () => {
2561
+ const top = sectionTop(el, x);
2562
+ noteOverflow(top + elHeight);
2563
+ currentPage.push({ elementId: el.id, type: "element", renderTop: top });
2564
+ occupy(x, elHeight);
2565
+ };
2566
+ if (elHeight <= remainingIn(x)) {
2567
+ place();
2493
2568
  if (paginationOf(el)?.keepWithNext && idx + 1 < sortedList.length) {
2494
2569
  const nextEl = sortedList[idx + 1];
2495
2570
  const nextHeight = measured.get(nextEl.id)?.measuredHeight ?? nextEl.options?.height ?? 0;
2496
2571
  if (elHeight + nextHeight > fullPageHeight()) {
2497
2572
  return idx + 1;
2498
2573
  }
2499
- if (nextHeight > remaining) {
2574
+ if (nextHeight > remainingIn(x)) {
2500
2575
  currentPage.pop();
2576
+ occupied.pop();
2501
2577
  finishPage();
2502
- currentPage.push({ elementId: el.id, type: "element", renderTop: sectionTop(el) });
2503
- remaining -= elHeight;
2578
+ place();
2504
2579
  }
2505
2580
  }
2506
2581
  return idx + 1;
2507
2582
  }
2508
2583
  finishPage();
2509
- const top = pageBroken ? pageCursorTop() : el.options?.top ?? 0;
2510
- if (top + elHeight > contentHeight) overflowOnCurrent = true;
2511
- currentPage.push({ elementId: el.id, type: "element", renderTop: sectionTop(el) });
2512
- remaining -= elHeight;
2584
+ place();
2513
2585
  return idx + 1;
2514
2586
  }
2515
2587
  function paginateUnit(unit, idx) {
@@ -2522,8 +2594,9 @@ function paginate(template, measuredElements) {
2522
2594
  maxBottom = Math.max(maxBottom, top2 + effectiveHeight(m, measuredElements));
2523
2595
  }
2524
2596
  const unitHeight = Math.max(maxBottom - minTop, 0);
2597
+ const band = unit.band;
2525
2598
  const place = () => {
2526
- const offset = pageBroken ? pageCursorTop() - minTop : 0;
2599
+ const offset = pageBroken ? pageCursorTop(band) - minTop : 0;
2527
2600
  noteOverflow(minTop + offset + unitHeight);
2528
2601
  for (const m of members) {
2529
2602
  currentPage.push({
@@ -2532,9 +2605,9 @@ function paginate(template, measuredElements) {
2532
2605
  renderTop: (m.options?.top ?? 0) + offset
2533
2606
  });
2534
2607
  }
2535
- remaining -= unitHeight;
2608
+ occupy(band, unitHeight);
2536
2609
  };
2537
- if (unitHeight <= remaining) {
2610
+ if (unitHeight <= remainingIn(band)) {
2538
2611
  place();
2539
2612
  return idx + 1;
2540
2613
  }
@@ -2558,7 +2631,8 @@ function paginate(template, measuredElements) {
2558
2631
  const rowCount = Math.min(bodyRows.length, bodyHeights.length);
2559
2632
  const repeatCount = opts._repeatHeaderCount ?? 0;
2560
2633
  const repeatH = measured.get(el.id)?.repeatHeaderHeight ?? rowHeights.slice(0, repeatCount).reduce((s, h) => s + h, 0);
2561
- const freshAvail = contentHeight - SAFETY_MARGIN;
2634
+ const freshAvail = fullPageHeight();
2635
+ const tb = xRange(el);
2562
2636
  if (repeatH > freshAvail) {
2563
2637
  throw new Error(
2564
2638
  `\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`
@@ -2571,16 +2645,16 @@ function paginate(template, measuredElements) {
2571
2645
  startRow: 0,
2572
2646
  endRow: 0,
2573
2647
  summary: summaryH > 0,
2574
- renderTop: sectionTop(el)
2648
+ renderTop: sectionTop(el, tb)
2575
2649
  });
2576
- remaining -= summaryH;
2577
- applyFollowGroup(el, measured);
2650
+ occupy(tb, summaryH);
2651
+ applyFollowGroup(el, measured, tb);
2578
2652
  return idx + 1;
2579
2653
  }
2580
2654
  const totalH = bodyHeights.slice(0, rowCount).reduce((s, h) => s + h, 0);
2581
2655
  const totalNeed = totalH + subtotalH + summaryH;
2582
2656
  if (tablePaginationOf(el)?.enabled === false) {
2583
- if (totalNeed > remaining && currentPage.length > 0) finishPage();
2657
+ if (totalNeed > remainingIn(tb) && currentPage.length > 0) finishPage();
2584
2658
  currentPage.push({
2585
2659
  elementId: el.id,
2586
2660
  type: "table-slice",
@@ -2588,78 +2662,78 @@ function paginate(template, measuredElements) {
2588
2662
  endRow: rowCount,
2589
2663
  subtotal: subtotalH > 0,
2590
2664
  summary: summaryH > 0,
2591
- renderTop: sectionTop(el)
2665
+ renderTop: sectionTop(el, tb)
2592
2666
  });
2593
- remaining -= totalNeed;
2594
- applyFollowGroup(el, measured);
2667
+ occupy(tb, totalNeed);
2668
+ applyFollowGroup(el, measured, tb);
2595
2669
  return idx + 1;
2596
2670
  }
2597
2671
  const groups = buildRowGroups(bodyRows, rowCount);
2598
2672
  let sliceStart = 0;
2599
2673
  let firstSlice = true;
2600
- let sliceTop = sectionTop(el);
2674
+ let sliceTop = sectionTop(el, tb);
2675
+ let sliceUsed = 0;
2676
+ const avail = () => remainingIn(tb) - sliceUsed;
2677
+ const pushSlice = (endRow, repeatHeader) => {
2678
+ currentPage.push({
2679
+ elementId: el.id,
2680
+ type: "table-slice",
2681
+ startRow: sliceStart,
2682
+ endRow,
2683
+ subtotal: subtotalH > 0,
2684
+ ...!firstSlice && repeatHeader ? { repeatHeader: true } : {},
2685
+ renderTop: sliceTop
2686
+ });
2687
+ occupy(tb, sliceUsed);
2688
+ sliceUsed = 0;
2689
+ };
2601
2690
  for (const g of groups) {
2602
2691
  const gh = bodyHeights.slice(g.start, g.end).reduce((s, h) => s + h, 0);
2603
- if (gh + subtotalH <= remaining) {
2604
- remaining -= gh;
2692
+ if (gh + subtotalH <= avail()) {
2693
+ sliceUsed += gh;
2605
2694
  continue;
2606
2695
  }
2607
2696
  if (g.start > sliceStart) {
2608
- currentPage.push({
2609
- elementId: el.id,
2610
- type: "table-slice",
2611
- startRow: sliceStart,
2612
- endRow: g.start,
2613
- subtotal: subtotalH > 0,
2614
- ...!firstSlice && repeatCount > 0 ? { repeatHeader: true } : {},
2615
- renderTop: sliceTop
2616
- });
2697
+ pushSlice(g.start, repeatCount > 0);
2617
2698
  firstSlice = false;
2618
2699
  sliceStart = g.start;
2619
2700
  }
2620
2701
  finishPage();
2621
- sliceTop = sectionTop(el);
2622
- if (!firstSlice) remaining -= repeatH;
2623
- remaining -= gh;
2702
+ sliceTop = sectionTop(el, tb);
2703
+ sliceUsed = 0;
2704
+ if (!firstSlice) sliceUsed += repeatH;
2705
+ sliceUsed += gh;
2624
2706
  }
2625
2707
  if (sliceStart < rowCount) {
2626
- currentPage.push({
2627
- elementId: el.id,
2628
- type: "table-slice",
2629
- startRow: sliceStart,
2630
- endRow: rowCount,
2631
- subtotal: subtotalH > 0,
2632
- ...!firstSlice && repeatCount > 0 ? { repeatHeader: true } : {},
2633
- renderTop: sliceTop
2634
- });
2708
+ pushSlice(rowCount, repeatCount > 0);
2635
2709
  }
2636
2710
  if (summaryH > 0) {
2637
- if (summaryH <= remaining) {
2711
+ if (summaryH <= avail()) {
2638
2712
  const lastIdx = currentPage.length - 1;
2639
2713
  const last = currentPage[lastIdx];
2640
2714
  if (last && last.type === "table-slice" && last.elementId === el.id) {
2641
2715
  currentPage[lastIdx] = { ...last, summary: true };
2642
- remaining -= summaryH;
2716
+ occupy(tb, summaryH);
2643
2717
  } else {
2644
2718
  finishPage();
2645
2719
  currentPage.push({ elementId: el.id, type: "table-slice", startRow: rowCount, endRow: rowCount, summary: true, renderTop: 0 });
2646
- remaining -= summaryH;
2720
+ occupy(tb, summaryH);
2647
2721
  }
2648
2722
  } else {
2649
2723
  finishPage();
2650
2724
  currentPage.push({ elementId: el.id, type: "table-slice", startRow: rowCount, endRow: rowCount, summary: true, renderTop: 0 });
2651
- remaining -= summaryH;
2725
+ occupy(tb, summaryH);
2652
2726
  }
2653
2727
  }
2654
- applyFollowGroup(el, measured);
2728
+ applyFollowGroup(el, measured, tb);
2655
2729
  return idx + 1;
2656
2730
  }
2657
- function applyFollowGroup(el, measured) {
2731
+ function applyFollowGroup(el, measured, band) {
2658
2732
  const members = followMap.get(el.id) ?? [];
2659
2733
  if (members.length === 0) return;
2660
2734
  const followH = followGroupHeight(el, members, template, measured);
2661
2735
  const opts = el.options ?? {};
2662
- if (followH <= remaining) {
2736
+ if (followH <= remainingIn(band)) {
2663
2737
  const lastIdx = currentPage.length - 1;
2664
2738
  const last = currentPage[lastIdx];
2665
2739
  if (last && last.type === "table-slice" && last.elementId === el.id) {
@@ -2670,7 +2744,7 @@ function paginate(template, measuredElements) {
2670
2744
  // 沿用该切片在本页的定位 top(续片已 stamp 为 0)
2671
2745
  groupTop: last.renderTop ?? opts.top ?? 0
2672
2746
  };
2673
- remaining -= followH;
2747
+ occupy(band, followH);
2674
2748
  return;
2675
2749
  }
2676
2750
  }
@@ -2683,7 +2757,7 @@ function paginate(template, measuredElements) {
2683
2757
  followElementIds: members,
2684
2758
  groupTop: 0
2685
2759
  });
2686
- remaining -= followH;
2760
+ occupy(band, followH);
2687
2761
  }
2688
2762
  }
2689
2763
  function buildRowGroups(renderRows, rowCount) {
@@ -2890,12 +2964,23 @@ function textStyle(opts) {
2890
2964
  if (opts.lineHeight) parts.push(`line-height:${opts.lineHeight}pt`);
2891
2965
  if (opts.letterSpacing) parts.push(`letter-spacing:${opts.letterSpacing}pt`);
2892
2966
  if (opts.verticalAlign) {
2893
- parts.push(`display:flex;align-items:${V_ALIGN_FLEX[opts.verticalAlign] ?? "flex-start"}`);
2894
- parts.push(`justify-content:${H_ALIGN_FLEX[opts.textAlign ?? "left"] ?? "flex-start"}`);
2967
+ if (opts.textAlign === "justify") {
2968
+ parts.push(`display:flex;flex-direction:column;justify-content:${V_ALIGN_FLEX[opts.verticalAlign] ?? "flex-start"}`);
2969
+ } else {
2970
+ parts.push(`display:flex;align-items:${V_ALIGN_FLEX[opts.verticalAlign] ?? "flex-start"}`);
2971
+ parts.push(`justify-content:${H_ALIGN_FLEX[opts.textAlign ?? "left"] ?? "flex-start"}`);
2972
+ }
2895
2973
  }
2896
2974
  if (opts.textAlign) parts.push(`text-align:${opts.textAlign}`);
2975
+ if (opts.textAlign === "justify") parts.push("text-align-last:justify");
2897
2976
  return parts.length ? parts.join(";") + ";" : "";
2898
2977
  }
2978
+ function indentStyle(opts) {
2979
+ return opts.longTextIndent ? `text-indent:${opts.longTextIndent}em;` : "";
2980
+ }
2981
+ function cropStyle(fit, fitScale, objectPosition) {
2982
+ return Object.entries(cropStyleDeclarations(fit, fitScale, objectPosition)).map(([key, value]) => `${key.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`)}:${escapeInlineStyleValue(value)};`).join("");
2983
+ }
2899
2984
  function elementFitStyle(fit, opts) {
2900
2985
  if (fit === "autoHeight") return "overflow:visible;";
2901
2986
  if (opts.wordWrap === false) return "white-space:nowrap;text-overflow:ellipsis;";
@@ -2934,7 +3019,7 @@ function renderElement(el, isMeasure, containerStyle, overrideTop, ctx) {
2934
3019
  return renderTableElement(el, isMeasure, measureAttr, ctx);
2935
3020
  case "image":
2936
3021
  return `<div class="print-element" style="${style}"${measureAttr}>
2937
- <img src="${esc(opts.src ?? "")}" style="width:100%;height:100%;object-fit:contain;" />
3022
+ <img src="${esc(opts.src ?? "")}" style="width:100%;height:100%;object-fit:${opts.fit || "contain"};${cropStyle(opts.fit, opts.fitScale, opts.objectPosition)}" />
2938
3023
  </div>`;
2939
3024
  case "barcode":
2940
3025
  case "qrcode": {
@@ -2961,7 +3046,7 @@ function renderElement(el, isMeasure, containerStyle, overrideTop, ctx) {
2961
3046
  case "oval":
2962
3047
  return `<div class="print-element" style="${style};border:${opts.borderWidth || 1}pt ${opts.borderStyle || "solid"} ${opts.borderColor || "#333"};border-radius:50%;"${measureAttr}></div>`;
2963
3048
  case "longText":
2964
- return `<div class="print-element" style="${style}${textStyle(opts)}${elementFitStyle(fit, opts)}"${measureAttr}${fitAttr}>${esc(opts.formatter ?? opts.testData ?? "")}</div>`;
3049
+ return `<div class="print-element" style="${style}${textStyle(opts)}${indentStyle(opts)}${elementFitStyle(fit, opts)}"${measureAttr}${fitAttr}>${esc(opts.formatter ?? opts.testData ?? "")}</div>`;
2965
3050
  case "html":
2966
3051
  return `<div class="print-element" style="${style}"${measureAttr}>${opts.testData ?? opts.title ?? ""}</div>`;
2967
3052
  default:
@@ -2976,6 +3061,8 @@ function codeImgHtml(value, cellType, opts, io) {
2976
3061
  const svg = codeRenderer.render(value, cellType, {
2977
3062
  barcodeType: opts.barcodeType,
2978
3063
  qrCodeLevel: opts.qrCodeLevel != null ? String(opts.qrCodeLevel) : void 0,
3064
+ // 0 是合法档(无白边),必须用 typeof 判定而非 or/?? 之类假值判断
3065
+ qrQuietZone: typeof opts.qrQuietZone === "number" ? opts.qrQuietZone : void 0,
2979
3066
  showText: opts.hideTitle !== void 0 ? !opts.hideTitle : opts.showBarcodeText,
2980
3067
  barWidth: typeof opts.barWidth === "number" ? opts.barWidth : void 0,
2981
3068
  fontSize: typeof opts.fontSize === "number" ? opts.fontSize : void 0,
@@ -2986,10 +3073,8 @@ function codeImgHtml(value, cellType, opts, io) {
2986
3073
  if (isBarcode && /^<svg[\s>]/i.test(svg)) return inlineCodeSvgHtml(svg);
2987
3074
  const src = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`;
2988
3075
  const styleParts = [];
2989
- styleParts.push("max-width:100%", "max-height:100%", "display:block", "margin:auto");
2990
- if (fit) {
2991
- styleParts.push(`object-fit:${fit}`);
2992
- }
3076
+ styleParts.push("width:100%", "height:100%", "max-width:100%", "max-height:100%", "display:block", "margin:auto");
3077
+ styleParts.push(`object-fit:${fit || "contain"}`);
2993
3078
  if (maxWidth) {
2994
3079
  styleParts.push(`max-width:${maxWidth}mm`);
2995
3080
  }
@@ -3017,6 +3102,7 @@ function matrixCellStyle(cell, opts) {
3017
3102
  if (color) parts.push(`color:${color}`);
3018
3103
  if (cell.backgroundColor) parts.push(`background-color:${cell.backgroundColor}`);
3019
3104
  parts.push(`text-align:${cell.align ?? "left"}`);
3105
+ if (cell.align === "justify") parts.push("text-align-last:justify");
3020
3106
  parts.push(`vertical-align:${cell.valign ?? "middle"}`);
3021
3107
  parts.push(`padding:${padding}mm`);
3022
3108
  parts.push(cell.wordWrap === false ? "white-space:nowrap;overflow:hidden" : "word-break:break-all");
@@ -3056,7 +3142,10 @@ function renderMatrixRows(renderRows, start, end, opts, withRowIndex, ctx, fitOw
3056
3142
  const fit = cell.fit || "contain";
3057
3143
  const maxWidth = cell.maxWidth ? `max-width:${cell.maxWidth}mm;` : "max-width:100%;";
3058
3144
  const maxHeight = cell.maxHeight ? `max-height:${cell.maxHeight}mm;` : "max-height:100%;";
3059
- 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>`;
3145
+ const boxMm = cellImageBoxMm(fit, cellFitCapMm(renderRows, r, cell, defaultPadding));
3146
+ const boxHeight = boxMm === void 0 ? "height:100%" : `height:${boxMm}mm`;
3147
+ const imgBox = boxMm === void 0 ? "" : "width:100%;height:100%;";
3148
+ 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>`;
3060
3149
  } else {
3061
3150
  inner = renderTextCellInner(renderRows, r, colIndex, cell, opts, fitOwner, defaultPadding, ctx);
3062
3151
  }
@@ -3245,7 +3334,8 @@ function codeSpecKey(value, cellType, opts = {}) {
3245
3334
  opts.qrCodeLevel ?? null,
3246
3335
  opts.showText ?? null,
3247
3336
  opts.barWidth ?? null,
3248
- opts.fontSize ?? null
3337
+ opts.fontSize ?? null,
3338
+ opts.qrQuietZone ?? null
3249
3339
  ]);
3250
3340
  }
3251
3341
  function createMapCodeRenderer(map) {
@@ -1,11 +1,11 @@
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 { P as PageLayout, T as TemplateData, M as MultiPageTemplateData, C as CodeRenderer, D as DriverFactory, R as RawMeasurement, a as CodeSpec } from '../driver-CFm0C_yX.cjs';
8
- import { P as PrintRuntime, F as FitFontSize } from '../ports-BXYcHExa.cjs';
7
+ import { P as PageLayout, T as TemplateData, M as MultiPageTemplateData, C as CodeRenderer, D as DriverFactory, R as RawMeasurement, a as CodeSpec } from '../driver-hJxBl2_n.cjs';
8
+ import { P as PrintRuntime, F as FitFontSize } from '../ports-B-Jb2XwT.cjs';
9
9
 
10
10
  interface BrowserRenderResult {
11
11
  /** 最终多页 HTML 字符串 */
@@ -1,11 +1,11 @@
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 { P as PageLayout, T as TemplateData, M as MultiPageTemplateData, C as CodeRenderer, D as DriverFactory, R as RawMeasurement, a as CodeSpec } from '../driver-CFm0C_yX.js';
8
- import { P as PrintRuntime, F as FitFontSize } from '../ports-Bdst9MWJ.js';
7
+ import { P as PageLayout, T as TemplateData, M as MultiPageTemplateData, C as CodeRenderer, D as DriverFactory, R as RawMeasurement, a as CodeSpec } from '../driver-hJxBl2_n.js';
8
+ import { P as PrintRuntime, F as FitFontSize } from '../ports-CJ0SUYFt.js';
9
9
 
10
10
  interface BrowserRenderResult {
11
11
  /** 最终多页 HTML 字符串 */
@@ -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
@@ -17,7 +17,7 @@ import {
17
17
  prepareDocument,
18
18
  resolveBarcodeSize,
19
19
  resolveShrinkMinFontSize
20
- } from "../chunk-JQUJ7AF3.js";
20
+ } from "../chunk-TQJGFV2V.js";
21
21
 
22
22
  // src/browser/browser-code-renderer.ts
23
23
  import JsBarcode from "jsbarcode";
@@ -62,6 +62,10 @@ function renderBarcodeSvg(value, opts) {
62
62
  svg.setAttribute("shape-rendering", "crispEdges");
63
63
  return svg.outerHTML;
64
64
  }
65
+ var QR_DEFAULT_QUIET_MODULES = 0;
66
+ function qrQuietModules(value) {
67
+ return typeof value === "number" && Number.isFinite(value) ? Math.max(0, Math.floor(value)) : QR_DEFAULT_QUIET_MODULES;
68
+ }
65
69
  function renderQrSvg(value, opts) {
66
70
  const level = (opts.qrCodeLevel ?? "M").toUpperCase();
67
71
  const qr = QRCode.create(value, {
@@ -70,7 +74,7 @@ function renderQrSvg(value, opts) {
70
74
  const modules = qr.modules;
71
75
  const size = modules.size;
72
76
  const dot = 4;
73
- const quiet = 4 * dot;
77
+ const quiet = qrQuietModules(opts.qrQuietZone) * dot;
74
78
  const dim = size * dot + quiet * 2;
75
79
  let rects = "";
76
80
  for (let y = 0; y < size; y++) {