pdfdancer-client-typescript 1.0.12 → 1.0.13

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.
Files changed (65) hide show
  1. package/.github/workflows/ci.yml +1 -1
  2. package/README.md +1 -1
  3. package/dist/__tests__/e2e/pdf-assertions.d.ts +1 -0
  4. package/dist/__tests__/e2e/pdf-assertions.d.ts.map +1 -1
  5. package/dist/__tests__/e2e/pdf-assertions.js +9 -3
  6. package/dist/__tests__/e2e/pdf-assertions.js.map +1 -1
  7. package/dist/fingerprint.d.ts +12 -0
  8. package/dist/fingerprint.d.ts.map +1 -0
  9. package/dist/fingerprint.js +196 -0
  10. package/dist/fingerprint.js.map +1 -0
  11. package/dist/image-builder.d.ts +4 -2
  12. package/dist/image-builder.d.ts.map +1 -1
  13. package/dist/image-builder.js +12 -3
  14. package/dist/image-builder.js.map +1 -1
  15. package/dist/index.d.ts +2 -1
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +7 -1
  18. package/dist/index.js.map +1 -1
  19. package/dist/models.d.ts +75 -8
  20. package/dist/models.d.ts.map +1 -1
  21. package/dist/models.js +179 -21
  22. package/dist/models.js.map +1 -1
  23. package/dist/page-builder.d.ts +24 -0
  24. package/dist/page-builder.d.ts.map +1 -0
  25. package/dist/page-builder.js +107 -0
  26. package/dist/page-builder.js.map +1 -0
  27. package/dist/paragraph-builder.d.ts +48 -54
  28. package/dist/paragraph-builder.d.ts.map +1 -1
  29. package/dist/paragraph-builder.js +408 -135
  30. package/dist/paragraph-builder.js.map +1 -1
  31. package/dist/pdfdancer_v1.d.ts +90 -9
  32. package/dist/pdfdancer_v1.d.ts.map +1 -1
  33. package/dist/pdfdancer_v1.js +535 -50
  34. package/dist/pdfdancer_v1.js.map +1 -1
  35. package/dist/types.d.ts +24 -3
  36. package/dist/types.d.ts.map +1 -1
  37. package/dist/types.js +117 -2
  38. package/dist/types.js.map +1 -1
  39. package/docs/openapi.yml +2076 -0
  40. package/fixtures/Showcase.pdf +0 -0
  41. package/package.json +1 -1
  42. package/src/__tests__/e2e/acroform.test.ts +5 -5
  43. package/src/__tests__/e2e/context-manager-showcase.test.ts +267 -0
  44. package/src/__tests__/e2e/form_x_object.test.ts +1 -1
  45. package/src/__tests__/e2e/image-showcase.test.ts +133 -0
  46. package/src/__tests__/e2e/image.test.ts +1 -1
  47. package/src/__tests__/e2e/line-showcase.test.ts +118 -0
  48. package/src/__tests__/e2e/line.test.ts +1 -16
  49. package/src/__tests__/e2e/page-showcase.test.ts +154 -0
  50. package/src/__tests__/e2e/paragraph-showcase.test.ts +523 -0
  51. package/src/__tests__/e2e/paragraph.test.ts +8 -8
  52. package/src/__tests__/e2e/pdf-assertions.ts +10 -3
  53. package/src/__tests__/e2e/pdfdancer-showcase.test.ts +40 -0
  54. package/src/__tests__/e2e/snapshot-showcase.test.ts +158 -0
  55. package/src/__tests__/e2e/snapshot.test.ts +296 -0
  56. package/src/__tests__/fingerprint.test.ts +36 -0
  57. package/src/fingerprint.ts +169 -0
  58. package/src/image-builder.ts +13 -6
  59. package/src/index.ts +6 -1
  60. package/src/models.ts +208 -24
  61. package/src/page-builder.ts +130 -0
  62. package/src/paragraph-builder.ts +517 -159
  63. package/src/pdfdancer_v1.ts +630 -51
  64. package/src/types.ts +145 -2
  65. package/update-api-spec.sh +3 -0
