Skip to content

Instantly share code, notes, and snippets.

@lardratboy
Last active September 13, 2025 22:57
Show Gist options
  • Select an option

  • Save lardratboy/a07a64b2243cc492a8cb21bc23aa76b0 to your computer and use it in GitHub Desktop.

Select an option

Save lardratboy/a07a64b2243cc492a8cb21bc23aa76b0 to your computer and use it in GitHub Desktop.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>GGUF Information Dump</title>
<style>
* {
box-sizing: border-box;
}
body {
font-family: 'Courier New', monospace;
margin: 0;
padding: 20px;
background: #1a1a1a;
color: #e0e0e0;
line-height: 1.4;
}
.container {
max-width: 1400px;
margin: 0 auto;
}
header {
text-align: center;
margin-bottom: 30px;
padding: 20px;
background: #2d2d2d;
border-radius: 8px;
border: 2px solid #444;
}
h1 {
color: #4CAF50;
margin: 0;
font-size: 2em;
}
.file-input-section {
margin-bottom: 30px;
padding: 20px;
background: #2d2d2d;
border-radius: 8px;
border: 2px solid #444;
}
.file-input {
width: 100%;
padding: 10px;
background: #1a1a1a;
color: #e0e0e0;
border: 2px solid #666;
border-radius: 4px;
font-family: inherit;
}
.controls {
display: flex;
gap: 20px;
align-items: center;
margin: 20px 0;
flex-wrap: wrap;
}
.control-group {
display: flex;
align-items: center;
gap: 10px;
}
label {
color: #4CAF50;
font-weight: bold;
}
input[type="number"], select {
padding: 5px;
background: #1a1a1a;
color: #e0e0e0;
border: 1px solid #666;
border-radius: 4px;
font-family: inherit;
}
.info-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
margin-bottom: 30px;
}
.info-panel {
background: #2d2d2d;
padding: 20px;
border-radius: 8px;
border: 2px solid #444;
}
.info-panel h3 {
color: #4CAF50;
margin-top: 0;
border-bottom: 1px solid #666;
padding-bottom: 10px;
}
.tensor-list {
max-height: 300px;
overflow-y: auto;
background: #1a1a1a;
padding: 10px;
border-radius: 4px;
border: 1px solid #666;
}
.tensor-item {
padding: 8px;
margin: 4px 0;
background: #333;
border-radius: 4px;
cursor: pointer;
transition: background 0.2s;
}
.tensor-item:hover {
background: #444;
}
.tensor-item.selected {
background: #4CAF50;
color: #000;
}
.tensor-name {
font-weight: bold;
color: #4CAF50;
}
.tensor-details {
font-size: 0.9em;
color: #ccc;
margin-top: 4px;
}
.hexdump-container {
background: #2d2d2d;
padding: 20px;
border-radius: 8px;
border: 2px solid #444;
}
.hexdump {
background: #1a1a1a;
padding: 15px;
border-radius: 4px;
border: 1px solid #666;
font-family: 'Courier New', monospace;
font-size: 14px;
white-space: pre;
overflow-x: auto;
max-height: 500px;
overflow-y: auto;
}
.hex-offset {
color: #888;
}
.hex-bytes {
color: #4CAF50;
}
.hex-ascii {
color: #FFA500;
}
.status {
padding: 10px;
margin: 10px 0;
border-radius: 4px;
border-left: 4px solid #4CAF50;
background: #2d2d2d;
}
.error {
border-left-color: #f44336;
background: #3d2d2d;
color: #ffcccb;
}
.export-button, .rgba-button, .quantized-button {
background: #4CAF50;
color: #000;
border: none;
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
font-family: inherit;
font-weight: bold;
margin: 5px 5px 5px 0;
transition: background 0.2s;
}
.rgba-button {
background: #FF9800;
}
.quantized-button {
background: #9C27B0;
}
.export-button:hover {
background: #45a049;
}
.rgba-button:hover {
background: #F57C00;
}
.quantized-button:hover {
background: #7B1FA2;
}
.export-button:disabled, .rgba-button:disabled, .quantized-button:disabled {
background: #666;
color: #999;
cursor: not-allowed;
}
.metadata-grid {
display: grid;
grid-template-columns: 1fr 2fr;
gap: 10px;
font-size: 0.9em;
}
.metadata-key {
color: #4CAF50;
font-weight: bold;
}
.metadata-value {
color: #e0e0e0;
word-break: break-all;
}
.rgba-viewer {
background: #2d2d2d;
padding: 20px;
border-radius: 8px;
border: 2px solid #444;
margin-top: 20px;
display: none;
}
.rgba-viewer h3 {
color: #FF9800;
margin-top: 0;
border-bottom: 1px solid #666;
padding-bottom: 10px;
}
.quantized-viewer {
background: #2d2d2d;
padding: 20px;
border-radius: 8px;
border: 2px solid #444;
margin-top: 20px;
display: none;
}
.quantized-viewer h3 {
color: #9C27B0;
margin-top: 0;
border-bottom: 1px solid #666;
padding-bottom: 10px;
}
.rgba-canvas-container, .quantized-canvas-container {
text-align: center;
background: #1a1a1a;
padding: 20px;
border-radius: 4px;
border: 1px solid #666;
}
.rgba-canvas, .quantized-canvas {
max-width: 100%;
height: auto;
border: 2px solid #666;
image-rendering: pixelated;
}
.rgba-info, .quantized-info {
margin-top: 15px;
padding: 10px;
background: #333;
border-radius: 4px;
}
.progress-bar {
width: 100%;
height: 20px;
background: #1a1a1a;
border-radius: 10px;
margin: 10px 0;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #4CAF50, #45a049);
width: 0%;
transition: width 0.3s ease;
}
.stratification-controls {
margin: 15px 0;
padding: 15px;
background: #1a1a1a;
border-radius: 4px;
border: 1px solid #666;
}
.stratification-controls h4 {
color: #9C27B0;
margin: 0 0 10px 0;
font-size: 14px;
}
.radio-group {
display: flex;
flex-direction: column;
gap: 8px;
}
.radio-option {
display: flex;
align-items: center;
gap: 8px;
}
.radio-option input[type="radio"] {
accent-color: #9C27B0;
}
.radio-option label {
font-size: 12px;
color: #ccc;
font-weight: normal;
}
@media (max-width: 768px) {
.info-grid {
grid-template-columns: 1fr;
}
.controls {
flex-direction: column;
align-items: stretch;
}
.metadata-grid {
grid-template-columns: 1fr;
}
}
</style>
</head>
<body>
<div class="container">
<header>
<h1>🔬 GGUF Information Dump</h1>
<p>Analyze GGUF model files with metadata, tensor information, hexdump visualization, and stratified quantized block expansion</p>
</header>
<div class="file-input-section">
<input type="file" id="fileInput" class="file-input" accept=".gguf,.bin">
<div class="controls">
<div class="control-group">
<label for="hexdumpSize">Hexdump Size:</label>
<input type="number" id="hexdumpSize" min="64" max="4096" value="512" step="64">
<span>bytes</span>
</div>
<div class="control-group">
<label for="hexdumpOffset">Offset:</label>
<input type="number" id="hexdumpOffset" min="0" value="0" step="16">
</div>
<div class="control-group">
<label for="bytesPerLine">Bytes per line:</label>
<select id="bytesPerLine">
<option value="8">8</option>
<option value="16" selected>16</option>
<option value="32">32</option>
</select>
</div>
</div>
</div>
<div id="statusContainer"></div>
<div class="info-grid" id="infoGrid" style="display: none;">
<div class="info-panel">
<h3>📋 File Information</h3>
<div id="fileInfo"></div>
</div>
<div class="info-panel">
<h3>🏷️ GGUF Header</h3>
<div id="ggufHeader"></div>
</div>
</div>
<div class="info-grid" id="metadataGrid" style="display: none;">
<div class="info-panel">
<h3>📊 Metadata</h3>
<div id="metadataList" class="tensor-list"></div>
</div>
<div class="info-panel">
<h3>🧮 Tensors</h3>
<div class="tensor-list" id="tensorList"></div>
</div>
</div>
<div class="info-grid" id="tensorGrid" style="display: none;">
<div class="info-panel">
<h3>🔍 Selected Tensor Details</h3>
<div id="selectedTensorInfo">Select a tensor to view details</div>
<button id="exportButton" class="export-button" style="display: none;" disabled>
💾 Export Tensor as .bin
</button>
<button id="rgbaButton" class="rgba-button" style="display: none;" disabled>
🎨 View as RGBA Image
</button>
<button id="quantizedButton" class="quantized-button" style="display: none;" disabled>
🔢 View Stratified Quantized RGBA
</button>
</div>
<div class="info-panel">
<h3>📈 Tensor Statistics</h3>
<div id="tensorStats">Select a tensor to view statistics</div>
</div>
</div>
<div class="rgba-viewer" id="rgbaViewer">
<h3>🎨 RGBA Image Viewer</h3>
<div class="rgba-canvas-container">
<canvas id="rgbaCanvas" class="rgba-canvas"></canvas>
<div class="rgba-info" id="rgbaInfo"></div>
</div>
</div>
<div class="quantized-viewer" id="quantizedViewer">
<h3>🔢 Stratified Quantized RGBA Viewer</h3>
<div class="stratification-controls">
<h4>Channel Stratification Method:</h4>
<div class="radio-group">
<div class="radio-option">
<input type="radio" id="stratifySequential" name="stratification" value="sequential" checked>
<label for="stratifySequential">Sequential RGBA Cycling (R,G,B,A...R,G,B,A...)</label>
</div>
<div class="radio-option">
<input type="radio" id="stratifyChunked" name="stratification" value="chunked">
<label for="stratifyChunked">Chunked RGBA Stratification (RRR...R,GGG...G,BBB...B,AAA...A)</label>
</div>
<div class="radio-option">
<input type="radio" id="stratifyBands" name="stratification" value="bands">
<label for="stratifyBands">Horizontal Band Stratification (R|G|B|A as separate strips)</label>
</div>
<div class="radio-option">
<input type="radio" id="stratifyGrayscale" name="stratification" value="grayscale">
<label for="stratifyGrayscale">Normalized Grayscale (traditional)</label>
</div>
</div>
</div>
<div class="progress-bar" id="progressBar" style="display: none;">
<div class="progress-fill" id="progressFill"></div>
</div>
<div class="quantized-canvas-container">
<canvas id="quantizedCanvas" class="quantized-canvas"></canvas>
<div class="quantized-info" id="quantizedInfo"></div>
</div>
</div>
<div class="hexdump-container" id="hexdumpContainer" style="display: none;">
<h3>🔢 Hexdump</h3>
<div class="hexdump" id="hexdump"></div>
</div>
</div>
<script>
class GGUFDump {
constructor() {
this.fileData = null;
this.header = null;
this.metadata = null;
this.tensors = null;
this.selectedTensor = null;
this.tensorDataOffset = 0;
this.lastExpandedData = null; // Cache for re-rendering with different stratification
this.initializeEventListeners();
}
initializeEventListeners() {
document.getElementById('fileInput').addEventListener('change', (e) => {
this.handleFileInput(e);
});
document.getElementById('hexdumpSize').addEventListener('input', () => {
this.updateHexdump();
});
document.getElementById('hexdumpOffset').addEventListener('input', () => {
this.updateHexdump();
});
document.getElementById('bytesPerLine').addEventListener('change', () => {
this.updateHexdump();
});
document.getElementById('exportButton').addEventListener('click', () => {
this.exportSelectedTensor();
});
document.getElementById('rgbaButton').addEventListener('click', () => {
this.viewTensorAsRGBA();
});
document.getElementById('quantizedButton').addEventListener('click', () => {
this.viewQuantizedAsRGBA();
});
// Add stratification method change listeners
document.querySelectorAll('input[name="stratification"]').forEach(radio => {
radio.addEventListener('change', () => {
if (this.lastExpandedData) {
this.renderStratifiedRGBA(this.lastExpandedData);
}
});
});
}
showStatus(message, isError = false) {
const container = document.getElementById('statusContainer');
container.innerHTML = `<div class="status ${isError ? 'error' : ''}">${message}</div>`;
}
updateProgress(percent) {
const progressBar = document.getElementById('progressBar');
const progressFill = document.getElementById('progressFill');
if (percent >= 0) {
progressBar.style.display = 'block';
progressFill.style.width = percent + '%';
} else {
progressBar.style.display = 'none';
}
}
async handleFileInput(event) {
const file = event.target.files[0];
if (!file) return;
this.showStatus('📖 Reading file...');
try {
const arrayBuffer = await file.arrayBuffer();
this.fileData = new Uint8Array(arrayBuffer);
this.showStatus('🔍 Analyzing GGUF structure...');
await this.analyzeGGUF(file);
} catch (error) {
console.error('Error reading file:', error);
this.showStatus('❌ Error reading file: ' + error.message, true);
}
}
async analyzeGGUF(file) {
try {
if (!this.parseGGUFHeader()) {
throw new Error('Not a valid GGUF file');
}
this.parseMetadata();
this.parseTensorInfo();
this.showStatus('✅ GGUF file parsed successfully');
this.displayFileInfo(file);
this.displayMetadata();
this.displayTensors();
this.updateHexdump();
} catch (error) {
console.error('Error analyzing GGUF file:', error);
this.showStatus('❌ Error analyzing GGUF file: ' + error.message, true);
}
}
parseGGUFHeader() {
if (this.fileData.length < 16) return false;
const view = new DataView(this.fileData.buffer);
// Check magic number "GGUF"
const magic = new TextDecoder().decode(this.fileData.slice(0, 4));
if (magic !== 'GGUF') return false;
// Parse header
this.header = {
magic: magic,
version: view.getUint32(4, true),
tensorCount: view.getBigUint64(8, true),
metadataKVCount: view.getBigUint64(16, true)
};
this.offset = 24; // Start after header
return true;
}
parseMetadata() {
this.metadata = {};
const metadataCount = Number(this.header.metadataKVCount);
for (let i = 0; i < metadataCount; i++) {
const key = this.readString();
const valueType = this.readUint32();
const value = this.readValue(valueType);
this.metadata[key] = { type: valueType, value: value };
}
}
parseTensorInfo() {
this.tensors = [];
const tensorCount = Number(this.header.tensorCount);
for (let i = 0; i < tensorCount; i++) {
const name = this.readString();
const nDims = this.readUint32();
const shape = [];
for (let j = 0; j < nDims; j++) {
shape.push(Number(this.readUint64()));
}
const type = this.readUint32();
const offset = this.readUint64();
this.tensors.push({
name: name,
shape: shape,
type: type,
typeString: this.getTypeString(type),
offset: Number(offset),
nDims: nDims
});
}
// Calculate tensor data offset (aligned to 32 bytes)
this.tensorDataOffset = Math.ceil(this.offset / 32) * 32;
}
readString() {
const length = this.readUint64();
const str = new TextDecoder().decode(
this.fileData.slice(this.offset, this.offset + Number(length))
);
this.offset += Number(length);
return str;
}
readUint32() {
const value = new DataView(this.fileData.buffer).getUint32(this.offset, true);
this.offset += 4;
return value;
}
readUint64() {
const value = new DataView(this.fileData.buffer).getBigUint64(this.offset, true);
this.offset += 8;
return value;
}
readValue(type) {
switch (type) {
case 0: // UINT8
return this.fileData[this.offset++];
case 1: // INT8
return new DataView(this.fileData.buffer).getInt8(this.offset++);
case 2: // UINT16
const uint16 = new DataView(this.fileData.buffer).getUint16(this.offset, true);
this.offset += 2;
return uint16;
case 3: // INT16
const int16 = new DataView(this.fileData.buffer).getInt16(this.offset, true);
this.offset += 2;
return int16;
case 4: // UINT32
return this.readUint32();
case 5: // INT32
const int32 = new DataView(this.fileData.buffer).getInt32(this.offset, true);
this.offset += 4;
return int32;
case 6: // FLOAT32
const float32 = new DataView(this.fileData.buffer).getFloat32(this.offset, true);
this.offset += 4;
return float32;
case 7: // BOOL
return Boolean(this.fileData[this.offset++]);
case 8: // STRING
return this.readString();
case 9: // ARRAY
const arrayType = this.readUint32();
const arrayLength = Number(this.readUint64());
const array = [];
for (let i = 0; i < arrayLength; i++) {
array.push(this.readValue(arrayType));
}
return array;
case 10: // UINT64
return Number(this.readUint64());
case 11: // INT64
const int64 = new DataView(this.fileData.buffer).getBigInt64(this.offset, true);
this.offset += 8;
return Number(int64);
case 12: // FLOAT64
const float64 = new DataView(this.fileData.buffer).getFloat64(this.offset, true);
this.offset += 8;
return float64;
default:
throw new Error(`Unknown value type: ${type}`);
}
}
getTypeString(type) {
const types = {
0: 'F32', 1: 'F16', 2: 'Q4_0', 3: 'Q4_1', 4: 'Q5_0', 5: 'Q5_1',
6: 'Q8_0', 7: 'Q8_1', 8: 'Q2_K', 9: 'Q3_K', 10: 'Q4_K',
11: 'Q5_K', 12: 'Q6_K', 13: 'Q8_K', 14: 'IQ2_XXS', 15: 'IQ2_XS',
16: 'IQ3_XXS', 17: 'IQ1_S', 18: 'IQ4_NL', 19: 'IQ3_S', 20: 'IQ2_S',
21: 'IQ4_XS', 22: 'I8', 23: 'I16', 24: 'I32', 25: 'I64',
26: 'F64', 27: 'IQ1_M'
};
return types[type] || `UNKNOWN(${type})`;
}
displayFileInfo(file) {
const fileInfo = document.getElementById('fileInfo');
const ggufHeader = document.getElementById('ggufHeader');
const fileSizeKB = (file.size / 1024).toFixed(2);
const fileSizeMB = (file.size / (1024 * 1024)).toFixed(2);
fileInfo.innerHTML = `
<div><strong>Name:</strong> ${file.name}</div>
<div><strong>Size:</strong> ${fileSizeKB} KB (${fileSizeMB} MB)</div>
<div><strong>Type:</strong> ${file.type || 'application/octet-stream'}</div>
<div><strong>Last Modified:</strong> ${new Date(file.lastModified).toLocaleString()}</div>
<div><strong>Tensor Data Offset:</strong> ${this.tensorDataOffset} bytes</div>
`;
ggufHeader.innerHTML = `
<div><strong>Magic:</strong> ${this.header.magic}</div>
<div><strong>Version:</strong> ${this.header.version}</div>
<div><strong>Tensor Count:</strong> ${this.header.tensorCount}</div>
<div><strong>Metadata KV Count:</strong> ${this.header.metadataKVCount}</div>
`;
document.getElementById('infoGrid').style.display = 'grid';
}
displayMetadata() {
const metadataList = document.getElementById('metadataList');
metadataList.innerHTML = '';
if (Object.keys(this.metadata).length === 0) {
metadataList.innerHTML = '<div>No metadata found</div>';
return;
}
for (const [key, data] of Object.entries(this.metadata)) {
const metadataItem = document.createElement('div');
metadataItem.className = 'tensor-item';
let valueStr = '';
if (Array.isArray(data.value)) {
valueStr = `[${data.value.slice(0, 5).join(', ')}${data.value.length > 5 ? '...' : ''}]`;
} else if (typeof data.value === 'string' && data.value.length > 50) {
valueStr = data.value.substring(0, 50) + '...';
} else {
valueStr = String(data.value);
}
metadataItem.innerHTML = `
<div class="tensor-name">${key}</div>
<div class="tensor-details">
Type: ${this.getValueTypeString(data.type)}<br>
Value: ${valueStr}
</div>
`;
metadataList.appendChild(metadataItem);
}
document.getElementById('metadataGrid').style.display = 'grid';
}
getValueTypeString(type) {
const types = {
0: 'UINT8', 1: 'INT8', 2: 'UINT16', 3: 'INT16', 4: 'UINT32',
5: 'INT32', 6: 'FLOAT32', 7: 'BOOL', 8: 'STRING', 9: 'ARRAY',
10: 'UINT64', 11: 'INT64', 12: 'FLOAT64'
};
return types[type] || `UNKNOWN(${type})`;
}
displayTensors() {
const tensorList = document.getElementById('tensorList');
tensorList.innerHTML = '';
if (this.tensors.length === 0) {
tensorList.innerHTML = '<div>No tensors found</div>';
return;
}
this.tensors.forEach((tensor, index) => {
const tensorItem = document.createElement('div');
tensorItem.className = 'tensor-item';
const totalElements = tensor.shape.reduce((a, b) => a * b, 1);
const sizeBytes = this.calculateTensorSize(tensor);
// Add indicators for compatible tensors
const rgbaCompatible = this.isRGBACompatible(tensor);
const quantizedCompatible = this.isQuantizedCompatible(tensor);
let indicators = '';
if (rgbaCompatible) indicators += ' 🎨';
if (quantizedCompatible) indicators += ' 🔢';
tensorItem.innerHTML = `
<div class="tensor-name">${tensor.name}${indicators}</div>
<div class="tensor-details">
Shape: [${tensor.shape.join(', ')}]<br>
Type: ${tensor.typeString}<br>
Elements: ${totalElements.toLocaleString()}<br>
Size: ${(sizeBytes / 1024).toFixed(2)} KB
</div>
`;
tensorItem.addEventListener('click', () => {
this.selectTensor(tensor, tensorItem);
});
tensorList.appendChild(tensorItem);
});
}
isRGBACompatible(tensor) {
// Check if tensor is 2D and has 4 bytes per element
return tensor.nDims === 2 && this.getBytesPerElement(tensor.type) === 4;
}
isQuantizedCompatible(tensor) {
// Check if tensor is quantized (supports dequantization)
const supportedTypes = [2, 3, 6, 8, 9, 10]; // Q4_0, Q4_1, Q8_0, Q2_K, Q3_K, Q4_K
return supportedTypes.includes(tensor.type);
}
calculateTensorSize(tensor) {
const totalElements = tensor.shape.reduce((a, b) => a * b, 1);
const bytesPerElement = this.getBytesPerElement(tensor.type);
return totalElements * bytesPerElement;
}
getBytesPerElement(type) {
// Approximate bytes per element for quantized types
const sizes = {
0: 4, 1: 2, 2: 0.5, 3: 0.5625, 4: 0.5, 5: 0.5625,
6: 1, 7: 1, 8: 0.25, 9: 0.375, 10: 0.5,
11: 0.5625, 12: 0.75, 13: 1, 22: 1, 23: 2,
24: 4, 25: 8, 26: 8
};
return sizes[type] || 4;
}
selectTensor(tensor, element) {
document.querySelectorAll('.tensor-item').forEach(item => {
item.classList.remove('selected');
});
element.classList.add('selected');
this.selectedTensor = tensor;
this.lastExpandedData = null; // Clear cache when selecting new tensor
const selectedInfo = document.getElementById('selectedTensorInfo');
const tensorStats = document.getElementById('tensorStats');
const totalElements = tensor.shape.reduce((a, b) => a * b, 1);
const sizeBytes = this.calculateTensorSize(tensor);
const actualOffset = this.tensorDataOffset + tensor.offset;
const rgbaCompatible = this.isRGBACompatible(tensor);
const quantizedCompatible = this.isQuantizedCompatible(tensor);
selectedInfo.innerHTML = `
<div><strong>Name:</strong> ${tensor.name}</div>
<div><strong>Shape:</strong> [${tensor.shape.join(', ')}]</div>
<div><strong>Dimensions:</strong> ${tensor.nDims}D</div>
<div><strong>Data Type:</strong> ${tensor.typeString}</div>
<div><strong>Elements:</strong> ${totalElements.toLocaleString()}</div>
<div><strong>Size:</strong> ${(sizeBytes / 1024).toFixed(2)} KB</div>
<div><strong>File Offset:</strong> ${actualOffset}</div>
${rgbaCompatible ? '<div><strong>RGBA Compatible:</strong> ✅ Yes</div>' : ''}
${quantizedCompatible ? '<div><strong>Quantized Expandable:</strong> ✅ Yes</div>' : ''}
`;
tensorStats.innerHTML = `
<div><strong>Memory Layout:</strong> Row-major</div>
<div><strong>Bytes per Element:</strong> ${this.getBytesPerElement(tensor.type)}</div>
<div><strong>Quantization:</strong> ${this.isQuantized(tensor.type) ? 'Yes' : 'No'}</div>
<div><strong>Compression Ratio:</strong> ${this.getCompressionRatio(tensor.type)}x</div>
${quantizedCompatible ? `<div><strong>Block Size:</strong> ${this.getBlockSize(tensor.type)} elements</div>` : ''}
`;
// Update hexdump to show tensor data
document.getElementById('hexdumpOffset').value = actualOffset;
this.updateHexdump();
// Show export button
const exportButton = document.getElementById('exportButton');
exportButton.style.display = 'block';
exportButton.disabled = false;
// Show RGBA button if compatible
const rgbaButton = document.getElementById('rgbaButton');
if (rgbaCompatible) {
rgbaButton.style.display = 'block';
rgbaButton.disabled = false;
} else {
rgbaButton.style.display = 'none';
document.getElementById('rgbaViewer').style.display = 'none';
}
// Show quantized button if compatible
const quantizedButton = document.getElementById('quantizedButton');
if (quantizedCompatible) {
quantizedButton.style.display = 'block';
quantizedButton.disabled = false;
} else {
quantizedButton.style.display = 'none';
document.getElementById('quantizedViewer').style.display = 'none';
}
document.getElementById('tensorGrid').style.display = 'grid';
}
isQuantized(type) {
return type >= 2 && type <= 21; // Q4_0 through IQ1_M
}
getCompressionRatio(type) {
const bytesPerElement = this.getBytesPerElement(type);
return (4 / bytesPerElement).toFixed(1); // Compared to F32
}
getBlockSize(type) {
const blockSizes = {
2: 32, // Q4_0
3: 32, // Q4_1
6: 32, // Q8_0
8: 256, // Q2_K
9: 256, // Q3_K
10: 256, // Q4_K
};
return blockSizes[type] || 32;
}
async viewQuantizedAsRGBA() {
if (!this.selectedTensor || !this.fileData) {
this.showStatus('❌ No tensor selected or no file loaded', true);
return;
}
const tensor = this.selectedTensor;
if (!this.isQuantizedCompatible(tensor)) {
this.showStatus('❌ Selected tensor is not quantized or not supported for expansion', true);
return;
}
try {
this.showStatus('🔢 Expanding quantized tensor...');
this.updateProgress(10);
const actualOffset = this.tensorDataOffset + tensor.offset;
// Dequantize the tensor if not cached
if (!this.lastExpandedData) {
this.lastExpandedData = await this.dequantizeTensor(tensor, actualOffset);
this.updateProgress(70);
}
// Render with current stratification method
this.renderStratifiedRGBA(this.lastExpandedData);
this.updateProgress(-1);
} catch (error) {
console.error('Error viewing quantized tensor as RGBA:', error);
this.updateProgress(-1);
this.showStatus('❌ Error viewing quantized tensor as RGBA: ' + error.message, true);
}
}
renderStratifiedRGBA(expandedData) {
const stratificationMethod = document.querySelector('input[name="stratification"]:checked').value;
const totalElements = expandedData.length;
// Calculate optimal dimensions for visualization
const { width, height } = this.calculateOptimalDimensions(totalElements);
// Convert based on stratification method
let rgbaData;
switch (stratificationMethod) {
case 'sequential':
rgbaData = this.convertToSequentialRGBA(expandedData, width, height);
break;
case 'chunked':
rgbaData = this.convertToChunkedRGBA(expandedData, width, height);
break;
case 'grayscale':
default:
rgbaData = this.convertToGrayscaleRGBA(expandedData, width, height);
break;
}
// Render to canvas
const canvas = document.getElementById('quantizedCanvas');
const ctx = canvas.getContext('2d');
canvas.width = width;
canvas.height = height;
const imageData = ctx.createImageData(width, height);
for (let i = 0; i < rgbaData.length; i++) {
imageData.data[i] = rgbaData[i];
}
ctx.putImageData(imageData, 0, 0);
// Show viewer and update info
const viewer = document.getElementById('quantizedViewer');
const info = document.getElementById('quantizedInfo');
const stratificationDesc = this.getStratificationDescription(stratificationMethod);
info.innerHTML = `
<div><strong>Original Tensor:</strong> ${this.selectedTensor.name}</div>
<div><strong>Original Type:</strong> ${this.selectedTensor.typeString}</div>
<div><strong>Expanded Elements:</strong> ${totalElements.toLocaleString()}</div>
<div><strong>Display Dimensions:</strong> ${width} × ${height}</div>
<div><strong>Stratification Method:</strong> ${stratificationDesc}</div>
<div><strong>Block Size:</strong> ${this.getBlockSize(this.selectedTensor.type)} elements</div>
`;
viewer.style.display = 'block';
this.showStatus(`✅ Displayed stratified "${this.selectedTensor.name}" as RGBA (${width}×${height})`);
}
getStratificationDescription(method) {
switch (method) {
case 'sequential':
return 'Sequential RGBA cycling - values cycle through R→G→B→A channels';
case 'chunked':
return 'Chunked RGBA stratification - data split into R, G, B, A quarters';
case 'grayscale':
return 'Normalized grayscale - traditional min-max normalization';
default:
return 'Unknown method';
}
}
convertToSequentialRGBA(floatData, width, height) {
const pixelCount = width * height;
const rgbaData = new Uint8Array(pixelCount * 4);
// Find min/max for normalization
let min = Infinity, max = -Infinity;
for (let i = 0; i < floatData.length; i++) {
if (isFinite(floatData[i])) {
min = Math.min(min, floatData[i]);
max = Math.max(max, floatData[i]);
}
}
const range = max - min;
const scale = range > 0 ? 255 / range : 0;
// Sequential assignment: R,G,B,A,R,G,B,A...
for (let i = 0; i < pixelCount; i++) {
const pixelIndex = i * 4;
// Fill RGBA channels with sequential data
for (let channel = 0; channel < 4; channel++) {
const dataIndex = i * 4 + channel;
if (dataIndex < floatData.length) {
const normalized = isFinite(floatData[dataIndex])
? Math.round((floatData[dataIndex] - min) * scale)
: 0;
const clamped = Math.max(0, Math.min(255, normalized));
rgbaData[pixelIndex + channel] = clamped;
} else {
rgbaData[pixelIndex + channel] = channel === 3 ? 255 : 0; // Alpha = 255, others = 0
}
}
}
return rgbaData;
}
convertToChunkedRGBA(floatData, width, height) {
const pixelCount = width * height;
const rgbaData = new Uint8Array(pixelCount * 4);
// Find min/max for normalization
let min = Infinity, max = -Infinity;
for (let i = 0; i < floatData.length; i++) {
if (isFinite(floatData[i])) {
min = Math.min(min, floatData[i]);
max = Math.max(max, floatData[i]);
}
}
const range = max - min;
const scale = range > 0 ? 255 / range : 0;
// Split data into 4 chunks for R, G, B, A
const chunkSize = Math.ceil(floatData.length / 4);
const chunks = [
floatData.slice(0, chunkSize), // R chunk
floatData.slice(chunkSize, chunkSize * 2), // G chunk
floatData.slice(chunkSize * 2, chunkSize * 3), // B chunk
floatData.slice(chunkSize * 3, chunkSize * 4) // A chunk
];
for (let i = 0; i < pixelCount; i++) {
const pixelIndex = i * 4;
for (let channel = 0; channel < 4; channel++) {
const chunk = chunks[channel];
if (i < chunk.length) {
const normalized = isFinite(chunk[i])
? Math.round((chunk[i] - min) * scale)
: 0;
const clamped = Math.max(0, Math.min(255, normalized));
rgbaData[pixelIndex + channel] = clamped;
} else {
rgbaData[pixelIndex + channel] = channel === 3 ? 255 : 0; // Alpha = 255, others = 0
}
}
}
return rgbaData;
}
convertToBandedRGBA(floatData, width, height) {
const pixelCount = width * height;
const rgbaData = new Uint8Array(pixelCount * 4);
// Find min/max for normalization
let min = Infinity, max = -Infinity;
for (let i = 0; i < floatData.length; i++) {
if (isFinite(floatData[i])) {
min = Math.min(min, floatData[i]);
max = Math.max(max, floatData[i]);
}
}
const range = max - min;
const scale = range > 0 ? 255 / range : 0;
// Split data into 4 chunks for R, G, B, A bands
const chunkSize = Math.ceil(floatData.length / 4);
const chunks = [
floatData.slice(0, chunkSize), // R chunk
floatData.slice(chunkSize, chunkSize * 2), // G chunk
floatData.slice(chunkSize * 2, chunkSize * 3), // B chunk
floatData.slice(chunkSize * 3, chunkSize * 4) // A chunk
];
// Create horizontal bands - each quarter of height gets one channel
const bandHeight = Math.floor(height / 4);
const bandWidth = width;
for (let y = 0; y < height; y++) {
// Determine which band we're in
const bandIndex = Math.min(3, Math.floor(y / bandHeight));
const chunk = chunks[bandIndex];
for (let x = 0; x < width; x++) {
const pixelIndex = (y * width + x) * 4;
// Calculate position within this band
const yInBand = y - (bandIndex * bandHeight);
const dataIndex = yInBand * bandWidth + x;
if (dataIndex < chunk.length) {
const normalized = isFinite(chunk[dataIndex])
? Math.round((chunk[dataIndex] - min) * scale)
: 0;
const clamped = Math.max(0, Math.min(255, normalized));
// Color each band with its respective channel color
switch (bandIndex) {
case 0: // Red band
rgbaData[pixelIndex] = clamped; // R
rgbaData[pixelIndex + 1] = 0; // G
rgbaData[pixelIndex + 2] = 0; // B
rgbaData[pixelIndex + 3] = 255; // A
break;
case 1: // Green band
rgbaData[pixelIndex] = 0; // R
rgbaData[pixelIndex + 1] = clamped; // G
rgbaData[pixelIndex + 2] = 0; // B
rgbaData[pixelIndex + 3] = 255; // A
break;
case 2: // Blue band
rgbaData[pixelIndex] = 0; // R
rgbaData[pixelIndex + 1] = 0; // G
rgbaData[pixelIndex + 2] = clamped; // B
rgbaData[pixelIndex + 3] = 255; // A
break;
case 3: // Alpha band (shown as yellow/gold)
rgbaData[pixelIndex] = clamped; // R
rgbaData[pixelIndex + 1] = clamped; // G
rgbaData[pixelIndex + 2] = 0; // B
rgbaData[pixelIndex + 3] = 255; // A
break;
}
} else {
// Fill remaining pixels with black
rgbaData[pixelIndex] = 0;
rgbaData[pixelIndex + 1] = 0;
rgbaData[pixelIndex + 2] = 0;
rgbaData[pixelIndex + 3] = 255;
}
}
}
return rgbaData;
}
convertToGrayscaleRGBA(floatData, width, height) {
const pixelCount = width * height;
const rgbaData = new Uint8Array(pixelCount * 4);
// Find min/max for normalization
let min = Infinity, max = -Infinity;
for (let i = 0; i < floatData.length; i++) {
if (isFinite(floatData[i])) {
min = Math.min(min, floatData[i]);
max = Math.max(max, floatData[i]);
}
}
const range = max - min;
const scale = range > 0 ? 255 / range : 0;
for (let i = 0; i < pixelCount; i++) {
const pixelIndex = i * 4;
if (i < floatData.length) {
const normalized = isFinite(floatData[i])
? Math.round((floatData[i] - min) * scale)
: 0;
const clamped = Math.max(0, Math.min(255, normalized));
rgbaData[pixelIndex] = clamped; // R
rgbaData[pixelIndex + 1] = clamped; // G
rgbaData[pixelIndex + 2] = clamped; // B
rgbaData[pixelIndex + 3] = 255; // A (full opacity)
} else {
// Fill remaining pixels with black
rgbaData[pixelIndex] = 0;
rgbaData[pixelIndex + 1] = 0;
rgbaData[pixelIndex + 2] = 0;
rgbaData[pixelIndex + 3] = 255;
}
}
return rgbaData;
}
calculateOptimalDimensions(totalElements) {
// Try to make a roughly square image, or use a reasonable aspect ratio
const sqrt = Math.sqrt(totalElements);
let width = Math.ceil(sqrt);
let height = Math.ceil(totalElements / width);
// Ensure we don't go over the total elements
while (width * height < totalElements) {
width++;
height = Math.ceil(totalElements / width);
}
// Limit maximum dimensions for performance
const maxDim = 2048;
if (width > maxDim) {
width = maxDim;
height = Math.ceil(totalElements / width);
}
if (height > maxDim) {
height = maxDim;
width = Math.ceil(totalElements / height);
}
return { width, height };
}
async dequantizeTensor(tensor, offset) {
const totalElements = tensor.shape.reduce((a, b) => a * b, 1);
const result = new Float32Array(totalElements);
switch (tensor.type) {
case 2: // Q4_0
return this.dequantizeQ4_0(offset, totalElements);
case 3: // Q4_1
return this.dequantizeQ4_1(offset, totalElements);
case 6: // Q8_0
return this.dequantizeQ8_0(offset, totalElements);
case 8: // Q2_K
return this.dequantizeQ2_K(offset, totalElements);
case 9: // Q3_K
return this.dequantizeQ3_K(offset, totalElements);
case 10: // Q4_K
return this.dequantizeQ4_K(offset, totalElements);
default:
throw new Error(`Unsupported quantization type: ${tensor.typeString}`);
}
}
dequantizeQ4_0(offset, totalElements) {
const blockSize = 32;
const numBlocks = Math.ceil(totalElements / blockSize);
const result = new Float32Array(totalElements);
const view = new DataView(this.fileData.buffer);
let resultIndex = 0;
let byteOffset = offset;
for (let block = 0; block < numBlocks; block++) {
// Read scale (16-bit float)
const scaleBytes = view.getUint16(byteOffset, true);
const scale = this.float16ToFloat32(scaleBytes);
byteOffset += 2;
// Read 16 bytes of 4-bit values (32 values total)
for (let i = 0; i < 16 && resultIndex < totalElements; i++) {
const byte = this.fileData[byteOffset + i];
// Extract two 4-bit values from each byte
const val1 = (byte & 0x0F) - 8; // Convert to signed -8 to +7
const val2 = ((byte >> 4) & 0x0F) - 8;
if (resultIndex < totalElements) {
result[resultIndex++] = val1 * scale;
}
if (resultIndex < totalElements) {
result[resultIndex++] = val2 * scale;
}
}
byteOffset += 16;
}
return result;
}
dequantizeQ4_1(offset, totalElements) {
const blockSize = 32;
const numBlocks = Math.ceil(totalElements / blockSize);
const result = new Float32Array(totalElements);
const view = new DataView(this.fileData.buffer);
let resultIndex = 0;
let byteOffset = offset;
for (let block = 0; block < numBlocks; block++) {
// Read scale and bias (16-bit floats)
const scaleBytes = view.getUint16(byteOffset, true);
const biasBytes = view.getUint16(byteOffset + 2, true);
const scale = this.float16ToFloat32(scaleBytes);
const bias = this.float16ToFloat32(biasBytes);
byteOffset += 4;
// Read 16 bytes of 4-bit values
for (let i = 0; i < 16 && resultIndex < totalElements; i++) {
const byte = this.fileData[byteOffset + i];
const val1 = byte & 0x0F; // 0 to 15
const val2 = (byte >> 4) & 0x0F;
if (resultIndex < totalElements) {
result[resultIndex++] = val1 * scale + bias;
}
if (resultIndex < totalElements) {
result[resultIndex++] = val2 * scale + bias;
}
}
byteOffset += 16;
}
return result;
}
dequantizeQ8_0(offset, totalElements) {
const blockSize = 32;
const numBlocks = Math.ceil(totalElements / blockSize);
const result = new Float32Array(totalElements);
const view = new DataView(this.fileData.buffer);
let resultIndex = 0;
let byteOffset = offset;
for (let block = 0; block < numBlocks; block++) {
// Read scale (16-bit float)
const scaleBytes = view.getUint16(byteOffset, true);
const scale = this.float16ToFloat32(scaleBytes);
byteOffset += 2;
// Read 32 bytes of 8-bit values
for (let i = 0; i < 32 && resultIndex < totalElements; i++) {
const val = view.getInt8(byteOffset + i); // Signed -128 to +127
result[resultIndex++] = val * scale;
}
byteOffset += 32;
}
return result;
}
dequantizeQ2_K(offset, totalElements) {
// Simplified Q2_K implementation
const blockSize = 256;
const numBlocks = Math.ceil(totalElements / blockSize);
const result = new Float32Array(totalElements);
// This is a simplified implementation - real Q2_K is more complex
let resultIndex = 0;
let byteOffset = offset;
for (let block = 0; block < numBlocks && resultIndex < totalElements; block++) {
// Skip complex structure, just use basic dequantization
const scale = 1.0; // Simplified
for (let i = 0; i < blockSize / 4 && resultIndex < totalElements; i++) {
const byte = this.fileData[byteOffset + i] || 0;
// Extract four 2-bit values
for (let j = 0; j < 4 && resultIndex < totalElements; j++) {
const val = ((byte >> (j * 2)) & 0x03) - 1; // -1 to +2
result[resultIndex++] = val * scale;
}
}
byteOffset += 80; // Approximate block size for Q2_K
}
return result;
}
dequantizeQ3_K(offset, totalElements) {
// Simplified Q3_K implementation
const blockSize = 256;
const numBlocks = Math.ceil(totalElements / blockSize);
const result = new Float32Array(totalElements);
let resultIndex = 0;
let byteOffset = offset;
for (let block = 0; block < numBlocks && resultIndex < totalElements; block++) {
const scale = 1.0; // Simplified
// Process 3-bit values (simplified)
for (let i = 0; i < blockSize / 2 && resultIndex < totalElements; i++) {
const byte = this.fileData[byteOffset + i] || 0;
const val1 = (byte & 0x07) - 4; // 3 bits: -4 to +3
const val2 = ((byte >> 3) & 0x07) - 4;
if (resultIndex < totalElements) {
result[resultIndex++] = val1 * scale;
}
if (resultIndex < totalElements) {
result[resultIndex++] = val2 * scale;
}
}
byteOffset += 96; // Approximate block size for Q3_K
}
return result;
}
dequantizeQ4_K(offset, totalElements) {
// Simplified Q4_K implementation
const blockSize = 256;
const numBlocks = Math.ceil(totalElements / blockSize);
const result = new Float32Array(totalElements);
let resultIndex = 0;
let byteOffset = offset;
for (let block = 0; block < numBlocks && resultIndex < totalElements; block++) {
const scale = 1.0; // Simplified
for (let i = 0; i < blockSize / 2 && resultIndex < totalElements; i++) {
const byte = this.fileData[byteOffset + i] || 0;
const val1 = (byte & 0x0F) - 8; // 4 bits: -8 to +7
const val2 = ((byte >> 4) & 0x0F) - 8;
if (resultIndex < totalElements) {
result[resultIndex++] = val1 * scale;
}
if (resultIndex < totalElements) {
result[resultIndex++] = val2 * scale;
}
}
byteOffset += 144; // Approximate block size for Q4_K
}
return result;
}
float16ToFloat32(bytes) {
// Convert 16-bit float to 32-bit float
const sign = (bytes & 0x8000) >> 15;
const exponent = (bytes & 0x7C00) >> 10;
const mantissa = bytes & 0x03FF;
if (exponent === 0) {
return (sign ? -1 : 1) * Math.pow(2, -14) * (mantissa / 1024);
} else if (exponent === 0x1F) {
return mantissa ? NaN : (sign ? -Infinity : Infinity);
} else {
return (sign ? -1 : 1) * Math.pow(2, exponent - 15) * (1 + mantissa / 1024);
}
}
viewTensorAsRGBA() {
if (!this.selectedTensor || !this.fileData) {
this.showStatus('❌ No tensor selected or no file loaded', true);
return;
}
const tensor = this.selectedTensor;
if (!this.isRGBACompatible(tensor)) {
this.showStatus('❌ Selected tensor is not RGBA compatible', true);
return;
}
try {
const actualOffset = this.tensorDataOffset + tensor.offset;
const width = tensor.shape[1];
const height = tensor.shape[0];
const bytesNeeded = width * height * 4;
// Get tensor data
const tensorData = this.fileData.slice(actualOffset, actualOffset + bytesNeeded);
// Create canvas and render image
const canvas = document.getElementById('rgbaCanvas');
const ctx = canvas.getContext('2d');
canvas.width = width;
canvas.height = height;
// Create ImageData
const imageData = ctx.createImageData(width, height);
// Copy tensor data to ImageData
// Handle different data types
if (tensor.type === 0) { // F32
this.convertF32ToRGBA(tensorData, imageData.data);
} else if (tensor.type === 24) { // I32
this.convertI32ToRGBA(tensorData, imageData.data);
} else {
// Default: treat as raw bytes
for (let i = 0; i < tensorData.length && i < imageData.data.length; i++) {
imageData.data[i] = tensorData[i];
}
}
// Draw image to canvas
ctx.putImageData(imageData, 0, 0);
// Show viewer and update info
const viewer = document.getElementById('rgbaViewer');
const info = document.getElementById('rgbaInfo');
info.innerHTML = `
<div><strong>Tensor:</strong> ${tensor.name}</div>
<div><strong>Dimensions:</strong> ${width} × ${height}</div>
<div><strong>Data Type:</strong> ${tensor.typeString}</div>
<div><strong>Bytes Used:</strong> ${bytesNeeded.toLocaleString()}</div>
<div><strong>Interpretation:</strong> ${this.getRGBAInterpretation(tensor.type)}</div>
`;
viewer.style.display = 'block';
this.showStatus(`✅ Displayed "${tensor.name}" as RGBA image (${width}×${height})`);
} catch (error) {
console.error('Error viewing tensor as RGBA:', error);
this.showStatus('❌ Error viewing tensor as RGBA: ' + error.message, true);
}
}
convertF32ToRGBA(tensorData, rgbaData) {
const view = new DataView(tensorData.buffer, tensorData.byteOffset);
for (let i = 0; i < tensorData.length; i += 4) {
const float32 = view.getFloat32(i, true);
// Normalize float to 0-255 range (assuming values are in 0-1 range)
const normalized = Math.max(0, Math.min(255, Math.round(float32 * 255)));
rgbaData[i] = normalized; // R
rgbaData[i + 1] = normalized; // G
rgbaData[i + 2] = normalized; // B
rgbaData[i + 3] = 255; // A (full opacity)
}
}
convertI32ToRGBA(tensorData, rgbaData) {
const view = new DataView(tensorData.buffer, tensorData.byteOffset);
for (let i = 0; i < tensorData.length; i += 4) {
const int32 = view.getInt32(i, true);
// Extract RGBA bytes from int32 (assuming RGBA packed format)
rgbaData[i] = (int32 >>> 0) & 0xFF; // R
rgbaData[i + 1] = (int32 >>> 8) & 0xFF; // G
rgbaData[i + 2] = (int32 >>> 16) & 0xFF; // B
rgbaData[i + 3] = (int32 >>> 24) & 0xFF; // A
}
}
getRGBAInterpretation(type) {
switch (type) {
case 0: return 'F32 values normalized to 0-255 (grayscale with alpha)';
case 24: return 'I32 values as packed RGBA bytes';
default: return 'Raw bytes as RGBA';
}
}
exportSelectedTensor() {
if (!this.selectedTensor || !this.fileData) {
this.showStatus('❌ No tensor selected or no file loaded', true);
return;
}
try {
const tensor = this.selectedTensor;
const actualOffset = this.tensorDataOffset + tensor.offset;
const sizeBytes = this.calculateTensorSize(tensor);
// Find the next tensor to determine end offset
const currentIndex = this.tensors.indexOf(tensor);
let endOffset;
if (currentIndex < this.tensors.length - 1) {
const nextTensor = this.tensors[currentIndex + 1];
endOffset = this.tensorDataOffset + nextTensor.offset;
} else {
endOffset = this.fileData.length;
}
const actualSize = Math.min(sizeBytes, endOffset - actualOffset);
const tensorData = this.fileData.slice(actualOffset, actualOffset + actualSize);
// Create blob and download
const blob = new Blob([tensorData], { type: 'application/octet-stream' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${tensor.name.replace(/[/\\?%*:|"<>]/g, '_')}.bin`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
const sizeKB = (actualSize / 1024).toFixed(2);
this.showStatus(`✅ Exported "${tensor.name}" (${sizeKB} KB)`);
} catch (error) {
console.error('Error exporting tensor:', error);
this.showStatus('❌ Error exporting tensor: ' + error.message, true);
}
}
updateHexdump() {
if (!this.fileData) return;
const size = parseInt(document.getElementById('hexdumpSize').value);
const offset = parseInt(document.getElementById('hexdumpOffset').value);
const bytesPerLine = parseInt(document.getElementById('bytesPerLine').value);
const hexdump = this.generateHexdump(this.fileData, offset, size, bytesPerLine);
document.getElementById('hexdump').textContent = hexdump;
document.getElementById('hexdumpContainer').style.display = 'block';
}
generateHexdump(data, offset, size, bytesPerLine) {
const end = Math.min(offset + size, data.length);
let result = '';
for (let i = offset; i < end; i += bytesPerLine) {
// Offset
const addr = i.toString(16).padStart(8, '0').toUpperCase();
result += addr + ' ';
// Hex bytes
let hexLine = '';
let asciiLine = '';
for (let j = 0; j < bytesPerLine; j++) {
if (i + j < end) {
const byte = data[i + j];
hexLine += byte.toString(16).padStart(2, '0').toUpperCase() + ' ';
asciiLine += (byte >= 32 && byte <= 126) ? String.fromCharCode(byte) : '.';
} else {
hexLine += ' ';
asciiLine += ' ';
}
// Add extra space in the middle for 16-byte lines
if (bytesPerLine === 16 && j === 7) {
hexLine += ' ';
}
}
result += hexLine + ' |' + asciiLine + '|\n';
}
return result;
}
}
// Initialize the application
document.addEventListener('DOMContentLoaded', () => {
new GGUFDump();
});
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment