Browse Source

proper GIF conversion with size limit (now async)

Mike L 4 years ago
parent
commit
1ae4a2376e
4 changed files with 142 additions and 70 deletions
  1. 66 30
      dist/gifski.js
  2. 29 19
      dist/webshot.js
  3. 31 9
      src/gifski.ts
  4. 16 12
      src/webshot.ts

+ 66 - 30
dist/gifski.js

@@ -1,41 +1,77 @@
 "use strict";
+var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
+    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
+    return new (P || (P = Promise))(function (resolve, reject) {
+        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
+        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
+        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
+        step((generator = generator.apply(thisArg, _arguments || [])).next());
+    });
+};
 Object.defineProperty(exports, "__esModule", { value: true });
 const child_process_1 = require("child_process");
 const fs_1 = require("fs");
 const temp = require("temp");
 const loggers_1 = require("./loggers");
 const logger = loggers_1.getLogger('gifski');
+const sizeLimit = 10 * Math.pow(2, 20);
+const roundToEven = (n) => Math.ceil(n / 2) * 2;
 function default_1(data, targetWidth) {
-    const outputFilePath = temp.path({ suffix: '.gif' });
-    // temp.track();
-    try {
-        const inputFile = temp.openSync();
-        fs_1.writeSync(inputFile.fd, Buffer.from(data));
-        fs_1.closeSync(inputFile.fd);
-        logger.info(`saved video file to ${inputFile.path}, starting gif conversion...`);
-        const args = [
-            inputFile.path,
-            '-o',
-            outputFilePath,
-            '--fps',
-            '12.5',
-            '--quiet',
-            '--quality',
-            '90',
-        ];
-        if (typeof (targetWidth) === 'number') {
-            args.push('--width', (Math.ceil(targetWidth / 2) * 2).toString());
+    return __awaiter(this, void 0, void 0, function* () {
+        const outputFilePath = temp.path({ suffix: '.gif' });
+        temp.track();
+        try {
+            const inputFile = temp.openSync();
+            fs_1.writeSync(inputFile.fd, Buffer.from(data));
+            fs_1.closeSync(inputFile.fd);
+            logger.info(`saved video file to ${inputFile.path}, starting gif conversion...`);
+            const args = [
+                inputFile.path,
+                '-o',
+                outputFilePath,
+                '--fps',
+                '12.5',
+                '--quiet',
+                '--quality',
+                '90',
+            ];
+            if (typeof (targetWidth) === 'number') {
+                args.push('--width', roundToEven(targetWidth).toString());
+            }
+            logger.info(` gifski ${args.join(' ')}`);
+            const gifskiSpawn = child_process_1.spawn('gifski', args);
+            const gifskiResult = new Promise((resolve, reject) => {
+                const sizeChecker = setInterval(() => {
+                    if (fs_1.existsSync(outputFilePath) && fs_1.statSync(outputFilePath).size > sizeLimit)
+                        gifskiSpawn.kill();
+                }, 5000);
+                gifskiSpawn.on('exit', () => {
+                    clearInterval(sizeChecker);
+                    if (!fs_1.existsSync(outputFilePath))
+                        reject('no file was created on exit');
+                    logger.info(`gif conversion succeeded, file path: ${outputFilePath}`);
+                    resolve(fs_1.readFileSync(outputFilePath).buffer);
+                });
+            });
+            const stderr = [];
+            gifskiSpawn.stderr.on('data', errdata => {
+                if (!gifskiSpawn.killed)
+                    gifskiSpawn.kill();
+                stderr.concat(errdata);
+            });
+            gifskiSpawn.stderr.on('end', () => {
+                if (stderr.length !== 0)
+                    throw Error(Buffer.concat(stderr).toString());
+            });
+            return yield gifskiResult;
         }
-        logger.info(` gifski ${args.join(' ')}`);
-        const gifskiInvocation = child_process_1.spawnSync('gifski', args, { encoding: 'utf8', timeout: 90000 });
-        if (gifskiInvocation.stderr)
-            throw Error(gifskiInvocation.stderr);
-        logger.info(`gif conversion succeeded, file path: ${outputFilePath}`);
-        return fs_1.readFileSync(outputFilePath).buffer;
-    }
-    catch (error) {
-        logger.error('error converting video to gif' + error ? `message: ${error}` : '');
-        throw Error('error converting video to gif');
-    }
+        catch (error) {
+            logger.error('error converting video to gif' + error ? `message: ${error}` : '');
+            throw Error('error converting video to gif');
+        }
+        finally {
+            temp.cleanup();
+        }
+    });
 }
 exports.default = default_1;

+ 29 - 19
dist/webshot.js

@@ -1,4 +1,13 @@
 "use strict";
+var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
+    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
+    return new (P || (P = Promise))(function (resolve, reject) {
+        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
+        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
+        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
+        step((generator = generator.apply(thisArg, _arguments || [])).next());
+    });
+};
 Object.defineProperty(exports, "__esModule", { value: true });
 const axios_1 = require("axios");
 const CallableInstance = require("callable-instance");
