Skip to content

Instantly share code, notes, and snippets.

@bryanberger
Last active August 18, 2023 17:19
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save bryanberger/4c2f34c693fc295b8a4f1e1223de03c0 to your computer and use it in GitHub Desktop.
Save bryanberger/4c2f34c693fc295b8a4f1e1223de03c0 to your computer and use it in GitHub Desktop.
Aggressively Optimize Lottie JSON files
import { readdir, readFileSync, writeFileSync, stat } from 'fs';
import { join, basename } from 'path';
function processJSONFiles(inputFolderPath, outputFolderPath) {
readdir(inputFolderPath, (err, files) => {
if (err) {
console.error('Error reading folder:', err);
return;
}
files.forEach(file => {
if (file.endsWith('.json')) {
const inputFilePath = join(inputFolderPath, file);
const outputFilePath = join(outputFolderPath, file);
optimizeJSONFile(inputFilePath, outputFilePath);
}
});
});
}
function optimizeJSONFile(inputFilePath, outputFilePath) {
try {
// Read the JSON file
const content = readFileSync(inputFilePath, 'utf-8');
const jsonData = JSON.parse(content);
// Counters & References
let idCounter = 1;
const idReferences = {};
// Generates a trimmed ID based on an incremental counter
function generateTrimmedId() {
const letter = String.fromCharCode(97 + (idCounter - 1) % 26); // 'a' to 'z'
const trimmedId = `${letter}${Math.floor((idCounter - 1) / 26)}`;
idCounter++;
return trimmedId;
}
// Processes numerical values, rounding them to a lesser precision
function processNumbers(value) {
if (typeof value === 'number' && !isNaN(value)) {
return parseFloat(value.toFixed(1));
}
return value;
}
// Processes a sub-item within an array, handling both numerical values and nested objects
function processArraySubItem(subItem) {
if (typeof subItem === 'number' && !isNaN(subItem)) {
return processNumbers(subItem);
} else if (typeof subItem === 'object' && subItem !== null) {
return optimizeValues(subItem);
} else {
return subItem;
}
}
// Processes an array, iterating through its items and handling nested arrays and objects
function processArray(item) {
if (Array.isArray(item)) {
return item.map(processArraySubItem);
} else if (typeof item === 'object' && item !== null) {
return optimizeValues(item);
} else {
return processNumbers(item);
}
}
// Process string values
function processValue(value) {
if (typeof value === 'string' && idReferences[value]) {
return idReferences[value];
} else if (typeof value === 'string' && /^#(?:[0-9a-fA-F]{3}){1,2}$/.test(value)) {
if (value.length === 7) {
const shorthand = `#${value[1]}${value[2]}${value[3]}`;
return shorthand.toLowerCase();
}
}
return value;
}
// Function to simplify and shorten variable and property names
function optimizeValues(obj) {
if (Array.isArray(obj)) {
return obj.map(processArray);
} else if (typeof obj === 'object' && obj !== null) {
const optimizedObj = {};
if (obj.meta) {
delete obj.meta;
}
for (const key in obj) {
if (typeof obj[key] === 'object' && obj[key] !== null) {
optimizedObj[key] = optimizeValues(obj[key]);
continue;
}
if (key === "ddd" || key === "ix" || key === "p") {
continue;
}
if (key === "v") {
optimizedObj[key] = obj[key];
continue;
}
if (key === "id") {
const generatedId = generateTrimmedId();
optimizedObj[key] = generatedId;
idReferences[obj[key]] = generatedId;
continue;
}
if (key === "nm" || key === "mn") {
optimizedObj[key] = generateTrimmedId();
continue;
}
if (typeof obj[key] === 'number' && !isNaN(obj[key])) {
optimizedObj[key] = processNumbers(obj[key])
continue;
}
if (typeof obj[key] === 'boolean') {
optimizedObj[key] = obj[key] ? 1 : 0;
continue;
}
optimizedObj[key] = processValue(obj[key]);
}
return optimizedObj;
} else {
return obj;
}
}
// Apply optimizations
const optimizedData = optimizeValues(jsonData);
// Convert optimized data to a minified JSON string
const optimizedJSON = JSON.stringify(optimizedData, null, 0);
// Write the optimized JSON back to the file, overwriting the original content
writeFileSync(outputFilePath, optimizedJSON, 'utf-8');
// Get file stats for the input and output files
stat(inputFilePath, (err, inputStats) => {
if (err) {
console.error('Error:', err);
return;
}
stat(outputFilePath, (err, outputStats) => {
if (err) {
console.error('Error:', err);
return;
}
// Get the filename from the inputFilePath
const filename = basename(outputFilePath);
// Calculate file size differences
const inputSizeKB = inputStats.size / 1024;
const outputSizeKB = outputStats.size / 1024;
const differenceKB = inputSizeKB - outputSizeKB;
const percentageDifference = (differenceKB / inputSizeKB) * 100;
console.log(`Optimization successful: ${filename}`);
console.log(`Input file size: ${inputSizeKB.toFixed(2)} KB`);
console.log(`Output file size: ${outputSizeKB.toFixed(2)} KB`);
console.log(`Difference: ${differenceKB.toFixed(2)} KB (${percentageDifference.toFixed(2)}%)`);
});
});
} catch (error) {
console.error('Error optimizing JSON:', error);
}
}
// Call the function with the path to your input and output JSON folders
const inputFolderPath = 'input'; // Path to the input JSON folder
const outputFolderPath = 'output'; // Path to the output JSON folder
processJSONFiles(inputFolderPath, outputFolderPath);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment