刘光辉
11 小时以前 0dfe84494048ce27ba8449831782128412d3eb13
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
import type { Workbook } from '@univerjs/core';
 
import { Disposable, DisposableCollection, generateRandomId, Inject, Injector, IUniverInstanceService } from '@univerjs/core';
import {
  CanvasRenderMode,
  DEFAULT_FONTFACE_PLANE,
  Engine,
  IRenderManagerService,
  ITextWatermarkConfig,
  Scene,
  SHEET_VIEWPORT_KEY,
  Spreadsheet,
  SpreadsheetSkeleton,
  Viewport,
} from '@univerjs/engine-render';
import { UniverType } from '@univerjs/protocol';
import { SheetPrintInterceptorService, SheetSkeletonManagerService } from '@univerjs/sheets-ui';
 
import { translateHeaderFooterText } from '../utils';
import { JnpfPrintAlignEnum, JnpfPrintCustomFooterEnum, JnpfPrintCustomHeaderEnum, JnpfPrintOtherParamsEnum } from '../utils/define';
import { buildUUID } from '../utils/uuid';
 
export class JnpfSheetsPrintUiService extends Disposable {
  // 获取容器元素
  get container() {
    return this._containerEle;
  }
 
  get root() {
    return this._root;
  }
 
  private _containerEle = document.createElement('div');
  private _dirty: boolean = false;
 
  private _effects: DisposableCollection = new DisposableCollection();
  private _engine!: Engine;
  private _key: string;
  private _layoutConfig: any;
 
  private _leftTopViewport!: Viewport;
  private _leftViewport!: Viewport;
  private _mainViewport!: Viewport;
 
  private readonly _previewContainerScale: number;
  private readonly _printConfig: any;
  private _root: HTMLDivElement = document.createElement('div');
 
  private _scene!: Scene;
 
  private readonly _sceneKey: string;
  private _sheetPrintInterceptorService: SheetPrintInterceptorService;
  private readonly _skeleton!: SpreadsheetSkeleton;
  private _spreadsheetObject: any;
 
  private _topViewport!: Viewport;
 
  private _totalHeight: number = 0;
  private _totalWidth: number = 0;
  private _univerInstanceService: IUniverInstanceService;
  private readonly _watermarkConfig: any;
 
  constructor(
    @Inject(Injector) private readonly _accessor: Injector,
    layoutConfig = {},
    printConfig = {},
    watermarkConfig = {},
    previewContainerScale = 1, // 打印预览容器的伸缩比
  ) {
    super();
 
    this._univerInstanceService = this._getUniverInstanceService();
    this._sheetPrintInterceptorService = this._getSheetPrintInterceptorService();
 
    this._layoutConfig = layoutConfig as any;
    this._printConfig = printConfig as any;
    this._watermarkConfig = watermarkConfig as any;
    this._previewContainerScale = previewContainerScale;
 
    const { subUnitId, unitId } = this._layoutConfig;
    this._sceneKey = this._getSceneKey(unitId, subUnitId);
 
    this._key = `${unitId}_${subUnitId}`;
    const skeleton = this._getSkeleton(unitId, subUnitId);
    if (!skeleton) {
      return;
    }
    this._skeleton = skeleton;
    this._root.className = '__root';
    this._initRender(); // 初始化渲染
  }
 
  public _prepare() {
    this._resizeViewport();
    this._effects.dispose();
    this._effects = new DisposableCollection();
    const { offsetX, offsetY, scale } = this._getTranslateOffset();
    const { subUnitId, unitId } = this._layoutConfig;
    this._root.style.left = `${offsetX}px`;
    this._root.style.top = `${offsetY}px`;
    const width = this._totalWidth * scale;
    const height = this._totalHeight * scale;
    this._root.style.width = `${width}px`;
    this._root.style.height = `${height}px`;
    this._root.style.position = 'absolute';
    this._root.style.overflow = 'hidden';
    this._sheetPrintInterceptorService.interceptor.fetchThroughInterceptors(
      this._sheetPrintInterceptorService.interceptor.getInterceptPoints().PRINTING_DOM_COLLECT,
    )(this._effects, {
      unitId,
      subUnitId,
      scene: this._scene,
      engine: this._engine,
      root: this._root,
      worksheet: this._skeleton.worksheet,
      skeleton: this._skeleton,
      offset: this._getTranslateOffset(),
    });
  }
 
