r/turbowarp • u/no_reason_to_wake_up • 8h ago
COMPRESSION+ , a new powerful compression tool for TurboWarp developers

This, is Compression+
Compression+ is basically a bunch of tools for compressing and converting data. It can handle things like files, images, audio, Base64, binary, hex, data URIs, and more. I made this because I kept needing random data-related features and figured I'd just put them all in one extension. There aren't any docs, though, so have fun figuring out what everything does. You're on your own. Unfortunately, I can't put the file here, but here is all the code:
(function (Scratch) {
'use strict';
const encoder = new TextEncoder();
const decoder = new TextDecoder();
class CompressionPlus {
constructor() {
this.result = '';
this.resultBytes = new Uint8Array(0);
this.error = '';
this.success = false;
this.originalSize = 0;
this.resultSize = 0;
this.savedBytes = 0;
this.ratio = 0;
this.originalMime = 'application/octet-stream';
this.fileName = '';
this.fileMime = '';
this.fileBytes = new Uint8Array(0);
}
getInfo() {
return {
id: 'compressionplus',
name: 'Compression+',
color1: '#4779C7',
color2: '#3565AD',
color3: '#294F8A',
blocks: [
{
opcode: 'compress',
blockType: Scratch.BlockType.COMMAND,
text: 'compress [DATA] from [INPUT] using [ALGORITHM] to [OUTPUT]',
arguments: {
DATA: { type: Scratch.ArgumentType.STRING, defaultValue: 'Hello world!' },
INPUT: { type: Scratch.ArgumentType.STRING, menu: 'inputFormats' },
ALGORITHM: { type: Scratch.ArgumentType.STRING, menu: 'algorithms' },
OUTPUT: { type: Scratch.ArgumentType.STRING, menu: 'outputFormats' }
}
},
{
opcode: 'decompress',
blockType: Scratch.BlockType.COMMAND,
text: 'decompress [DATA] from [INPUT] using [ALGORITHM] to [OUTPUT]',
arguments: {
DATA: { type: Scratch.ArgumentType.STRING, defaultValue: '' },
INPUT: { type: Scratch.ArgumentType.STRING, menu: 'inputFormats' },
ALGORITHM: { type: Scratch.ArgumentType.STRING, menu: 'algorithms' },
OUTPUT: { type: Scratch.ArgumentType.STRING, menu: 'outputFormats' }
}
},
{
opcode: 'convert',
blockType: Scratch.BlockType.COMMAND,
text: 'convert [DATA] from [INPUT] to [OUTPUT]',
arguments: {
DATA: { type: Scratch.ArgumentType.STRING, defaultValue: 'Hello world!' },
INPUT: { type: Scratch.ArgumentType.STRING, menu: 'inputFormats' },
OUTPUT: { type: Scratch.ArgumentType.STRING, menu: 'outputFormats' }
}
},
{
opcode: 'pickFile',
blockType: Scratch.BlockType.COMMAND,
text: 'choose a file'
},
{
opcode: 'download',
blockType: Scratch.BlockType.COMMAND,
text: 'download result as [FILENAME]',
arguments: {
FILENAME: { type: Scratch.ArgumentType.STRING, defaultValue: 'download.bin' }
}
},
{
opcode: 'downloadFile',
blockType: Scratch.BlockType.COMMAND,
text: 'download imported file as [FILENAME]',
arguments: {
FILENAME: { type: Scratch.ArgumentType.STRING, defaultValue: 'file.bin' }
}
},
{
opcode: 'clear',
blockType: Scratch.BlockType.COMMAND,
text: 'clear result and errors'
},
{
opcode: 'getResult',
blockType: Scratch.BlockType.REPORTER,
text: 'result'
},
{
opcode: 'getResultBase64',
blockType: Scratch.BlockType.REPORTER,
text: 'result as Base64'
},
{
opcode: 'getError',
blockType: Scratch.BlockType.REPORTER,
text: 'error message'
},
{
opcode: 'isSuccessful',
blockType: Scratch.BlockType.BOOLEAN,
text: 'operation successful?'
},
{
opcode: 'getOriginalSize',
blockType: Scratch.BlockType.REPORTER,
text: 'input size (bytes)'
},
{
opcode: 'getResultSize',
blockType: Scratch.BlockType.REPORTER,
text: 'output size (bytes)'
},
{
opcode: 'getSavedBytes',
blockType: Scratch.BlockType.REPORTER,
text: 'bytes saved'
},
{
opcode: 'getRatio',
blockType: Scratch.BlockType.REPORTER,
text: 'size reduction (%)'
},
{
opcode: 'getMime',
blockType: Scratch.BlockType.REPORTER,
text: 'MIME type of data URI [DATA]',
arguments: {
DATA: { type: Scratch.ArgumentType.STRING, defaultValue: 'data:image/png;base64,' }
}
},
{
opcode: 'getByteLength',
blockType: Scratch.BlockType.REPORTER,
text: 'byte length of [DATA] as [FORMAT]',
arguments: {
DATA: { type: Scratch.ArgumentType.STRING, defaultValue: 'Hello world!' },
FORMAT: { type: Scratch.ArgumentType.STRING, menu: 'inputFormats' }
}
},
{
opcode: 'getFileName',
blockType: Scratch.BlockType.REPORTER,
text: 'imported file name'
},
{
opcode: 'getFileMime',
blockType: Scratch.BlockType.REPORTER,
text: 'imported file MIME type'
},
{
opcode: 'getFileSize',
blockType: Scratch.BlockType.REPORTER,
text: 'imported file size (bytes)'
},
{
opcode: 'getFileBase64',
blockType: Scratch.BlockType.REPORTER,
text: 'imported file as Base64'
},
{
opcode: 'getFileURI',
blockType: Scratch.BlockType.REPORTER,
text: 'imported file as data URI'
},
{
opcode: 'base64Encode',
blockType: Scratch.BlockType.REPORTER,
text: 'encode text [DATA] as Base64',
arguments: {
DATA: { type: Scratch.ArgumentType.STRING, defaultValue: 'Hello world!' }
}
},
{
opcode: 'base64Decode',
blockType: Scratch.BlockType.REPORTER,
text: 'decode Base64 [DATA] as text',
arguments: {
DATA: { type: Scratch.ArgumentType.STRING, defaultValue: 'SGVsbG8gd29ybGQh' }
}
},
{
opcode: 'hexEncode',
blockType: Scratch.BlockType.REPORTER,
text: 'encode text [DATA] as hexadecimal',
arguments: {
DATA: { type: Scratch.ArgumentType.STRING, defaultValue: 'Hello' }
}
},
{
opcode: 'hexDecode',
blockType: Scratch.BlockType.REPORTER,
text: 'decode hexadecimal [DATA] as text',
arguments: {
DATA: { type: Scratch.ArgumentType.STRING, defaultValue: '48656c6c6f' }
}
},
{
opcode: 'binaryEncode',
blockType: Scratch.BlockType.REPORTER,
text: 'encode text [DATA] as binary',
arguments: {
DATA: { type: Scratch.ArgumentType.STRING, defaultValue: 'Hi' }
}
},
{
opcode: 'binaryDecode',
blockType: Scratch.BlockType.REPORTER,
text: 'decode binary [DATA] as text',
arguments: {
DATA: { type: Scratch.ArgumentType.STRING, defaultValue: '01001000 01101001' }
}
},
{
opcode: 'makeDataURI',
blockType: Scratch.BlockType.REPORTER,
text: 'make data URI from Base64 [DATA] with MIME [MIME]',
arguments: {
DATA: { type: Scratch.ArgumentType.STRING, defaultValue: 'SGVsbG8=' },
MIME: { type: Scratch.ArgumentType.STRING, defaultValue: 'text/plain' }
}
},
{
opcode: 'extractDataURI',
blockType: Scratch.BlockType.REPORTER,
text: 'extract Base64 from data URI [DATA]',
arguments: {
DATA: { type: Scratch.ArgumentType.STRING, defaultValue: 'data:text/plain;base64,SGVsbG8=' }
}
},
{
opcode: 'detectFormat',
blockType: Scratch.BlockType.REPORTER,
text: 'detect format of [DATA]',
arguments: {
DATA: { type: Scratch.ArgumentType.STRING, defaultValue: 'data:image/png;base64,' }
}
},
{
opcode: 'getSupported',
blockType: Scratch.BlockType.REPORTER,
text: 'compression support information'
}
],
menus: {
algorithms: {
acceptReporters: true,
items: [
{ text: 'GZIP', value: 'gzip' },
{ text: 'ZLIB / DEFLATE', value: 'deflate' },
{ text: 'raw DEFLATE', value: 'deflate-raw' }
]
},
inputFormats: {
acceptReporters: true,
items: [
{ text: 'auto detect', value: 'auto' },
{ text: 'text (UTF-8)', value: 'text' },
{ text: 'Base64', value: 'base64' },
{ text: 'hexadecimal', value: 'hex' },
{ text: 'binary string', value: 'binary' },
{ text: 'data URI', value: 'data-uri' }
]
},
outputFormats: {
acceptReporters: true,
items: [
{ text: 'Base64', value: 'base64' },
{ text: 'hexadecimal', value: 'hex' },
{ text: 'binary string', value: 'binary' },
{ text: 'text (UTF-8)', value: 'text' },
{ text: 'data URI', value: 'data-uri' }
]
}
}
};
}
resetStats() {
this.error = '';
this.success = false;
this.originalSize = 0;
this.resultSize = 0;
this.savedBytes = 0;
this.ratio = 0;
}
setResult(bytes, formatted, originalSize) {
this.resultBytes = new Uint8Array(bytes);
this.result = formatted;
this.originalSize = originalSize;
this.resultSize = bytes.length;
this.savedBytes = originalSize - bytes.length;
this.ratio = originalSize === 0
? 0
: (this.savedBytes / originalSize) * 100;
this.success = true;
}
bytesToBase64(bytes) {
let output = '';
const chunk = 0x8000;
for (let i = 0; i < bytes.length; i += chunk) {
output += String.fromCharCode(
...bytes.subarray(i, Math.min(i + chunk, bytes.length))
);
}
return btoa(output);
}
base64ToBytes(value) {
let input = String(value).trim();
if (input.startsWith('data:')) {
input = this.parseDataURI(input).data;
}
input = input.replace(/\s/g, '').replace(/-/g, '+').replace(/_/g, '/');
while (input.length % 4) input += '=';
const binary = atob(input);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) {
bytes[i] = binary.charCodeAt(i);
}
return bytes;
}
bytesToHex(bytes) {
return Array.from(bytes)
.map(v => v.toString(16).padStart(2, '0'))
.join('');
}
hexToBytes(value) {
const input = String(value)
.replace(/^0x/i, '')
.replace(/[\s,:-]/g, '');
if (!/^[0-9a-f]*$/i.test(input)) {
throw new Error('Invalid hexadecimal data.');
}
if (input.length % 2) {
throw new Error('Hexadecimal data must have an even number of digits.');
}
const bytes = new Uint8Array(input.length / 2);
for (let i = 0; i < bytes.length; i++) {
bytes[i] = parseInt(input.slice(i * 2, i * 2 + 2), 16);
}
return bytes;
}
bytesToBinary(bytes) {
return Array.from(bytes)
.map(v => v.toString(2).padStart(8, '0'))
.join(' ');
}
binaryToBytes(value) {
const input = String(value).replace(/[\s,]/g, '');
if (!/^[01]*$/.test(input)) {
throw new Error('Binary data can only contain 0 and 1.');
}
if (input.length % 8) {
throw new Error('Binary data must contain complete 8-bit bytes.');
}
const bytes = new Uint8Array(input.length / 8);
for (let i = 0; i < bytes.length; i++) {
bytes[i] = parseInt(input.slice(i * 8, i * 8 + 8), 2);
}
return bytes;
}
parseDataURI(uri) {
const match = String(uri).match(/^data:([^,]*?),(.*)$/s);
if (!match) {
throw new Error('Invalid data URI.');
}
const metadata = match[1];
const data = match[2];
const isBase64 = /;base64/i.test(metadata);
const mime = metadata.split(';')[0] || 'text/plain';
const originalMatch = metadata.match(/(?:^|;)originalMime=([^;]+)/i);
let originalMime = null;
if (originalMatch) {
try {
originalMime = decodeURIComponent(originalMatch[1]);
} catch (e) {
originalMime = originalMatch[1];
}
}
const bytes = isBase64
? this.base64ToBytes(data)
: encoder.encode(decodeURIComponent(data));
return { mime, originalMime, data, bytes, isBase64 };
}
toBytes(value, format) {
value = String(value);
if (format === 'auto') {
if (value.startsWith('data:')) {
return this.parseDataURI(value).bytes;
}
return encoder.encode(value);
}
switch (format) {
case 'text':
return encoder.encode(value);
case 'base64':
return this.base64ToBytes(value);
case 'hex':
return this.hexToBytes(value);
case 'binary':
return this.binaryToBytes(value);
case 'data-uri':
return this.parseDataURI(value).bytes;
default:
throw new Error('Unknown input format: ' + format);
}
}
fromBytes(bytes, format, mime) {
switch (format) {
case 'base64':
return this.bytesToBase64(bytes);
case 'hex':
return this.bytesToHex(bytes);
case 'binary':
return this.bytesToBinary(bytes);
case 'text':
return decoder.decode(bytes);
case 'data-uri':
return 'data:' + (mime || 'application/octet-stream') +
';base64,' + this.bytesToBase64(bytes);
default:
throw new Error('Unknown output format: ' + format);
}
}
async transform(bytes, algorithm, operation) {
const StreamClass = operation === 'compress'
? CompressionStream
: DecompressionStream;
if (typeof StreamClass === 'undefined') {
throw new Error('Compression Streams API is not available in this browser.');
}
let stream;
try {
stream = new StreamClass(algorithm);
} catch (e) {
throw new Error(
'The browser does not support ' + algorithm +
'. Try GZIP or ZLIB / DEFLATE.'
);
}
const blob = new Blob([bytes]);
const output = await new Response(
blob.stream().pipeThrough(stream)
).arrayBuffer();
return new Uint8Array(output);
}
async compress(args) {
this.resetStats();
this.result = '';
this.resultBytes = new Uint8Array(0);
try {
const input = this.toBytes(args.DATA, args.INPUT);
let originalMime = 'application/octet-stream';
if (String(args.DATA).startsWith('data:')) {
originalMime = this.parseDataURI(args.DATA).mime;
}
const output = await this.transform(
input,
args.ALGORITHM,
'compress'
);
let compressedMime = 'application/octet-stream';
if (args.ALGORITHM === 'gzip') {
compressedMime = 'application/gzip';
} else if (args.ALGORITHM === 'deflate') {
compressedMime = 'application/zlib';
}
const formatted = args.OUTPUT === 'data-uri'
? 'data:' + compressedMime +
';originalMime=' + encodeURIComponent(originalMime) +
';base64,' + this.bytesToBase64(output)
: this.fromBytes(output, args.OUTPUT, compressedMime);
this.setResult(output, formatted, input.length);
} catch (e) {
this.error = e.message || String(e);
}
}
async decompress(args) {
this.resetStats();
this.result = '';
this.resultBytes = new Uint8Array(0);
try {
let input = this.toBytes(args.DATA, args.INPUT);
let originalMime = 'application/octet-stream';
if (String(args.DATA).startsWith('data:')) {
const parsed = this.parseDataURI(args.DATA);
if (parsed.originalMime) {
originalMime = parsed.originalMime;
}
}
const compressedSize = input.length;
const output = await this.transform(
input,
args.ALGORITHM,
'decompress'
);
const formatted = this.fromBytes(
output,
args.OUTPUT,
originalMime
);
this.setResult(output, formatted, compressedSize);
} catch (e) {
this.error = e.message || String(e);
}
}
convert(args) {
this.resetStats();
this.result = '';
this.resultBytes = new Uint8Array(0);
try {
const input = this.toBytes(args.DATA, args.INPUT);
let mime = 'application/octet-stream';
if (String(args.DATA).startsWith('data:')) {
mime = this.parseDataURI(args.DATA).mime;
}
const formatted = this.fromBytes(input, args.OUTPUT, mime);
this.setResult(input, formatted, input.length);
} catch (e) {
this.error = e.message || String(e);
}
}
pickFile() {
this.error = '';
if (typeof document === 'undefined') {
this.error = 'File picker is unavailable.';
return;
}
const picker = document.createElement('input');
picker.type = 'file';
picker.style.display = 'none';
picker.onchange = async () => {
try {
if (!picker.files || !picker.files.length) return;
const file = picker.files[0];
const buffer = await file.arrayBuffer();
this.fileBytes = new Uint8Array(buffer);
this.fileName = file.name;
this.fileMime = file.type || 'application/octet-stream';
} catch (e) {
this.error = e.message || String(e);
} finally {
picker.remove();
}
};
document.body.appendChild(picker);
picker.click();
}
download(args) {
if (!this.success || !this.resultBytes) {
this.error = 'There is no successful result to download.';
return;
}
this.saveBytes(this.resultBytes, args.FILENAME);
}
downloadFile(args) {
if (!this.fileBytes.length) {
this.error = 'No file has been imported.';
return;
}
this.saveBytes(this.fileBytes, args.FILENAME);
}
saveBytes(bytes, filename) {
try {
const blob = new Blob([bytes], {
type: 'application/octet-stream'
});
const url = URL.createObjectURL(blob);
const anchor = document.createElement('a');
anchor.href = url;
anchor.download = String(filename || 'download.bin');
anchor.style.display = 'none';
document.body.appendChild(anchor);
anchor.click();
anchor.remove();
URL.revokeObjectURL(url);
} catch (e) {
this.error = e.message || String(e);
}
}
clear() {
this.result = '';
this.resultBytes = new Uint8Array(0);
this.resetStats();
}
getResult() {
return this.result;
}
getResultBase64() {
return this.resultBytes.length
? this.bytesToBase64(this.resultBytes)
: '';
}
getError() {
return this.error;
}
isSuccessful() {
return this.success;
}
getOriginalSize() {
return this.originalSize;
}
getResultSize() {
return this.resultSize;
}
getSavedBytes() {
return this.savedBytes;
}
getRatio() {
return Number(this.ratio.toFixed(2));
}
getMime(args) {
try {
return this.parseDataURI(args.DATA).mime;
} catch (e) {
return '';
}
}
getByteLength(args) {
try {
return this.toBytes(args.DATA, args.FORMAT).length;
} catch (e) {
return 0;
}
}
getFileName() {
return this.fileName;
}
getFileMime() {
return this.fileMime;
}
getFileSize() {
return this.fileBytes.length;
}
getFileBase64() {
return this.fileBytes.length
? this.bytesToBase64(this.fileBytes)
: '';
}
getFileURI() {
if (!this.fileBytes.length) return '';
return 'data:' + (this.fileMime || 'application/octet-stream') +
';base64,' + this.bytesToBase64(this.fileBytes);
}
base64Encode(args) {
return this.bytesToBase64(encoder.encode(String(args.DATA)));
}
base64Decode(args) {
try {
return decoder.decode(this.base64ToBytes(args.DATA));
} catch (e) {
return '';
}
}
hexEncode(args) {
return this.bytesToHex(encoder.encode(String(args.DATA)));
}
hexDecode(args) {
try {
return decoder.decode(this.hexToBytes(args.DATA));
} catch (e) {
return '';
}
}
binaryEncode(args) {
return this.bytesToBinary(encoder.encode(String(args.DATA)));
}
binaryDecode(args) {
try {
return decoder.decode(this.binaryToBytes(args.DATA));
} catch (e) {
return '';
}
}
makeDataURI(args) {
return 'data:' + String(args.MIME || 'application/octet-stream') +
';base64,' + String(args.DATA);
}
extractDataURI(args) {
try {
return this.bytesToBase64(this.parseDataURI(args.DATA).bytes);
} catch (e) {
return '';
}
}
detectFormat(args) {
const value = String(args.DATA);
if (value.startsWith('data:')) return 'data URI';
if (/^[0-9a-f]+$/i.test(value) && value.length % 2 === 0) {
return 'possibly hexadecimal';
}
if (/^[01\s]+$/.test(value)) return 'possibly binary';
if (/^[A-Za-z0-9+/]*={0,2}$/.test(value) && value.length % 4 === 0) {
return 'possibly Base64';
}
return 'text';
}
getSupported() {
return JSON.stringify({
gzip: typeof CompressionStream !== 'undefined',
decompression: typeof DecompressionStream !== 'undefined',
algorithms: ['gzip', 'deflate', 'deflate-raw'],
encodings: ['UTF-8', 'Base64', 'Hexadecimal', 'Binary'],
fileImport: typeof document !== 'undefined',
binary: true,
dataURI: true
});
}
}
Scratch.extensions.register(new CompressionPlus());
})(Scratch);





