simple-photo-gallery 2.0.11 → 2.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.
- package/dist/index.cjs +90 -80
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +89 -79
- package/dist/index.js.map +1 -1
- package/dist/lib/index.cjs +19 -9
- package/dist/lib/index.cjs.map +1 -1
- package/dist/lib/index.js +19 -9
- package/dist/lib/index.js.map +1 -1
- package/package.json +2 -2
package/dist/lib/index.cjs
CHANGED
|
@@ -138,20 +138,27 @@ function findSocialCardFontPath() {
|
|
|
138
138
|
const fontCandidates = [
|
|
139
139
|
resolveFromCurrentDir("../../../../", SOCIAL_CARD_FONT_RELATIVE_PATH),
|
|
140
140
|
path__default.default.resolve(__dirname$1, "../", SOCIAL_CARD_FONT_RELATIVE_PATH),
|
|
141
|
-
path__default.default.resolve(
|
|
141
|
+
path__default.default.resolve(__dirname$1, "../../", SOCIAL_CARD_FONT_RELATIVE_PATH),
|
|
142
|
+
path__default.default.resolve(process__default.default.cwd(), SOCIAL_CARD_FONT_RELATIVE_PATH),
|
|
143
|
+
path__default.default.resolve(process__default.default.cwd(), "../", SOCIAL_CARD_FONT_RELATIVE_PATH),
|
|
144
|
+
path__default.default.resolve(process__default.default.cwd(), "../simple-photo-gallery/", SOCIAL_CARD_FONT_RELATIVE_PATH)
|
|
142
145
|
];
|
|
143
146
|
for (const candidate of fontCandidates) {
|
|
144
147
|
if (fs2__default.default.existsSync(candidate)) {
|
|
145
148
|
return candidate;
|
|
146
149
|
}
|
|
147
150
|
}
|
|
148
|
-
|
|
151
|
+
return null;
|
|
149
152
|
}
|
|
150
153
|
function getSocialCardFontBase64() {
|
|
151
|
-
if (socialCardFontBase64) {
|
|
154
|
+
if (socialCardFontBase64 !== void 0) {
|
|
152
155
|
return socialCardFontBase64;
|
|
153
156
|
}
|
|
154
157
|
const fontPath = findSocialCardFontPath();
|
|
158
|
+
if (!fontPath) {
|
|
159
|
+
socialCardFontBase64 = null;
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
155
162
|
socialCardFontBase64 = fs2__default.default.readFileSync(fontPath).toString("base64");
|
|
156
163
|
return socialCardFontBase64;
|
|
157
164
|
}
|
|
@@ -167,17 +174,20 @@ async function createGallerySocialMediaCardImage(headerPhotoPath, title, ouputPa
|
|
|
167
174
|
const outputPath = ouputPath;
|
|
168
175
|
await sharp3__default.default(resizedImageBuffer).toFile(outputPath);
|
|
169
176
|
const fontBase64 = getSocialCardFontBase64();
|
|
170
|
-
const
|
|
171
|
-
<svg width="1200" height="631" xmlns="http://www.w3.org/2000/svg">
|
|
172
|
-
<defs>
|
|
173
|
-
<style>
|
|
177
|
+
const fontFace = fontBase64 ? `
|
|
174
178
|
@font-face {
|
|
175
179
|
font-family: 'DejaVu Sans';
|
|
176
180
|
src: url('data:font/ttf;base64,${fontBase64}') format('truetype');
|
|
177
181
|
font-weight: 700;
|
|
178
182
|
font-style: normal;
|
|
179
|
-
}
|
|
180
|
-
|
|
183
|
+
}` : "";
|
|
184
|
+
const fontFamily = fontBase64 ? "'DejaVu Sans', Arial, sans-serif" : "Arial, sans-serif";
|
|
185
|
+
const svgText = `
|
|
186
|
+
<svg width="1200" height="631" xmlns="http://www.w3.org/2000/svg">
|
|
187
|
+
<defs>
|
|
188
|
+
<style>
|
|
189
|
+
${fontFace}
|
|
190
|
+
.title { font-family: ${fontFamily}; font-size: 96px; font-weight: bold; fill: white; stroke: black; stroke-width: 5; paint-order: stroke; text-anchor: middle; }
|
|
181
191
|
</style>
|
|
182
192
|
</defs>
|
|
183
193
|
<text x="600" y="250" class="title">${title}</text>
|
package/dist/lib/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/utils/image.ts","../../src/utils/blurhash.ts","../../src/utils/video.ts","../../src/modules/build/utils/index.ts"],"names":["sharp","encode","ffprobe","spawn","fs","__dirname","path","fileURLToPath","process","Buffer"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AAUA,eAAsB,UAAU,SAAA,EAAmC;AACjE,EAAA,OAAOA,uBAAA,CAAM,SAAS,CAAA,CAAE,MAAA,EAAO;AACjC;AAOA,eAAsB,sBAAsB,SAAA,EAA+C;AACzF,EAAA,MAAM,KAAA,GAAQA,wBAAM,SAAS,CAAA;AAC7B,EAAA,MAAM,QAAA,GAAW,MAAM,KAAA,CAAM,QAAA,EAAS;AAGtC,EAAA,KAAA,CAAM,MAAA,EAAO;AAGb,EAAA,MAAM,qBAAqB,QAAA,CAAS,WAAA,IAAe,SAAS,WAAA,IAAe,CAAA,IAAK,SAAS,WAAA,IAAe,CAAA;AAGxG,EAAA,IAAI,kBAAA,EAAoB;AACtB,IAAA,MAAM,gBAAgB,QAAA,CAAS,KAAA;AAC/B,IAAA,QAAA,CAAS,QAAQ,QAAA,CAAS,MAAA;AAC1B,IAAA,QAAA,CAAS,MAAA,GAAS,aAAA;AAAA,EACpB;AAEA,EAAA,OAAO,EAAE,OAAO,QAAA,EAAS;AAC3B;AASA,eAAsB,YACpB,KAAA,EACA,UAAA,EACA,KAAA,EACA,MAAA,EACA,SAA2B,MAAA,EACZ;AAEf,EAAA,MAAM,KAAA,CAAM,MAAA,CAAO,KAAA,EAAO,MAAA,EAAQ,EAAE,kBAAA,EAAoB,IAAA,EAAM,CAAA,CAAE,QAAA,CAAS,MAAM,CAAA,CAAE,OAAO,UAAU,CAAA;AACpG;AASA,eAAsB,mBACpB,KAAA,EACA,UAAA,EACA,KAAA,EACA,MAAA,EACA,SAA2B,MAAA,EACZ;AAEf,EAAA,MAAM,KAAA,CACH,MAAA,CAAO,KAAA,EAAO,MAAA,EAAQ;AAAA,IACrB,GAAA,EAAK,OAAA;AAAA,IACL,kBAAA,EAAoB;AAAA,GACrB,CAAA,CACA,QAAA,CAAS,MAAM,CAAA,CACf,OAAO,UAAU,CAAA;AACtB;AAWA,eAAsB,qBAAA,CACpB,KAAA,EACA,QAAA,EACA,UAAA,EACA,kBACA,IAAA,EACqB;AAErB,EAAA,MAAM,aAAA,GAAgB,SAAS,KAAA,IAAS,CAAA;AACxC,EAAA,MAAM,cAAA,GAAiB,SAAS,MAAA,IAAU,CAAA;AAE1C,EAAA,IAAI,aAAA,KAAkB,CAAA,IAAK,cAAA,KAAmB,CAAA,EAAG;AAC/C,IAAA,MAAM,IAAI,MAAM,0BAA0B,CAAA;AAAA,EAC5C;AAGA,EAAA,MAAM,cAAc,aAAA,GAAgB,cAAA;AAEpC,EAAA,IAAI,KAAA;AACJ,EAAA,IAAI,MAAA;AAEJ,EAAA,IAAI,gBAAgB,cAAA,EAAgB;AAClC,IAAA,KAAA,GAAQ,IAAA;AACR,IAAA,MAAA,GAAS,IAAA,CAAK,KAAA,CAAM,IAAA,GAAO,WAAW,CAAA;AAAA,EACxC,CAAA,MAAO;AACL,IAAA,KAAA,GAAQ,IAAA,CAAK,KAAA,CAAM,IAAA,GAAO,WAAW,CAAA;AACrC,IAAA,MAAA,GAAS,IAAA;AAAA,EACX;AAGA,EAAA,MAAM,WAAA,CAAY,KAAA,EAAO,UAAA,EAAY,KAAA,EAAO,MAAM,CAAA;AAClD,EAAA,MAAM,YAAY,KAAA,EAAO,gBAAA,EAAkB,KAAA,GAAQ,CAAA,EAAG,SAAS,CAAC,CAAA;AAGhE,EAAA,OAAO,EAAE,OAAO,MAAA,EAAO;AACzB;;;AClHA,eAAsB,gBAAA,CAAiB,SAAA,EAAmB,UAAA,GAAqB,CAAA,EAAG,aAAqB,CAAA,EAAoB;AACzH,EAAA,MAAM,KAAA,GAAQ,MAAM,SAAA,CAAU,SAAS,CAAA;AAIvC,EAAA,MAAM,EAAE,MAAM,IAAA,EAAK,GAAI,MAAM,KAAA,CAC1B,MAAA,CAAO,EAAA,EAAI,EAAA,EAAI,EAAE,GAAA,EAAK,UAAU,CAAA,CAChC,aAAY,CACZ,GAAA,GACA,QAAA,CAAS,EAAE,iBAAA,EAAmB,IAAA,EAAM,CAAA;AAGvC,EAAA,MAAM,MAAA,GAAS,IAAI,iBAAA,CAAkB,IAAA,CAAK,MAAM,CAAA;AAGhD,EAAA,OAAOC,gBAAO,MAAA,EAAQ,IAAA,CAAK,OAAO,IAAA,CAAK,MAAA,EAAQ,YAAY,UAAU,CAAA;AACvE;ACVA,eAAsB,mBAAmB,QAAA,EAAuC;AAC9E,EAAA,MAAM,IAAA,GAAO,MAAMC,wBAAA,CAAQ,QAAQ,CAAA;AACnC,EAAA,MAAM,WAAA,GAAc,KAAK,OAAA,CAAQ,IAAA,CAAK,CAAC,MAAA,KAAW,MAAA,CAAO,eAAe,OAAO,CAAA;AAE/E,EAAA,IAAI,CAAC,WAAA,EAAa;AAChB,IAAA,MAAM,IAAI,MAAM,uBAAuB,CAAA;AAAA,EACzC;AAEA,EAAA,MAAM,UAAA,GAAa;AAAA,IACjB,KAAA,EAAO,YAAY,KAAA,IAAS,CAAA;AAAA,IAC5B,MAAA,EAAQ,YAAY,MAAA,IAAU;AAAA,GAChC;AAEA,EAAA,IAAI,UAAA,CAAW,KAAA,KAAU,CAAA,IAAK,UAAA,CAAW,WAAW,CAAA,EAAG;AACrD,IAAA,MAAM,IAAI,MAAM,0BAA0B,CAAA;AAAA,EAC5C;AAEA,EAAA,OAAO,UAAA;AACT;AAYA,eAAsB,sBACpB,SAAA,EACA,eAAA,EACA,YACA,gBAAA,EACA,MAAA,EACA,UAAmB,KAAA,EACE;AAErB,EAAA,MAAM,WAAA,GAAc,eAAA,CAAgB,KAAA,GAAQ,eAAA,CAAgB,MAAA;AAC5D,EAAA,MAAM,KAAA,GAAQ,IAAA,CAAK,KAAA,CAAM,MAAA,GAAS,WAAW,CAAA;AAG7C,EAAA,MAAM,aAAA,GAAgB,GAAG,UAAU,CAAA,SAAA,CAAA;AAEnC,EAAA,OAAO,IAAI,OAAA,CAAQ,CAAC,OAAA,EAAS,MAAA,KAAW;AAEtC,IAAA,MAAM,MAAA,GAASC,oBAAM,QAAA,EAAU;AAAA,MAC7B,IAAA;AAAA,MACA,SAAA;AAAA,MACA,UAAA;AAAA,MACA,GAAA;AAAA,MACA,IAAA;AAAA,MACA,WAAA;AAAA,MACA,UAAU,OAAA,GAAU,OAAA;AAAA,MACpB;AAAA,KACD,CAAA;AAED,IAAA,MAAA,CAAO,MAAA,CAAO,EAAA,CAAG,MAAA,EAAQ,CAAC,IAAA,KAAiB;AAEzC,MAAA,OAAA,CAAQ,GAAA,CAAI,CAAA,QAAA,EAAW,IAAA,CAAK,QAAA,EAAU,CAAA,CAAE,CAAA;AAAA,IAC1C,CAAC,CAAA;AAED,IAAA,MAAA,CAAO,EAAA,CAAG,OAAA,EAAS,OAAO,IAAA,KAAiB;AACzC,MAAA,IAAI,SAAS,CAAA,EAAG;AACd,QAAA,IAAI;AAEF,UAAA,MAAM,UAAA,GAAaH,wBAAM,aAAa,CAAA;AACtC,UAAA,MAAM,WAAA,CAAY,UAAA,EAAY,UAAA,EAAY,KAAA,EAAO,MAAM,CAAA;AACvD,UAAA,MAAM,YAAY,UAAA,EAAY,gBAAA,EAAkB,KAAA,GAAQ,CAAA,EAAG,SAAS,CAAC,CAAA;AAGrE,UAAA,IAAI;AACF,YAAA,MAAMI,YAAA,CAAG,OAAO,aAAa,CAAA;AAAA,UAC/B,CAAA,CAAA,MAAQ;AAAA,UAER;AAEA,UAAA,OAAA,CAAQ,EAAE,KAAA,EAAO,MAAA,EAAQ,CAAA;AAAA,QAC3B,SAAS,UAAA,EAAY;AACnB,UAAA,MAAA,CAAO,IAAI,KAAA,CAAM,CAAA,mCAAA,EAAsC,UAAU,EAAE,CAAC,CAAA;AAAA,QACtE;AAAA,MACF,CAAA,MAAO;AACL,QAAA,MAAA,CAAO,IAAI,KAAA,CAAM,CAAA,wBAAA,EAA2B,IAAI,EAAE,CAAC,CAAA;AAAA,MACrD;AAAA,IACF,CAAC,CAAA;AAED,IAAA,MAAA,CAAO,EAAA,CAAG,OAAA,EAAS,CAAC,KAAA,KAAiB;AACnC,MAAA,MAAA,CAAO,IAAI,KAAA,CAAM,CAAA,wBAAA,EAA2B,KAAA,CAAM,OAAO,EAAE,CAAC,CAAA;AAAA,IAC9D,CAAC,CAAA;AAAA,EACH,CAAC,CAAA;AACH;AC7FA,IAAMC,cAAYC,qBAAA,CAAK,OAAA,CAAQC,iBAAA,CAAc,2PAAe,CAAC,CAAA;AAG7D,IAAM,iCAAiCD,qBAAA,CAAK,IAAA,CAAK,QAAA,EAAU,OAAA,EAAS,UAAU,qBAAqB,CAAA;AAGnG,IAAI,oBAAA;AAOG,SAAS,yBAAyB,QAAA,EAA4B;AACnE,EAAA,OAAOA,qBAAA,CAAK,OAAA,CAAQD,WAAA,EAAW,GAAG,QAAQ,CAAA;AAC5C;AAMA,SAAS,sBAAA,GAAiC;AACxC,EAAA,MAAM,cAAA,GAAiB;AAAA,IACrB,qBAAA,CAAsB,gBAAgB,8BAA8B,CAAA;AAAA,IACpEC,qBAAA,CAAK,OAAA,CAAQD,WAAA,EAAW,KAAA,EAAO,8BAA8B,CAAA;AAAA,IAC7DC,qBAAA,CAAK,OAAA,CAAQE,wBAAA,CAAQ,GAAA,IAAO,8BAA8B;AAAA,GAC5D;AAEA,EAAA,KAAA,MAAW,aAAa,cAAA,EAAgB;AACtC,IAAA,IAAIJ,oBAAAA,CAAG,UAAA,CAAW,SAAS,CAAA,EAAG;AAC5B,MAAA,OAAO,SAAA;AAAA,IACT;AAAA,EACF;AAEA,EAAA,MAAM,IAAI,MAAM,uCAAuC,CAAA;AACzD;AAMA,SAAS,uBAAA,GAAkC;AACzC,EAAA,IAAI,oBAAA,EAAsB;AACxB,IAAA,OAAO,oBAAA;AAAA,EACT;AAEA,EAAA,MAAM,WAAW,sBAAA,EAAuB;AACxC,EAAA,oBAAA,GAAuBA,oBAAAA,CAAG,YAAA,CAAa,QAAQ,CAAA,CAAE,SAAS,QAAQ,CAAA;AAClE,EAAA,OAAO,oBAAA;AACT;AAUA,eAAsB,iCAAA,CACpB,eAAA,EACA,KAAA,EACA,SAAA,EACA,EAAA,EACiB;AACjB,EAAA,EAAA,EAAI,MAAM,CAAA,gCAAA,CAAkC,CAAA;AAE5C,EAAA,MAAM,iBAAiBE,qBAAA,CAAK,QAAA,CAAS,iBAAiBA,qBAAA,CAAK,OAAA,CAAQ,eAAe,CAAC,CAAA;AAEnF,EAAA,IAAIF,oBAAAA,CAAG,UAAA,CAAW,SAAS,CAAA,EAAG;AAC5B,IAAA,EAAA,EAAI,QAAQ,CAAA,sCAAA,CAAwC,CAAA;AACpD,IAAA,OAAO,cAAA;AAAA,EACT;AAGA,EAAA,MAAM,KAAA,GAAQ,MAAM,SAAA,CAAU,eAAe,CAAA;AAC7C,EAAA,MAAM,qBAAqB,MAAM,KAAA,CAAM,MAAA,CAAO,IAAA,EAAM,KAAK,EAAE,GAAA,EAAK,OAAA,EAAS,EAAE,IAAA,CAAK,EAAE,SAAS,EAAA,EAAI,EAAE,QAAA,EAAS;AAG1G,EAAA,MAAM,UAAA,GAAa,SAAA;AACnB,EAAA,MAAMJ,uBAAAA,CAAM,kBAAkB,CAAA,CAAE,MAAA,CAAO,UAAU,CAAA;AAGjD,EAAA,MAAM,aAAa,uBAAA,EAAwB;AAC3C,EAAA,MAAM,OAAA,GAAU;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,2CAAA,EAM2B,UAAU,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,0CAAA,EAOX,KAAK,CAAA;AAAA;AAAA,EAAA,CAAA;AAK/C,EAAA,MAAM,gBAAA,GAAmB,MAAMA,uBAAAA,CAAM,kBAAkB,CAAA,CACpD,SAAA,CAAU,CAAC,EAAE,KAAA,EAAOS,aAAA,CAAO,IAAA,CAAK,OAAO,CAAA,EAAG,KAAK,CAAA,EAAG,IAAA,EAAM,CAAA,EAAG,CAAC,CAAA,CAC5D,IAAA,CAAK,EAAE,OAAA,EAAS,EAAA,EAAI,CAAA,CACpB,QAAA,EAAS;AAGZ,EAAA,MAAMT,uBAAAA,CAAM,gBAAgB,CAAA,CAAE,MAAA,CAAO,UAAU,CAAA;AAE/C,EAAA,EAAA,EAAI,QAAQ,CAAA,4CAAA,CAA8C,CAAA;AAC1D,EAAA,OAAO,cAAA;AACT","file":"index.cjs","sourcesContent":["import sharp from 'sharp';\n\nimport type { Dimensions, ImageWithMetadata } from '../types';\nimport type { FormatEnum, Metadata, Sharp } from 'sharp';\n\n/**\n * Loads an image and auto-rotates it based on EXIF orientation.\n * @param imagePath - Path to the image file\n * @returns Promise resolving to Sharp image instance\n */\nexport async function loadImage(imagePath: string): Promise<Sharp> {\n return sharp(imagePath).rotate();\n}\n\n/**\n * Loads an image and its metadata, auto-rotating it based on EXIF orientation and swapping dimensions if needed.\n * @param imagePath - Path to the image file\n * @returns Promise resolving to ImageWithMetadata object containing Sharp image instance and metadata\n */\nexport async function loadImageWithMetadata(imagePath: string): Promise<ImageWithMetadata> {\n const image = sharp(imagePath);\n const metadata = await image.metadata();\n\n // Auto-rotate based on EXIF orientation\n image.rotate();\n\n // EXIF orientation values 5, 6, 7, 8 require dimension swap after rotation\n const needsDimensionSwap = metadata.orientation && metadata.orientation >= 5 && metadata.orientation <= 8;\n\n // Update metadata with swapped dimensions if needed\n if (needsDimensionSwap) {\n const originalWidth = metadata.width;\n metadata.width = metadata.height;\n metadata.height = originalWidth;\n }\n\n return { image, metadata };\n}\n\n/**\n * Utility function to resize and save thumbnail using Sharp. The functions avoids upscaling the image and only reduces the size if necessary.\n * @param image - Sharp image instance\n * @param outputPath - Path where thumbnail should be saved\n * @param width - Target width for thumbnail\n * @param height - Target height for thumbnail\n */\nexport async function resizeImage(\n image: Sharp,\n outputPath: string,\n width: number,\n height: number,\n format: keyof FormatEnum = 'avif',\n): Promise<void> {\n // Resize the image without enlarging it\n await image.resize(width, height, { withoutEnlargement: true }).toFormat(format).toFile(outputPath);\n}\n\n/**\n * Crops and resizes an image to a target aspect ratio, avoiding upscaling the image.\n * @param image - Sharp image instance\n * @param outputPath - Path where the image should be saved\n * @param width - Target width for the image\n * @param height - Target height for the image\n */\nexport async function cropAndResizeImage(\n image: Sharp,\n outputPath: string,\n width: number,\n height: number,\n format: keyof FormatEnum = 'avif',\n): Promise<void> {\n // Apply resize with cover fit and without enlargement\n await image\n .resize(width, height, {\n fit: 'cover',\n withoutEnlargement: true,\n })\n .toFormat(format)\n .toFile(outputPath);\n}\n\n/**\n * Creates regular and retina thumbnails for an image while maintaining aspect ratio\n * @param image - Sharp image instance\n * @param metadata - Image metadata containing dimensions\n * @param outputPath - Path where thumbnail should be saved\n * @param outputPathRetina - Path where retina thumbnail should be saved\n * @param size - Target size of the longer side of the thumbnail\n * @returns Promise resolving to thumbnail dimensions\n */\nexport async function createImageThumbnails(\n image: Sharp,\n metadata: Metadata,\n outputPath: string,\n outputPathRetina: string,\n size: number,\n): Promise<Dimensions> {\n // Get the original dimensions\n const originalWidth = metadata.width || 0;\n const originalHeight = metadata.height || 0;\n\n if (originalWidth === 0 || originalHeight === 0) {\n throw new Error('Invalid image dimensions');\n }\n\n // Calculate the new size maintaining aspect ratio\n const aspectRatio = originalWidth / originalHeight;\n\n let width: number;\n let height: number;\n\n if (originalWidth > originalHeight) {\n width = size;\n height = Math.round(size / aspectRatio);\n } else {\n width = Math.round(size * aspectRatio);\n height = size;\n }\n\n // Resize the image and create the thumbnails\n await resizeImage(image, outputPath, width, height);\n await resizeImage(image, outputPathRetina, width * 2, height * 2);\n\n // Return the dimensions of the thumbnail\n return { width, height };\n}\n","import { encode } from 'blurhash';\n\nimport { loadImage } from './image';\n\n/**\n * Generates a BlurHash from an image file or Sharp instance\n * @param imagePath - Path to image file or Sharp instance\n * @param componentX - Number of x components (default: 4)\n * @param componentY - Number of y components (default: 3)\n * @returns Promise resolving to BlurHash string\n */\nexport async function generateBlurHash(imagePath: string, componentX: number = 4, componentY: number = 3): Promise<string> {\n const image = await loadImage(imagePath);\n\n // Resize to small size for BlurHash computation to improve performance\n // BlurHash doesn't need high resolution\n const { data, info } = await image\n .resize(32, 32, { fit: 'inside' })\n .ensureAlpha()\n .raw()\n .toBuffer({ resolveWithObject: true });\n\n // Convert to Uint8ClampedArray format expected by blurhash\n const pixels = new Uint8ClampedArray(data.buffer);\n\n // Generate BlurHash\n return encode(pixels, info.width, info.height, componentX, componentY);\n}\n","import { spawn } from 'node:child_process';\nimport { promises as fs } from 'node:fs';\n\nimport ffprobe from 'node-ffprobe';\nimport sharp from 'sharp';\n\nimport { resizeImage } from './image';\n\nimport type { Dimensions } from '../types';\nimport type { Buffer } from 'node:buffer';\n\n/**\n * Gets video dimensions using ffprobe\n * @param filePath - Path to the video file\n * @returns Promise resolving to video dimensions\n * @throws Error if no video stream found or invalid dimensions\n */\nexport async function getVideoDimensions(filePath: string): Promise<Dimensions> {\n const data = await ffprobe(filePath);\n const videoStream = data.streams.find((stream) => stream.codec_type === 'video');\n\n if (!videoStream) {\n throw new Error('No video stream found');\n }\n\n const dimensions = {\n width: videoStream.width || 0,\n height: videoStream.height || 0,\n };\n\n if (dimensions.width === 0 || dimensions.height === 0) {\n throw new Error('Invalid video dimensions');\n }\n\n return dimensions;\n}\n\n/**\n * Creates regular and retina thumbnails for a video by extracting the first frame\n * @param inputPath - Path to the video file\n * @param videoDimensions - Original video dimensions\n * @param outputPath - Path where thumbnail should be saved\n * @param outputPathRetina - Path where retina thumbnail should be saved\n * @param height - Target height for thumbnail\n * @param verbose - Whether to enable verbose ffmpeg output\n * @returns Promise resolving to thumbnail dimensions\n */\nexport async function createVideoThumbnails(\n inputPath: string,\n videoDimensions: Dimensions,\n outputPath: string,\n outputPathRetina: string,\n height: number,\n verbose: boolean = false,\n): Promise<Dimensions> {\n // Calculate width maintaining aspect ratio\n const aspectRatio = videoDimensions.width / videoDimensions.height;\n const width = Math.round(height * aspectRatio);\n\n // Use ffmpeg to extract first frame as a temporary file, then process with sharp\n const tempFramePath = `${outputPath}.temp.png`;\n\n return new Promise((resolve, reject) => {\n // Extract first frame using ffmpeg\n const ffmpeg = spawn('ffmpeg', [\n '-i',\n inputPath,\n '-vframes',\n '1',\n '-y',\n '-loglevel',\n verbose ? 'error' : 'quiet',\n tempFramePath,\n ]);\n\n ffmpeg.stderr.on('data', (data: Buffer) => {\n // FFmpeg writes normal output to stderr, so we don't treat this as an error\n console.log(`ffmpeg: ${data.toString()}`);\n });\n\n ffmpeg.on('close', async (code: number) => {\n if (code === 0) {\n try {\n // Process the extracted frame with sharp\n const frameImage = sharp(tempFramePath);\n await resizeImage(frameImage, outputPath, width, height);\n await resizeImage(frameImage, outputPathRetina, width * 2, height * 2);\n\n // Clean up temporary file\n try {\n await fs.unlink(tempFramePath);\n } catch {\n // Ignore cleanup errors\n }\n\n resolve({ width, height });\n } catch (sharpError) {\n reject(new Error(`Failed to process extracted frame: ${sharpError}`));\n }\n } else {\n reject(new Error(`ffmpeg exited with code ${code}`));\n }\n });\n\n ffmpeg.on('error', (error: Error) => {\n reject(new Error(`Failed to start ffmpeg: ${error.message}`));\n });\n });\n}\n","import { Buffer } from 'node:buffer';\nimport fs from 'node:fs';\nimport path from 'node:path';\nimport process from 'node:process';\nimport { fileURLToPath } from 'node:url';\n\nimport sharp from 'sharp';\n\nimport { HEADER_IMAGE_LANDSCAPE_WIDTHS, HEADER_IMAGE_PORTRAIT_WIDTHS } from '../../../config';\nimport { generateBlurHash } from '../../../utils/blurhash';\nimport { cropAndResizeImage, loadImage } from '../../../utils/image';\n\nimport type { ConsolaInstance } from 'consola';\n\n/** __dirname workaround for ESM modules */\nconst __dirname = path.dirname(fileURLToPath(import.meta.url));\n\n/** Relative path to the bundled font used for social cards */\nconst SOCIAL_CARD_FONT_RELATIVE_PATH = path.join('assets', 'fonts', 'dejavu', 'DejaVuSans-Bold.ttf');\n\n/** Cached base64 font data to avoid repeated disk reads */\nlet socialCardFontBase64: string | undefined;\n\n/**\n * Helper function to resolve paths relative to current file\n * @param segments - Path segments to resolve relative to current directory\n * @returns Resolved absolute path\n */\nexport function resolveFromCurrentDir(...segments: string[]): string {\n return path.resolve(__dirname, ...segments);\n}\n\n/**\n * Locate the font used for rendering social media card text.\n * Tries multiple candidate paths to support both source and built distributions.\n */\nfunction findSocialCardFontPath(): string {\n const fontCandidates = [\n resolveFromCurrentDir('../../../../', SOCIAL_CARD_FONT_RELATIVE_PATH),\n path.resolve(__dirname, '../', SOCIAL_CARD_FONT_RELATIVE_PATH),\n path.resolve(process.cwd(), SOCIAL_CARD_FONT_RELATIVE_PATH),\n ];\n\n for (const candidate of fontCandidates) {\n if (fs.existsSync(candidate)) {\n return candidate;\n }\n }\n\n throw new Error('Social media card font file not found');\n}\n\n/**\n * Loads the social media card font and returns its base64 representation.\n * The data is cached to avoid repeated disk access within the same process.\n */\nfunction getSocialCardFontBase64(): string {\n if (socialCardFontBase64) {\n return socialCardFontBase64;\n }\n\n const fontPath = findSocialCardFontPath();\n socialCardFontBase64 = fs.readFileSync(fontPath).toString('base64');\n return socialCardFontBase64;\n}\n\n/**\n * Creates a social media card image for a gallery\n * @param headerPhotoPath - Path to the header photo\n * @param title - Title of the gallery\n * @param ouputPath - Output path for the social media card image\n * @param ui - ConsolaInstance for logging\n * @returns The basename of the header photo used\n */\nexport async function createGallerySocialMediaCardImage(\n headerPhotoPath: string,\n title: string,\n ouputPath: string,\n ui?: ConsolaInstance,\n): Promise<string> {\n ui?.start(`Creating social media card image`);\n\n const headerBasename = path.basename(headerPhotoPath, path.extname(headerPhotoPath));\n\n if (fs.existsSync(ouputPath)) {\n ui?.success(`Social media card image already exists`);\n return headerBasename;\n }\n\n // Read and resize the header image to 1200x631 using fit\n const image = await loadImage(headerPhotoPath);\n const resizedImageBuffer = await image.resize(1200, 631, { fit: 'cover' }).jpeg({ quality: 90 }).toBuffer();\n\n // Save the resized image as social media card\n const outputPath = ouputPath;\n await sharp(resizedImageBuffer).toFile(outputPath);\n\n // Create SVG with title and description\n const fontBase64 = getSocialCardFontBase64();\n const svgText = `\n <svg width=\"1200\" height=\"631\" xmlns=\"http://www.w3.org/2000/svg\">\n <defs>\n <style>\n @font-face {\n font-family: 'DejaVu Sans';\n src: url('data:font/ttf;base64,${fontBase64}') format('truetype');\n font-weight: 700;\n font-style: normal;\n }\n .title { font-family: 'DejaVu Sans', Arial, sans-serif; font-size: 96px; font-weight: bold; fill: white; stroke: black; stroke-width: 5; paint-order: stroke; text-anchor: middle; }\n </style>\n </defs>\n <text x=\"600\" y=\"250\" class=\"title\">${title}</text>\n </svg>\n `;\n\n // Composite the text overlay on top of the resized image\n const finalImageBuffer = await sharp(resizedImageBuffer)\n .composite([{ input: Buffer.from(svgText), top: 0, left: 0 }])\n .jpeg({ quality: 90 })\n .toBuffer();\n\n // Save the final image with text overlay\n await sharp(finalImageBuffer).toFile(outputPath);\n\n ui?.success(`Created social media card image successfully`);\n return headerBasename;\n}\n\n/**\n * Creates optimized header images for different orientations and sizes\n * @param headerPhotoPath - Path to the header photo\n * @param outputFolder - Folder where header images should be saved\n * @param ui - ConsolaInstance for logging\n * @returns Object containing the header basename, array of generated file paths, and blurhash\n */\nexport async function createOptimizedHeaderImage(\n headerPhotoPath: string,\n outputFolder: string,\n ui?: ConsolaInstance,\n): Promise<{ headerBasename: string; generatedFiles: string[]; blurHash: string }> {\n ui?.start(`Creating optimized header images`);\n\n const image = await loadImage(headerPhotoPath);\n const headerBasename = path.basename(headerPhotoPath, path.extname(headerPhotoPath));\n const generatedFiles: string[] = [];\n\n // Generate blurhash for the header image\n ui?.debug('Generating blurhash for header image');\n const blurHash = await generateBlurHash(headerPhotoPath);\n\n // Create landscape header images\n const landscapeYFactor = 3 / 4;\n for (const width of HEADER_IMAGE_LANDSCAPE_WIDTHS) {\n ui?.debug(`Creating landscape header image ${width}`);\n\n const avifFilename = `${headerBasename}_landscape_${width}.avif`;\n const jpgFilename = `${headerBasename}_landscape_${width}.jpg`;\n\n if (fs.existsSync(path.join(outputFolder, avifFilename))) {\n ui?.debug(`Landscape header image ${width} AVIF already exists`);\n } else {\n await cropAndResizeImage(\n image.clone(),\n path.join(outputFolder, avifFilename),\n width,\n width * landscapeYFactor,\n 'avif',\n );\n }\n generatedFiles.push(avifFilename);\n\n if (fs.existsSync(path.join(outputFolder, jpgFilename))) {\n ui?.debug(`Landscape header image ${width} JPG already exists`);\n } else {\n await cropAndResizeImage(image.clone(), path.join(outputFolder, jpgFilename), width, width * landscapeYFactor, 'jpg');\n }\n generatedFiles.push(jpgFilename);\n }\n\n // Create portrait header images\n const portraitYFactor = 4 / 3;\n for (const width of HEADER_IMAGE_PORTRAIT_WIDTHS) {\n ui?.debug(`Creating portrait header image ${width}`);\n\n const avifFilename = `${headerBasename}_portrait_${width}.avif`;\n const jpgFilename = `${headerBasename}_portrait_${width}.jpg`;\n\n if (fs.existsSync(path.join(outputFolder, avifFilename))) {\n ui?.debug(`Portrait header image ${width} AVIF already exists`);\n } else {\n await cropAndResizeImage(image.clone(), path.join(outputFolder, avifFilename), width, width * portraitYFactor, 'avif');\n }\n generatedFiles.push(avifFilename);\n\n if (fs.existsSync(path.join(outputFolder, jpgFilename))) {\n ui?.debug(`Portrait header image ${width} JPG already exists`);\n } else {\n await cropAndResizeImage(image.clone(), path.join(outputFolder, jpgFilename), width, width * portraitYFactor, 'jpg');\n }\n generatedFiles.push(jpgFilename);\n }\n\n ui?.success(`Created optimized header image successfully`);\n return { headerBasename, generatedFiles, blurHash };\n}\n\n/**\n * Checks if there are old header images with a different basename than the current one\n * @param outputFolder - Folder containing the header images\n * @param currentHeaderBasename - Basename of the current header image\n * @returns True if old header images with different basename exist, false otherwise\n */\nexport function hasOldHeaderImages(outputFolder: string, currentHeaderBasename: string): boolean {\n if (!fs.existsSync(outputFolder)) {\n return false;\n }\n\n const files = fs.readdirSync(outputFolder);\n\n for (const file of files) {\n // Check if file is a header image (landscape or portrait) with different basename\n const landscapeMatch = file.match(/^(.+)_landscape_\\d+\\.(avif|jpg)$/);\n const portraitMatch = file.match(/^(.+)_portrait_\\d+\\.(avif|jpg)$/);\n\n if (\n (landscapeMatch && landscapeMatch[1] !== currentHeaderBasename) ||\n (portraitMatch && portraitMatch[1] !== currentHeaderBasename)\n ) {\n return true;\n }\n }\n\n return false;\n}\n\n/**\n * Cleans up old header images that don't match the current header image\n * @param outputFolder - Folder containing the header images\n * @param currentHeaderBasename - Basename of the current header image\n * @param ui - ConsolaInstance for logging\n */\nexport function cleanupOldHeaderImages(outputFolder: string, currentHeaderBasename: string, ui?: ConsolaInstance): void {\n ui?.start(`Cleaning up old header images`);\n\n if (!fs.existsSync(outputFolder)) {\n ui?.debug(`Output folder ${outputFolder} does not exist, skipping cleanup`);\n return;\n }\n\n const files = fs.readdirSync(outputFolder);\n let deletedCount = 0;\n\n for (const file of files) {\n // Check if file is a header image (landscape or portrait) with different basename\n const landscapeMatch = file.match(/^(.+)_landscape_\\d+\\.(avif|jpg)$/);\n const portraitMatch = file.match(/^(.+)_portrait_\\d+\\.(avif|jpg)$/);\n\n if (landscapeMatch && landscapeMatch[1] !== currentHeaderBasename) {\n const filePath = path.join(outputFolder, file);\n ui?.debug(`Deleting old landscape header image: ${file}`);\n fs.unlinkSync(filePath);\n deletedCount++;\n } else if (portraitMatch && portraitMatch[1] !== currentHeaderBasename) {\n const filePath = path.join(outputFolder, file);\n ui?.debug(`Deleting old portrait header image: ${file}`);\n fs.unlinkSync(filePath);\n deletedCount++;\n }\n }\n\n if (deletedCount > 0) {\n ui?.success(`Deleted ${deletedCount} old header image(s)`);\n } else {\n ui?.debug(`No old header images to clean up`);\n }\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/utils/image.ts","../../src/utils/blurhash.ts","../../src/utils/video.ts","../../src/modules/build/utils/index.ts"],"names":["sharp","encode","ffprobe","spawn","fs","__dirname","path","fileURLToPath","process","Buffer"],"mappings":"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","file":"index.cjs","sourcesContent":["import sharp from 'sharp';\n\nimport type { Dimensions, ImageWithMetadata } from '../types';\nimport type { FormatEnum, Metadata, Sharp } from 'sharp';\n\n/**\n * Loads an image and auto-rotates it based on EXIF orientation.\n * @param imagePath - Path to the image file\n * @returns Promise resolving to Sharp image instance\n */\nexport async function loadImage(imagePath: string): Promise<Sharp> {\n return sharp(imagePath).rotate();\n}\n\n/**\n * Loads an image and its metadata, auto-rotating it based on EXIF orientation and swapping dimensions if needed.\n * @param imagePath - Path to the image file\n * @returns Promise resolving to ImageWithMetadata object containing Sharp image instance and metadata\n */\nexport async function loadImageWithMetadata(imagePath: string): Promise<ImageWithMetadata> {\n const image = sharp(imagePath);\n const metadata = await image.metadata();\n\n // Auto-rotate based on EXIF orientation\n image.rotate();\n\n // EXIF orientation values 5, 6, 7, 8 require dimension swap after rotation\n const needsDimensionSwap = metadata.orientation && metadata.orientation >= 5 && metadata.orientation <= 8;\n\n // Update metadata with swapped dimensions if needed\n if (needsDimensionSwap) {\n const originalWidth = metadata.width;\n metadata.width = metadata.height;\n metadata.height = originalWidth;\n }\n\n return { image, metadata };\n}\n\n/**\n * Utility function to resize and save thumbnail using Sharp. The functions avoids upscaling the image and only reduces the size if necessary.\n * @param image - Sharp image instance\n * @param outputPath - Path where thumbnail should be saved\n * @param width - Target width for thumbnail\n * @param height - Target height for thumbnail\n */\nexport async function resizeImage(\n image: Sharp,\n outputPath: string,\n width: number,\n height: number,\n format: keyof FormatEnum = 'avif',\n): Promise<void> {\n // Resize the image without enlarging it\n await image.resize(width, height, { withoutEnlargement: true }).toFormat(format).toFile(outputPath);\n}\n\n/**\n * Crops and resizes an image to a target aspect ratio, avoiding upscaling the image.\n * @param image - Sharp image instance\n * @param outputPath - Path where the image should be saved\n * @param width - Target width for the image\n * @param height - Target height for the image\n */\nexport async function cropAndResizeImage(\n image: Sharp,\n outputPath: string,\n width: number,\n height: number,\n format: keyof FormatEnum = 'avif',\n): Promise<void> {\n // Apply resize with cover fit and without enlargement\n await image\n .resize(width, height, {\n fit: 'cover',\n withoutEnlargement: true,\n })\n .toFormat(format)\n .toFile(outputPath);\n}\n\n/**\n * Creates regular and retina thumbnails for an image while maintaining aspect ratio\n * @param image - Sharp image instance\n * @param metadata - Image metadata containing dimensions\n * @param outputPath - Path where thumbnail should be saved\n * @param outputPathRetina - Path where retina thumbnail should be saved\n * @param size - Target size of the longer side of the thumbnail\n * @returns Promise resolving to thumbnail dimensions\n */\nexport async function createImageThumbnails(\n image: Sharp,\n metadata: Metadata,\n outputPath: string,\n outputPathRetina: string,\n size: number,\n): Promise<Dimensions> {\n // Get the original dimensions\n const originalWidth = metadata.width || 0;\n const originalHeight = metadata.height || 0;\n\n if (originalWidth === 0 || originalHeight === 0) {\n throw new Error('Invalid image dimensions');\n }\n\n // Calculate the new size maintaining aspect ratio\n const aspectRatio = originalWidth / originalHeight;\n\n let width: number;\n let height: number;\n\n if (originalWidth > originalHeight) {\n width = size;\n height = Math.round(size / aspectRatio);\n } else {\n width = Math.round(size * aspectRatio);\n height = size;\n }\n\n // Resize the image and create the thumbnails\n await resizeImage(image, outputPath, width, height);\n await resizeImage(image, outputPathRetina, width * 2, height * 2);\n\n // Return the dimensions of the thumbnail\n return { width, height };\n}\n","import { encode } from 'blurhash';\n\nimport { loadImage } from './image';\n\n/**\n * Generates a BlurHash from an image file or Sharp instance\n * @param imagePath - Path to image file or Sharp instance\n * @param componentX - Number of x components (default: 4)\n * @param componentY - Number of y components (default: 3)\n * @returns Promise resolving to BlurHash string\n */\nexport async function generateBlurHash(imagePath: string, componentX: number = 4, componentY: number = 3): Promise<string> {\n const image = await loadImage(imagePath);\n\n // Resize to small size for BlurHash computation to improve performance\n // BlurHash doesn't need high resolution\n const { data, info } = await image\n .resize(32, 32, { fit: 'inside' })\n .ensureAlpha()\n .raw()\n .toBuffer({ resolveWithObject: true });\n\n // Convert to Uint8ClampedArray format expected by blurhash\n const pixels = new Uint8ClampedArray(data.buffer);\n\n // Generate BlurHash\n return encode(pixels, info.width, info.height, componentX, componentY);\n}\n","import { spawn } from 'node:child_process';\nimport { promises as fs } from 'node:fs';\n\nimport ffprobe from 'node-ffprobe';\nimport sharp from 'sharp';\n\nimport { resizeImage } from './image';\n\nimport type { Dimensions } from '../types';\nimport type { Buffer } from 'node:buffer';\n\n/**\n * Gets video dimensions using ffprobe\n * @param filePath - Path to the video file\n * @returns Promise resolving to video dimensions\n * @throws Error if no video stream found or invalid dimensions\n */\nexport async function getVideoDimensions(filePath: string): Promise<Dimensions> {\n const data = await ffprobe(filePath);\n const videoStream = data.streams.find((stream) => stream.codec_type === 'video');\n\n if (!videoStream) {\n throw new Error('No video stream found');\n }\n\n const dimensions = {\n width: videoStream.width || 0,\n height: videoStream.height || 0,\n };\n\n if (dimensions.width === 0 || dimensions.height === 0) {\n throw new Error('Invalid video dimensions');\n }\n\n return dimensions;\n}\n\n/**\n * Creates regular and retina thumbnails for a video by extracting the first frame\n * @param inputPath - Path to the video file\n * @param videoDimensions - Original video dimensions\n * @param outputPath - Path where thumbnail should be saved\n * @param outputPathRetina - Path where retina thumbnail should be saved\n * @param height - Target height for thumbnail\n * @param verbose - Whether to enable verbose ffmpeg output\n * @returns Promise resolving to thumbnail dimensions\n */\nexport async function createVideoThumbnails(\n inputPath: string,\n videoDimensions: Dimensions,\n outputPath: string,\n outputPathRetina: string,\n height: number,\n verbose: boolean = false,\n): Promise<Dimensions> {\n // Calculate width maintaining aspect ratio\n const aspectRatio = videoDimensions.width / videoDimensions.height;\n const width = Math.round(height * aspectRatio);\n\n // Use ffmpeg to extract first frame as a temporary file, then process with sharp\n const tempFramePath = `${outputPath}.temp.png`;\n\n return new Promise((resolve, reject) => {\n // Extract first frame using ffmpeg\n const ffmpeg = spawn('ffmpeg', [\n '-i',\n inputPath,\n '-vframes',\n '1',\n '-y',\n '-loglevel',\n verbose ? 'error' : 'quiet',\n tempFramePath,\n ]);\n\n ffmpeg.stderr.on('data', (data: Buffer) => {\n // FFmpeg writes normal output to stderr, so we don't treat this as an error\n console.log(`ffmpeg: ${data.toString()}`);\n });\n\n ffmpeg.on('close', async (code: number) => {\n if (code === 0) {\n try {\n // Process the extracted frame with sharp\n const frameImage = sharp(tempFramePath);\n await resizeImage(frameImage, outputPath, width, height);\n await resizeImage(frameImage, outputPathRetina, width * 2, height * 2);\n\n // Clean up temporary file\n try {\n await fs.unlink(tempFramePath);\n } catch {\n // Ignore cleanup errors\n }\n\n resolve({ width, height });\n } catch (sharpError) {\n reject(new Error(`Failed to process extracted frame: ${sharpError}`));\n }\n } else {\n reject(new Error(`ffmpeg exited with code ${code}`));\n }\n });\n\n ffmpeg.on('error', (error: Error) => {\n reject(new Error(`Failed to start ffmpeg: ${error.message}`));\n });\n });\n}\n","import { Buffer } from 'node:buffer';\nimport fs from 'node:fs';\nimport path from 'node:path';\nimport process from 'node:process';\nimport { fileURLToPath } from 'node:url';\n\nimport sharp from 'sharp';\n\nimport { HEADER_IMAGE_LANDSCAPE_WIDTHS, HEADER_IMAGE_PORTRAIT_WIDTHS } from '../../../config';\nimport { generateBlurHash } from '../../../utils/blurhash';\nimport { cropAndResizeImage, loadImage } from '../../../utils/image';\n\nimport type { ConsolaInstance } from 'consola';\n\n/** __dirname workaround for ESM modules */\nconst __dirname = path.dirname(fileURLToPath(import.meta.url));\n\n/** Relative path to the bundled font used for social cards */\nconst SOCIAL_CARD_FONT_RELATIVE_PATH = path.join('assets', 'fonts', 'dejavu', 'DejaVuSans-Bold.ttf');\n\n/** Cached base64 font data to avoid repeated disk reads */\nlet socialCardFontBase64: string | null | undefined;\n\n/**\n * Helper function to resolve paths relative to current file\n * @param segments - Path segments to resolve relative to current directory\n * @returns Resolved absolute path\n */\nexport function resolveFromCurrentDir(...segments: string[]): string {\n return path.resolve(__dirname, ...segments);\n}\n\n/**\n * Locate the font used for rendering social media card text.\n * Tries multiple candidate paths to support both source and built distributions.\n * @returns Font path if found, null otherwise\n */\nfunction findSocialCardFontPath(): string | null {\n const fontCandidates = [\n resolveFromCurrentDir('../../../../', SOCIAL_CARD_FONT_RELATIVE_PATH),\n path.resolve(__dirname, '../', SOCIAL_CARD_FONT_RELATIVE_PATH),\n path.resolve(__dirname, '../../', SOCIAL_CARD_FONT_RELATIVE_PATH),\n path.resolve(process.cwd(), SOCIAL_CARD_FONT_RELATIVE_PATH),\n path.resolve(process.cwd(), '../', SOCIAL_CARD_FONT_RELATIVE_PATH),\n path.resolve(process.cwd(), '../simple-photo-gallery/', SOCIAL_CARD_FONT_RELATIVE_PATH),\n ];\n\n for (const candidate of fontCandidates) {\n if (fs.existsSync(candidate)) {\n return candidate;\n }\n }\n\n return null;\n}\n\n/**\n * Loads the social media card font and returns its base64 representation.\n * The data is cached to avoid repeated disk access within the same process.\n * @returns Base64 font data if font file is found, null otherwise\n */\nfunction getSocialCardFontBase64(): string | null {\n if (socialCardFontBase64 !== undefined) {\n return socialCardFontBase64;\n }\n\n const fontPath = findSocialCardFontPath();\n if (!fontPath) {\n socialCardFontBase64 = null;\n return null;\n }\n\n socialCardFontBase64 = fs.readFileSync(fontPath).toString('base64');\n return socialCardFontBase64;\n}\n\n/**\n * Creates a social media card image for a gallery\n * @param headerPhotoPath - Path to the header photo\n * @param title - Title of the gallery\n * @param ouputPath - Output path for the social media card image\n * @param ui - ConsolaInstance for logging\n * @returns The basename of the header photo used\n */\nexport async function createGallerySocialMediaCardImage(\n headerPhotoPath: string,\n title: string,\n ouputPath: string,\n ui?: ConsolaInstance,\n): Promise<string> {\n ui?.start(`Creating social media card image`);\n\n const headerBasename = path.basename(headerPhotoPath, path.extname(headerPhotoPath));\n\n if (fs.existsSync(ouputPath)) {\n ui?.success(`Social media card image already exists`);\n return headerBasename;\n }\n\n // Read and resize the header image to 1200x631 using fit\n const image = await loadImage(headerPhotoPath);\n const resizedImageBuffer = await image.resize(1200, 631, { fit: 'cover' }).jpeg({ quality: 90 }).toBuffer();\n\n // Save the resized image as social media card\n const outputPath = ouputPath;\n await sharp(resizedImageBuffer).toFile(outputPath);\n\n // Create SVG with title and description\n const fontBase64 = getSocialCardFontBase64();\n const fontFace = fontBase64\n ? `\n @font-face {\n font-family: 'DejaVu Sans';\n src: url('data:font/ttf;base64,${fontBase64}') format('truetype');\n font-weight: 700;\n font-style: normal;\n }`\n : '';\n const fontFamily = fontBase64 ? \"'DejaVu Sans', Arial, sans-serif\" : 'Arial, sans-serif';\n const svgText = `\n <svg width=\"1200\" height=\"631\" xmlns=\"http://www.w3.org/2000/svg\">\n <defs>\n <style>\n ${fontFace}\n .title { font-family: ${fontFamily}; font-size: 96px; font-weight: bold; fill: white; stroke: black; stroke-width: 5; paint-order: stroke; text-anchor: middle; }\n </style>\n </defs>\n <text x=\"600\" y=\"250\" class=\"title\">${title}</text>\n </svg>\n `;\n\n // Composite the text overlay on top of the resized image\n const finalImageBuffer = await sharp(resizedImageBuffer)\n .composite([{ input: Buffer.from(svgText), top: 0, left: 0 }])\n .jpeg({ quality: 90 })\n .toBuffer();\n\n // Save the final image with text overlay\n await sharp(finalImageBuffer).toFile(outputPath);\n\n ui?.success(`Created social media card image successfully`);\n return headerBasename;\n}\n\n/**\n * Creates optimized header images for different orientations and sizes\n * @param headerPhotoPath - Path to the header photo\n * @param outputFolder - Folder where header images should be saved\n * @param ui - ConsolaInstance for logging\n * @returns Object containing the header basename, array of generated file paths, and blurhash\n */\nexport async function createOptimizedHeaderImage(\n headerPhotoPath: string,\n outputFolder: string,\n ui?: ConsolaInstance,\n): Promise<{ headerBasename: string; generatedFiles: string[]; blurHash: string }> {\n ui?.start(`Creating optimized header images`);\n\n const image = await loadImage(headerPhotoPath);\n const headerBasename = path.basename(headerPhotoPath, path.extname(headerPhotoPath));\n const generatedFiles: string[] = [];\n\n // Generate blurhash for the header image\n ui?.debug('Generating blurhash for header image');\n const blurHash = await generateBlurHash(headerPhotoPath);\n\n // Create landscape header images\n const landscapeYFactor = 3 / 4;\n for (const width of HEADER_IMAGE_LANDSCAPE_WIDTHS) {\n ui?.debug(`Creating landscape header image ${width}`);\n\n const avifFilename = `${headerBasename}_landscape_${width}.avif`;\n const jpgFilename = `${headerBasename}_landscape_${width}.jpg`;\n\n if (fs.existsSync(path.join(outputFolder, avifFilename))) {\n ui?.debug(`Landscape header image ${width} AVIF already exists`);\n } else {\n await cropAndResizeImage(\n image.clone(),\n path.join(outputFolder, avifFilename),\n width,\n width * landscapeYFactor,\n 'avif',\n );\n }\n generatedFiles.push(avifFilename);\n\n if (fs.existsSync(path.join(outputFolder, jpgFilename))) {\n ui?.debug(`Landscape header image ${width} JPG already exists`);\n } else {\n await cropAndResizeImage(image.clone(), path.join(outputFolder, jpgFilename), width, width * landscapeYFactor, 'jpg');\n }\n generatedFiles.push(jpgFilename);\n }\n\n // Create portrait header images\n const portraitYFactor = 4 / 3;\n for (const width of HEADER_IMAGE_PORTRAIT_WIDTHS) {\n ui?.debug(`Creating portrait header image ${width}`);\n\n const avifFilename = `${headerBasename}_portrait_${width}.avif`;\n const jpgFilename = `${headerBasename}_portrait_${width}.jpg`;\n\n if (fs.existsSync(path.join(outputFolder, avifFilename))) {\n ui?.debug(`Portrait header image ${width} AVIF already exists`);\n } else {\n await cropAndResizeImage(image.clone(), path.join(outputFolder, avifFilename), width, width * portraitYFactor, 'avif');\n }\n generatedFiles.push(avifFilename);\n\n if (fs.existsSync(path.join(outputFolder, jpgFilename))) {\n ui?.debug(`Portrait header image ${width} JPG already exists`);\n } else {\n await cropAndResizeImage(image.clone(), path.join(outputFolder, jpgFilename), width, width * portraitYFactor, 'jpg');\n }\n generatedFiles.push(jpgFilename);\n }\n\n ui?.success(`Created optimized header image successfully`);\n return { headerBasename, generatedFiles, blurHash };\n}\n\n/**\n * Checks if there are old header images with a different basename than the current one\n * @param outputFolder - Folder containing the header images\n * @param currentHeaderBasename - Basename of the current header image\n * @returns True if old header images with different basename exist, false otherwise\n */\nexport function hasOldHeaderImages(outputFolder: string, currentHeaderBasename: string): boolean {\n if (!fs.existsSync(outputFolder)) {\n return false;\n }\n\n const files = fs.readdirSync(outputFolder);\n\n for (const file of files) {\n // Check if file is a header image (landscape or portrait) with different basename\n const landscapeMatch = file.match(/^(.+)_landscape_\\d+\\.(avif|jpg)$/);\n const portraitMatch = file.match(/^(.+)_portrait_\\d+\\.(avif|jpg)$/);\n\n if (\n (landscapeMatch && landscapeMatch[1] !== currentHeaderBasename) ||\n (portraitMatch && portraitMatch[1] !== currentHeaderBasename)\n ) {\n return true;\n }\n }\n\n return false;\n}\n\n/**\n * Cleans up old header images that don't match the current header image\n * @param outputFolder - Folder containing the header images\n * @param currentHeaderBasename - Basename of the current header image\n * @param ui - ConsolaInstance for logging\n */\nexport function cleanupOldHeaderImages(outputFolder: string, currentHeaderBasename: string, ui?: ConsolaInstance): void {\n ui?.start(`Cleaning up old header images`);\n\n if (!fs.existsSync(outputFolder)) {\n ui?.debug(`Output folder ${outputFolder} does not exist, skipping cleanup`);\n return;\n }\n\n const files = fs.readdirSync(outputFolder);\n let deletedCount = 0;\n\n for (const file of files) {\n // Check if file is a header image (landscape or portrait) with different basename\n const landscapeMatch = file.match(/^(.+)_landscape_\\d+\\.(avif|jpg)$/);\n const portraitMatch = file.match(/^(.+)_portrait_\\d+\\.(avif|jpg)$/);\n\n if (landscapeMatch && landscapeMatch[1] !== currentHeaderBasename) {\n const filePath = path.join(outputFolder, file);\n ui?.debug(`Deleting old landscape header image: ${file}`);\n fs.unlinkSync(filePath);\n deletedCount++;\n } else if (portraitMatch && portraitMatch[1] !== currentHeaderBasename) {\n const filePath = path.join(outputFolder, file);\n ui?.debug(`Deleting old portrait header image: ${file}`);\n fs.unlinkSync(filePath);\n deletedCount++;\n }\n }\n\n if (deletedCount > 0) {\n ui?.success(`Deleted ${deletedCount} old header image(s)`);\n } else {\n ui?.debug(`No old header images to clean up`);\n }\n}\n"]}
|
package/dist/lib/index.js
CHANGED
|
@@ -127,20 +127,27 @@ function findSocialCardFontPath() {
|
|
|
127
127
|
const fontCandidates = [
|
|
128
128
|
resolveFromCurrentDir("../../../../", SOCIAL_CARD_FONT_RELATIVE_PATH),
|
|
129
129
|
path.resolve(__dirname, "../", SOCIAL_CARD_FONT_RELATIVE_PATH),
|
|
130
|
-
path.resolve(
|
|
130
|
+
path.resolve(__dirname, "../../", SOCIAL_CARD_FONT_RELATIVE_PATH),
|
|
131
|
+
path.resolve(process.cwd(), SOCIAL_CARD_FONT_RELATIVE_PATH),
|
|
132
|
+
path.resolve(process.cwd(), "../", SOCIAL_CARD_FONT_RELATIVE_PATH),
|
|
133
|
+
path.resolve(process.cwd(), "../simple-photo-gallery/", SOCIAL_CARD_FONT_RELATIVE_PATH)
|
|
131
134
|
];
|
|
132
135
|
for (const candidate of fontCandidates) {
|
|
133
136
|
if (fs2.existsSync(candidate)) {
|
|
134
137
|
return candidate;
|
|
135
138
|
}
|
|
136
139
|
}
|
|
137
|
-
|
|
140
|
+
return null;
|
|
138
141
|
}
|
|
139
142
|
function getSocialCardFontBase64() {
|
|
140
|
-
if (socialCardFontBase64) {
|
|
143
|
+
if (socialCardFontBase64 !== void 0) {
|
|
141
144
|
return socialCardFontBase64;
|
|
142
145
|
}
|
|
143
146
|
const fontPath = findSocialCardFontPath();
|
|
147
|
+
if (!fontPath) {
|
|
148
|
+
socialCardFontBase64 = null;
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
144
151
|
socialCardFontBase64 = fs2.readFileSync(fontPath).toString("base64");
|
|
145
152
|
return socialCardFontBase64;
|
|
146
153
|
}
|
|
@@ -156,17 +163,20 @@ async function createGallerySocialMediaCardImage(headerPhotoPath, title, ouputPa
|
|
|
156
163
|
const outputPath = ouputPath;
|
|
157
164
|
await sharp3(resizedImageBuffer).toFile(outputPath);
|
|
158
165
|
const fontBase64 = getSocialCardFontBase64();
|
|
159
|
-
const
|
|
160
|
-
<svg width="1200" height="631" xmlns="http://www.w3.org/2000/svg">
|
|
161
|
-
<defs>
|
|
162
|
-
<style>
|
|
166
|
+
const fontFace = fontBase64 ? `
|
|
163
167
|
@font-face {
|
|
164
168
|
font-family: 'DejaVu Sans';
|
|
165
169
|
src: url('data:font/ttf;base64,${fontBase64}') format('truetype');
|
|
166
170
|
font-weight: 700;
|
|
167
171
|
font-style: normal;
|
|
168
|
-
}
|
|
169
|
-
|
|
172
|
+
}` : "";
|
|
173
|
+
const fontFamily = fontBase64 ? "'DejaVu Sans', Arial, sans-serif" : "Arial, sans-serif";
|
|
174
|
+
const svgText = `
|
|
175
|
+
<svg width="1200" height="631" xmlns="http://www.w3.org/2000/svg">
|
|
176
|
+
<defs>
|
|
177
|
+
<style>
|
|
178
|
+
${fontFace}
|
|
179
|
+
.title { font-family: ${fontFamily}; font-size: 96px; font-weight: bold; fill: white; stroke: black; stroke-width: 5; paint-order: stroke; text-anchor: middle; }
|
|
170
180
|
</style>
|
|
171
181
|
</defs>
|
|
172
182
|
<text x="600" y="250" class="title">${title}</text>
|
package/dist/lib/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/utils/image.ts","../../src/utils/blurhash.ts","../../src/utils/video.ts","../../src/modules/build/utils/index.ts"],"names":["sharp","fs"],"mappings":"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","file":"index.js","sourcesContent":["import sharp from 'sharp';\n\nimport type { Dimensions, ImageWithMetadata } from '../types';\nimport type { FormatEnum, Metadata, Sharp } from 'sharp';\n\n/**\n * Loads an image and auto-rotates it based on EXIF orientation.\n * @param imagePath - Path to the image file\n * @returns Promise resolving to Sharp image instance\n */\nexport async function loadImage(imagePath: string): Promise<Sharp> {\n return sharp(imagePath).rotate();\n}\n\n/**\n * Loads an image and its metadata, auto-rotating it based on EXIF orientation and swapping dimensions if needed.\n * @param imagePath - Path to the image file\n * @returns Promise resolving to ImageWithMetadata object containing Sharp image instance and metadata\n */\nexport async function loadImageWithMetadata(imagePath: string): Promise<ImageWithMetadata> {\n const image = sharp(imagePath);\n const metadata = await image.metadata();\n\n // Auto-rotate based on EXIF orientation\n image.rotate();\n\n // EXIF orientation values 5, 6, 7, 8 require dimension swap after rotation\n const needsDimensionSwap = metadata.orientation && metadata.orientation >= 5 && metadata.orientation <= 8;\n\n // Update metadata with swapped dimensions if needed\n if (needsDimensionSwap) {\n const originalWidth = metadata.width;\n metadata.width = metadata.height;\n metadata.height = originalWidth;\n }\n\n return { image, metadata };\n}\n\n/**\n * Utility function to resize and save thumbnail using Sharp. The functions avoids upscaling the image and only reduces the size if necessary.\n * @param image - Sharp image instance\n * @param outputPath - Path where thumbnail should be saved\n * @param width - Target width for thumbnail\n * @param height - Target height for thumbnail\n */\nexport async function resizeImage(\n image: Sharp,\n outputPath: string,\n width: number,\n height: number,\n format: keyof FormatEnum = 'avif',\n): Promise<void> {\n // Resize the image without enlarging it\n await image.resize(width, height, { withoutEnlargement: true }).toFormat(format).toFile(outputPath);\n}\n\n/**\n * Crops and resizes an image to a target aspect ratio, avoiding upscaling the image.\n * @param image - Sharp image instance\n * @param outputPath - Path where the image should be saved\n * @param width - Target width for the image\n * @param height - Target height for the image\n */\nexport async function cropAndResizeImage(\n image: Sharp,\n outputPath: string,\n width: number,\n height: number,\n format: keyof FormatEnum = 'avif',\n): Promise<void> {\n // Apply resize with cover fit and without enlargement\n await image\n .resize(width, height, {\n fit: 'cover',\n withoutEnlargement: true,\n })\n .toFormat(format)\n .toFile(outputPath);\n}\n\n/**\n * Creates regular and retina thumbnails for an image while maintaining aspect ratio\n * @param image - Sharp image instance\n * @param metadata - Image metadata containing dimensions\n * @param outputPath - Path where thumbnail should be saved\n * @param outputPathRetina - Path where retina thumbnail should be saved\n * @param size - Target size of the longer side of the thumbnail\n * @returns Promise resolving to thumbnail dimensions\n */\nexport async function createImageThumbnails(\n image: Sharp,\n metadata: Metadata,\n outputPath: string,\n outputPathRetina: string,\n size: number,\n): Promise<Dimensions> {\n // Get the original dimensions\n const originalWidth = metadata.width || 0;\n const originalHeight = metadata.height || 0;\n\n if (originalWidth === 0 || originalHeight === 0) {\n throw new Error('Invalid image dimensions');\n }\n\n // Calculate the new size maintaining aspect ratio\n const aspectRatio = originalWidth / originalHeight;\n\n let width: number;\n let height: number;\n\n if (originalWidth > originalHeight) {\n width = size;\n height = Math.round(size / aspectRatio);\n } else {\n width = Math.round(size * aspectRatio);\n height = size;\n }\n\n // Resize the image and create the thumbnails\n await resizeImage(image, outputPath, width, height);\n await resizeImage(image, outputPathRetina, width * 2, height * 2);\n\n // Return the dimensions of the thumbnail\n return { width, height };\n}\n","import { encode } from 'blurhash';\n\nimport { loadImage } from './image';\n\n/**\n * Generates a BlurHash from an image file or Sharp instance\n * @param imagePath - Path to image file or Sharp instance\n * @param componentX - Number of x components (default: 4)\n * @param componentY - Number of y components (default: 3)\n * @returns Promise resolving to BlurHash string\n */\nexport async function generateBlurHash(imagePath: string, componentX: number = 4, componentY: number = 3): Promise<string> {\n const image = await loadImage(imagePath);\n\n // Resize to small size for BlurHash computation to improve performance\n // BlurHash doesn't need high resolution\n const { data, info } = await image\n .resize(32, 32, { fit: 'inside' })\n .ensureAlpha()\n .raw()\n .toBuffer({ resolveWithObject: true });\n\n // Convert to Uint8ClampedArray format expected by blurhash\n const pixels = new Uint8ClampedArray(data.buffer);\n\n // Generate BlurHash\n return encode(pixels, info.width, info.height, componentX, componentY);\n}\n","import { spawn } from 'node:child_process';\nimport { promises as fs } from 'node:fs';\n\nimport ffprobe from 'node-ffprobe';\nimport sharp from 'sharp';\n\nimport { resizeImage } from './image';\n\nimport type { Dimensions } from '../types';\nimport type { Buffer } from 'node:buffer';\n\n/**\n * Gets video dimensions using ffprobe\n * @param filePath - Path to the video file\n * @returns Promise resolving to video dimensions\n * @throws Error if no video stream found or invalid dimensions\n */\nexport async function getVideoDimensions(filePath: string): Promise<Dimensions> {\n const data = await ffprobe(filePath);\n const videoStream = data.streams.find((stream) => stream.codec_type === 'video');\n\n if (!videoStream) {\n throw new Error('No video stream found');\n }\n\n const dimensions = {\n width: videoStream.width || 0,\n height: videoStream.height || 0,\n };\n\n if (dimensions.width === 0 || dimensions.height === 0) {\n throw new Error('Invalid video dimensions');\n }\n\n return dimensions;\n}\n\n/**\n * Creates regular and retina thumbnails for a video by extracting the first frame\n * @param inputPath - Path to the video file\n * @param videoDimensions - Original video dimensions\n * @param outputPath - Path where thumbnail should be saved\n * @param outputPathRetina - Path where retina thumbnail should be saved\n * @param height - Target height for thumbnail\n * @param verbose - Whether to enable verbose ffmpeg output\n * @returns Promise resolving to thumbnail dimensions\n */\nexport async function createVideoThumbnails(\n inputPath: string,\n videoDimensions: Dimensions,\n outputPath: string,\n outputPathRetina: string,\n height: number,\n verbose: boolean = false,\n): Promise<Dimensions> {\n // Calculate width maintaining aspect ratio\n const aspectRatio = videoDimensions.width / videoDimensions.height;\n const width = Math.round(height * aspectRatio);\n\n // Use ffmpeg to extract first frame as a temporary file, then process with sharp\n const tempFramePath = `${outputPath}.temp.png`;\n\n return new Promise((resolve, reject) => {\n // Extract first frame using ffmpeg\n const ffmpeg = spawn('ffmpeg', [\n '-i',\n inputPath,\n '-vframes',\n '1',\n '-y',\n '-loglevel',\n verbose ? 'error' : 'quiet',\n tempFramePath,\n ]);\n\n ffmpeg.stderr.on('data', (data: Buffer) => {\n // FFmpeg writes normal output to stderr, so we don't treat this as an error\n console.log(`ffmpeg: ${data.toString()}`);\n });\n\n ffmpeg.on('close', async (code: number) => {\n if (code === 0) {\n try {\n // Process the extracted frame with sharp\n const frameImage = sharp(tempFramePath);\n await resizeImage(frameImage, outputPath, width, height);\n await resizeImage(frameImage, outputPathRetina, width * 2, height * 2);\n\n // Clean up temporary file\n try {\n await fs.unlink(tempFramePath);\n } catch {\n // Ignore cleanup errors\n }\n\n resolve({ width, height });\n } catch (sharpError) {\n reject(new Error(`Failed to process extracted frame: ${sharpError}`));\n }\n } else {\n reject(new Error(`ffmpeg exited with code ${code}`));\n }\n });\n\n ffmpeg.on('error', (error: Error) => {\n reject(new Error(`Failed to start ffmpeg: ${error.message}`));\n });\n });\n}\n","import { Buffer } from 'node:buffer';\nimport fs from 'node:fs';\nimport path from 'node:path';\nimport process from 'node:process';\nimport { fileURLToPath } from 'node:url';\n\nimport sharp from 'sharp';\n\nimport { HEADER_IMAGE_LANDSCAPE_WIDTHS, HEADER_IMAGE_PORTRAIT_WIDTHS } from '../../../config';\nimport { generateBlurHash } from '../../../utils/blurhash';\nimport { cropAndResizeImage, loadImage } from '../../../utils/image';\n\nimport type { ConsolaInstance } from 'consola';\n\n/** __dirname workaround for ESM modules */\nconst __dirname = path.dirname(fileURLToPath(import.meta.url));\n\n/** Relative path to the bundled font used for social cards */\nconst SOCIAL_CARD_FONT_RELATIVE_PATH = path.join('assets', 'fonts', 'dejavu', 'DejaVuSans-Bold.ttf');\n\n/** Cached base64 font data to avoid repeated disk reads */\nlet socialCardFontBase64: string | undefined;\n\n/**\n * Helper function to resolve paths relative to current file\n * @param segments - Path segments to resolve relative to current directory\n * @returns Resolved absolute path\n */\nexport function resolveFromCurrentDir(...segments: string[]): string {\n return path.resolve(__dirname, ...segments);\n}\n\n/**\n * Locate the font used for rendering social media card text.\n * Tries multiple candidate paths to support both source and built distributions.\n */\nfunction findSocialCardFontPath(): string {\n const fontCandidates = [\n resolveFromCurrentDir('../../../../', SOCIAL_CARD_FONT_RELATIVE_PATH),\n path.resolve(__dirname, '../', SOCIAL_CARD_FONT_RELATIVE_PATH),\n path.resolve(process.cwd(), SOCIAL_CARD_FONT_RELATIVE_PATH),\n ];\n\n for (const candidate of fontCandidates) {\n if (fs.existsSync(candidate)) {\n return candidate;\n }\n }\n\n throw new Error('Social media card font file not found');\n}\n\n/**\n * Loads the social media card font and returns its base64 representation.\n * The data is cached to avoid repeated disk access within the same process.\n */\nfunction getSocialCardFontBase64(): string {\n if (socialCardFontBase64) {\n return socialCardFontBase64;\n }\n\n const fontPath = findSocialCardFontPath();\n socialCardFontBase64 = fs.readFileSync(fontPath).toString('base64');\n return socialCardFontBase64;\n}\n\n/**\n * Creates a social media card image for a gallery\n * @param headerPhotoPath - Path to the header photo\n * @param title - Title of the gallery\n * @param ouputPath - Output path for the social media card image\n * @param ui - ConsolaInstance for logging\n * @returns The basename of the header photo used\n */\nexport async function createGallerySocialMediaCardImage(\n headerPhotoPath: string,\n title: string,\n ouputPath: string,\n ui?: ConsolaInstance,\n): Promise<string> {\n ui?.start(`Creating social media card image`);\n\n const headerBasename = path.basename(headerPhotoPath, path.extname(headerPhotoPath));\n\n if (fs.existsSync(ouputPath)) {\n ui?.success(`Social media card image already exists`);\n return headerBasename;\n }\n\n // Read and resize the header image to 1200x631 using fit\n const image = await loadImage(headerPhotoPath);\n const resizedImageBuffer = await image.resize(1200, 631, { fit: 'cover' }).jpeg({ quality: 90 }).toBuffer();\n\n // Save the resized image as social media card\n const outputPath = ouputPath;\n await sharp(resizedImageBuffer).toFile(outputPath);\n\n // Create SVG with title and description\n const fontBase64 = getSocialCardFontBase64();\n const svgText = `\n <svg width=\"1200\" height=\"631\" xmlns=\"http://www.w3.org/2000/svg\">\n <defs>\n <style>\n @font-face {\n font-family: 'DejaVu Sans';\n src: url('data:font/ttf;base64,${fontBase64}') format('truetype');\n font-weight: 700;\n font-style: normal;\n }\n .title { font-family: 'DejaVu Sans', Arial, sans-serif; font-size: 96px; font-weight: bold; fill: white; stroke: black; stroke-width: 5; paint-order: stroke; text-anchor: middle; }\n </style>\n </defs>\n <text x=\"600\" y=\"250\" class=\"title\">${title}</text>\n </svg>\n `;\n\n // Composite the text overlay on top of the resized image\n const finalImageBuffer = await sharp(resizedImageBuffer)\n .composite([{ input: Buffer.from(svgText), top: 0, left: 0 }])\n .jpeg({ quality: 90 })\n .toBuffer();\n\n // Save the final image with text overlay\n await sharp(finalImageBuffer).toFile(outputPath);\n\n ui?.success(`Created social media card image successfully`);\n return headerBasename;\n}\n\n/**\n * Creates optimized header images for different orientations and sizes\n * @param headerPhotoPath - Path to the header photo\n * @param outputFolder - Folder where header images should be saved\n * @param ui - ConsolaInstance for logging\n * @returns Object containing the header basename, array of generated file paths, and blurhash\n */\nexport async function createOptimizedHeaderImage(\n headerPhotoPath: string,\n outputFolder: string,\n ui?: ConsolaInstance,\n): Promise<{ headerBasename: string; generatedFiles: string[]; blurHash: string }> {\n ui?.start(`Creating optimized header images`);\n\n const image = await loadImage(headerPhotoPath);\n const headerBasename = path.basename(headerPhotoPath, path.extname(headerPhotoPath));\n const generatedFiles: string[] = [];\n\n // Generate blurhash for the header image\n ui?.debug('Generating blurhash for header image');\n const blurHash = await generateBlurHash(headerPhotoPath);\n\n // Create landscape header images\n const landscapeYFactor = 3 / 4;\n for (const width of HEADER_IMAGE_LANDSCAPE_WIDTHS) {\n ui?.debug(`Creating landscape header image ${width}`);\n\n const avifFilename = `${headerBasename}_landscape_${width}.avif`;\n const jpgFilename = `${headerBasename}_landscape_${width}.jpg`;\n\n if (fs.existsSync(path.join(outputFolder, avifFilename))) {\n ui?.debug(`Landscape header image ${width} AVIF already exists`);\n } else {\n await cropAndResizeImage(\n image.clone(),\n path.join(outputFolder, avifFilename),\n width,\n width * landscapeYFactor,\n 'avif',\n );\n }\n generatedFiles.push(avifFilename);\n\n if (fs.existsSync(path.join(outputFolder, jpgFilename))) {\n ui?.debug(`Landscape header image ${width} JPG already exists`);\n } else {\n await cropAndResizeImage(image.clone(), path.join(outputFolder, jpgFilename), width, width * landscapeYFactor, 'jpg');\n }\n generatedFiles.push(jpgFilename);\n }\n\n // Create portrait header images\n const portraitYFactor = 4 / 3;\n for (const width of HEADER_IMAGE_PORTRAIT_WIDTHS) {\n ui?.debug(`Creating portrait header image ${width}`);\n\n const avifFilename = `${headerBasename}_portrait_${width}.avif`;\n const jpgFilename = `${headerBasename}_portrait_${width}.jpg`;\n\n if (fs.existsSync(path.join(outputFolder, avifFilename))) {\n ui?.debug(`Portrait header image ${width} AVIF already exists`);\n } else {\n await cropAndResizeImage(image.clone(), path.join(outputFolder, avifFilename), width, width * portraitYFactor, 'avif');\n }\n generatedFiles.push(avifFilename);\n\n if (fs.existsSync(path.join(outputFolder, jpgFilename))) {\n ui?.debug(`Portrait header image ${width} JPG already exists`);\n } else {\n await cropAndResizeImage(image.clone(), path.join(outputFolder, jpgFilename), width, width * portraitYFactor, 'jpg');\n }\n generatedFiles.push(jpgFilename);\n }\n\n ui?.success(`Created optimized header image successfully`);\n return { headerBasename, generatedFiles, blurHash };\n}\n\n/**\n * Checks if there are old header images with a different basename than the current one\n * @param outputFolder - Folder containing the header images\n * @param currentHeaderBasename - Basename of the current header image\n * @returns True if old header images with different basename exist, false otherwise\n */\nexport function hasOldHeaderImages(outputFolder: string, currentHeaderBasename: string): boolean {\n if (!fs.existsSync(outputFolder)) {\n return false;\n }\n\n const files = fs.readdirSync(outputFolder);\n\n for (const file of files) {\n // Check if file is a header image (landscape or portrait) with different basename\n const landscapeMatch = file.match(/^(.+)_landscape_\\d+\\.(avif|jpg)$/);\n const portraitMatch = file.match(/^(.+)_portrait_\\d+\\.(avif|jpg)$/);\n\n if (\n (landscapeMatch && landscapeMatch[1] !== currentHeaderBasename) ||\n (portraitMatch && portraitMatch[1] !== currentHeaderBasename)\n ) {\n return true;\n }\n }\n\n return false;\n}\n\n/**\n * Cleans up old header images that don't match the current header image\n * @param outputFolder - Folder containing the header images\n * @param currentHeaderBasename - Basename of the current header image\n * @param ui - ConsolaInstance for logging\n */\nexport function cleanupOldHeaderImages(outputFolder: string, currentHeaderBasename: string, ui?: ConsolaInstance): void {\n ui?.start(`Cleaning up old header images`);\n\n if (!fs.existsSync(outputFolder)) {\n ui?.debug(`Output folder ${outputFolder} does not exist, skipping cleanup`);\n return;\n }\n\n const files = fs.readdirSync(outputFolder);\n let deletedCount = 0;\n\n for (const file of files) {\n // Check if file is a header image (landscape or portrait) with different basename\n const landscapeMatch = file.match(/^(.+)_landscape_\\d+\\.(avif|jpg)$/);\n const portraitMatch = file.match(/^(.+)_portrait_\\d+\\.(avif|jpg)$/);\n\n if (landscapeMatch && landscapeMatch[1] !== currentHeaderBasename) {\n const filePath = path.join(outputFolder, file);\n ui?.debug(`Deleting old landscape header image: ${file}`);\n fs.unlinkSync(filePath);\n deletedCount++;\n } else if (portraitMatch && portraitMatch[1] !== currentHeaderBasename) {\n const filePath = path.join(outputFolder, file);\n ui?.debug(`Deleting old portrait header image: ${file}`);\n fs.unlinkSync(filePath);\n deletedCount++;\n }\n }\n\n if (deletedCount > 0) {\n ui?.success(`Deleted ${deletedCount} old header image(s)`);\n } else {\n ui?.debug(`No old header images to clean up`);\n }\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/utils/image.ts","../../src/utils/blurhash.ts","../../src/utils/video.ts","../../src/modules/build/utils/index.ts"],"names":["sharp","fs"],"mappings":"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","file":"index.js","sourcesContent":["import sharp from 'sharp';\n\nimport type { Dimensions, ImageWithMetadata } from '../types';\nimport type { FormatEnum, Metadata, Sharp } from 'sharp';\n\n/**\n * Loads an image and auto-rotates it based on EXIF orientation.\n * @param imagePath - Path to the image file\n * @returns Promise resolving to Sharp image instance\n */\nexport async function loadImage(imagePath: string): Promise<Sharp> {\n return sharp(imagePath).rotate();\n}\n\n/**\n * Loads an image and its metadata, auto-rotating it based on EXIF orientation and swapping dimensions if needed.\n * @param imagePath - Path to the image file\n * @returns Promise resolving to ImageWithMetadata object containing Sharp image instance and metadata\n */\nexport async function loadImageWithMetadata(imagePath: string): Promise<ImageWithMetadata> {\n const image = sharp(imagePath);\n const metadata = await image.metadata();\n\n // Auto-rotate based on EXIF orientation\n image.rotate();\n\n // EXIF orientation values 5, 6, 7, 8 require dimension swap after rotation\n const needsDimensionSwap = metadata.orientation && metadata.orientation >= 5 && metadata.orientation <= 8;\n\n // Update metadata with swapped dimensions if needed\n if (needsDimensionSwap) {\n const originalWidth = metadata.width;\n metadata.width = metadata.height;\n metadata.height = originalWidth;\n }\n\n return { image, metadata };\n}\n\n/**\n * Utility function to resize and save thumbnail using Sharp. The functions avoids upscaling the image and only reduces the size if necessary.\n * @param image - Sharp image instance\n * @param outputPath - Path where thumbnail should be saved\n * @param width - Target width for thumbnail\n * @param height - Target height for thumbnail\n */\nexport async function resizeImage(\n image: Sharp,\n outputPath: string,\n width: number,\n height: number,\n format: keyof FormatEnum = 'avif',\n): Promise<void> {\n // Resize the image without enlarging it\n await image.resize(width, height, { withoutEnlargement: true }).toFormat(format).toFile(outputPath);\n}\n\n/**\n * Crops and resizes an image to a target aspect ratio, avoiding upscaling the image.\n * @param image - Sharp image instance\n * @param outputPath - Path where the image should be saved\n * @param width - Target width for the image\n * @param height - Target height for the image\n */\nexport async function cropAndResizeImage(\n image: Sharp,\n outputPath: string,\n width: number,\n height: number,\n format: keyof FormatEnum = 'avif',\n): Promise<void> {\n // Apply resize with cover fit and without enlargement\n await image\n .resize(width, height, {\n fit: 'cover',\n withoutEnlargement: true,\n })\n .toFormat(format)\n .toFile(outputPath);\n}\n\n/**\n * Creates regular and retina thumbnails for an image while maintaining aspect ratio\n * @param image - Sharp image instance\n * @param metadata - Image metadata containing dimensions\n * @param outputPath - Path where thumbnail should be saved\n * @param outputPathRetina - Path where retina thumbnail should be saved\n * @param size - Target size of the longer side of the thumbnail\n * @returns Promise resolving to thumbnail dimensions\n */\nexport async function createImageThumbnails(\n image: Sharp,\n metadata: Metadata,\n outputPath: string,\n outputPathRetina: string,\n size: number,\n): Promise<Dimensions> {\n // Get the original dimensions\n const originalWidth = metadata.width || 0;\n const originalHeight = metadata.height || 0;\n\n if (originalWidth === 0 || originalHeight === 0) {\n throw new Error('Invalid image dimensions');\n }\n\n // Calculate the new size maintaining aspect ratio\n const aspectRatio = originalWidth / originalHeight;\n\n let width: number;\n let height: number;\n\n if (originalWidth > originalHeight) {\n width = size;\n height = Math.round(size / aspectRatio);\n } else {\n width = Math.round(size * aspectRatio);\n height = size;\n }\n\n // Resize the image and create the thumbnails\n await resizeImage(image, outputPath, width, height);\n await resizeImage(image, outputPathRetina, width * 2, height * 2);\n\n // Return the dimensions of the thumbnail\n return { width, height };\n}\n","import { encode } from 'blurhash';\n\nimport { loadImage } from './image';\n\n/**\n * Generates a BlurHash from an image file or Sharp instance\n * @param imagePath - Path to image file or Sharp instance\n * @param componentX - Number of x components (default: 4)\n * @param componentY - Number of y components (default: 3)\n * @returns Promise resolving to BlurHash string\n */\nexport async function generateBlurHash(imagePath: string, componentX: number = 4, componentY: number = 3): Promise<string> {\n const image = await loadImage(imagePath);\n\n // Resize to small size for BlurHash computation to improve performance\n // BlurHash doesn't need high resolution\n const { data, info } = await image\n .resize(32, 32, { fit: 'inside' })\n .ensureAlpha()\n .raw()\n .toBuffer({ resolveWithObject: true });\n\n // Convert to Uint8ClampedArray format expected by blurhash\n const pixels = new Uint8ClampedArray(data.buffer);\n\n // Generate BlurHash\n return encode(pixels, info.width, info.height, componentX, componentY);\n}\n","import { spawn } from 'node:child_process';\nimport { promises as fs } from 'node:fs';\n\nimport ffprobe from 'node-ffprobe';\nimport sharp from 'sharp';\n\nimport { resizeImage } from './image';\n\nimport type { Dimensions } from '../types';\nimport type { Buffer } from 'node:buffer';\n\n/**\n * Gets video dimensions using ffprobe\n * @param filePath - Path to the video file\n * @returns Promise resolving to video dimensions\n * @throws Error if no video stream found or invalid dimensions\n */\nexport async function getVideoDimensions(filePath: string): Promise<Dimensions> {\n const data = await ffprobe(filePath);\n const videoStream = data.streams.find((stream) => stream.codec_type === 'video');\n\n if (!videoStream) {\n throw new Error('No video stream found');\n }\n\n const dimensions = {\n width: videoStream.width || 0,\n height: videoStream.height || 0,\n };\n\n if (dimensions.width === 0 || dimensions.height === 0) {\n throw new Error('Invalid video dimensions');\n }\n\n return dimensions;\n}\n\n/**\n * Creates regular and retina thumbnails for a video by extracting the first frame\n * @param inputPath - Path to the video file\n * @param videoDimensions - Original video dimensions\n * @param outputPath - Path where thumbnail should be saved\n * @param outputPathRetina - Path where retina thumbnail should be saved\n * @param height - Target height for thumbnail\n * @param verbose - Whether to enable verbose ffmpeg output\n * @returns Promise resolving to thumbnail dimensions\n */\nexport async function createVideoThumbnails(\n inputPath: string,\n videoDimensions: Dimensions,\n outputPath: string,\n outputPathRetina: string,\n height: number,\n verbose: boolean = false,\n): Promise<Dimensions> {\n // Calculate width maintaining aspect ratio\n const aspectRatio = videoDimensions.width / videoDimensions.height;\n const width = Math.round(height * aspectRatio);\n\n // Use ffmpeg to extract first frame as a temporary file, then process with sharp\n const tempFramePath = `${outputPath}.temp.png`;\n\n return new Promise((resolve, reject) => {\n // Extract first frame using ffmpeg\n const ffmpeg = spawn('ffmpeg', [\n '-i',\n inputPath,\n '-vframes',\n '1',\n '-y',\n '-loglevel',\n verbose ? 'error' : 'quiet',\n tempFramePath,\n ]);\n\n ffmpeg.stderr.on('data', (data: Buffer) => {\n // FFmpeg writes normal output to stderr, so we don't treat this as an error\n console.log(`ffmpeg: ${data.toString()}`);\n });\n\n ffmpeg.on('close', async (code: number) => {\n if (code === 0) {\n try {\n // Process the extracted frame with sharp\n const frameImage = sharp(tempFramePath);\n await resizeImage(frameImage, outputPath, width, height);\n await resizeImage(frameImage, outputPathRetina, width * 2, height * 2);\n\n // Clean up temporary file\n try {\n await fs.unlink(tempFramePath);\n } catch {\n // Ignore cleanup errors\n }\n\n resolve({ width, height });\n } catch (sharpError) {\n reject(new Error(`Failed to process extracted frame: ${sharpError}`));\n }\n } else {\n reject(new Error(`ffmpeg exited with code ${code}`));\n }\n });\n\n ffmpeg.on('error', (error: Error) => {\n reject(new Error(`Failed to start ffmpeg: ${error.message}`));\n });\n });\n}\n","import { Buffer } from 'node:buffer';\nimport fs from 'node:fs';\nimport path from 'node:path';\nimport process from 'node:process';\nimport { fileURLToPath } from 'node:url';\n\nimport sharp from 'sharp';\n\nimport { HEADER_IMAGE_LANDSCAPE_WIDTHS, HEADER_IMAGE_PORTRAIT_WIDTHS } from '../../../config';\nimport { generateBlurHash } from '../../../utils/blurhash';\nimport { cropAndResizeImage, loadImage } from '../../../utils/image';\n\nimport type { ConsolaInstance } from 'consola';\n\n/** __dirname workaround for ESM modules */\nconst __dirname = path.dirname(fileURLToPath(import.meta.url));\n\n/** Relative path to the bundled font used for social cards */\nconst SOCIAL_CARD_FONT_RELATIVE_PATH = path.join('assets', 'fonts', 'dejavu', 'DejaVuSans-Bold.ttf');\n\n/** Cached base64 font data to avoid repeated disk reads */\nlet socialCardFontBase64: string | null | undefined;\n\n/**\n * Helper function to resolve paths relative to current file\n * @param segments - Path segments to resolve relative to current directory\n * @returns Resolved absolute path\n */\nexport function resolveFromCurrentDir(...segments: string[]): string {\n return path.resolve(__dirname, ...segments);\n}\n\n/**\n * Locate the font used for rendering social media card text.\n * Tries multiple candidate paths to support both source and built distributions.\n * @returns Font path if found, null otherwise\n */\nfunction findSocialCardFontPath(): string | null {\n const fontCandidates = [\n resolveFromCurrentDir('../../../../', SOCIAL_CARD_FONT_RELATIVE_PATH),\n path.resolve(__dirname, '../', SOCIAL_CARD_FONT_RELATIVE_PATH),\n path.resolve(__dirname, '../../', SOCIAL_CARD_FONT_RELATIVE_PATH),\n path.resolve(process.cwd(), SOCIAL_CARD_FONT_RELATIVE_PATH),\n path.resolve(process.cwd(), '../', SOCIAL_CARD_FONT_RELATIVE_PATH),\n path.resolve(process.cwd(), '../simple-photo-gallery/', SOCIAL_CARD_FONT_RELATIVE_PATH),\n ];\n\n for (const candidate of fontCandidates) {\n if (fs.existsSync(candidate)) {\n return candidate;\n }\n }\n\n return null;\n}\n\n/**\n * Loads the social media card font and returns its base64 representation.\n * The data is cached to avoid repeated disk access within the same process.\n * @returns Base64 font data if font file is found, null otherwise\n */\nfunction getSocialCardFontBase64(): string | null {\n if (socialCardFontBase64 !== undefined) {\n return socialCardFontBase64;\n }\n\n const fontPath = findSocialCardFontPath();\n if (!fontPath) {\n socialCardFontBase64 = null;\n return null;\n }\n\n socialCardFontBase64 = fs.readFileSync(fontPath).toString('base64');\n return socialCardFontBase64;\n}\n\n/**\n * Creates a social media card image for a gallery\n * @param headerPhotoPath - Path to the header photo\n * @param title - Title of the gallery\n * @param ouputPath - Output path for the social media card image\n * @param ui - ConsolaInstance for logging\n * @returns The basename of the header photo used\n */\nexport async function createGallerySocialMediaCardImage(\n headerPhotoPath: string,\n title: string,\n ouputPath: string,\n ui?: ConsolaInstance,\n): Promise<string> {\n ui?.start(`Creating social media card image`);\n\n const headerBasename = path.basename(headerPhotoPath, path.extname(headerPhotoPath));\n\n if (fs.existsSync(ouputPath)) {\n ui?.success(`Social media card image already exists`);\n return headerBasename;\n }\n\n // Read and resize the header image to 1200x631 using fit\n const image = await loadImage(headerPhotoPath);\n const resizedImageBuffer = await image.resize(1200, 631, { fit: 'cover' }).jpeg({ quality: 90 }).toBuffer();\n\n // Save the resized image as social media card\n const outputPath = ouputPath;\n await sharp(resizedImageBuffer).toFile(outputPath);\n\n // Create SVG with title and description\n const fontBase64 = getSocialCardFontBase64();\n const fontFace = fontBase64\n ? `\n @font-face {\n font-family: 'DejaVu Sans';\n src: url('data:font/ttf;base64,${fontBase64}') format('truetype');\n font-weight: 700;\n font-style: normal;\n }`\n : '';\n const fontFamily = fontBase64 ? \"'DejaVu Sans', Arial, sans-serif\" : 'Arial, sans-serif';\n const svgText = `\n <svg width=\"1200\" height=\"631\" xmlns=\"http://www.w3.org/2000/svg\">\n <defs>\n <style>\n ${fontFace}\n .title { font-family: ${fontFamily}; font-size: 96px; font-weight: bold; fill: white; stroke: black; stroke-width: 5; paint-order: stroke; text-anchor: middle; }\n </style>\n </defs>\n <text x=\"600\" y=\"250\" class=\"title\">${title}</text>\n </svg>\n `;\n\n // Composite the text overlay on top of the resized image\n const finalImageBuffer = await sharp(resizedImageBuffer)\n .composite([{ input: Buffer.from(svgText), top: 0, left: 0 }])\n .jpeg({ quality: 90 })\n .toBuffer();\n\n // Save the final image with text overlay\n await sharp(finalImageBuffer).toFile(outputPath);\n\n ui?.success(`Created social media card image successfully`);\n return headerBasename;\n}\n\n/**\n * Creates optimized header images for different orientations and sizes\n * @param headerPhotoPath - Path to the header photo\n * @param outputFolder - Folder where header images should be saved\n * @param ui - ConsolaInstance for logging\n * @returns Object containing the header basename, array of generated file paths, and blurhash\n */\nexport async function createOptimizedHeaderImage(\n headerPhotoPath: string,\n outputFolder: string,\n ui?: ConsolaInstance,\n): Promise<{ headerBasename: string; generatedFiles: string[]; blurHash: string }> {\n ui?.start(`Creating optimized header images`);\n\n const image = await loadImage(headerPhotoPath);\n const headerBasename = path.basename(headerPhotoPath, path.extname(headerPhotoPath));\n const generatedFiles: string[] = [];\n\n // Generate blurhash for the header image\n ui?.debug('Generating blurhash for header image');\n const blurHash = await generateBlurHash(headerPhotoPath);\n\n // Create landscape header images\n const landscapeYFactor = 3 / 4;\n for (const width of HEADER_IMAGE_LANDSCAPE_WIDTHS) {\n ui?.debug(`Creating landscape header image ${width}`);\n\n const avifFilename = `${headerBasename}_landscape_${width}.avif`;\n const jpgFilename = `${headerBasename}_landscape_${width}.jpg`;\n\n if (fs.existsSync(path.join(outputFolder, avifFilename))) {\n ui?.debug(`Landscape header image ${width} AVIF already exists`);\n } else {\n await cropAndResizeImage(\n image.clone(),\n path.join(outputFolder, avifFilename),\n width,\n width * landscapeYFactor,\n 'avif',\n );\n }\n generatedFiles.push(avifFilename);\n\n if (fs.existsSync(path.join(outputFolder, jpgFilename))) {\n ui?.debug(`Landscape header image ${width} JPG already exists`);\n } else {\n await cropAndResizeImage(image.clone(), path.join(outputFolder, jpgFilename), width, width * landscapeYFactor, 'jpg');\n }\n generatedFiles.push(jpgFilename);\n }\n\n // Create portrait header images\n const portraitYFactor = 4 / 3;\n for (const width of HEADER_IMAGE_PORTRAIT_WIDTHS) {\n ui?.debug(`Creating portrait header image ${width}`);\n\n const avifFilename = `${headerBasename}_portrait_${width}.avif`;\n const jpgFilename = `${headerBasename}_portrait_${width}.jpg`;\n\n if (fs.existsSync(path.join(outputFolder, avifFilename))) {\n ui?.debug(`Portrait header image ${width} AVIF already exists`);\n } else {\n await cropAndResizeImage(image.clone(), path.join(outputFolder, avifFilename), width, width * portraitYFactor, 'avif');\n }\n generatedFiles.push(avifFilename);\n\n if (fs.existsSync(path.join(outputFolder, jpgFilename))) {\n ui?.debug(`Portrait header image ${width} JPG already exists`);\n } else {\n await cropAndResizeImage(image.clone(), path.join(outputFolder, jpgFilename), width, width * portraitYFactor, 'jpg');\n }\n generatedFiles.push(jpgFilename);\n }\n\n ui?.success(`Created optimized header image successfully`);\n return { headerBasename, generatedFiles, blurHash };\n}\n\n/**\n * Checks if there are old header images with a different basename than the current one\n * @param outputFolder - Folder containing the header images\n * @param currentHeaderBasename - Basename of the current header image\n * @returns True if old header images with different basename exist, false otherwise\n */\nexport function hasOldHeaderImages(outputFolder: string, currentHeaderBasename: string): boolean {\n if (!fs.existsSync(outputFolder)) {\n return false;\n }\n\n const files = fs.readdirSync(outputFolder);\n\n for (const file of files) {\n // Check if file is a header image (landscape or portrait) with different basename\n const landscapeMatch = file.match(/^(.+)_landscape_\\d+\\.(avif|jpg)$/);\n const portraitMatch = file.match(/^(.+)_portrait_\\d+\\.(avif|jpg)$/);\n\n if (\n (landscapeMatch && landscapeMatch[1] !== currentHeaderBasename) ||\n (portraitMatch && portraitMatch[1] !== currentHeaderBasename)\n ) {\n return true;\n }\n }\n\n return false;\n}\n\n/**\n * Cleans up old header images that don't match the current header image\n * @param outputFolder - Folder containing the header images\n * @param currentHeaderBasename - Basename of the current header image\n * @param ui - ConsolaInstance for logging\n */\nexport function cleanupOldHeaderImages(outputFolder: string, currentHeaderBasename: string, ui?: ConsolaInstance): void {\n ui?.start(`Cleaning up old header images`);\n\n if (!fs.existsSync(outputFolder)) {\n ui?.debug(`Output folder ${outputFolder} does not exist, skipping cleanup`);\n return;\n }\n\n const files = fs.readdirSync(outputFolder);\n let deletedCount = 0;\n\n for (const file of files) {\n // Check if file is a header image (landscape or portrait) with different basename\n const landscapeMatch = file.match(/^(.+)_landscape_\\d+\\.(avif|jpg)$/);\n const portraitMatch = file.match(/^(.+)_portrait_\\d+\\.(avif|jpg)$/);\n\n if (landscapeMatch && landscapeMatch[1] !== currentHeaderBasename) {\n const filePath = path.join(outputFolder, file);\n ui?.debug(`Deleting old landscape header image: ${file}`);\n fs.unlinkSync(filePath);\n deletedCount++;\n } else if (portraitMatch && portraitMatch[1] !== currentHeaderBasename) {\n const filePath = path.join(outputFolder, file);\n ui?.debug(`Deleting old portrait header image: ${file}`);\n fs.unlinkSync(filePath);\n deletedCount++;\n }\n }\n\n if (deletedCount > 0) {\n ui?.success(`Deleted ${deletedCount} old header image(s)`);\n } else {\n ui?.debug(`No old header images to clean up`);\n }\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "simple-photo-gallery",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.13",
|
|
4
4
|
"description": "Simple Photo Gallery CLI",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Vladimir Haltakov, Tomasz Rusin",
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
},
|
|
50
50
|
"dependencies": {
|
|
51
51
|
"@simple-photo-gallery/common": "1.0.5",
|
|
52
|
-
"@simple-photo-gallery/theme-modern": "2.0.
|
|
52
|
+
"@simple-photo-gallery/theme-modern": "2.0.13",
|
|
53
53
|
"axios": "^1.12.2",
|
|
54
54
|
"blurhash": "^2.0.5",
|
|
55
55
|
"commander": "^12.0.0",
|