  clearMemory() {
    this._skeleton.resetCache();
  }
 
  /**
   * 释放资源
   */
  dispose() {
    super.dispose(); // 调用父类的dispose方法
 
    if (this._containerEle.parentElement) {
      // eslint-disable-next-line unicorn/prefer-dom-node-remove
      this._containerEle.parentElement?.removeChild(this._containerEle); // 移除容器
    }
  }
 
  /**
   * 监听变换并在准备好时渲染
   */
  renderOnReady() {
    const observable = this._engine?.onTransformChange$?.subscribeEvent(() => {
      this.renderPage();
    });
 
    this.disposeWithMe(observable); // 订阅事件并在销毁时清理
  }
 
  /**
   * 渲染页面
   */
  renderPage() {
    this._clearEngineCanvas(); // 清除画布
 
    this._renderMainScene(); // 渲染主场景(核心内容)
    this._renderHeaderFooter(); // 渲染页眉页脚
  }
 
  /**
   * 标记当前状态为脏(需要重新渲染)
   */
  setDirty(state: boolean) {
    this._dirty = state;
  }
 
  /**
   * 将电子表格对象添加到场景中
   */
  private _addSpreadsheetToScene() {
    // 创建电子表格对象,传入唯一标识符、骨架对象,并设置初始化选项
    const spreadsheet = new Spreadsheet('__JnpfSheetPrintRender__', this._skeleton, false);
 
    // 将电子表格设置为打印模式,确保以打印样式进行渲染
    spreadsheet.isPrinting = true;
 
    // 将电子表格对象添加到当前场景中,纳入渲染流程
    this._scene.addObject(spreadsheet);
 
    // 缓存电子表格对象,便于后续访问或操作
    this._spreadsheetObject = spreadsheet;
 
    // 从布局配置中获取单元 ID 和子单元 ID,用于拦截器逻辑
    const { subUnitId, unitId } = this._layoutConfig ?? {};
 
    // 获取拦截器实例与拦截点,确保存在后再进行触发
    const interceptor = this._sheetPrintInterceptorService?.interceptor;
    const interceptPoints = interceptor?.getInterceptPoints();
    const printCollectPoint = interceptPoints?.PRINTING_COMPONENT_COLLECT;
 
    // 如果拦截器和拦截点存在,则触发拦截逻辑
    if (interceptor && printCollectPoint) {
      interceptor.fetchThroughInterceptors(printCollectPoint)(
        undefined, // 没有特定请求参数,传入 undefined
        {
          scene: this._scene,
          subUnitId,
          unitId,
          engine: this._engine,
          root: this._root,
          worksheet: this._skeleton.worksheet,
          skeleton: this._skeleton,
          offset: this._getTranslateOffset(),
          spreadsheet: this._spreadsheetObject,
        }, // 将单元 ID、子单元 ID 和场景信息传递给拦截器
      );
    }
  }
 
  /**
   * 添加并初始化多个视口(Viewport)到场景中
   */
  private _addViewport() {
    // 从视口键名常量中解构出各个视口的标识符
    const { VIEW_MAIN, VIEW_MAIN_LEFT, VIEW_MAIN_LEFT_TOP, VIEW_MAIN_TOP } = SHEET_VIEWPORT_KEY;
 
    // 创建主视口,负责渲染主要内容区域
    this._mainViewport = new Viewport(VIEW_MAIN, this._scene);
 
    // 创建左侧视口,负责渲染左侧固定区域
    this._leftViewport = new Viewport(VIEW_MAIN_LEFT, this._scene);
 
    // 创建顶部视口,负责渲染顶部固定区域
    this._topViewport = new Viewport(VIEW_MAIN_TOP, this._scene);
 
    // 创建左上角视口,负责渲染左上角交叉固定区域
    this._leftTopViewport = new Viewport(VIEW_MAIN_LEFT_TOP, this._scene);
  }
 
