function splitOnSpaceHandleQuotesWithEscapes( str, splits = ' \t\n\r' ) { const strings = []; let quoteType; let escape; let s = []; for ( let i = 0; i < str.length; ++ i ) { const c = str[ i ]; if ( escape ) { escape = false; s.push( c ); } else { if ( quoteType ) { // we're inside quotes if ( c === quoteType ) { quoteType = undefined; strings.push( s.join( '' ) ); s = []; } else if ( c === '\\' ) { escape = true; } else { s.push( c ); } } else { // we're not in quotes if ( splits.indexOf( c ) >= 0 ) { if ( s.length ) { strings.push( s.join( '' ) ); s = []; } } else if ( c === '"' || c === '\'' ) { if ( s.length ) { // its in th middle of a word s.push( c ); } else { quoteType = c; } } else { s.push( c ); } } } } if ( s.length || strings.length === 0 ) { strings.push( s.join( '' ) ); } return strings; } const startWhitespaceRE = /^\s/; const intRE = /^\d+$/; const isNum = s => intRE.test( s ); const quotesRE = /^".*"$/; function trimQuotes( s ) { return quotesRE.test( s ) ? s.slice( 1, - 1 ) : s; } const splitToNumbers = s => s.split( ' ' ).map( parseFloat ); export function parseCSP( str ) { const data = []; const lut = { name: 'unknown', type: '1D', size: 0, data, min: [ 0, 0, 0 ], max: [ 1, 1, 1 ], }; const lines = str.split( '\n' ).map( s => s.trim() ).filter( s => s.length > 0 && ! startWhitespaceRE.test( s ) ); // check header lut.type = lines[ 1 ]; if ( lines[ 0 ] !== 'CSPLUTV100' || ( lut.type !== '1D' && lut.type !== '3D' ) ) { throw new Error( 'not CSP' ); } // skip meta (read to first number) let lineNdx = 2; for ( ; lineNdx < lines.length; ++ lineNdx ) { const line = lines[ lineNdx ]; if ( isNum( line ) ) { break; } if ( line.startsWith( 'TITLE ' ) ) { lut.name = trimQuotes( line.slice( 6 ).trim() ); } } // read ranges for ( let i = 0; i < 3; ++ i ) { ++ lineNdx; const input = splitToNumbers( lines[ lineNdx ++ ] ); const output = splitToNumbers( lines[ lineNdx ++ ] ); if ( input.length !== 2 || output.length !== 2 || input[ 0 ] !== 0 || input[ 1 ] !== 1 || output[ 0 ] !== 0 || output[ 1 ] !== 1 ) { throw new Error( 'mapped ranges not support' ); } } // read sizes const sizes = splitToNumbers( lines[ lineNdx ++ ] ); if ( sizes[ 0 ] !== sizes[ 1 ] || sizes[ 0 ] !== sizes[ 2 ] ) { throw new Error( 'only cubic sizes supported' ); } lut.size = sizes[ 0 ]; // read data for ( ; lineNdx < lines.length; ++ lineNdx ) { const parts = splitToNumbers( lines[ lineNdx ] ); if ( parts.length !== 3 ) { throw new Error( 'malformed file' ); } data.push( ...parts ); } return lut; } export function parseCUBE( str ) { const data = []; const lut = { name: 'unknown', type: '1D', size: 0, data, min: [ 0, 0, 0 ], max: [ 1, 1, 1 ], }; const lines = str.split( '\n' ); for ( const origLine of lines ) { const hashNdx = origLine.indexOf( '#' ); const line = hashNdx >= 0 ? origLine.substring( 0, hashNdx ) : origLine; const parts = splitOnSpaceHandleQuotesWithEscapes( line ); switch ( parts[ 0 ].toUpperCase() ) { case 'TITLE': lut.name = parts[ 1 ]; break; case 'LUT_1D_SIZE': lut.size = parseInt( parts[ 1 ] ); lut.type = '1D'; break; case 'LUT_3D_SIZE': lut.size = parseInt( parts[ 1 ] ); lut.type = '3D'; break; case 'DOMAIN_MIN': lut.min = parts.slice( 1 ).map( parseFloat ); break; case 'DOMAIN_MAX': lut.max = parts.slice( 1 ).map( parseFloat ); break; default: if ( parts.length === 3 ) { data.push( ...parts.map( parseFloat ) ); } break; } } if ( ! lut.size ) { lut.size = lut.type === '1D' ? ( data.length / 3 ) : Math.cbrt( data.length / 3 ); } return lut; } function lerp( a, b, t ) { return a + ( b - a ) * t; } function lut1Dto3D( lut ) { let src = lut.data; if ( src.length / 3 !== lut.size ) { src = []; for ( let i = 0; i < lut.size; ++ i ) { const u = i / lut.size * lut.data.length; const i0 = ( u | 0 ) * 3; const i1 = i0 + 3; const t = u % 1; src.push( lerp( lut.data[ i0 + 0 ], lut.data[ i1 + 0 ], t ), lerp( lut.data[ i0 + 0 ], lut.data[ i1 + 1 ], t ), lerp( lut.data[ i0 + 0 ], lut.data[ i1 + 2 ], t ), ); } } const data = []; for ( let i = 0; i < lut.size * lut.size; ++ i ) { data.push( ...src ); } return { ...lut, data }; } const parsers = { 'cube': parseCUBE, 'csp': parseCSP, }; // for backward compatibility export function parse( str, format = 'cube' ) { const parser = parsers[ format.toLowerCase() ]; if ( ! parser ) { throw new Error( `no parser for format: ${format}` ); } return parser( str ); } export function lutTo2D3Drgba8( lut ) { if ( lut.type === '1D' ) { lut = lut1Dto3D( lut ); } const { min, max, size } = lut; const range = min.map( ( min, ndx ) => { return max[ ndx ] - min; } ); const src = lut.data; const data = new Uint8Array( size * size * size * 4 ); const srcOffset = ( offX, offY, offZ ) => { return ( offX + offY * size + offZ * size * size ) * 3; }; const dOffset = ( offX, offY, offZ ) => { return ( offX + offY * size + offZ * size * size ) * 4; }; for ( let dz = 0; dz < size; ++ dz ) { for ( let dy = 0; dy < size; ++ dy ) { for ( let dx = 0; dx < size; ++ dx ) { const sx = dx; const sy = dz; const sz = dy; const sOff = srcOffset( sx, sy, sz ); const dOff = dOffset( dx, dy, dz ); data[ dOff + 0 ] = ( src[ sOff + 0 ] - min[ 0 ] ) / range[ 0 ] * 255; data[ dOff + 1 ] = ( src[ sOff + 1 ] - min[ 1 ] ) / range[ 1 ] * 255; data[ dOff + 2 ] = ( src[ sOff + 2 ] - min[ 2 ] ) / range[ 2 ] * 255; data[ dOff + 3 ] = 255; } } } return { ...lut, data }; }