medias-fakerator 1.0.0-dra → 1.0.0-dra.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.
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
package/index.js CHANGED
@@ -17,6 +17,7 @@ const {
17
17
  bratVidGenerator,
18
18
  generateAnimatedBratVid,
19
19
  } = require("./lib/generators.js");
20
+ const generateCard = require("./lib/ktpGenerator.js");
20
21
 
21
22
  module.exports = {
22
23
  generateQuote,
@@ -34,6 +35,10 @@ module.exports = {
34
35
  generateGoogleLyrics,
35
36
  bratGenerator,
36
37
  bratVidGenerator,
38
+ bratHdGenerator,
39
+ bratVidHdGenerator,
37
40
  generateAnimatedBratVid,
41
+ generateAnimatedBratVidHd,
42
+ generateCard,
38
43
  QuoteGenerator,
39
44
  };
package/lib/generators.js CHANGED
@@ -2799,6 +2799,31 @@ function generateAnimatedBratVid(tempFrameDir, outputPath) {
2799
2799
  }
2800
2800
  });
2801
2801
  }
2802
+ function generateAnimatedBratVidHd(tempFrameDir, outputPath) {
2803
+ return new Promise((resolve, reject) => {
2804
+ try {
2805
+ ffmpeg()
2806
+ .input(path.join(tempFrameDir, "frame_%d.png"))
2807
+ .inputOptions("-framerate", "1.5")
2808
+ .videoCodec("libwebp")
2809
+ .outputOptions(
2810
+ "-loop",
2811
+ "0",
2812
+ "-lossless",
2813
+ "1",
2814
+ "-preset",
2815
+ "picture",
2816
+ "-an"
2817
+ )
2818
+ .output(outputPath)
2819
+ .on("end", resolve)
2820
+ .on("error", reject)
2821
+ .run();
2822
+ } catch (err) {
2823
+ reject(err);
2824
+ }
2825
+ });
2826
+ }
2802
2827
 
2803
2828
  async function bratVidGenerator(
2804
2829
  text,
@@ -3199,6 +3224,403 @@ async function bratGenerator(teks, highlightWords = []) {
3199
3224
  throw error;
3200
3225
  }
3201
3226
  }