  /**
   * 清空引擎内画布
   */
  private _clearEngineCanvas() {
    this._engine?.clearCanvas();
  }
 
  /**
   * 绘制水印
   * @param ctx Canvas 2D 渲染上下文
   * @param config 水印配置参数
   */
  private _drawWatermark(ctx: CanvasRenderingContext2D, config: ITextWatermarkConfig) {
    const { bold, color, content, direction, fontSize: fontSizeConf, italic, opacity, repeat, rotate, spacingX, spacingY, x, y } = config;
 
    if (!content) return;
 
    const fontSize = fontSizeConf * this._previewContainerScale;
    const { h, w } = this._getPaperContainerSize();
 
    ctx.save();
    ctx.globalAlpha = opacity;
 
    const fontWeight = bold ? 'bold' : 'normal';
    const fontStyle = italic ? 'italic' : 'normal';
    ctx.font = `${fontStyle} ${fontWeight} ${fontSize}px ${DEFAULT_FONTFACE_PLANE}`;
    ctx.fillStyle = color;
    ctx.textBaseline = 'top';
 
    // **计算文本尺寸**
    const textWidth = ctx.measureText(content).width;
    const textHeight = fontSize; // 大部分情况下,高度近似等于字体大小
 
    // **方向控制**
    let stepX = spacingX + textWidth; // 修正水平间距
    let startX = x;
    let textAlign: CanvasTextAlign = 'left';
 
    if (direction === 'rtl') {
      stepX = -(spacingX + textWidth);
      startX = w - x;
      textAlign = 'right';
    }
 
    ctx.textAlign = textAlign;
 
    if (repeat) {
      // **修正垂直间距**
      const stepY = spacingY + textHeight;
 
      // **计算水印覆盖区域,防止边缘空白**
      const cols = Math.ceil(w / Math.abs(stepX)) + 2;
      const rows = Math.ceil(h / stepY) + 2;
 
      for (let col = -1; col < cols; col++) {
        for (let row = -1; row < rows; row++) {
          const posX = startX + col * stepX;
          const posY = y + row * stepY;
 
          ctx.save();
          ctx.translate(posX, posY);
          ctx.rotate((rotate * Math.PI) / 180);
          ctx.fillText(content, 0, 0);
          ctx.restore();
        }
      }
    } else {
      ctx.save();
      ctx.translate(startX, y);
      ctx.rotate((rotate * Math.PI) / 180);
      ctx.fillText(content, 0, 0);
      ctx.restore();
    }
 
    ctx.restore();
  }
 
  /**
   * 省略号裁剪文本
   */
  private _ellipsisText(ctx: CanvasRenderingContext2D, text: string, maxWidth: number) {
    if (ctx.measureText(text).width <= maxWidth) {
      return text;
    }
 
    let truncated = text;
    while (ctx.measureText(`${truncated}...`).width > maxWidth && truncated.length > 0) {
      truncated = truncated.slice(0, -1);
    }
 
    return `${truncated}...`;
  }
 
  /**
   * 获取纸张容器的尺寸
   */
  private _getPaperContainerSize() {
    // 从布局配置中解构出纸张尺寸(宽度 w 和高度 h)
    const { h = 0, w = 0 } = this._layoutConfig?.paperSize ?? {};
 
    // 检查宽度和高度是否有效
    if (w <= 0 || h <= 0) {
      console.warn('纸张尺寸配置无效,宽度或高度小于等于0。');
    }
 
    // 根据预览容器的缩放比例,计算实际渲染的尺寸
    const scaledWidth = w * this._previewContainerScale; // 宽度按缩放比例调整
    const scaledHeight = h * this._previewContainerScale; // 高度按缩放比例调整
 
    // 返回计算后的尺寸对象
    return {
      h: scaledHeight,
      w: scaledWidth,
    };
  }
 
  /**
   * 获取纸张边距(Padding)
   */
  private _getPaperPadding() {
    const { bottom = 0, left = 0, right = 0, top = 0 } = this._layoutConfig?.paperPadding ?? {};
    const previewContainerScale = this._previewContainerScale;
 
    return {
      bottom: bottom * previewContainerScale,
      left: left * previewContainerScale,
      right: right * previewContainerScale,
      top: top * previewContainerScale,
    };
  }
 
