medias-fakerator 1.0.0-dra.0 → 1.0.0-dra.2
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/assets/fonts/Frutiger.ttf +0 -0
- package/assets/fonts/Ocr.ttf +0 -0
- package/assets/fonts/Sign.ttf +0 -0
- package/assets/ktp/output.png +0 -0
- package/assets/test-assets/profile.jpg +0 -0
- package/assets/test-output/brat-animatedHd.webp +0 -0
- package/assets/test-output/brat-video-frame-1Hd.png +0 -0
- package/assets/test-output/bratHd.png +0 -0
- package/assets/test-output/ktp-card.png +0 -0
- package/index.js +8 -0
- package/lib/generators.js +425 -0
- package/lib/ktpGenerator.js +185 -0
- package/lib/quote-generator.js +10 -25
- package/lib/utils.js +13 -19
- package/package.json +2 -2
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/index.js
CHANGED
|
@@ -15,8 +15,12 @@ const {
|
|
|
15
15
|
generateGoogleLyrics,
|
|
16
16
|
bratGenerator,
|
|
17
17
|
bratVidGenerator,
|
|
18
|
+
bratHdGenerator,
|
|
19
|
+
bratVidHdGenerator,
|
|
18
20
|
generateAnimatedBratVid,
|
|
21
|
+
generateAnimatedBratVidHd,
|
|
19
22
|
} = require("./lib/generators.js");
|
|
23
|
+
const generateCard = require("./lib/ktpGenerator.js");
|
|
20
24
|
|
|
21
25
|
module.exports = {
|
|
22
26
|
generateQuote,
|
|
@@ -34,6 +38,10 @@ module.exports = {
|
|
|
34
38
|
generateGoogleLyrics,
|
|
35
39
|
bratGenerator,
|
|
36
40
|
bratVidGenerator,
|
|
41
|
+
bratHdGenerator,
|
|
42
|
+
bratVidHdGenerator,
|
|
37
43
|
generateAnimatedBratVid,
|
|
44
|
+
generateAnimatedBratVidHd,
|
|
45
|
+
generateCard,
|
|
38
46
|
QuoteGenerator,
|
|
39
47
|
};
|
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/quote-generator.js
CHANGED
|
@@ -2,31 +2,12 @@ const fs = require("fs");
|
|
|
2
2
|
const path = require("path");
|
|
3
3
|
const runes = require("runes");
|
|
4
4
|
const sharp = require("sharp");
|
|
5
|
+
const EmojiDbLib = require("emoji-db");
|
|
5
6
|
const { LRUCache } = require("lru-cache");
|
|
7
|
+
const emojiDb = new EmojiDbLib({ useDefaultDb: true });
|
|
6
8
|
const emojiImageByBrandPromise = require("emoji-cache");
|
|
7
9
|
const ALLOWED_MEDIA_DIRECTORY = path.resolve(__dirname, "../");
|
|
8
10
|
const { createCanvas, loadImage, registerFont } = require("canvas");
|
|
9
|
-
const emoji = require("node-emoji");
|
|
10
|
-
const emojiRegex =
|
|
11
|
-
/(\p{Extended_Pictographic}(?:\u200d\p{Extended_Pictographic})*)/gu;
|
|
12
|
-
|
|
13
|
-
function searchEmojiFromText(text) {
|
|
14
|
-
const results = [];
|
|
15
|
-
let match;
|
|
16
|
-
|
|
17
|
-
while ((match = emojiRegex.exec(text)) !== null) {
|
|
18
|
-
const emojiChar = match[0];
|
|
19
|
-
|
|
20
|
-
results.push({
|
|
21
|
-
emoji: emojiChar, // emoji unicode
|
|
22
|
-
found: emojiChar, // key untuk emoji-cache
|
|
23
|
-
offset: match.index,
|
|
24
|
-
length: emojiChar.length,
|
|
25
|
-
});
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
return results;
|
|
29
|
-
}
|
|
30
11
|
|
|
31
12
|
async function loadFont() {
|
|
32
13
|
const fontsDir = path.join(__dirname, "../assets/fonts");
|
|
@@ -357,8 +338,10 @@ class QuoteGenerate {
|
|
|
357
338
|
for (let i = 0; i < charStyles.length; i++) charStyles[i].push(entities);
|
|
358
339
|
}
|
|
359
340
|
const styledWords = [];
|
|
360
|
-
const emojiData =
|
|
361
|
-
|
|
341
|
+
const emojiData = emojiDb.searchFromText({
|
|
342
|
+
input: text,
|
|
343
|
+
fixCodePoints: true,
|
|
344
|
+
});
|
|
362
345
|
let currentIndex = 0;
|
|
363
346
|
const processPlainText = (plainText, startOffset) => {
|
|
364
347
|
if (!plainText) return;
|
|
@@ -557,8 +540,10 @@ class QuoteGenerate {
|
|
|
557
540
|
}
|
|
558
541
|
}
|
|
559
542
|
const styledWords = [];
|
|
560
|
-
const emojiData =
|
|
561
|
-
|
|
543
|
+
const emojiData = emojiDb.searchFromText({
|
|
544
|
+
input: text,
|
|
545
|
+
fixCodePoints: true,
|
|
546
|
+
});
|
|
562
547
|
let currentIndex = 0;
|
|
563
548
|
const processPlainText = (plainText, startOffset) => {
|
|
564
549
|
if (!plainText) return;
|
package/lib/utils.js
CHANGED
|
@@ -1,21 +1,13 @@
|
|
|
1
|
-
const
|
|
2
|
-
/(\p{Extended_Pictographic}(?:\u200d\p{Extended_Pictographic})*)/gu;
|
|
1
|
+
const EmojiDbLib = require("emoji-db");
|
|
3
2
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
emoji: emojiChar,
|
|
13
|
-
offset: match.index,
|
|
14
|
-
length: emojiChar.length,
|
|
15
|
-
});
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
return results;
|
|
3
|
+
let emojiDb;
|
|
4
|
+
try {
|
|
5
|
+
emojiDb = new EmojiDbLib({ useDefaultDb: true });
|
|
6
|
+
if (!emojiDb || typeof emojiDb.searchFromText !== "function")
|
|
7
|
+
throw new Error("Gagal menginisialisasi database emoji");
|
|
8
|
+
} catch (error) {
|
|
9
|
+
console.error("Error saat inisialisasi database emoji:", error);
|
|
10
|
+
throw error;
|
|
19
11
|
}
|
|
20
12
|
|
|
21
13
|
function parseTextToSegments(text, ctx, fontSize) {
|
|
@@ -29,8 +21,10 @@ function parseTextToSegments(text, ctx, fontSize) {
|
|
|
29
21
|
if (line === "") {
|
|
30
22
|
} else {
|
|
31
23
|
const segmentsInLine = [];
|
|
32
|
-
const emojiMatches =
|
|
33
|
-
|
|
24
|
+
const emojiMatches = emojiDb.searchFromText({
|
|
25
|
+
input: line,
|
|
26
|
+
fixCodePoints: true,
|
|
27
|
+
});
|
|
34
28
|
let lastIndex = 0;
|
|
35
29
|
const processChunk = (chunk) => {
|
|
36
30
|
if (!chunk) return;
|
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.2",
|
|
4
4
|
"description": "AIO Faker generator by Iruka Devs",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"author": "Iruka Devs",
|
|
@@ -11,10 +11,10 @@
|
|
|
11
11
|
"@ffmpeg-installer/ffmpeg": "1.1.0",
|
|
12
12
|
"canvas": "^3.2.0",
|
|
13
13
|
"emoji-cache": "^2.0.1",
|
|
14
|
+
"emoji-db": "^16.0.1",
|
|
14
15
|
"fluent-ffmpeg": "^2.1.3",
|
|
15
16
|
"fs-extra": "11.3.2",
|
|
16
17
|
"lru-cache": "^11.1.0",
|
|
17
|
-
"node-emoji": "^2.2.0",
|
|
18
18
|
"runes": "^0.4.3",
|
|
19
19
|
"sharp": "^0.34.3"
|
|
20
20
|
},
|