@@ -161,7 +170,7 @@ class Webshot extends CallableInstance {
             }).catch(error => new Promise(resolve => this.reconnect(error, resolve))
                 .then(() => this.renderWebshot(url, height, webshotDelay)));
         };
-        this.fetchMedia = (url) => new Promise(resolve => {
+        this.fetchMedia = (url) => new Promise((resolve, reject) => {
             logger.info(`fetching ${url}`);
             axios_1.default({
                 method: 'get',
@@ -174,28 +183,29 @@ class Webshot extends CallableInstance {
                 }
                 else {
                     logger.error(`failed to fetch ${url}: ${res.status}`);
-                    resolve();
+                    reject();
                 }
             }).catch(err => {
                 logger.error(`failed to fetch ${url}: ${err.message}`);
-                resolve();
+                reject();
             });
-        }).then(data => {
-            const mimetype = (ext => {
-                switch (ext) {
-                    case 'jpg':
-                        return 'image/jpeg';
-                    case 'png':
-                        return 'image/png';
-                    case 'mp4':
-                        const dims = url.match(/\/(\d+)x(\d+)\//).slice(1).map(Number);
-                        const factor = dims.some(x => x >= 960) ? 0.375 : 0.5;
-                        data = gifski_1.default(data, dims[0] * factor);
-                        return 'image/gif';
-                }
-            })(url.split('/').slice(-1)[0].match(/\.([^:?&]+)/)[1]);
-            return `data:${mimetype};base64,${Buffer.from(data).toString('base64')}`;
-        });
+        }).then(data => ((ext) => __awaiter(this, void 0, void 0, function* () {
+            switch (ext) {
+                case 'jpg':
+                    return { mimetype: 'image/jpeg', data };
+                case 'png':
+                    return { mimetype: 'image/png', data };
+                case 'mp4':
+                    const dims = url.match(/\/(\d+)x(\d+)\//).slice(1).map(Number);
+                    const factor = dims.some(x => x >= 960) ? 0.375 : 0.5;
+                    try {
+                        return { mimetype: 'image/gif', data: yield gifski_1.default(data, dims[0] * factor) };
+                    }
+                    catch (err) {
+                        throw Error(err);
+                    }
+            }
+        }))(url.split('/').slice(-1)[0].match(/\.([^:?&]+)/)[1])).then(typedData => `data:${typedData.mimetype};base64,${Buffer.from(typedData.data).toString('base64')}`);
         // tslint:disable-next-line: no-conditional-assignment
         if (this.mode = mode) {
             onready();

+ 31 - 9
src/gifski.ts

@@ -1,14 +1,17 @@
-import { spawnSync } from 'child_process';
-import { closeSync, readFileSync, writeSync } from 'fs';
+import { spawn } from 'child_process';
+import { closeSync, existsSync, readFileSync, statSync, writeSync } from 'fs';
 import * as temp from 'temp';
 
 import { getLogger } from './loggers';
 
 const logger = getLogger('gifski');
 
-export default function (data: ArrayBuffer, targetWidth?: number) {
+const sizeLimit = 10 * 2 ** 20;
+const roundToEven = (n: number) => Math.ceil(n / 2) * 2;
+
+export default async function (data: ArrayBuffer, targetWidth?: number) {
     const outputFilePath = temp.path({suffix: '.gif'});
-    // temp.track();
+    temp.track();
     try {
       const inputFile = temp.openSync();
       writeSync(inputFile.fd, Buffer.from(data));
@@ -25,15 +28,34 @@ export default function (data: ArrayBuffer, targetWidth?: number) {
         '90',
       ];
       if (typeof(targetWidth) === 'number') {
-        args.push('--width', (Math.ceil(targetWidth / 2) * 2).toString());
+        args.push('--width', roundToEven(targetWidth).toString());
       }
       logger.info(` gifski ${args.join(' ')}`);
-      const gifskiInvocation = spawnSync('gifski', args, {encoding: 'utf8', timeout: 90000});
-      if (gifskiInvocation.stderr) throw Error(gifskiInvocation.stderr);
-      logger.info(`gif conversion succeeded, file path: ${outputFilePath}`);
-      return readFileSync(outputFilePath).buffer;
+      const gifskiSpawn = spawn('gifski', args);
+      const gifskiResult = new Promise<ArrayBufferLike>((resolve, reject) => {
+        const sizeChecker = setInterval(() => {
+          if (existsSync(outputFilePath) && statSync(outputFilePath).size > sizeLimit) gifskiSpawn.kill();
+        }, 5000);
+        gifskiSpawn.on('exit', () => {
+          clearInterval(sizeChecker);
+          if (!existsSync(outputFilePath)) reject('no file was created on exit');
+          logger.info(`gif conversion succeeded, file path: ${outputFilePath}`);
+          resolve(readFileSync(outputFilePath).buffer);
+        });
+      });
+      const stderr = [];
+      gifskiSpawn.stderr.on('data', errdata => {
+        if (!gifskiSpawn.killed) gifskiSpawn.kill();
+        stderr.concat(errdata);
+      });
+      gifskiSpawn.stderr.on('end', () => {
+        if (stderr.length !== 0) throw Error(Buffer.concat(stderr).toString());
+      });
+      return await gifskiResult;
     } catch (error) {
       logger.error('error converting video to gif' + error ? `message: ${error}` : '');
       throw Error('error converting video to gif');
+    } finally {
+      temp.cleanup();
     }
 }

+ 16 - 12
src/webshot.ts

@@ -184,7 +184,7 @@ extends CallableInstance<
   }
 
   private fetchMedia = (url: string): Promise<string> =>
-    new Promise<ArrayBuffer>(resolve => {
+    new Promise<ArrayBuffer>((resolve, reject) => {
       logger.info(`fetching ${url}`);
       axios({
         method: 'get',
@@ -196,28 +196,32 @@ extends CallableInstance<
             resolve(res.data);
         } else {
           logger.error(`failed to fetch ${url}: ${res.status}`);
-          resolve();
+          reject();
         }
       }).catch (err => {
         logger.error(`failed to fetch ${url}: ${err.message}`);
-        resolve();
+        reject();
       });
-    }).then(data => {
-      const mimetype = (ext => {
+    }).then(data =>
+      (async ext => {
         switch (ext) {
           case 'jpg':
-            return 'image/jpeg';
+            return {mimetype: 'image/jpeg', data};
           case 'png':
-            return 'image/png';
+            return {mimetype: 'image/png', data};
           case 'mp4':
             const dims: number[] = url.match(/\/(\d+)x(\d+)\//).slice(1).map(Number);
             const factor = dims.some(x => x >= 960) ? 0.375 : 0.5;
-            data = gifski(data, dims[0] * factor);
-            return 'image/gif';
+            try {
+              return {mimetype: 'image/gif', data: await gifski(data, dims[0] * factor)};
+            } catch (err) {
+              throw Error(err);
+            }
         }
-      })(url.split('/').slice(-1)[0].match(/\.([^:?&]+)/)[1]);
-      return `data:${mimetype};base64,${Buffer.from(data).toString('base64')}`;
-    })
+      })(url.split('/').slice(-1)[0].match(/\.([^:?&]+)/)[1])
+    ).then(typedData => 
+      `data:${typedData.mimetype};base64,${Buffer.from(typedData.data).toString('base64')}`
+    )
 
   public webshot(
     tweets: Tweets,