  /**
   * 获得场景专一key
   * @param unitId
   * @param subUnitId
   */
  private _getSceneKey(unitId: string, subUnitId: string) {
    return `${unitId}_${subUnitId}_${buildUUID()}`;
  }
 
  /**
   * 获取到sheetPrintInterceptorService
   */
  private _getSheetPrintInterceptorService() {
    return this._accessor.get(SheetPrintInterceptorService);
  }
 
  /**
   * 获取到skeleton
   * @param unitId
   * @param subUnitId
   */
  private _getSkeleton(unitId: string, subUnitId: string) {
    return this._accessor.get(IRenderManagerService)?.getRenderById(unitId)?.with(SheetSkeletonManagerService)?.getOrCreateSkeleton({ sheetId: subUnitId });
  }
 
  /**
   * 获取平移偏移量,用于将打印内容定位到页面上的正确位置
   *
   * 根据纸张的尺寸、内边距、打印配置中的对齐方式以及总内容的宽度和高度,计算水平和垂直方向的偏移量。
   * 最终返回的偏移量用于定位打印内容的起始位置,以便它能够根据配置进行正确的对齐。
   */
  private _getTranslateOffset() {
    // 获取纸张的内边距和纸张容器的尺寸
    const paperContainerSize = this._getPaperContainerSize();
    const paperPadding = this._getPaperPadding();
 
    // 解构获取打印配置中的对齐方式(水平对齐和垂直对齐)
    const { hAlign, vAlign } = this._printConfig;
 
    // 计算最大缩放比例,确保内容在X和Y方向上都能够缩放适应
    const scale = Math.max(this._scene.scaleX, this._scene.scaleY);
 
    // 计算水平偏移量(offsetX)
    let offsetX: number;
    if (hAlign === JnpfPrintAlignEnum.start) {
      // 水平对齐方式为左对齐,偏移量等于左边距
      offsetX = paperPadding.left;
    } else if (hAlign === JnpfPrintAlignEnum.end) {
      // 水平对齐方式为右对齐,偏移量等于纸张宽度减去内容宽度和右边距
      offsetX = paperContainerSize.w - this._totalWidth * scale - paperPadding.right;
    } else {
      // 水平对齐方式为居中对齐,计算居中位置
      offsetX = paperPadding.left + (paperContainerSize.w - this._totalWidth * scale - paperPadding.left - paperPadding.right) / 2;
    }
 
    // 计算垂直偏移量(offsetY)
    let offsetY: number;
    if (vAlign === JnpfPrintAlignEnum.start) {
      // 垂直对齐方式为上对齐,偏移量等于上边距
      offsetY = paperPadding.top;
    } else if (vAlign === JnpfPrintAlignEnum.end) {
      // 垂直对齐方式为下对齐,偏移量等于纸张高度减去内容高度和下边距
      offsetY = paperContainerSize.h - this._totalHeight * scale - paperPadding.bottom;
    } else {
      // 垂直对齐方式为居中对齐,计算居中位置
      offsetY = (paperContainerSize.h - this._totalHeight * scale - paperPadding.top - paperPadding.bottom) / 2 + paperPadding.top;
    }
 
    // 返回四舍五入后的偏移量,确保偏移值为整数
    return { offsetX: Math.round(offsetX), offsetY: Math.round(offsetY), scale };
  }
 
  /**
   * 获取到univerInstanceService
   */
  private _getUniverInstanceService() {
    return this._accessor.get(IUniverInstanceService);
  }
 