@@ -0,0 +1,169 @@
1
+ /**
2
+ * Fingerprint generation for PDFDancer client
3
+ * Generates a unique fingerprint hash to identify client requests
4
+ */
5
+
6
+ import * as crypto from 'crypto';
7
+ import * as fs from 'fs';
8
+ import * as path from 'path';
9
+ import * as os from 'os';
10
+
11
+ /**
12
+ * Get the install salt from localStorage (browser) or file storage (Node.js)
13
+ * This creates a persistent identifier for this client installation
14
+ */
15
+ function getInstallSalt(): string {
16
+ const storageKey = 'pdfdancer_install_salt';
17
+
18
+ // Check if we're in a browser environment
19
+ if (typeof localStorage !== 'undefined') {
20
+ let salt = localStorage.getItem(storageKey);
21
+ if (!salt) {
22
+ salt = crypto.randomBytes(16).toString('hex');
23
+ localStorage.setItem(storageKey, salt);
24
+ }
25
+ return salt;
26
+ }
27
+
28
+ // Node.js environment - use file storage
29
+ try {
30
+ const saltDir = path.join(os.homedir(), '.pdfdancer');
31
+ const saltFile = path.join(saltDir, 'install_salt');
32
+
33
+ // Create directory if it doesn't exist
34
+ if (!fs.existsSync(saltDir)) {
35
+ fs.mkdirSync(saltDir, { recursive: true, mode: 0o700 });
36
+ }
37
+
38
+ // Read existing salt or generate new one
39
+ if (fs.existsSync(saltFile)) {
40
+ return fs.readFileSync(saltFile, 'utf8').trim();
41
+ } else {
42
+ const salt = crypto.randomBytes(16).toString('hex');
43
+ fs.writeFileSync(saltFile, salt, { mode: 0o600 });
44
+ return salt;
45
+ }
46
+ } catch (error) {
47
+ // Fallback to generating a new salt if file operations fail
48
+ return crypto.randomBytes(16).toString('hex');
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Attempt to get the client's IP address
54
+ * Note: This is limited on the client side and may not always be accurate
55
+ */
56
+ async function getClientIP(): Promise<string> {
57
+ try {
58
+ // In browser, we can't reliably get the real IP
59
+ // Return a placeholder that will be consistent per session
60
+ return 'client-unknown';
61
+ } catch {
62
+ return 'client-unknown';
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Get the OS type
68
+ */
69
+ function getOSType(): string {
70
+ // Check if we're in Node.js
71
+ if (typeof process !== 'undefined' && process.platform) {
72
+ return process.platform;
73
+ }
74
+
75
+ // Browser environment
76
+ if (typeof navigator !== 'undefined') {
77
+ const userAgent = navigator.userAgent.toLowerCase();
78
+ if (userAgent.includes('win')) return 'windows';
79
+ if (userAgent.includes('mac')) return 'macos';
80
+ if (userAgent.includes('linux')) return 'linux';
81
+ if (userAgent.includes('android')) return 'android';
82
+ if (userAgent.includes('iphone') || userAgent.includes('ipad')) return 'ios';
83
+ }
84
+
85
+ return 'unknown';
86
+ }
87
+
88
+ /**
89
+ * Get the current hostname
90
+ */
91
+ function getHostname(): string {
92
+ // Node.js environment
93
+ if (typeof process !== 'undefined') {
94
+ try {
95
+ return os.hostname();
96
+ } catch {
97
+ // Fall through to browser logic
98
+ }
99
+ }
100
+
101
+ // Browser environment
102
+ if (typeof window !== 'undefined' && window.location) {
103
+ return window.location.hostname;
104
+ }
105
+
106
+ return 'unknown';
107
+ }
108
+
109
+ /**
110
+ * Get the timezone
111
+ */
112
+ function getTimezone(): string {
113
+ try {
114
+ return Intl.DateTimeFormat().resolvedOptions().timeZone;
115
+ } catch {
116
+ return 'unknown';
117
+ }
118
+ }
119
+
120
+ /**
121
+ * Get the locale
122
+ */
123
+ function getLocale(): string {
124
+ try {
125
+ if (typeof navigator !== 'undefined' && navigator.language) {
126
+ return navigator.language;
127
+ }
128
+ return Intl.DateTimeFormat().resolvedOptions().locale;
129
+ } catch {
130
+ return 'unknown';
131
+ }
132
+ }
133
+
134
+ /**
135
+ * Generate a fingerprint hash from client data points
136
+ *
137
+ * @param userId Optional user ID to include in the fingerprint
138
+ * @returns SHA256 hash of fingerprint components
139
+ */
140
+ export async function generateFingerprint(userId?: string): Promise<string> {
141
+ const ip = await getClientIP();
142
+ const uid = userId || 'unknown';
143
+ const osType = getOSType();
144
+ const sdkLanguage = 'typescript';
145
+ const timezone = getTimezone();
146
+ const locale = getLocale();
147
+ const domain = getHostname();
148
+ const installSalt = getInstallSalt();
149
+
150
+ // Hash individual components
151
+ const ipHash = crypto.createHash('sha256').update(ip).digest('hex');
152
+ const uidHash = crypto.createHash('sha256').update(uid).digest('hex');
153
+ const domainHash = crypto.createHash('sha256').update(domain).digest('hex');
154
+
155
+ // Concatenate all components and hash
156
+ const fingerprintData =
157
+ ipHash +
158
+ uidHash +
159
+ osType +
160
+ sdkLanguage +
161
+ timezone +
162
+ locale +
163
+ domainHash +
164
+ installSalt;
165
+
166
+ const fingerprintHash = crypto.createHash('sha256').update(fingerprintData).digest('hex');
167
+
168
+ return fingerprintHash;
169
+ }
@@ -8,13 +8,11 @@ interface PDFDancerInternals {
8
8
  }
9
9
 
10
10
  export class ImageBuilder {
11
- private _client: PDFDancer;
12
11
  private _imageData: Uint8Array<ArrayBuffer> | undefined;
13
12
  private _position: Position | undefined;
14
- private _internals: PDFDancerInternals;
13
+ private readonly _internals: PDFDancerInternals;
15
14
 
16
- constructor(_client: PDFDancer) {
17
- this._client = _client;
15
+ constructor(private _client: PDFDancer, private readonly _defaultPageIndex?: number) {
18
16
  // Cast to the internal interface to get access
19
17
  this._internals = this._client as unknown as PDFDancerInternals;
20
18
  }
@@ -33,8 +31,17 @@ export class ImageBuilder {
33
31
  return this;
34
32
  }
35
33
 
36
- at(pageIndex: number, x: number, y: number) {
37
- this._position = Position.atPageCoordinates(pageIndex, x, y);
34
+ at(x: number, y: number): this;
35
+ at(pageIndex: number, x: number, y: number): this;
36
+ at(pageIndexOrX: number, xOrY: number, maybeY?: number): this {
37
+ if (maybeY === undefined) {
38
+ if (this._defaultPageIndex === undefined) {
39
+ throw new Error('Page index must be provided when adding an image');
40
+ }
41
+ this._position = Position.atPageCoordinates(this._defaultPageIndex, pageIndexOrX, xOrY);
42
+ } else {
43
+ this._position = Position.atPageCoordinates(pageIndexOrX, xOrY, maybeY);
44
+ }
38
45
  return this;
39
46
  }
40
47
 
package/src/index.ts CHANGED
@@ -6,6 +6,7 @@
6
6
 
7
7
  export { PDFDancer } from './pdfdancer_v1';
8
8
  export { ParagraphBuilder } from './paragraph-builder';
9
+ export { PageBuilder } from './page-builder';
9
10
 
10
11
  export {
11
12
  PdfDancerException,
@@ -28,15 +29,19 @@ export {
28
29
  Image,
29
30
  BoundingRect,
30
31
  Paragraph,
32
+ TextLine,
31
33
  PositionMode,
32
34
  ShapeType,
33
35
  Point,
34
36
  StandardFonts,
37
+ STANDARD_PAGE_SIZES,
35
38
  Orientation,
36
39
  CommandResult,
37
40
  TextStatus,
38
41
  FontRecommendation,
39
- FontType
42
+ FontType,
43
+ DocumentSnapshot,
44
+ PageSnapshot
40
45
  } from './models';
41
46
 
42
47
  export const VERSION = "1.0.0";
package/src/models.ts CHANGED
@@ -8,13 +8,16 @@ export enum ObjectType {
8
8
  PATH = "PATH",
9
9
  PARAGRAPH = "PARAGRAPH",
10
10
  TEXT_LINE = "TEXT_LINE",
11
+ TEXT_ELEMENT = "TEXT_ELEMENT",
11
12
  PAGE = "PAGE",
12
13
  FORM_FIELD = "FORM_FIELD",
13
14
  TEXT_FIELD = "TEXT_FIELD",
14
- CHECK_BOX = "CHECK_BOX",
15
+ CHECKBOX = "CHECKBOX",
15
16
  RADIO_BUTTON = "RADIO_BUTTON"
16
17
  }
17
18
 
19
+ const DEFAULT_LINE_SPACING = 1.2;
20
+
18
21
  /**
19
22
  * Defines how position matching should be performed when searching for objects.
20
23
  */
@@ -82,6 +85,7 @@ class PositioningError extends Error {
82
85
  */
83
86
  export class Position {
84
87
  public name?: string;
88
+ public tolerance?: number;
85
89
 
86
90
  constructor(
87
91
  public pageIndex?: number,
@@ -102,9 +106,12 @@ export class Position {
102
106
 
103
107
  /**
104
108
  * Creates a position specification for specific coordinates on a page.
109
+ * @param tolerance Optional tolerance in points for coordinate matching (default: 0)
105
110
  */
106
- static atPageCoordinates(pageIndex: number, x: number, y: number): Position {
107
- return Position.atPage(pageIndex).atCoordinates({x, y});
111
+ static atPageCoordinates(pageIndex: number, x: number, y: number, tolerance: number = 0): Position {
112
+ const pos = Position.atPage(pageIndex).atCoordinates({x, y});
113
+ pos.tolerance = tolerance;
114
+ return pos;
108
115
  }
109
116
 
110
117
  static atPageWithText(pageIndex: number, text: string) {
@@ -480,10 +487,60 @@ export class Image {
480
487
  export class Paragraph {
481
488
  constructor(
482
489
  public position?: Position,
483
- public textLines?: string[],
490
+ public textLines?: Array<TextLine | string>,
491
+ public font?: Font,
492
+ public color?: Color,
493
+ public lineSpacing: number = DEFAULT_LINE_SPACING,
494
+ public lineSpacings?: number[] | null
495
+ ) {
496
+ }
497
+
498
+ getPosition(): Position | undefined {
499
+ return this.position;
500
+ }
501
+
502
+ setPosition(position: Position): void {
503
+ this.position = position;
504
+ }
505
+
506
+ clearLines(): void {
507
+ this.textLines = [];
508
+ }
509
+
510
+ addLine(textLine: TextLine | string): void {
511
+ if (!this.textLines) {
512
+ this.textLines = [];
513
+ }
514
+ this.textLines.push(textLine);
515
+ }
516
+
517
+ getLines(): Array<TextLine | string> {
518
+ if (!this.textLines) {
519
+ this.textLines = [];
520
+ }
521
+ return this.textLines;
522
+ }
523
+
524
+ setLines(lines: Array<TextLine | string>): void {
525
+ this.textLines = [...lines];
526
+ }
527
+
528
+ setLineSpacings(spacings?: number[] | null): void {
529
+ this.lineSpacings = spacings ? [...spacings] : spacings ?? null;
530
+ }
531
+
532
+ getLineSpacings(): number[] | null | undefined {
533
+ return this.lineSpacings ? [...this.lineSpacings] : this.lineSpacings;
534
+ }
535
+ }
536
+
537
+ export class TextLine {
538
+ constructor(
539
+ public position?: Position,
484
540
  public font?: Font,
485
541
  public color?: Color,
486
- public lineSpacing: number = 1.2
542
+ public lineSpacing: number = DEFAULT_LINE_SPACING,
543
+ public text: string = ""
487
544
  ) {
488
545
  }
489
546
 
@@ -587,38 +644,76 @@ export class AddRequest {
587
644
  data: dataB64
588
645
  };
589
646
  } else if (obj instanceof Paragraph) {
647
+ const fontToDict = (font?: Font | null) => {
648
+ if (!font) {
649
+ return null;
650
+ }
651
+ return {name: font.name, size: font.size};
652
+ };
653
+
654
+ const colorToDict = (color?: Color | null) => {
655
+ if (!color) {
656
+ return null;
657
+ }
658
+ return {red: color.r, green: color.g, blue: color.b, alpha: color.a};
659
+ };
660
+
590
661
  const lines: any[] = [];
591
662
  if (obj.textLines) {
592
663
  for (const line of obj.textLines) {
664
+ let text: string;
665
+ let font = obj.font;
666
+ let color = obj.color;
667
+ let position = obj.position;
668
+ let spacing = obj.lineSpacing;
669
+
670
+ if (line instanceof TextLine) {
671
+ text = line.text;
672
+ font = line.font ?? font;
673
+ color = line.color ?? color;
674
+ position = line.position ?? position;
675
+ spacing = line.lineSpacing ?? spacing;
676
+ } else {
677
+ text = line;
678
+ }
679
+
593
680
  const textElement = {
594
- text: line,
595
- font: obj.font ? {name: obj.font.name, size: obj.font.size} : null,
596
- color: obj.color ? {red: obj.color.r, green: obj.color.g, blue: obj.color.b, alpha: obj.color.a} : null,
597
- position: obj.position ? positionToDict(obj.position) : null
681
+ text,
682
+ font: fontToDict(font),
683
+ color: colorToDict(color),
684
+ position: position ? positionToDict(position) : null
598
685
  };
599
686
 
600
687
  const textLine: any = {
601
688
  textElements: [textElement]
602
689
  };
603
690
 
604
- if (obj.color) {
605
- textLine.color = {red: obj.color.r, green: obj.color.g, blue: obj.color.b, alpha: obj.color.a};
691
+ if (color) {
692
+ textLine.color = colorToDict(color);
693
+ }
694
+ if (position) {
695
+ textLine.position = positionToDict(position);
606
696
  }
607
- if (obj.position) {
608
- textLine.position = positionToDict(obj.position);
697
+ if (spacing !== undefined && spacing !== null) {
698
+ textLine.lineSpacing = spacing;
609
699
  }
700
+
610
701
  lines.push(textLine);
611
702
  }
612
703
  }
613
704
 
614
- const lineSpacings = obj.lineSpacing !== undefined ? [obj.lineSpacing] : null;
705
+ const lineSpacings = obj.lineSpacings
706
+ ? [...obj.lineSpacings]
707
+ : (obj.lineSpacing !== undefined && obj.lineSpacing !== null
708
+ ? [obj.lineSpacing]
709
+ : null);
615
710
 
616
711
  return {
617
712
  type: "PARAGRAPH",
618
713
  position: obj.position ? positionToDict(obj.position) : null,
619
- lines,
714
+ lines: lines.length ? lines : null,
620
715
  lineSpacings,
621
- font: obj.font ? {name: obj.font.name, size: obj.font.size} : null
716
+ font: fontToDict(obj.font)
622
717
  };
623
718
  } else {
624
719
  throw new Error(`Unsupported object type: ${typeof obj}`);
@@ -775,18 +870,35 @@ export class CreatePdfRequest {
775
870
  */
776
871
  export class AddPageRequest {
777
872
  constructor(
778
- public pageIndex: number,
779
- public pageSize?: string,
780
- public orientation?: string
873
+ public pageIndex?: number,
874
+ public pageSize?: PageSize,
875
+ public orientation?: Orientation | string
781
876
  ) {
782
877
  }
783
878
 
784
879
  toDict(): Record<string, any> {
785
- return {
786
- pageIndex: this.pageIndex,
787
- pageSize: this.pageSize,
788
- orientation: this.orientation
789
- };
880
+ const payload: Record<string, any> = {};
881
+
882
+ if (this.pageIndex !== undefined) {
883
+ payload.pageIndex = this.pageIndex;
884
+ }
885
+
886
+ if (this.orientation !== undefined && this.orientation !== null) {
887
+ const value = typeof this.orientation === 'string'
888
+ ? this.orientation.trim().toUpperCase()
889
+ : this.orientation;
890
+ payload.orientation = typeof value === 'string' ? value : value;
891
+ }
892
+
893
+ if (this.pageSize) {
894
+ payload.pageSize = {
895
+ name: this.pageSize.name,
896
+ width: this.pageSize.width,
897
+ height: this.pageSize.height
898
+ };
899
+ }
900
+
901
+ return payload;
790
902
  }
791
903
  }
792
904
 
@@ -835,6 +947,78 @@ export class CommandResult {
835
947
  }
836
948
  }
837
949
 
950
+ /**
951
+ * Represents a snapshot of a single page in a PDF document.
952
+ * Contains the page reference and all elements on that page.
953
+ */
954
+ export class PageSnapshot {
955
+ constructor(
956
+ public pageRef: PageRef,
957
+ public elements: ObjectRef[]
958
+ ) {}
959
+
960
+ /**
961
+ * Filters elements by object type.
962
+ */
963
+ getElementsByType(type: ObjectType): ObjectRef[] {
964
+ return this.elements.filter(el => el.type === type);
965
+ }
966
+
967
+ /**
968
+ * Gets the page index from the page reference.
969
+ */
970
+ getPageIndex(): number | undefined {
971
+ return this.pageRef.position.pageIndex;
972
+ }
973
+
974
+ /**
975
+ * Returns the number of elements on this page.
976
+ */
977
+ getElementCount(): number {
978
+ return this.elements.length;
979
+ }
980
+ }
981
+
982
+ /**
983
+ * Represents a complete snapshot of a PDF document.
984
+ * Contains page count, font information, and snapshots of all pages.
985
+ */
986
+ export class DocumentSnapshot {
987
+ constructor(
988
+ public pageCount: number,
989
+ public fonts: FontRecommendation[],
990
+ public pages: PageSnapshot[]
991
+ ) {}
992
+
993
+ /**
994
+ * Gets the snapshot for a specific page by index.
995
+ */
996
+ getPageSnapshot(pageIndex: number): PageSnapshot | undefined {
997
+ return this.pages.find(page => page.getPageIndex() === pageIndex);
998
+ }
999
+
1000
+ /**
1001
+ * Gets all elements across all pages.
1002
+ */
1003
+ getAllElements(): ObjectRef[] {
1004
+ return this.pages.flatMap(page => page.elements);
1005
+ }
1006
+
1007
+ /**
1008
+ * Gets all elements of a specific type across all pages.
1009
+ */
1010
+ getElementsByType(type: ObjectType): ObjectRef[] {
1011
+ return this.getAllElements().filter(el => el.type === type);
1012
+ }
1013
+
1014
+ /**
1015
+ * Returns the total number of elements across all pages.
1016
+ */
1017
+ getTotalElementCount(): number {
1018
+ return this.pages.reduce((sum, page) => sum + page.getElementCount(), 0);
1019
+ }
1020
+ }
1021
+
838
1022
  // Helper function to convert Position to dictionary for JSON serialization
839
1023
  function positionToDict(position: Position): Record<string, any> {
840
1024
  const result: Record<string, any> = {
@@ -0,0 +1,130 @@
1
+ import {PDFDancer} from './pdfdancer_v1';
2
+ import {AddPageRequest, Orientation, PageRef, PageSize, STANDARD_PAGE_SIZES} from './models';
3
+ import {ValidationException} from './exceptions';
4
+
5
+ interface PDFDancerInternals {
6
+ addPage(request?: AddPageRequest | null): Promise<PageRef>;
7
+ }
8
+
9
+ const normalizeOrientation = (value: Orientation | string): Orientation => {
10
+ if (typeof value === 'string') {
11
+ const normalized = value.trim().toUpperCase();
12
+ if (!(normalized in Orientation)) {
13
+ throw new ValidationException(`Invalid orientation: ${value}`);
14
+ }
15
+ return Orientation[normalized as keyof typeof Orientation];
16
+ }
17
+ return value;
18
+ };
19
+
20
+ const normalizePageSize = (value: PageSize | string): PageSize => {
21
+ if (typeof value === 'string') {
22
+ const normalized = value.trim().toUpperCase();
23
+ const standard = STANDARD_PAGE_SIZES[normalized];
24
+ if (!standard) {
25
+ throw new ValidationException(`Unknown page size: ${value}`);
26
+ }
27
+ return {name: normalized, width: standard.width, height: standard.height};
28
+ }
29
+
30
+ const width = value.width;
31
+ const height = value.height;
32
+ if (width === undefined || height === undefined) {
33
+ throw new ValidationException('Custom page size must include width and height');
34
+ }
35
+ if (width <= 0 || height <= 0) {
36
+ throw new ValidationException('Page size width and height must be positive numbers');
37
+ }
38
+ return {
39
+ name: value.name?.toUpperCase(),
40
+ width,
41
+ height
42
+ };
43
+ };
44
+
45
+ export class PageBuilder {
46
+ private readonly _client: PDFDancer;
47
+ private readonly _internals: PDFDancerInternals;
48
+ private _pageIndex?: number;
49
+ private _orientation?: Orientation;
50
+ private _pageSize?: PageSize;
51
+
52
+ constructor(client: PDFDancer) {
53
+ if (!client) {
54
+ throw new ValidationException('Client cannot be null');
55
+ }
56
+ this._client = client;
57
+ this._internals = this._client as unknown as PDFDancerInternals;
58
+ }
59
+
60
+ atIndex(pageIndex: number): this {
61
+ if (pageIndex === null || pageIndex === undefined) {
62
+ throw new ValidationException('Page index cannot be null');
63
+ }
64
+ if (!Number.isInteger(pageIndex) || pageIndex < 0) {
65
+ throw new ValidationException('Page index must be a non-negative integer');
66
+ }
67
+ this._pageIndex = pageIndex;
68
+ return this;
69
+ }
70
+
71
+ orientation(orientation: Orientation | string): this {
72
+ this._orientation = normalizeOrientation(orientation);
73
+ return this;
74
+ }
75
+
76
+ portrait(): this {
77
+ this._orientation = Orientation.PORTRAIT;
78
+ return this;
79
+ }
80
+
81
+ landscape(): this {
82
+ this._orientation = Orientation.LANDSCAPE;
83
+ return this;
84
+ }
85
+
86
+ pageSize(pageSize: PageSize | string): this {
87
+ this._pageSize = normalizePageSize(pageSize);
88
+ return this;
89
+ }
90
+
91
+ a4(): this {
92
+ return this.pageSize('A4');
93
+ }
94
+
95
+ letter(): this {
96
+ return this.pageSize('LETTER');
97
+ }
98
+
99
+ a3(): this {
100
+ return this.pageSize('A3');
101
+ }
102
+
103
+ a5(): this {
104
+ return this.pageSize('A5');
105
+ }
106
+
107
+ legal(): this {
108
+ return this.pageSize('LEGAL');
109
+ }
110
+
111
+ customSize(width: number, height: number): this {
112
+ if (width <= 0 || height <= 0) {
113
+ throw new ValidationException('Custom page size dimensions must be positive');
114
+ }
115
+ this._pageSize = {width, height};
116
+ return this;
117
+ }
118
+
119
+ async add(): Promise<PageRef> {
120
+ const request = this._buildRequest();
121
+ return this._internals.addPage(request);
122
+ }
123
+
124
+ private _buildRequest(): AddPageRequest | null {
125
+ if (this._pageIndex === undefined && this._orientation === undefined && this._pageSize === undefined) {
126
+ return null;
127
+ }
128
+ return new AddPageRequest(this._pageIndex, this._pageSize, this._orientation);
129
+ }
130
+ }