mcard-js
Version:
A JavaScript implementation of MCard - A data model for persistently storing content with cryptographic hashing and timestamping
251 lines (214 loc) • 8.2 kB
JavaScript
/**
* Content processing utilities for handling different content types
* Standardizes how we process and display binary and text content
*/
/**
* Process content based on its structure and content type
* @param {any} content - Raw content from API (Buffer JSON, string, etc.)
* @param {Object} contentType - Content type metadata with mimeType, extension
* @returns {Object} Processed content with type information
*/
export function processContent(content, contentType) {
// Handle null/undefined content
if (content === null || content === undefined) {
return { type: 'empty', data: null };
}
// Case 1: Buffer JSON format (most common for binary data)
if (content && typeof content === 'object' && content.type === 'Buffer' && Array.isArray(content.data)) {
return processBufferContent(content, contentType);
}
// Case 2: String content (possibly JSON)
if (typeof content === 'string') {
// Check if it's a Buffer JSON serialized as string
if (content.startsWith('{') && (content.includes('"type":"Buffer"') || content.includes('"type": "Buffer"'))) {
try {
const parsed = JSON.parse(content);
if (parsed.type === 'Buffer' && Array.isArray(parsed.data)) {
return processBufferContent(parsed, contentType);
}
} catch (e) {
// Not valid JSON, continue with string processing
console.error('Failed to parse potential Buffer JSON:', e);
}
}
// Check if content is a data URL
if (content.startsWith('data:')) {
return {
type: 'dataUrl',
url: content
};
}
// Check if it's base64 encoded (for images, etc.)
if (/^[A-Za-z0-9+/=]+$/.test(content) && content.length > 0) {
if (contentType.mimeType?.startsWith('image/')) {
return {
type: 'dataUrl',
url: `data:${contentType.mimeType};base64,${content}`
};
}
}
// Regular string content
return { type: 'text', data: content };
}
// Case 3: Already processed content with type
if (content && typeof content === 'object' && content.type && (content.data || content.url)) {
return content; // Already in our format
}
// Default: return as unknown type
return { type: 'unknown', data: content };
}
/**
* Process a Buffer JSON object into appropriate format based on content type
* @param {Object} buffer - Buffer JSON object {type: 'Buffer', data: [...]}
* @param {Object} contentType - Content type metadata with mimeType
* @returns {Object} Processed content in appropriate format
*/
export function processBufferContent(buffer, contentType) {
if (!buffer || !Array.isArray(buffer.data)) {
throw new Error('Invalid buffer object');
}
const mimeType = contentType?.mimeType || detectMimeTypeFromBuffer(buffer.data);
// For image content types, create a data URL
if (mimeType?.startsWith('image/')) {
return createDataUrlFromBuffer(buffer.data, mimeType);
}
// For audio/video content
if (mimeType?.startsWith('audio/') || mimeType?.startsWith('video/')) {
return createDataUrlFromBuffer(buffer.data, mimeType);
}
// For text-based content types
if (mimeType?.startsWith('text/') ||
mimeType === 'application/json' ||
mimeType === 'application/xml' ||
mimeType === 'application/javascript') {
return createTextFromBuffer(buffer.data);
}
// For other binary formats, return as binary data
return {
type: 'binary',
data: new Uint8Array(buffer.data),
mimeType: mimeType
};
}
/**
* Create a data URL from buffer data
* @param {Array|Uint8Array} bufferData - Array of byte values
* @param {string} mimeType - MIME type for the data URL
* @returns {Object} Object with dataUrl type and url
*/
export function createDataUrlFromBuffer(bufferData, mimeType) {
try {
const bytes = Array.isArray(bufferData) ? bufferData : Array.from(bufferData);
const binary = bytes.map(byte => String.fromCharCode(byte)).join('');
const base64 = window.btoa(binary);
return {
type: 'dataUrl',
url: `data:${mimeType};base64,${base64}`
};
} catch (error) {
console.error('Failed to create data URL from buffer:', error);
return { type: 'error', error: 'Failed to create data URL' };
}
}
/**
* Create text content from buffer data
* @param {Array|Uint8Array} bufferData - Array of byte values
* @returns {Object} Object with text type and data
*/
export function createTextFromBuffer(bufferData) {
try {
// Use TextDecoder for proper UTF-8 handling
const textDecoder = new TextDecoder('utf-8');
const data = textDecoder.decode(new Uint8Array(bufferData));
return { type: 'text', data };
} catch (error) {
console.error('Failed to decode text from buffer:', error);
return { type: 'error', error: 'Failed to decode text content' };
}
}
/**
* Attempt to detect MIME type from buffer content
* Uses binary signatures/magic numbers
* @param {Array|Uint8Array} bufferData - Buffer data to analyze
* @returns {string|null} Detected MIME type or null if unknown
*/
export function detectMimeTypeFromBuffer(bufferData) {
if (!bufferData || bufferData.length < 4) {
return null;
}
const data = Array.isArray(bufferData) ? bufferData : Array.from(bufferData);
// Check for PNG signature: 89 50 4E 47
if (data[0] === 137 && data[1] === 80 && data[2] === 78 && data[3] === 71) {
return 'image/png';
}
// Check for JPEG signature: FF D8 FF
if (data[0] === 255 && data[1] === 216 && data[2] === 255) {
return 'image/jpeg';
}
// Check for GIF signature: 'GIF87a' or 'GIF89a'
if (data[0] === 71 && data[1] === 73 && data[2] === 70 &&
data[3] === 56 && (data[4] === 55 || data[4] === 57) && data[5] === 97) {
return 'image/gif';
}
// Check for WEBP signature: RIFF + filesize + WEBP
if (data[0] === 82 && data[1] === 73 && data[2] === 70 && data[3] === 70 &&
data[8] === 87 && data[9] === 69 && data[10] === 66 && data[11] === 80) {
return 'image/webp';
}
// Check for SVG (text-based format)
const potentialText = createTextFromBuffer(data.slice(0, Math.min(30, data.length)));
if (potentialText.type === 'text' &&
(potentialText.data.startsWith('<?xml') || potentialText.data.startsWith('<svg'))) {
return 'image/svg+xml';
}
// Check for PDF signature: %PDF
if (data[0] === 37 && data[1] === 80 && data[2] === 68 && data[3] === 70) {
return 'application/pdf';
}
// Check for ZIP signature: PK
if (data[0] === 80 && data[1] === 75) {
return 'application/zip';
}
// Check for WAVE audio: 'RIFF' + filesize + 'WAVE'
if (data[0] === 82 && data[1] === 73 && data[2] === 70 && data[3] === 70 &&
data[8] === 87 && data[9] === 65 && data[10] === 86 && data[11] === 69) {
return 'audio/wav';
}
// MP3 signature check
if ((data[0] === 73 && data[1] === 68 && data[2] === 51) || // ID3v2
(data[0] === 255 && (data[1] & 0xE0) === 0xE0)) { // MPEG sync
return 'audio/mpeg';
}
// Default to octet-stream for unknown binary
return 'application/octet-stream';
}
/**
* Helper to determine if content is of type image
* @param {Object} contentType - Content type with mimeType
* @returns {boolean} True if content is an image
*/
export function isImageType(contentType) {
return contentType?.mimeType?.startsWith('image/') || false;
}
/**
* Get a formatted content type display string
* @param {Object} contentType - Content type object
* @returns {string} Formatted content type for display
*/
export function getContentTypeDisplay(contentType) {
if (!contentType) return 'Unknown';
const mimeType = contentType.mimeType || 'unknown';
const extension = contentType.extension || '';
// Get type name from extension or mime type
let typeName = extension.toUpperCase();
if (!typeName && mimeType) {
// Extract type from MIME
const mimeParts = mimeType.split('/');
if (mimeParts.length > 1) {
typeName = mimeParts[1].toUpperCase();
} else {
typeName = mimeParts[0].toUpperCase();
}
}
return `${typeName} (${mimeType})`;
}