  /**
   * 初始化渲染引擎、场景、视口,并配置缩放比例和资源管理。
   */
  private _initRender() {
    // 获取纸张容器的宽度和高度,确保渲染区域的尺寸准确
    const { h: paperContainerH, w: paperContainerW } = this._getPaperContainerSize();
 
    // 设置像素比为 1,通常用于标准屏幕显示
    const pixelRatio = 1;
 
    // 创建渲染引擎实例
    // 参数:宽度、高度、像素比、渲染模式
    this._engine = new Engine('', {
      elementWidth: paperContainerW,
      elementHeight: paperContainerH,
      dpr: pixelRatio,
      renderMode: CanvasRenderMode.Printing, // 设置渲染模式为打印模式(Printing),确保画布的内容适配打印需求
    });
 
    // 创建场景对象,并将其与渲染引擎关联
    // 参数:场景的唯一标识符、渲染引擎实例
    this._scene = new Scene(this._sceneKey, this._engine);
 
    // 将渲染引擎绑定到指定的 DOM 容器中
    // 参数:容器元素、是否清除现有内容(false 表示不清除)
    this._engine.setContainer(this._containerEle, false);
    this._engine
      .getCanvas()
      .getContext()
      .setId(`${this._key}_${generateRandomId(4)}`);
    // 从布局配置中获取打印缩放比例,若未配置则默认为 1
    const { printScale = 1 } = this._layoutConfig;
 
    // 计算场景的最终缩放比例,结合打印缩放比例和预览容器的缩放比例
    const sceneScale = printScale * this._previewContainerScale;
 
    // 将计算得到的缩放比例应用到场景中
    // 参数:x 方向缩放比例,y 方向缩放比例
    this._scene.scale(sceneScale, sceneScale);
 
    // 将电子表格组件添加到场景中
    this._addSpreadsheetToScene();
 
    // 创建视口并与当前场景关联,确保视口正确渲染内容
    this._addViewport();
 
    // 注册资源销毁回调,确保在对象生命周期结束时释放资源,防止内存泄漏
    this.disposeWithMe({
      dispose: () => {
        // 释放引擎资源,确保渲染引擎的内存被回收
        this._engine.dispose();
 
        // 释放场景资源,确保场景相关的内存被回收
        this._scene.dispose();
        this.clearMemory();
      },
    });
  }
 
