@thecodecodex/tools 0.0.0-stage → 0.1.1
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/LICENSE +21 -0
- package/README.md +44 -2
- package/package.json +46 -4
- package/src/base64.ts +36 -0
- package/src/br/cnpj.ts +55 -0
- package/src/br/cpf.ts +61 -0
- package/src/cli/main.ts +93 -0
- package/src/cli/tools.ts +643 -0
- package/src/compensation-comparator.ts +124 -0
- package/src/css-playground.ts +699 -0
- package/src/dev-ref.ts +295 -0
- package/src/diff.ts +301 -0
- package/src/finance/clt.ts +189 -0
- package/src/finance/compound.ts +78 -0
- package/src/finance/pj.ts +146 -0
- package/src/fov.ts +145 -0
- package/src/hash.ts +13 -0
- package/src/image-to-pdf.ts +299 -0
- package/src/invoice-pdf.ts +134 -0
- package/src/invoice.ts +94 -0
- package/src/jwt.ts +44 -0
- package/src/market.ts +49 -0
- package/src/password.ts +51 -0
- package/src/pdf-studio.ts +453 -0
- package/src/random/index.ts +19 -0
- package/src/regex.ts +426 -0
- package/src/skills.json +60 -0
- package/src/skills.ts +52 -0
- package/src/state-holidays.ts +94 -0
- package/src/table-convert/code.ts +99 -0
- package/src/table-convert/index.ts +42 -0
- package/src/table-convert/model.ts +72 -0
- package/src/table-convert/parse.ts +242 -0
- package/src/table-convert/serialize.ts +435 -0
- package/src/table-convert/transform.ts +133 -0
- package/src/table-convert/xlsx.ts +262 -0
- package/src/table-convert/zip.ts +127 -0
- package/src/timestamp.ts +50 -0
- package/src/url-codec.ts +25 -0
- package/src/uuid.ts +53 -0
- package/src/vacation.ts +171 -0
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
import { degrees, PDFDocument, rgb, StandardFonts } from "pdf-lib"
|
|
2
|
+
|
|
3
|
+
export const TABS = ["organize", "stamp", "metadata", "images"] as const
|
|
4
|
+
export type PdfStudioTab = (typeof TABS)[number]
|
|
5
|
+
|
|
6
|
+
export const ROTATIONS = [0, 90, 180, 270] as const
|
|
7
|
+
export type Rotation = (typeof ROTATIONS)[number]
|
|
8
|
+
|
|
9
|
+
export const NUMBER_POSITIONS = ["bottom-center", "bottom-right", "bottom-left", "top-center", "top-right", "top-left"] as const
|
|
10
|
+
export type NumberPosition = (typeof NUMBER_POSITIONS)[number]
|
|
11
|
+
|
|
12
|
+
export const NUMBER_FORMATS = ["slash", "number", "label"] as const
|
|
13
|
+
export type NumberFormat = (typeof NUMBER_FORMATS)[number]
|
|
14
|
+
|
|
15
|
+
export const WATERMARK_COLORS = ["gray", "red", "blue", "black"] as const
|
|
16
|
+
export type WatermarkColor = (typeof WATERMARK_COLORS)[number]
|
|
17
|
+
|
|
18
|
+
export const IMAGE_FORMATS = ["png", "jpeg"] as const
|
|
19
|
+
export type ExportImageFormat = (typeof IMAGE_FORMATS)[number]
|
|
20
|
+
|
|
21
|
+
export const IMAGE_SCALES = [1, 2, 3] as const
|
|
22
|
+
export type ExportImageScale = (typeof IMAGE_SCALES)[number]
|
|
23
|
+
|
|
24
|
+
export interface PdfMetadata {
|
|
25
|
+
title: string
|
|
26
|
+
author: string
|
|
27
|
+
subject: string
|
|
28
|
+
keywords: string
|
|
29
|
+
creator: string
|
|
30
|
+
producer: string
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface PdfPageInfo {
|
|
34
|
+
index: number
|
|
35
|
+
width: number
|
|
36
|
+
height: number
|
|
37
|
+
rotation: number
|
|
38
|
+
paper: string
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface PdfInspection {
|
|
42
|
+
pageCount: number
|
|
43
|
+
pages: PdfPageInfo[]
|
|
44
|
+
metadata: PdfMetadata
|
|
45
|
+
formFieldCount: number
|
|
46
|
+
byteLength: number
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface PagePlanItem {
|
|
50
|
+
id: string
|
|
51
|
+
sourceIndex: number
|
|
52
|
+
sourceName: string
|
|
53
|
+
pageIndex: number
|
|
54
|
+
rotationDelta: Rotation
|
|
55
|
+
width: number
|
|
56
|
+
height: number
|
|
57
|
+
originalRotation: number
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface PdfStudioOptions {
|
|
61
|
+
pageRange?: string
|
|
62
|
+
rotation?: Rotation
|
|
63
|
+
pageNumbers?: boolean
|
|
64
|
+
numberPosition?: NumberPosition
|
|
65
|
+
numberFormat?: NumberFormat
|
|
66
|
+
numberStart?: number
|
|
67
|
+
numberLang?: "pt" | "en"
|
|
68
|
+
watermarkText?: string
|
|
69
|
+
watermarkOpacity?: number
|
|
70
|
+
watermarkAngle?: number
|
|
71
|
+
watermarkSize?: number
|
|
72
|
+
watermarkColor?: WatermarkColor
|
|
73
|
+
flattenForms?: boolean
|
|
74
|
+
clearMetadata?: boolean
|
|
75
|
+
metadata?: Partial<PdfMetadata>
|
|
76
|
+
pagePlan?: PagePlanItem[]
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export const DEFAULT_STUDIO_OPTIONS: Required<Omit<PdfStudioOptions, "metadata" | "pagePlan">> = {
|
|
80
|
+
pageRange: "",
|
|
81
|
+
rotation: 0,
|
|
82
|
+
pageNumbers: false,
|
|
83
|
+
numberPosition: "bottom-center",
|
|
84
|
+
numberFormat: "slash",
|
|
85
|
+
numberStart: 1,
|
|
86
|
+
numberLang: "pt",
|
|
87
|
+
watermarkText: "",
|
|
88
|
+
watermarkOpacity: 22,
|
|
89
|
+
watermarkAngle: -35,
|
|
90
|
+
watermarkSize: 48,
|
|
91
|
+
watermarkColor: "gray",
|
|
92
|
+
flattenForms: false,
|
|
93
|
+
clearMetadata: false,
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const COLOR_RGB: Record<WatermarkColor, [number, number, number]> = {
|
|
97
|
+
gray: [0.45, 0.48, 0.52],
|
|
98
|
+
red: [0.82, 0.16, 0.16],
|
|
99
|
+
blue: [0.12, 0.36, 0.78],
|
|
100
|
+
black: [0.08, 0.09, 0.12],
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function normalizeRotation(deg: number): Rotation {
|
|
104
|
+
const mod = ((Math.round(deg / 90) * 90) % 360 + 360) % 360
|
|
105
|
+
return (mod === 90 || mod === 180 || mod === 270 ? mod : 0) as Rotation
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Identifica formatos de papel conhecidos a partir das dimensões em pontos (1 pt = 1/72 pol.)
|
|
109
|
+
export function detectPaperSize(widthPt: number, heightPt: number): string {
|
|
110
|
+
const w = Math.min(widthPt, heightPt)
|
|
111
|
+
const h = Math.max(widthPt, heightPt)
|
|
112
|
+
const near = (a: number, b: number) => Math.abs(a - b) <= 6
|
|
113
|
+
if (near(w, 595) && near(h, 842)) return "A4"
|
|
114
|
+
if (near(w, 420) && near(h, 595)) return "A5"
|
|
115
|
+
if (near(w, 842) && near(h, 1191)) return "A3"
|
|
116
|
+
if (near(w, 612) && near(h, 792)) return "Letter"
|
|
117
|
+
if (near(w, 612) && near(h, 1008)) return "Legal"
|
|
118
|
+
return `${Math.round(widthPt)}×${Math.round(heightPt)} pt`
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Converte uma expressão como "1-3, 5, 4-2" em índices 0-based na ordem solicitada
|
|
122
|
+
export function parsePageRanges(spec: string, totalPages: number): number[] {
|
|
123
|
+
if (totalPages <= 0) return []
|
|
124
|
+
const trimmed = spec.trim()
|
|
125
|
+
if (!trimmed) return Array.from({ length: totalPages }, (_, i) => i)
|
|
126
|
+
|
|
127
|
+
const indices: number[] = []
|
|
128
|
+
const parts = trimmed.split(",").map((s) => s.trim()).filter(Boolean)
|
|
129
|
+
for (const part of parts) {
|
|
130
|
+
const rangeMatch = /^(\d+)\s*-\s*(\d*)$/.exec(part)
|
|
131
|
+
if (rangeMatch) {
|
|
132
|
+
const start = Number(rangeMatch[1])
|
|
133
|
+
const end = rangeMatch[2] ? Number(rangeMatch[2]) : totalPages
|
|
134
|
+
if (start < 1 || end < 1 || start > totalPages || end > totalPages) {
|
|
135
|
+
throw new Error(`Intervalo fora do limite (1-${totalPages}): ${part}`)
|
|
136
|
+
}
|
|
137
|
+
const step = start <= end ? 1 : -1
|
|
138
|
+
for (let p = start; step > 0 ? p <= end : p >= end; p += step) {
|
|
139
|
+
indices.push(p - 1)
|
|
140
|
+
}
|
|
141
|
+
continue
|
|
142
|
+
}
|
|
143
|
+
if (/^\d+$/.test(part)) {
|
|
144
|
+
const page = Number(part)
|
|
145
|
+
if (page < 1 || page > totalPages) {
|
|
146
|
+
throw new Error(`Página fora do limite (1-${totalPages}): ${part}`)
|
|
147
|
+
}
|
|
148
|
+
indices.push(page - 1)
|
|
149
|
+
continue
|
|
150
|
+
}
|
|
151
|
+
throw new Error(`Intervalo de páginas inválido: ${part}`)
|
|
152
|
+
}
|
|
153
|
+
return indices
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function formatPageNumber(index: number, total: number, start: number, format: NumberFormat, lang: "pt" | "en" = "pt"): string {
|
|
157
|
+
const current = index + start
|
|
158
|
+
const last = total - 1 + start
|
|
159
|
+
if (format === "number") return String(current)
|
|
160
|
+
if (format === "label") return lang === "en" ? `Page ${current} of ${last}` : `Página ${current} de ${last}`
|
|
161
|
+
return `${current} / ${last}`
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export async function inspectPdf(bytes: Uint8Array): Promise<PdfInspection> {
|
|
165
|
+
const doc = await PDFDocument.load(bytes, { ignoreEncryption: true })
|
|
166
|
+
const pages = doc.getPages().map((page, index) => {
|
|
167
|
+
const { width, height } = page.getSize()
|
|
168
|
+
const rotation = normalizeRotation(page.getRotation().angle)
|
|
169
|
+
return {
|
|
170
|
+
index,
|
|
171
|
+
width: Math.round(width * 100) / 100,
|
|
172
|
+
height: Math.round(height * 100) / 100,
|
|
173
|
+
rotation,
|
|
174
|
+
paper: detectPaperSize(width, height),
|
|
175
|
+
}
|
|
176
|
+
})
|
|
177
|
+
let formFieldCount = 0
|
|
178
|
+
try {
|
|
179
|
+
formFieldCount = doc.getForm().getFields().length
|
|
180
|
+
} catch {
|
|
181
|
+
formFieldCount = 0
|
|
182
|
+
}
|
|
183
|
+
const keywordsRaw = doc.getKeywords()
|
|
184
|
+
return {
|
|
185
|
+
pageCount: pages.length,
|
|
186
|
+
pages,
|
|
187
|
+
metadata: {
|
|
188
|
+
title: doc.getTitle() ?? "",
|
|
189
|
+
author: doc.getAuthor() ?? "",
|
|
190
|
+
subject: doc.getSubject() ?? "",
|
|
191
|
+
keywords: keywordsRaw ?? "",
|
|
192
|
+
creator: doc.getCreator() ?? "",
|
|
193
|
+
producer: doc.getProducer() ?? "",
|
|
194
|
+
},
|
|
195
|
+
formFieldCount,
|
|
196
|
+
byteLength: bytes.byteLength,
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export async function buildInitialPagePlan(sources: { name: string, bytes: Uint8Array }[]): Promise<PagePlanItem[]> {
|
|
201
|
+
const items: PagePlanItem[] = []
|
|
202
|
+
for (let s = 0; s < sources.length; s++) {
|
|
203
|
+
const source = sources[s]
|
|
204
|
+
if (!source) continue
|
|
205
|
+
const info = await inspectPdf(source.bytes)
|
|
206
|
+
for (const p of info.pages) {
|
|
207
|
+
items.push({
|
|
208
|
+
id: `${s}-${p.index}-${Math.random().toString(36).slice(2, 7)}`,
|
|
209
|
+
sourceIndex: s,
|
|
210
|
+
sourceName: source.name,
|
|
211
|
+
pageIndex: p.index,
|
|
212
|
+
rotationDelta: 0,
|
|
213
|
+
width: p.width,
|
|
214
|
+
height: p.height,
|
|
215
|
+
originalRotation: p.rotation,
|
|
216
|
+
})
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return items
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export async function processPdf(sources: Uint8Array[], rawOptions: PdfStudioOptions = {}): Promise<{ bytes: Uint8Array, inspection: PdfInspection }> {
|
|
223
|
+
if (sources.length === 0) throw new Error("Nenhum PDF de entrada fornecido")
|
|
224
|
+
const opts = { ...DEFAULT_STUDIO_OPTIONS, ...rawOptions }
|
|
225
|
+
|
|
226
|
+
const loadedDocs: PDFDocument[] = []
|
|
227
|
+
for (const src of sources) {
|
|
228
|
+
const doc = await PDFDocument.load(src, { ignoreEncryption: true })
|
|
229
|
+
if (opts.flattenForms) {
|
|
230
|
+
try {
|
|
231
|
+
doc.getForm().flatten()
|
|
232
|
+
} catch {
|
|
233
|
+
// Documento sem formulário válido: ignora
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
loadedDocs.push(doc)
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Monta a lista base de páginas (do pagePlan interativo ou de todos os documentos na ordem)
|
|
240
|
+
const basePlan: { sourceIndex: number, pageIndex: number, rotationDelta: Rotation }[] = []
|
|
241
|
+
if (rawOptions.pagePlan && rawOptions.pagePlan.length > 0) {
|
|
242
|
+
for (const item of rawOptions.pagePlan) {
|
|
243
|
+
basePlan.push({ sourceIndex: item.sourceIndex, pageIndex: item.pageIndex, rotationDelta: item.rotationDelta })
|
|
244
|
+
}
|
|
245
|
+
} else {
|
|
246
|
+
for (let s = 0; s < loadedDocs.length; s++) {
|
|
247
|
+
const doc = loadedDocs[s]
|
|
248
|
+
if (!doc) continue
|
|
249
|
+
const count = doc.getPageCount()
|
|
250
|
+
for (let p = 0; p < count; p++) {
|
|
251
|
+
basePlan.push({ sourceIndex: s, pageIndex: p, rotationDelta: 0 })
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const selectedIndices = parsePageRanges(opts.pageRange, basePlan.length)
|
|
257
|
+
if (selectedIndices.length === 0) throw new Error("Nenhuma página selecionada")
|
|
258
|
+
|
|
259
|
+
const out = await PDFDocument.create()
|
|
260
|
+
const firstDoc = loadedDocs[0]
|
|
261
|
+
|
|
262
|
+
for (const idx of selectedIndices) {
|
|
263
|
+
const entry = basePlan[idx]
|
|
264
|
+
if (!entry) continue
|
|
265
|
+
const srcDoc = loadedDocs[entry.sourceIndex]
|
|
266
|
+
if (!srcDoc) continue
|
|
267
|
+
const [copied] = await out.copyPages(srcDoc, [entry.pageIndex])
|
|
268
|
+
if (!copied) continue
|
|
269
|
+
const currentAngle = copied.getRotation().angle
|
|
270
|
+
const nextAngle = normalizeRotation(currentAngle + entry.rotationDelta + opts.rotation)
|
|
271
|
+
copied.setRotation(degrees(nextAngle))
|
|
272
|
+
out.addPage(copied)
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const outPages = out.getPages()
|
|
276
|
+
const watermarkText = opts.watermarkText.trim()
|
|
277
|
+
|
|
278
|
+
if (watermarkText) {
|
|
279
|
+
const boldFont = await out.embedFont(StandardFonts.HelveticaBold)
|
|
280
|
+
const [r, g, b] = COLOR_RGB[opts.watermarkColor] ?? COLOR_RGB.gray
|
|
281
|
+
const size = Math.min(140, Math.max(10, opts.watermarkSize))
|
|
282
|
+
const opacity = Math.min(1, Math.max(0.03, opts.watermarkOpacity / 100))
|
|
283
|
+
const angleRad = (opts.watermarkAngle * Math.PI) / 180
|
|
284
|
+
|
|
285
|
+
for (const page of outPages) {
|
|
286
|
+
const { width, height } = page.getSize()
|
|
287
|
+
const textWidth = boldFont.widthOfTextAtSize(watermarkText, size)
|
|
288
|
+
const textHeight = boldFont.heightAtSize(size)
|
|
289
|
+
const cx = width / 2
|
|
290
|
+
const cy = height / 2
|
|
291
|
+
const dx = (textWidth / 2) * Math.cos(angleRad) - (textHeight / 3) * Math.sin(angleRad)
|
|
292
|
+
const dy = (textWidth / 2) * Math.sin(angleRad) + (textHeight / 3) * Math.cos(angleRad)
|
|
293
|
+
page.drawText(watermarkText, {
|
|
294
|
+
x: cx - dx,
|
|
295
|
+
y: cy - dy,
|
|
296
|
+
size,
|
|
297
|
+
font: boldFont,
|
|
298
|
+
color: rgb(r, g, b),
|
|
299
|
+
opacity,
|
|
300
|
+
rotate: degrees(opts.watermarkAngle),
|
|
301
|
+
})
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (opts.pageNumbers) {
|
|
306
|
+
const font = await out.embedFont(StandardFonts.Helvetica)
|
|
307
|
+
const fontSize = 10
|
|
308
|
+
const margin = 24
|
|
309
|
+
const total = outPages.length
|
|
310
|
+
|
|
311
|
+
for (let i = 0; i < total; i++) {
|
|
312
|
+
const page = outPages[i]
|
|
313
|
+
if (!page) continue
|
|
314
|
+
const { width, height } = page.getSize()
|
|
315
|
+
const label = formatPageNumber(i, total, opts.numberStart, opts.numberFormat, opts.numberLang)
|
|
316
|
+
const textWidth = font.widthOfTextAtSize(label, fontSize)
|
|
317
|
+
const isTop = opts.numberPosition.startsWith("top-")
|
|
318
|
+
const y = isTop ? height - margin - fontSize : margin
|
|
319
|
+
let x = (width - textWidth) / 2
|
|
320
|
+
if (opts.numberPosition.endsWith("-left")) x = margin
|
|
321
|
+
else if (opts.numberPosition.endsWith("-right")) x = width - margin - textWidth
|
|
322
|
+
|
|
323
|
+
page.drawText(label, {
|
|
324
|
+
x: Math.max(8, x),
|
|
325
|
+
y: Math.max(8, y),
|
|
326
|
+
size: fontSize,
|
|
327
|
+
font,
|
|
328
|
+
color: rgb(0.25, 0.27, 0.32),
|
|
329
|
+
})
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Metadados: preserva os do primeiro PDF salvo se clearMetadata ou se sobrescritos
|
|
334
|
+
if (opts.clearMetadata) {
|
|
335
|
+
out.setTitle("")
|
|
336
|
+
out.setAuthor("")
|
|
337
|
+
out.setSubject("")
|
|
338
|
+
out.setKeywords([])
|
|
339
|
+
out.setCreator("")
|
|
340
|
+
out.setProducer("")
|
|
341
|
+
} else {
|
|
342
|
+
const meta = rawOptions.metadata
|
|
343
|
+
const title = meta?.title !== undefined ? meta.title : (firstDoc?.getTitle() ?? "")
|
|
344
|
+
const author = meta?.author !== undefined ? meta.author : (firstDoc?.getAuthor() ?? "")
|
|
345
|
+
const subject = meta?.subject !== undefined ? meta.subject : (firstDoc?.getSubject() ?? "")
|
|
346
|
+
const keywords = meta?.keywords !== undefined ? meta.keywords : (firstDoc?.getKeywords() ?? "")
|
|
347
|
+
const creator = meta?.creator !== undefined ? meta.creator : (firstDoc?.getCreator() ?? "")
|
|
348
|
+
if (title) out.setTitle(title)
|
|
349
|
+
if (author) out.setAuthor(author)
|
|
350
|
+
if (subject) out.setSubject(subject)
|
|
351
|
+
if (keywords) out.setKeywords(keywords.split(",").map((k) => k.trim()).filter(Boolean))
|
|
352
|
+
if (creator) out.setCreator(creator)
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const bytes = await out.save()
|
|
356
|
+
const inspection = await inspectPdf(bytes)
|
|
357
|
+
return { bytes, inspection }
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Divide o documento em múltiplos PDFs (um por intervalo informado ou um por página se vazio)
|
|
361
|
+
export async function splitPdf(source: Uint8Array, rangeSpec = ""): Promise<{ name: string, bytes: Uint8Array, pages: number[] }[]> {
|
|
362
|
+
const srcDoc = await PDFDocument.load(source, { ignoreEncryption: true })
|
|
363
|
+
const total = srcDoc.getPageCount()
|
|
364
|
+
if (total === 0) return []
|
|
365
|
+
|
|
366
|
+
const groups: { label: string, indices: number[] }[] = []
|
|
367
|
+
const trimmed = rangeSpec.trim()
|
|
368
|
+
if (!trimmed) {
|
|
369
|
+
for (let i = 0; i < total; i++) {
|
|
370
|
+
groups.push({ label: `page-${i + 1}`, indices: [i] })
|
|
371
|
+
}
|
|
372
|
+
} else {
|
|
373
|
+
const parts = trimmed.split(",").map((s) => s.trim()).filter(Boolean)
|
|
374
|
+
for (const part of parts) {
|
|
375
|
+
const indices = parsePageRanges(part, total)
|
|
376
|
+
if (indices.length > 0) {
|
|
377
|
+
groups.push({ label: `pages-${part.replace(/\s+/g, "")}`, indices })
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const results: { name: string, bytes: Uint8Array, pages: number[] }[] = []
|
|
383
|
+
for (const group of groups) {
|
|
384
|
+
const out = await PDFDocument.create()
|
|
385
|
+
const copied = await out.copyPages(srcDoc, group.indices)
|
|
386
|
+
for (const p of copied) out.addPage(p)
|
|
387
|
+
results.push({
|
|
388
|
+
name: `${group.label}.pdf`,
|
|
389
|
+
bytes: await out.save(),
|
|
390
|
+
pages: group.indices.map((i) => i + 1),
|
|
391
|
+
})
|
|
392
|
+
}
|
|
393
|
+
return results
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Gera um PDF de demonstração de 4 páginas (com texto, cores e um campo de formulário) para testes imediatos
|
|
397
|
+
export async function createSamplePdf(): Promise<Uint8Array> {
|
|
398
|
+
const doc = await PDFDocument.create()
|
|
399
|
+
doc.setTitle("The Code Codex — Sample Document")
|
|
400
|
+
doc.setAuthor("Douglas Capeletti")
|
|
401
|
+
doc.setSubject("PDF Studio Demo & Test Suite")
|
|
402
|
+
doc.setKeywords(["pdf", "sample", "codex"])
|
|
403
|
+
doc.setCreator("The Code Codex PDF Studio")
|
|
404
|
+
|
|
405
|
+
const font = await doc.embedFont(StandardFonts.Helvetica)
|
|
406
|
+
const bold = await doc.embedFont(StandardFonts.HelveticaBold)
|
|
407
|
+
const form = doc.getForm()
|
|
408
|
+
|
|
409
|
+
const pagesSpec = [
|
|
410
|
+
{ title: "1. Cover & Interactive Form", subtitle: "A4 Portrait · 595 × 842 pt", accent: rgb(0.12, 0.36, 0.78), width: 595.28, height: 841.89 },
|
|
411
|
+
{ title: "2. Architecture Overview", subtitle: "A4 Portrait · Client-side PDF processing", accent: rgb(0.08, 0.52, 0.38), width: 595.28, height: 841.89 },
|
|
412
|
+
{ title: "3. Wide Diagram Layout", subtitle: "A4 Landscape · 842 × 595 pt", accent: rgb(0.68, 0.28, 0.12), width: 841.89, height: 595.28 },
|
|
413
|
+
{ title: "4. Appendix & Notes", subtitle: "A4 Portrait · Ready to split, rotate or stamp", accent: rgb(0.42, 0.2, 0.65), width: 595.28, height: 841.89 },
|
|
414
|
+
]
|
|
415
|
+
|
|
416
|
+
pagesSpec.forEach((spec, idx) => {
|
|
417
|
+
const page = doc.addPage([spec.width, spec.height])
|
|
418
|
+
// Moldura e faixa superior
|
|
419
|
+
page.drawRectangle({ x: 36, y: 36, width: spec.width - 72, height: spec.height - 72, borderColor: rgb(0.82, 0.84, 0.88), borderWidth: 1.5 })
|
|
420
|
+
page.drawRectangle({ x: 36, y: spec.height - 108, width: spec.width - 72, height: 72, color: spec.accent })
|
|
421
|
+
page.drawText("THE CODE CODEX · PDF STUDIO", { x: 56, y: spec.height - 68, size: 10, font: bold, color: rgb(0.92, 0.95, 1) })
|
|
422
|
+
page.drawText(spec.title, { x: 56, y: spec.height - 92, size: 18, font: bold, color: rgb(1, 1, 1) })
|
|
423
|
+
|
|
424
|
+
page.drawText(spec.subtitle, { x: 56, y: spec.height - 140, size: 12, font: bold, color: rgb(0.2, 0.22, 0.28) })
|
|
425
|
+
page.drawText(`Original page #${idx + 1} of ${pagesSpec.length}. Try rotating, reordering, watermarking, or adding page numbers.`, {
|
|
426
|
+
x: 56,
|
|
427
|
+
y: spec.height - 165,
|
|
428
|
+
size: 10.5,
|
|
429
|
+
font,
|
|
430
|
+
color: rgb(0.35, 0.38, 0.44),
|
|
431
|
+
})
|
|
432
|
+
|
|
433
|
+
// Número grande no centro para facilitar visualização de rotação nas miniaturas
|
|
434
|
+
const bigNum = String(idx + 1)
|
|
435
|
+
const numWidth = bold.widthOfTextAtSize(bigNum, 96)
|
|
436
|
+
page.drawText(bigNum, {
|
|
437
|
+
x: (spec.width - numWidth) / 2,
|
|
438
|
+
y: spec.height / 2 - 36,
|
|
439
|
+
size: 96,
|
|
440
|
+
font: bold,
|
|
441
|
+
color: rgb(0.9, 0.92, 0.95),
|
|
442
|
+
})
|
|
443
|
+
|
|
444
|
+
if (idx === 0) {
|
|
445
|
+
page.drawText("Reviewer Note (Form Field):", { x: 56, y: spec.height - 210, size: 10, font: bold, color: rgb(0.2, 0.22, 0.28) })
|
|
446
|
+
const field = form.createTextField("reviewer_note")
|
|
447
|
+
field.setText("Approved — test form flattening in the Stamp tab")
|
|
448
|
+
field.addToPage(page, { x: 56, y: spec.height - 244, width: 320, height: 24 })
|
|
449
|
+
}
|
|
450
|
+
})
|
|
451
|
+
|
|
452
|
+
return doc.save()
|
|
453
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// Fonte de aleatoriedade injetável: usa crypto no navegador e permite valores fixos nos testes
|
|
2
|
+
export type Random = (max: number) => number
|
|
3
|
+
|
|
4
|
+
// Inteiro uniforme em [0, max) sem viés de módulo (rejection sampling sobre Uint32)
|
|
5
|
+
export const secureRandom: Random = (max) => {
|
|
6
|
+
if (max <= 0) return 0
|
|
7
|
+
const limit = Math.floor(0x1_0000_0000 / max) * max
|
|
8
|
+
const buffer = new Uint32Array(1)
|
|
9
|
+
let value: number
|
|
10
|
+
do {
|
|
11
|
+
crypto.getRandomValues(buffer)
|
|
12
|
+
value = buffer[0] ?? 0
|
|
13
|
+
} while (value >= limit)
|
|
14
|
+
return value % max
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function pick<T>(items: readonly T[], random: Random = secureRandom): T {
|
|
18
|
+
return items[random(items.length)] as T
|
|
19
|
+
}
|