3227
+ async function bratVidHdGenerator(
3228
+ text,
3229
+ width,
3230
+ height,
3231
+ bgColor = "#FFFFFF",
3232
+ textColor = "#000000",
3233
+ highlightWords = []
3234
+ ) {
3235
+ try {
3236
+ if (typeof text !== "string" || text.trim().length === 0)
3237
+ throw new Error("Text must be a non-empty string");
3238
+ if (!Array.isArray(highlightWords))
3239
+ throw new TypeError("highlightWords must be an array.");
3240
+ if (
3241
+ !Number.isInteger(width) ||
3242
+ !Number.isInteger(height) ||
3243
+ width <= 0 ||
3244
+ height <= 0
3245
+ )
3246
+ throw new Error("Width and height must be positive integers");
3247
+ if (!/^#[0-9A-F]{6}$/i.test(bgColor) || !/^#[0-9A-F]{6}$/i.test(textColor))
3248
+ throw new Error("Colors must be in hex format (#RRGGBB)");
3249
+ const allEmojiImages = await emojiImageByBrandPromise;
3250
+ const emojiCache = allEmojiImages["apple"] || {};
3251
+ const padding = 20;
3252
+ const availableWidth = width - padding * 2;
3253
+ const tempCanvas = createCanvas(1, 1);
3254
+ const tempCtx = tempCanvas.getContext("2d");
3255
+ if (!tempCtx) throw new Error("Failed to create canvas context");
3256
+ const tokens = text.match(/\S+|\n/g) || [];
3257
+ if (tokens.length === 0)
3258
+ throw new Error("No valid content tokens found in the text");
3259
+ let frames = [];
3260
+ const recalculateSegmentWidths = (segments, fontSize, ctx) => {
3261
+ return segments.map((seg) => {
3262
+ let newWidth = seg.width;
3263
+ switch (seg.type) {
3264
+ case "bold":
3265
+ ctx.font = `bold ${fontSize}px Arial`;
3266
+ newWidth = ctx.measureText(seg.content).width;
3267
+ break;
3268
+ case "italic":
3269
+ ctx.font = `italic ${fontSize}px Arial`;
3270
+ newWidth = ctx.measureText(seg.content).width;
3271
+ break;
3272
+ case "bolditalic":
3273
+ ctx.font = `bold italic ${fontSize}px Arial`;
3274
+ newWidth = ctx.measureText(seg.content).width;
3275
+ break;
3276
+ case "monospace":
3277
+ ctx.font = `${fontSize}px 'Courier New', monospace`;
3278
+ newWidth = ctx.measureText(seg.content).width;
3279
+ break;
3280
+ case "strikethrough":
3281
+ case "text":
3282
+ ctx.font = `${fontSize}px Arial`;
3283
+ newWidth = ctx.measureText(seg.content).width;
3284
+ break;
3285
+ case "emoji":
3286
+ newWidth = fontSize * 1.2;
3287
+ break;
3288
+ }
3289
+ return { ...seg, width: newWidth };
3290
+ });
3291
+ };
3292
+ const renderSegment = async (ctx, segment, x, y, fontSize, lineHeight) => {
3293
+ ctx.fillStyle = highlightWords.includes(segment.content)
3294
+ ? "red"
3295
+ : textColor;
3296
+ switch (segment.type) {
3297
+ case "bold":
3298
+ ctx.font = `bold ${fontSize}px Arial`;
3299
+ break;
3300
+ case "italic":
3301
+ ctx.font = `italic ${fontSize}px Arial`;
3302
+ break;
3303
+ case "bolditalic":
3304
+ ctx.font = `bold italic ${fontSize}px Arial`;
3305
+ break;
3306
+ case "monospace":
3307
+ ctx.font = `${fontSize}px 'Courier New', monospace`;
3308
+ break;
3309
+ default:
3310
+ ctx.font = `${fontSize}px Arial`;
3311
+ break;
3312
+ }
3313
+ if (segment.type === "emoji") {
3314
+ const emojiSize = fontSize * 1.2;
3315
+ const emojiY = y + (lineHeight - emojiSize) / 2;
3316
+ if (!emojiCache[segment.content])
3317
+ throw new Error(`Emoji ${segment.content} tidak ditemukan`);
3318
+ const emojiImg = await loadImage(
3319
+ Buffer.from(emojiCache[segment.content], "base64")
3320
+ );
3321
+ ctx.drawImage(emojiImg, x, emojiY, emojiSize, emojiSize);
3322
+ } else {
3323
+ ctx.fillText(segment.content, x, y);
3324
+ if (segment.type === "strikethrough") {
3325
+ ctx.strokeStyle = ctx.fillStyle;
3326
+ ctx.lineWidth = Math.max(1, fontSize / 15);
3327
+ const lineY = y + lineHeight / 2.1;
3328
+ ctx.beginPath();
3329
+ ctx.moveTo(x, lineY);
3330
+ ctx.lineTo(x + segment.width, lineY);
3331
+ ctx.stroke();
3332
+ }
3333
+ }
3334
+ };
3335
+ for (let i = 1; i <= tokens.length; i++) {
3336
+ const frameTokens = tokens.slice(0, i);
3337
+ const currentText = frameTokens
3338
+ .join(" ")
3339
+ .replace(/ \n /g, "\n")
3340
+ .replace(/\n /g, "\n")
3341
+ .replace(/ \n/g, "\n");
3342
+ if (currentText.trim() === "") continue;
3343
+ let fontSize = 200;
3344
+ let finalLines = [];
3345
+ let lineHeight = 0;
3346
+ const lineHeightMultiplier = 1.3;
3347
+ while (fontSize > 10) {
3348
+ let currentRenderLines = [];
3349
+ const textLines = currentText.split("\n");
3350
+ for (const singleLineText of textLines) {
3351
+ if (singleLineText === "") {
3352
+ currentRenderLines.push([]);
3353
+ continue;
3354
+ }
3355
+ let segments = parseTextToSegments(singleLineText, tempCtx, fontSize);
3356
+ let segmentsForSizing = recalculateSegmentWidths(
3357
+ segments,
3358
+ fontSize,
3359
+ tempCtx
3360
+ );
3361
+ let wrappedLines = rebuildLinesFromSegments(
3362
+ segmentsForSizing,
3363
+ availableWidth,
3364
+ tempCtx,
3365
+ fontSize
3366
+ );
3367
+ currentRenderLines.push(...wrappedLines);
3368
+ }
3369
+ const currentLineHeight = fontSize * lineHeightMultiplier;
3370
+ const totalTextHeight = currentRenderLines.length * currentLineHeight;
3371
+ if (totalTextHeight <= height - padding * 2) {
3372
+ finalLines = currentRenderLines;
3373
+ lineHeight = currentLineHeight;
3374
+ break;
3375
+ }
3376
+ fontSize -= 2;
3377
+ }
3378
+ const canvas = createCanvas(width, height);
3379
+ const ctx = canvas.getContext("2d");
3380
+ if (!ctx) throw new Error("Failed to create canvas context");
3381
+ ctx.fillStyle = bgColor;
3382
+ ctx.fillRect(0, 0, width, height);
3383
+ ctx.textBaseline = "top";
3384
+ const totalTextBlockHeight = finalLines.length * lineHeight;
3385
+ const startY = (height - totalTextBlockHeight) / 2;
3386
+ for (let j = 0; j < finalLines.length; j++) {
3387
+ const line = finalLines[j];
3388
+ const positionY = startY + j * lineHeight;
3389
+ const contentSegments = line.filter((seg) => seg.type !== "whitespace");
3390
+ if (contentSegments.length <= 1) {
3391
+ let positionX = padding;
3392
+ for (const segment of line) {
3393
+ await renderSegment(
3394
+ ctx,
3395
+ segment,
3396
+ positionX,
3397
+ positionY,
3398
+ fontSize,
3399
+ lineHeight
3400
+ );
3401
+ positionX += segment.width;
3402
+ }
3403
+ } else {
3404
+ const totalContentWidth = contentSegments.reduce(
3405
+ (sum, seg) => sum + seg.width,
3406
+ 0
3407
+ );
3408
+ const spaceBetween =
3409
+ (availableWidth - totalContentWidth) / (contentSegments.length - 1);
3410
+ let positionX = padding;
3411
+ for (let k = 0; k < contentSegments.length; k++) {
3412
+ const segment = contentSegments[k];
3413
+ await renderSegment(
3414
+ ctx,
3415
+ segment,
3416
+ positionX,
3417
+ positionY,
3418
+ fontSize,
3419
+ lineHeight
3420
+ );
3421
+ positionX += segment.width;
3422
+ if (k < contentSegments.length - 1) {
3423
+ positionX += spaceBetween;
3424
+ }
3425
+ }
3426
+ }
3427
+ }
3428
+ const buffer = canvas.toBuffer("image/png");
3429
+ frames.push(buffer);
3430
+ }
3431
+ return frames;
3432
+ } catch (error) {
3433
+ throw error;
3434
+ }
3435
+ }
3436
+ async function bratHdGenerator(teks, highlightWords = []) {
3437
+ try {
3438
+ if (typeof teks !== "string" || teks.trim().length === 0)
3439
+ throw new Error("Teks tidak boleh kosong.");
3440
+ if (!Array.isArray(highlightWords))
3441
+ throw new TypeError("highlightWords harus berupa array.");
3442
+ const allEmojiImages = await emojiImageByBrandPromise;
3443
+ const emojiCache = allEmojiImages["apple"] || {};
3444
+ let width = 512,
3445
+ height = 512,
3446
+ margin = 8,
3447
+ verticalPadding = 8;
3448
+ const canvas = createCanvas(width, height);
3449
+ const ctx = canvas.getContext("2d");
3450
+ if (!ctx) throw new Error("Gagal membuat konteks kanvas.");
3451
+ ctx.fillStyle = "white";
3452
+ ctx.fillRect(0, 0, width, height);
3453
+ ctx.textAlign = "left";
3454
+ ctx.textBaseline = "top";
3455
+ let fontSize = 200;
3456
+ let lineHeightMultiplier = 1.3;
3457
+ const availableWidth = width - 2 * margin;
3458
+ let finalLines = [];
3459
+ let finalFontSize = 0;
3460
+ let lineHeight = 0;
3461
+ const wordCount = (
3462
+ teks.trim().match(/(\p{L}|\p{N}|\p{Emoji_Presentation})+/gu) || []
3463
+ ).length;
3464
+ let lastKnownGoodSolution = null;
3465
+ while (fontSize > 10) {
3466
+ let currentRenderLines = [];
3467
+ const textLines = teks.split("\n");
3468
+ for (const singleLineText of textLines) {
3469
+ if (singleLineText === "") {
3470
+ currentRenderLines.push([]);
3471
+ continue;
3472
+ }
3473
+ let segments = parseTextToSegments(singleLineText, ctx, fontSize);
3474
+ let wrappedLines = rebuildLinesFromSegments(
3475
+ segments,
3476
+ availableWidth,
3477
+ ctx,
3478
+ fontSize
3479
+ );
3480
+ currentRenderLines.push(...wrappedLines);
3481
+ }
3482
+ if (
3483
+ currentRenderLines.length === 1 &&
3484
+ currentRenderLines[0].filter((seg) => seg.type !== "whitespace")
3485
+ .length === 2 &&
3486
+ currentRenderLines[0].some((seg) => seg.type === "text") &&
3487
+ currentRenderLines[0].some((seg) => seg.type === "emoji")
3488
+ ) {
3489
+ const textSeg = currentRenderLines[0].find(
3490
+ (seg) => seg.type === "text"
3491
+ );
3492
+ const emojiSeg = currentRenderLines[0].find(
3493
+ (seg) => seg.type === "emoji"
3494
+ );
3495
+ currentRenderLines = [[textSeg], [emojiSeg]];
3496
+ }
3497
+ const currentLineHeight = fontSize * lineHeightMultiplier;
3498
+ const totalTextHeight = currentRenderLines.length * currentLineHeight;
3499
+ if (totalTextHeight <= height - 2 * verticalPadding) {
3500
+ lastKnownGoodSolution = {
3501
+ lines: currentRenderLines,
3502
+ fontSize: fontSize,
3503
+ lineHeight: currentLineHeight,
3504
+ };
3505
+ if (wordCount === 4) {
3506
+ if (currentRenderLines.length === 2) {
3507
+ finalLines = currentRenderLines;
3508
+ finalFontSize = fontSize;
3509
+ lineHeight = currentLineHeight;
3510
+ break;
3511
+ }
3512
+ } else {
3513
+ finalLines = currentRenderLines;
3514
+ finalFontSize = fontSize;
3515
+ lineHeight = currentLineHeight;
3516
+ break;
3517
+ }
3518
+ }
3519
+ fontSize -= 2;
3520
+ }
3521
+ if (finalLines.length === 0 && lastKnownGoodSolution) {
3522
+ finalLines = lastKnownGoodSolution.lines;
3523
+ finalFontSize = lastKnownGoodSolution.fontSize;
3524
+ lineHeight = lastKnownGoodSolution.lineHeight;
3525
+ }
3526
+ if (
3527
+ finalLines.length === 1 &&
3528
+ finalLines[0].length === 1 &&
3529
+ finalLines[0][0].type === "text"
3530
+ ) {
3531
+ const theOnlyWord = finalLines[0][0].content;
3532
+ const heightBasedSize =
3533
+ (height - 2 * verticalPadding) / lineHeightMultiplier;
3534
+ ctx.font = `200px Arial`;
3535
+ const referenceWidth = ctx.measureText(theOnlyWord).width;
3536
+ const widthBasedSize = (availableWidth / referenceWidth) * 200;
3537
+ finalFontSize = Math.floor(Math.min(heightBasedSize, widthBasedSize));
3538
+ lineHeight = finalFontSize * lineHeightMultiplier;
3539
+ }
3540
+ const totalFinalHeight = finalLines.length * lineHeight;
3541
+ let y =
3542
+ finalLines.length === 1
3543
+ ? verticalPadding
3544
+ : (height - totalFinalHeight) / 2;
3545
+ const renderSegment = async (segment, x, y) => {
3546
+ ctx.fillStyle = isHighlighted(highlightWords, segment.content)
3547
+ ? "red"
3548
+ : "black";
3549
+ switch (segment.type) {
3550
+ case "bold":
3551
+ ctx.font = `bold ${finalFontSize}px Arial`;
3552
+ break;
3553
+ case "italic":
3554
+ ctx.font = `italic ${finalFontSize}px Arial`;
3555
+ break;
3556
+ case "bolditalic":
3557
+ ctx.font = `bold italic ${finalFontSize}px Arial`;
3558
+ break;
3559
+ case "monospace":
3560
+ ctx.font = `${finalFontSize}px 'Courier New', monospace`;
3561
+ break;
3562
+ case "strikethrough":
3563
+ case "text":
3564
+ default:
3565
+ ctx.font = `${finalFontSize}px Arial`;
3566
+ break;
3567
+ }
3568
+ if (segment.type === "emoji") {
3569
+ const emojiSize = finalFontSize * 1.2;
3570
+ const emojiY = y + (lineHeight - emojiSize) / 2;
3571
+ if (!emojiCache[segment.content])
3572
+ throw new Error(`Emoji ${segment.content} tidak ditemukan di cache`);
3573
+ const emojiImg = await loadImage(
3574
+ Buffer.from(emojiCache[segment.content], "base64")
3575
+ );
3576
+ ctx.drawImage(emojiImg, x, emojiY, emojiSize, emojiSize);
3577
+ } else {
3578
+ ctx.fillText(segment.content, x, y);
3579
+ if (segment.type === "strikethrough") {
3580
+ ctx.strokeStyle = ctx.fillStyle;
3581
+ ctx.lineWidth = Math.max(1, finalFontSize / 15);
3582
+ const lineY = y + lineHeight / 2.1;
3583
+ ctx.beginPath();
3584
+ ctx.moveTo(x, lineY);
3585
+ ctx.lineTo(x + segment.width, lineY);
3586
+ ctx.stroke();
3587
+ }
3588
+ }
3589
+ };
3590
+ for (const line of finalLines) {
3591
+ const contentSegments = line.filter((seg) => seg.type !== "whitespace");
3592
+ if (contentSegments.length <= 1) {
3593
+ let x = margin;
3594
+ for (const segment of line) {
3595
+ await renderSegment(segment, x, y);
3596
+ x += segment.width;
3597
+ }
3598
+ } else {
3599
+ const totalContentWidth = contentSegments.reduce(
3600
+ (sum, seg) => sum + seg.width,
3601
+ 0
3602
+ );
3603
+ const spacePerGap =
3604
+ (availableWidth - totalContentWidth) / (contentSegments.length - 1);
3605
+ let currentX = margin;
3606
+ for (let i = 0; i < contentSegments.length; i++) {
3607
+ const segment = contentSegments[i];
3608
+ await renderSegment(segment, currentX, y);
3609
+ currentX += segment.width;
3610
+ if (i < contentSegments.length - 1) {
3611
+ currentX += spacePerGap;
3612
+ }
3613
+ }
3614
+ }
3615
+ y += lineHeight;
3616
+ }
3617
+ const buffer = canvas.toBuffer("image/png");
3618
+ return buffer;
3619
+ } catch (error) {
3620
+ console.error("Terjadi error di bratGenerator:", error);
3621
+ throw error;
3622
+ }
3623
+ }
3202
3624
 
3203
3625
  module.exports = {
3204
3626
  generateQuote,
@@ -3216,5 +3638,8 @@ module.exports = {
3216
3638
  generateGoogleLyrics,
3217
3639
  bratGenerator,
3218
3640
  bratVidGenerator,
3641
+ bratHdGenerator,
3642
+ bratVidHdGenerator,
3219
3643
  generateAnimatedBratVid,
3644
+ generateAnimatedBratVidHd,
3220
3645
  };
@@ -0,0 +1,185 @@
1
+ const fs = require("fs-extra");
2
+ const path = require("path");
3
+ const { createCanvas, loadImage, registerFont } = require("canvas");
4
+
5
+ /* ================= FONT REGISTRATION ================= */
6
+ try {
7
+ registerFont(path.join(process.cwd(), "assets/fonts/Ocr.ttf"), {
8
+ family: "Ocr",
9
+ });
10
+ registerFont(path.join(process.cwd(), "assets/fonts/Sign.ttf"), {
11
+ family: "Sign",
12
+ });
13
+ registerFont(path.join(process.cwd(), "assets/fonts/Frutiger.ttf"), {
14
+ family: "Frutiger",
15
+ });
16
+ } catch (error) {
17
+ console.error("⚠️ Gagal mendaftarkan font:", error);
18
+ }
19
+
20
+ /* ================= FONT CONSTANT ================= */
21
+ const FONT_OCR = "Ocr";
22
+ const FONT_NORMAL = "Frutiger";
23
+
24
+ /* ================= DEFAULT DATA ================= */
25
+ const defaultUserData = {
26
+ NIK: "1234567890123456",
27
+ Nama: "YANTO OLI GARDAN",
28
+ "Tempat/Tgl Lahir": "Zimbabwe, 69-69-6969",
29
+ "Jenis Kelamin": "LAKI-LAKI",
30
+ "Gol. Darah": "O",
31
+ Alamat: "JL. Kemana Aja",
32
+ "RT/RW": "001/002",
33
+ "Kel/Desa": "JOLODONG",
34
+ Kecamatan: "KECIMITIN",
35
+ Agama: "KEPO",
36
+ "Status Perkawinan": "SERING KAWIN",
37
+ Pekerjaan: "PENGANGGURAN BANYAK ACARA",
38
+ Kewarganegaraan: "PMO",
39
+ "Berlaku Hingga": "SAMPE MENINGGOY",
40
+ };
41
+
42
+ const defaultHeaderFooterData = {
43
+ Provinsi: "PROVINSI",
44
+ Kabupaten: "KABUPATEN",
45
+ LokasiTTD: "KONTOLODON",
46
+ TanggalTTD: "12-34-5678",
47
+ };
48
+
49
+ /* ================= COORDINATES ================= */
50
+ const coordinates = {
51
+ // ✅ HANYA NIK PAKAI OCR
52
+ NIK: { x: 195, y: 139, fontSize: 34, font: FONT_OCR },
53
+
54
+ // ❌ SELAIN NIK → FONT NORMAL
55
+ Nama: { x: 215, y: 174, fontSize: 19 },
56
+ "Tempat/Tgl Lahir": { x: 215, y: 200, fontSize: 19 },
57
+ "Jenis Kelamin": { x: 215, y: 224, fontSize: 19 },
58
+ "Gol. Darah": { x: 520, y: 224, fontSize: 19 },
59
+ Alamat: { x: 215, y: 249, fontSize: 19 },
60
+ "RT/RW": { x: 215, y: 274, fontSize: 19 },
61
+ "Kel/Desa": { x: 215, y: 298, fontSize: 19 },
62
+ Kecamatan: { x: 215, y: 322, fontSize: 19 },
63
+ Agama: { x: 215, y: 348, fontSize: 19 },
64
+ "Status Perkawinan": { x: 215, y: 372, fontSize: 19 },
65
+ Pekerjaan: { x: 215, y: 397, fontSize: 19 },
66
+ Kewarganegaraan: { x: 215, y: 422, fontSize: 19 },
67
+ "Berlaku Hingga": { x: 215, y: 446, fontSize: 19 },
68
+ };
69
+
70
+ const headerFooterCoordinates = {
71
+ Provinsi: { y: 55, fontSize: 34, align: "center" },
72
+ Kabupaten: { y: 90, fontSize: 30, align: "center" },
73
+ LokasiTTD: {
74
+ x: 690,
75
+ y: 410,
76
+ fontSize: 19,
77
+ align: "center",
78
+ },
79
+ TanggalTTD: {
80
+ x: 690,
81
+ y: 430,
82
+ fontSize: 16,
83
+ align: "center",
84
+ },
85
+ };
86
+
87
+ /* ================= MAIN FUNCTION ================= */
88
+ async function generateCard(options = {}) {
89
+ const {
90
+ userData = {},
91
+ headerFooterData = {},
92
+ profilePicPath,
93
+ signatureText,
94
+ } = options;
95
+
96
+ const finalUserData = { ...defaultUserData, ...userData };
97
+ const finalHeaderFooterData = {
98
+ ...defaultHeaderFooterData,
99
+ ...headerFooterData,
100
+ };
101
+
102
+ /* ===== LOAD PROFILE PICTURE ===== */
103
+ let profilePic;
104
+ try {
105
+ if (Buffer.isBuffer(profilePicPath)) {
106
+ profilePic = await loadImage(profilePicPath);
107
+ } else if (
108
+ typeof profilePicPath === "string" &&
109
+ (await fs.pathExists(profilePicPath))
110
+ ) {
111
+ profilePic = await loadImage(profilePicPath);
112
+ } else {
113
+ console.error("⚠️ profilePicPath tidak valid");
114
+ return null;
115
+ }
116
+ } catch (err) {
117
+ console.error("⚠️ Gagal load foto profil:", err);
118
+ return null;
119
+ }
120
+
121
+ try {
122
+ const background = await loadImage(
123
+ path.join(process.cwd(), "assets/ktp/output.png")
124
+ );
125
+
126
+ const canvas = createCanvas(background.width, background.height);
127
+ const ctx = canvas.getContext("2d");
128
+
129
+ ctx.textBaseline = "alphabetic";
130
+ ctx.fillStyle = "black";
131
+
132
+ ctx.drawImage(background, 0, 0);
133
+ ctx.drawImage(profilePic, 584, 136, 201, 251);
134
+
135
+ /* ===== DRAW USER DATA ===== */
136
+ for (const [label, value] of Object.entries(finalUserData)) {
137
+ const style = coordinates[label];
138
+ if (!style) continue;
139
+
140
+ const fontFamily = style.font || FONT_NORMAL;
141
+
142
+ ctx.textAlign = "left";
143
+ ctx.font = `${style.fontWeight || "normal"} ${
144
+ style.fontSize || 18
145
+ }px ${fontFamily}`;
146
+
147
+ ctx.lineWidth = 0.6;
148
+ ctx.strokeText(String(value).toUpperCase(), style.x, style.y);
149
+ ctx.fillText(String(value).toUpperCase(), style.x, style.y);
150
+ }
151
+
152
+ /* ===== HEADER & FOOTER ===== */
153
+ for (const [label, value] of Object.entries(finalHeaderFooterData)) {
154
+ const style = headerFooterCoordinates[label];
155
+ if (!style) continue;
156
+
157
+ const xPos = style.x || background.width / 2;
158
+
159
+ ctx.textAlign = style.align || "left";
160
+ ctx.font = `${style.fontWeight || "normal"} ${
161
+ style.fontSize || 18
162
+ }px ${FONT_NORMAL}`;
163
+
164
+ ctx.lineWidth = 0.8;
165
+ ctx.strokeText(String(value).toUpperCase(), xPos, style.y);
166
+ ctx.fillText(String(value).toUpperCase(), xPos, style.y);
167
+ }
168
+
169
+ /* ===== SIGNATURE ===== */
170
+ const signText = signatureText || finalUserData.Nama.split(" ")[0];
171
+
172
+ ctx.textAlign = "center";
173
+ ctx.font = "23px Sign";
174
+ ctx.lineWidth = 1;
175
+ ctx.strokeText(signText.toUpperCase(), 660, 450);
176
+ ctx.fillText(signText.toUpperCase(), 660, 450);
177
+
178
+ return canvas.toBuffer("image/png");
179
+ } catch (error) {
180
+ console.error("❌ Terjadi kesalahan saat membuat kartu:", error);
181
+ return null;
182
+ }
183
+ }
184
+
185
+ module.exports = { generateCard };
package/lib/utils.js CHANGED
@@ -1,66 +1,109 @@
1
- const EmojiDbLib = require('emoji-db');
1
+ const EmojiDbLib = require("emoji-db");
2
2
 
3
3
  let emojiDb;
4
4
  try {
5
5
  emojiDb = new EmojiDbLib({ useDefaultDb: true });
6
- if (!emojiDb || typeof emojiDb.searchFromText !== 'function') throw new Error('Gagal menginisialisasi database emoji');
6
+ if (!emojiDb || typeof emojiDb.searchFromText !== "function")
7
+ throw new Error("Gagal menginisialisasi database emoji");
7
8
  } catch (error) {
8
- console.error('Error saat inisialisasi database emoji:', error);
9
+ console.error("Error saat inisialisasi database emoji:", error);
9
10
  throw error;
10
11
  }
11
12
 
12
13
  function parseTextToSegments(text, ctx, fontSize) {
13
14
  try {
14
- if (typeof text !== 'string') text = String(text);
15
- if (!ctx || typeof ctx.measureText !== 'function') throw new TypeError('Invalid canvas context');
15
+ if (typeof text !== "string") text = String(text);
16
+ if (!ctx || typeof ctx.measureText !== "function")
17
+ throw new TypeError("Invalid canvas context");
16
18
  const finalSegments = [];
17
- const rawLines = text.split('\n');
19
+ const rawLines = text.split("\n");
18
20
  rawLines.forEach((line, index) => {
19
- if (line === '') {
21
+ if (line === "") {
20
22
  } else {
21
23
  const segmentsInLine = [];
22
- const emojiMatches = emojiDb.searchFromText({ input: line, fixCodePoints: true });
24
+ const emojiMatches = emojiDb.searchFromText({
25
+ input: line,
26
+ fixCodePoints: true,
27
+ });
23
28
  let lastIndex = 0;
24
29
  const processChunk = (chunk) => {
25
30
  if (!chunk) return;
26
- const tokenizerRegex = /(\*_.*?_\*|_\*.*?\*_)|(\*.*?\*)|(_.*?_)|(~.*?~)|(```.*?```)|(\s+)|([^\s*~_`]+)/g;
31
+ const tokenizerRegex =
32
+ /(\*_.*?_\*|_\*.*?\*_)|(\*.*?\*)|(_.*?_)|(~.*?~)|(```.*?```)|(\s+)|([^\s*~_`]+)/g;
27
33
  let match;
28
34
  while ((match = tokenizerRegex.exec(chunk)) !== null) {
29
- const [fullMatch, boldItalic, bold, italic, strikethrough, monospace, whitespace, textContent] = match;
35
+ const [
36
+ fullMatch,
37
+ boldItalic,
38
+ bold,
39
+ italic,
40
+ strikethrough,
41
+ monospace,
42
+ whitespace,
43
+ textContent,
44
+ ] = match;
30
45
  if (boldItalic) {
31
46
  const content = boldItalic.slice(2, -2);
32
47
  ctx.font = `italic bold ${fontSize}px Arial`;
33
- segmentsInLine.push({ type: 'bolditalic', content, width: ctx.measureText(content).width });
48
+ segmentsInLine.push({
49
+ type: "bolditalic",
50
+ content,
51
+ width: ctx.measureText(content).width,
52
+ });
34
53
  } else if (bold) {
35
54
  const content = bold.slice(1, -1);
36
55
  ctx.font = `bold ${fontSize}px Arial`;
37
- segmentsInLine.push({ type: 'bold', content, width: ctx.measureText(content).width });
56
+ segmentsInLine.push({
57
+ type: "bold",
58
+ content,
59
+ width: ctx.measureText(content).width,
60
+ });
38
61
  } else if (italic) {
39
62
  const content = italic.slice(1, -1);
40
63
  ctx.font = `italic ${fontSize}px Arial`;
41
- segmentsInLine.push({ type: 'italic', content, width: ctx.measureText(content).width });
64
+ segmentsInLine.push({
65
+ type: "italic",
66
+ content,
67
+ width: ctx.measureText(content).width,
68
+ });
42
69
  } else if (strikethrough) {
43
70
  const content = strikethrough.slice(1, -1);
44
71
  ctx.font = `${fontSize}px Arial`;
45
- segmentsInLine.push({ type: 'strikethrough', content, width: ctx.measureText(content).width });
72
+ segmentsInLine.push({
73
+ type: "strikethrough",
74
+ content,
75
+ width: ctx.measureText(content).width,
76
+ });
46
77
  } else if (monospace) {
47
78
  const content = monospace.slice(3, -3);
48
79
  ctx.font = `${fontSize}px 'Courier New', monospace`;
49
- segmentsInLine.push({ type: 'monospace', content, width: ctx.measureText(content).width });
80
+ segmentsInLine.push({
81
+ type: "monospace",
82
+ content,
83
+ width: ctx.measureText(content).width,
84
+ });
50
85
  } else if (whitespace) {
51
86
  ctx.font = `${fontSize}px Arial`;
52
- segmentsInLine.push({ type: 'whitespace', content: whitespace, width: ctx.measureText(whitespace).width });
87
+ segmentsInLine.push({
88
+ type: "whitespace",
89
+ content: whitespace,
90
+ width: ctx.measureText(whitespace).width,
91
+ });
53
92
  } else if (textContent) {
54
93
  ctx.font = `${fontSize}px Arial`;
55
- segmentsInLine.push({ type: 'text', content: textContent, width: ctx.measureText(textContent).width });
94
+ segmentsInLine.push({
95
+ type: "text",
96
+ content: textContent,
97
+ width: ctx.measureText(textContent).width,
98
+ });
56
99
  }
57
100
  }
58
101
  };
59
- emojiMatches.forEach(emojiInfo => {
102
+ emojiMatches.forEach((emojiInfo) => {
60
103
  const plainText = line.substring(lastIndex, emojiInfo.offset);
61
104
  processChunk(plainText);
62
105
  segmentsInLine.push({
63
- type: 'emoji',
106
+ type: "emoji",
64
107
  content: emojiInfo.found,
65
108
  width: fontSize * 1.2,
66
109
  });
@@ -72,13 +115,13 @@ function parseTextToSegments(text, ctx, fontSize) {
72
115
  finalSegments.push(...segmentsInLine);
73
116
  }
74
117
  if (index < rawLines.length - 1) {
75
- finalSegments.push({ type: 'newline', content: '\n', width: 0 });
118
+ finalSegments.push({ type: "newline", content: "\n", width: 0 });
76
119
  }
77
120
  });
78
121
  ctx.font = `${fontSize}px Arial`;
79
122
  return finalSegments;
80
123
  } catch (error) {
81
- console.error('Error in parseTextToSegments:', error);
124
+ console.error("Error in parseTextToSegments:", error);
82
125
  return [];
83
126
  }
84
127
  }
@@ -90,50 +133,70 @@ function rebuildLinesFromSegments(segments, maxWidth, ctx, fontSize) {
90
133
  let currentLineWidth = 0;
91
134
  const getFontString = (type, size) => {
92
135
  switch (type) {
93
- case 'bold': return `bold ${size}px Arial`;
94
- case 'italic': return `italic ${size}px Arial`;
95
- case 'bolditalic': return `italic bold ${size}px Arial`;
96
- case 'monospace': return `${size}px 'Courier New', monospace`;
97
- default: return `${size}px Arial`;
136
+ case "bold":
137
+ return `bold ${size}px Arial`;
138
+ case "italic":
139
+ return `italic ${size}px Arial`;
140
+ case "bolditalic":
141
+ return `italic bold ${size}px Arial`;
142
+ case "monospace":
143
+ return `${size}px 'Courier New', monospace`;
144
+ default:
145
+ return `${size}px Arial`;
98
146
  }
99
147
  };
100
- segments.forEach(segment => {
101
- if (segment.type === 'newline') {
148
+ segments.forEach((segment) => {
149
+ if (segment.type === "newline") {
102
150
  lines.push(currentLine);
103
151
  currentLine = [];
104
152
  currentLineWidth = 0;
105
153
  return;
106
154
  }
107
- if (segment.width > maxWidth && segment.type !== 'emoji') {
155
+ if (segment.width > maxWidth && segment.type !== "emoji") {
108
156
  if (currentLine.length > 0) {
109
157
  lines.push(currentLine);
110
158
  currentLine = [];
111
159
  currentLineWidth = 0;
112
160
  }
113
- let tempStr = '';
161
+ let tempStr = "";
114
162
  const originalFont = getFontString(segment.type, fontSize);
115
163
  ctx.font = originalFont;
116
164
  for (const char of segment.content) {
117
165
  const measuredWidth = ctx.measureText(tempStr + char).width;
118
166
  if (measuredWidth > maxWidth) {
119
- lines.push([{ type: segment.type, content: tempStr, width: ctx.measureText(tempStr).width }]);
167
+ lines.push([
168
+ {
169
+ type: segment.type,
170
+ content: tempStr,
171
+ width: ctx.measureText(tempStr).width,
172
+ },
173
+ ]);
120
174
  tempStr = char;
121
175
  } else {
122
176
  tempStr += char;
123
177
  }
124
178
  }
125
179
  if (tempStr) {
126
- currentLine = [{ type: segment.type, content: tempStr, width: ctx.measureText(tempStr).width }];
180
+ currentLine = [
181
+ {
182
+ type: segment.type,
183
+ content: tempStr,
184
+ width: ctx.measureText(tempStr).width,
185
+ },
186
+ ];
127
187
  currentLineWidth = currentLine[0].width;
128
188
  }
129
189
  return;
130
190
  }
131
- if (currentLineWidth + segment.width > maxWidth && currentLine.length > 0) {
191
+ if (
192
+ currentLineWidth + segment.width > maxWidth &&
193
+ currentLine.length > 0
194
+ ) {
132
195
  lines.push(currentLine);
133
196
  currentLine = [segment];
134
197
  currentLineWidth = segment.width;
135
198
  } else {
136
- if (segment.type === 'whitespace' && currentLine.length === 0) return;
199
+ if (segment.type === "whitespace" && currentLine.length === 0) return;
137
200
  currentLine.push(segment);
138
201
  currentLineWidth += segment.width;
139
202
  }
@@ -143,24 +206,24 @@ function rebuildLinesFromSegments(segments, maxWidth, ctx, fontSize) {
143
206
  }
144
207
  return lines;
145
208
  } catch (error) {
146
- console.error('Error in rebuildLinesFromSegments:', error);
209
+ console.error("Error in rebuildLinesFromSegments:", error);
147
210
  return [[]];
148
211
  }
149
212
  }
150
213
 
151
214
  function getContrastColor(hexColor) {
152
- if (!hexColor || typeof hexColor !== 'string') return '#FFFFFF';
153
- const hex = hexColor.replace('#', '');
154
- if (hex.length !== 6) return '#FFFFFF';
215
+ if (!hexColor || typeof hexColor !== "string") return "#FFFFFF";
216
+ const hex = hexColor.replace("#", "");
217
+ if (hex.length !== 6) return "#FFFFFF";
155
218
  const r = parseInt(hex.substring(0, 2), 16);
156
219
  const g = parseInt(hex.substring(2, 4), 16);
157
220
  const b = parseInt(hex.substring(4, 6), 16);
158
- const luminance = (0.299 * r + 0.587 * g + 0.114 * b);
159
- return luminance > 140 ? '#000000' : '#FFFFFF';
160
- };
221
+ const luminance = 0.299 * r + 0.587 * g + 0.114 * b;
222
+ return luminance > 140 ? "#000000" : "#FFFFFF";
223
+ }
161
224
 
162
225
  module.exports = {
163
226
  parseTextToSegments,
164
227
  rebuildLinesFromSegments,
165
- getContrastColor
166
- };
228
+ getContrastColor,
229
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "medias-fakerator",
3
- "version": "1.0.0-dra",
3
+ "version": "1.0.0-dra.1",
4
4
  "description": "AIO Faker generator by Iruka Devs",
5
5
  "main": "index.js",
6
6
  "author": "Iruka Devs",
@@ -11,7 +11,7 @@
11
11
  "@ffmpeg-installer/ffmpeg": "1.1.0",
12
12
  "canvas": "^3.2.0",
13
13
  "emoji-cache": "^2.0.1",
14
- "emoji-db": "github:Terror-Machine/emoji-db#master",
14
+ "emoji-db": "^16.0.1",
15
15
  "fluent-ffmpeg": "^2.1.3",
16
16
  "fs-extra": "11.3.2",
17
17
  "lru-cache": "^11.1.0",