  /**
   * 渲染页眉和页脚
   */
  private _renderHeaderFooter() {
    // 从布局配置中解构获取单元ID和子单元ID
    const { subUnitId, unitId } = this._layoutConfig ?? {};
 
    // 获取工作簿对象
    const workbook = this._univerInstanceService?.getUnit<Workbook>(unitId, UniverType.UNIVER_SHEET);
    if (!workbook) return; // 如果未找到工作簿,直接返回
 
    // 获取工作表对象
    const worksheet = workbook?.getSheetBySheetId(subUnitId);
    if (!worksheet) return; // 如果未找到工作表,直接返回
 
    // 获取Canvas上下文对象
    const ctx = this._engine?.getCanvas()?.getContext();
    ctx.save(); // 保存当前绘图状态
    ctx.font = `13px ${DEFAULT_FONTFACE_PLANE}`; // 设置字体样式
 
    // 获取纸张尺寸并计算绘制位置
    const { h, w } = this._getPaperContainerSize();
    const x = 20 * this._previewContainerScale;
    const y = 20 * this._previewContainerScale;
 
    // 获取页眉页脚的参数
    const { headerFooterParams, headerFooterType } = this._printConfig ?? {};
 
    if (headerFooterType === 'default') {
      const maxTextWidth = (w - 2 * x) / 2;
 
      // 【左上角】渲染工作簿名称
      if (headerFooterParams?.includes(JnpfPrintOtherParamsEnum?.workbookTitle)) {
        const truncatedText = this._ellipsisText(ctx, this._printConfig.workbookTitleText, maxTextWidth);
        ctx.fillText(truncatedText, x, y);
      }
 
      // 【右上角】渲染工作表名称
      if (headerFooterParams?.includes(JnpfPrintOtherParamsEnum?.worksheetTitle)) {
        const sheetName = worksheet?.getName() ?? '';
        const truncatedText = this._ellipsisText(ctx, sheetName, maxTextWidth);
        const textMetrics = ctx.measureText(truncatedText);
        const textWidth = textMetrics.width;
        ctx.fillText(truncatedText, w - textWidth - x, y); // 右对齐
      }
 
      // 【左下角】渲染日期和时间
      if (headerFooterParams?.includes(JnpfPrintOtherParamsEnum?.printDate) || headerFooterParams?.includes(JnpfPrintOtherParamsEnum?.printTime)) {
        let dateStr = new Date().toLocaleString();
        if (headerFooterParams?.includes(JnpfPrintOtherParamsEnum?.printDate) && !headerFooterParams?.includes(JnpfPrintOtherParamsEnum?.printTime)) {
          dateStr = new Date().toLocaleDateString();
        } else if (headerFooterParams?.includes(JnpfPrintOtherParamsEnum?.printTime) && !headerFooterParams?.includes(JnpfPrintOtherParamsEnum?.printDate)) {
          dateStr = new Date().toLocaleTimeString();
        }
 
        const truncatedText = this._ellipsisText(ctx, dateStr, maxTextWidth);
        ctx.fillText(truncatedText, x, h - y); // 绘制时间/日期
      }
 
      // 【右下角】渲染页码
      if (headerFooterParams?.includes(JnpfPrintOtherParamsEnum?.pageNumber)) {
        const pageNumberText = `${this._layoutConfig?.sheetPageNumber}`;
        const truncatedText = this._ellipsisText(ctx, pageNumberText, maxTextWidth);
        const textMetrics = ctx.measureText(truncatedText);
        const textWidth = textMetrics.width;
        ctx.fillText(truncatedText, w - textWidth - x, h - y); // 右对齐
      }
    } else {
      const maxTextWidth = (w - 3 * x) / 3;
 
      const printConfigCache = {
        ...this._printConfig,
        bookPageNumber: this._layoutConfig?.bookPageNumber,
        bookTotalPage: this._layoutConfig?.bookTotalPage,
        sheetPageNumber: this._layoutConfig?.sheetPageNumber,
        sheetTotalPage: this._layoutConfig?.sheetTotalPage,
        workSheetTitleText: worksheet?.getName(),
      };
 
      // 左上角渲染
      if (headerFooterParams?.includes(JnpfPrintCustomHeaderEnum?.leftTop)) {
        const text = translateHeaderFooterText(JnpfPrintCustomHeaderEnum.leftTop, printConfigCache);
        const truncatedText = this._ellipsisText(ctx, text, maxTextWidth);
        ctx.fillText(truncatedText, x, y);
      }
 
      // 中上角渲染
      if (headerFooterParams?.includes(JnpfPrintCustomHeaderEnum?.centerTop)) {
        const text = translateHeaderFooterText(JnpfPrintCustomHeaderEnum.centerTop, printConfigCache);
        const truncatedText = this._ellipsisText(ctx, text, maxTextWidth);
        const textMetrics = ctx.measureText(truncatedText);
        const textWidth = textMetrics.width;
        ctx.fillText(truncatedText, (w - textWidth) / 2, y);
      }
 
      // 右上角渲染
      if (headerFooterParams?.includes(JnpfPrintCustomHeaderEnum?.rightTop)) {
        const text = translateHeaderFooterText(JnpfPrintCustomHeaderEnum.rightTop, printConfigCache);
        const truncatedText = this._ellipsisText(ctx, text, maxTextWidth);
        const textMetrics = ctx.measureText(truncatedText);
        const textWidth = textMetrics.width;
        ctx.fillText(truncatedText, w - textWidth - x, y);
      }
 
      // 左下角渲染
      if (headerFooterParams?.includes(JnpfPrintCustomFooterEnum?.leftBottom)) {
        const text = translateHeaderFooterText(JnpfPrintCustomFooterEnum.leftBottom, printConfigCache);
        const truncatedText = this._ellipsisText(ctx, text, maxTextWidth);
        ctx.fillText(truncatedText, x, h - y);
      }
 
      // 中下角渲染
      if (headerFooterParams?.includes(JnpfPrintCustomFooterEnum?.centerBottom)) {
        const text = translateHeaderFooterText(JnpfPrintCustomFooterEnum.centerBottom, printConfigCache);
        const truncatedText = this._ellipsisText(ctx, text, maxTextWidth);
        const textMetrics = ctx.measureText(truncatedText);
        const textWidth = textMetrics.width;
        ctx.fillText(truncatedText, (w - textWidth) / 2, h - y);
      }
 
      // 右下角渲染
      if (headerFooterParams?.includes(JnpfPrintCustomFooterEnum?.rightBottom)) {
        const text = translateHeaderFooterText(JnpfPrintCustomFooterEnum.rightBottom, printConfigCache);
        const truncatedText = this._ellipsisText(ctx, text, maxTextWidth);
        const textMetrics = ctx.measureText(truncatedText);
        const textWidth = textMetrics.width;
        ctx.fillText(truncatedText, w - textWidth - x, h - y);
      }
    }
 
    ctx.restore(); // 恢复绘图状态
  }
 
  /**
   * 渲染主场景
   */
  private _renderMainScene() {
    // 如果场景被标记为脏,需要重新调整视口大小
    this._dirty && this._prepare();
 
    // 获取Canvas的绘图上下文
    const context = this._engine.getCanvas().getContext();
 
    // 获取打印配置中的网格线设置和偏移量
    const { gridlines } = this._printConfig;
    const { offsetX, offsetY } = this._getTranslateOffset();
 
    // 根据网格线设置禁用或启用网格线
    this._spreadsheetObject.setForceDisableGridlines(!gridlines);
 
    // 强制标记表格为脏,以便重新渲染
    this._spreadsheetObject.makeForceDirty();
    this._scene.makeDirty();
 
    // 保存当前绘图上下文状态
    context.save();
    // 应用偏移量
    context.translateWithPrecision(offsetX, offsetY);
 
    // 渲染场景
    this._scene.render();
 
    // 恢复绘图上下文状态
    context.restore();
 
    // 添加水印(判断下,如果不需要水印就不用配置了)!!!目前和0.6.2版本一致,都是盖在了内容上面
    if (this._watermarkConfig?.show) {
      this._drawWatermark(context, this._watermarkConfig?.config);
    }
  }
 
  /**
   * 调整视口大小和滚动值
   */
  private _resizeViewport() {
    // 从布局配置中解构冻结和范围设置
    const { freeze, printScale = 1, range } = this._layoutConfig;
    // 从冻结设置中解构x轴和y轴的分割点以及起始行列
    const { startColumn, startRow, xSplit, ySplit } = freeze;
    // 从骨架配置中解构行头宽度和列头高度
    const { columnHeaderHeight, rowHeaderWidth } = this._skeleton;
 
    // 缩放场景以适应预览容器的缩放比例
    const sceneScale = printScale * this._previewContainerScale;
    this._scene.scale(sceneScale, sceneScale);
 
    // 获取无合并单元格的位置,基于起始行列
    const cellPosition = this._skeleton?.getNoMergeCellPositionByIndexWithNoHeader(startRow, startColumn);
    // 获取无合并单元格的位置,基于起始行列减去分割点
    const cellPositionSplit = this._skeleton?.getNoMergeCellPositionByIndexWithNoHeader(startRow - ySplit, startColumn - xSplit);
 
    // 计算起始X分割位置
    const startXSplit = xSplit > 0 ? cellPosition.startX - cellPositionSplit.startX : 0;
    // 计算起始Y分割位置
    const startYSplit = ySplit > 0 ? cellPosition.startY - cellPositionSplit.startY : 0;
 
    // 获取场景的精度缩放比例
    const precisionScale = this._scene.getPrecisionScale();
    // 定义用于缩放X值的函数
    const getScaleX = (value: number) => Math.round(value * precisionScale.scaleX) / precisionScale.scaleX;
    // 定义用于缩放Y值的函数
    const getScaleY = (value: number) => Math.round(value * precisionScale.scaleY) / precisionScale.scaleY;
 
    // 获取范围起始单元格的位置
    const startCellPosition = this._skeleton.getNoMergeCellPositionByIndexWithNoHeader(range.startRow, range.startColumn);
    // 计算结束单元格的位置,并处理边界情况
    const endCellPosition = this._skeleton.getNoMergeCellPositionByIndexWithNoHeader(range.endRow, range.endColumn);
 
    // 定义范围对象,包含起始和结束的X、Y坐标
    const _range = {
      endX: xSplit > 0 ? cellPosition.startX : 0,
      endY: ySplit > 0 ? cellPosition.startY : 0,
      startX: xSplit > 0 ? cellPositionSplit.startX : 0,
      startY: ySplit > 0 ? cellPositionSplit.startY : 0,
    };
 
    const endX = Math.max(0, getScaleX(endCellPosition.endX) - Math.max(startCellPosition.startX, _range.endX));
    const endY = Math.max(0, getScaleY(endCellPosition.endY) - getScaleY(Math.max(startCellPosition.startY, _range.endY)));
    // 计算缩放比例
    const scale = 1 / Math.max(precisionScale.scaleX, precisionScale.scaleY);
 
    this._totalWidth = startXSplit + endX + scale;
    this._totalHeight = startYSplit + endY + scale;
 
    const startSplit = { x: startXSplit, y: startYSplit };
 
    const scaleXY = {
      x: getScaleX(endCellPosition.endX) - getScaleX(endX) - getScaleX(Math.max(startCellPosition.startX, _range.endX)),
      y: getScaleY(endCellPosition.endY) - getScaleY(endY) - getScaleY(Math.max(startCellPosition.startY, _range.endY)),
    };
 
    // 如果x轴和y轴都有分割点,启用左上角视口并调整其大小和滚动值
    if (xSplit > 0 && ySplit > 0) {
      this._leftTopViewport.enable();
      this._leftTopViewport.resizeWhenFreezeChange({
        height: getScaleY(startSplit.y),
        left: 0,
        top: 0,
        width: getScaleX(startSplit.x),
      });
      this._leftTopViewport.updateScrollVal({
        viewportScrollX: getScaleX(_range.startX) + getScaleX(rowHeaderWidth),
        viewportScrollY: getScaleY(_range.startY) + getScaleY(columnHeaderHeight),
      });
    } else {
      // 否则禁用左上角视口
      this._leftTopViewport.disable();
    }
 
    // 如果x轴有分割点,启用左视口并调整其大小和滚动值
    if (xSplit > 0) {
      this._leftViewport.enable();
      this._leftViewport.resizeWhenFreezeChange({
        height: getScaleY(endY) + scale + scaleXY.y,
        left: 0,
        top: getScaleY(startSplit.y),
        width: getScaleX(startSplit.x),
      });
      this._leftViewport.updateScrollVal({
        viewportScrollX: getScaleX(_range.startX) + getScaleX(rowHeaderWidth),
        viewportScrollY: getScaleY(startCellPosition.startY) + getScaleY(columnHeaderHeight) - getScaleY(startSplit.y),
      });
    } else {
      // 否则禁用左视口
      this._leftViewport.disable();
    }
 
    // 如果y轴有分割点,启用顶视口并调整其大小和滚动值
    if (ySplit > 0) {
      this._topViewport.enable();
      this._topViewport.resizeWhenFreezeChange({
        height: getScaleY(startSplit.y),
        left: getScaleX(startSplit.x),
        top: 0,
        width: getScaleX(endX) + scale + scaleXY.x,
      });
      this._topViewport.updateScrollVal({
        viewportScrollX: getScaleX(startCellPosition.startX) + getScaleX(rowHeaderWidth) - getScaleX(startSplit.x),
        viewportScrollY: getScaleY(_range.startY) + getScaleY(columnHeaderHeight),
      });
    } else {
      // 否则禁用顶视口
      this._topViewport.disable();
    }
 
    // 调整主视口的大小和滚动值
    this._mainViewport.resizeWhenFreezeChange({
      height: getScaleY(endY) + scale + scaleXY.y,
      left: getScaleX(startSplit.x),
      top: getScaleY(startSplit.y),
      width: getScaleX(endX) + scale + scaleXY.x,
    });
    this._mainViewport.updateScrollVal({
      viewportScrollX: getScaleX(startCellPosition.startX) + getScaleX(rowHeaderWidth) - getScaleX(startSplit.x),
      viewportScrollY: getScaleY(startCellPosition.startY) + getScaleY(columnHeaderHeight) - getScaleY(startSplit.y),
    });
  }
}