PNG to ICO (Favicon) Converter

Upload a PNG image to convert it into a multi-resolution ICO (Windows Icon) file, suitable for favicons. Select the desired output sizes.

Image preview will appear here.

Select Output Sizes (pixels):

Please select a PNG image file.

'; pngFileInput.value = ''; // Clear invalid file selection originalFileName = ""; originalImageBase64 = ""; return; } originalFileName = file.name; const reader = new FileReader(); reader.onload = function(e) { originalImageBase64 = e.target.result; const img = new Image(); img.onload = function() { imagePreview.innerHTML = ''; imagePreview.appendChild(img); imagePreview.innerHTML += `

${originalFileName} (${img.width}x${img.height} pixels)

`; }; img.onerror = function() { imagePreview.innerHTML = '

Could not load image preview.

'; originalImageBase64 = ""; }; img.src = originalImageBase64; }; reader.readAsDataURL(file); } /** * Converts the loaded PNG image data to an ICO format with selected sizes. */ async function convertPngToIco() { const resultArea = document.getElementById('resultArea'); const downloadPdfButton = document.getElementById('downloadPdfButton'); clearResultArea(); if (!originalImageBase64) { resultArea.style.display = 'block'; resultArea.innerHTML = '

Please select a PNG image file first.

'; return; } const selectedSizes = Array.from(document.querySelectorAll('input[name="iconSize"]:checked')) .map(cb => parseInt(cb.value, 10)) .sort((a, b) => a - b); // Sort sizes ascending if (selectedSizes.length === 0) { resultArea.style.display = 'block'; resultArea.innerHTML = '

Please select at least one output size.

'; return; } try { const img = new Image(); img.src = originalImageBase64; await img.decode(); // Ensure image is fully loaded/decoded let imageBuffers = []; let currentOffset = ICONDIR_SIZE + (selectedSizes.length * ICONDIRENTRY_SIZE); // Initial offset after ICONDIR and all ICONDIRENTRYs const iconDirEntriesBuffer = new ArrayBuffer(selectedSizes.length * ICONDIRENTRY_SIZE); const iconDirEntriesView = new DataView(iconDirEntriesBuffer); let entryIndex = 0; for (const size of selectedSizes) { // Resize image to current size on offscreen canvas offscreenCanvas.width = size; offscreenCanvas.height = size; offscreenCtx.clearRect(0, 0, size, size); // Clear previous content offscreenCtx.drawImage(img, 0, 0, size, size); const imageData = offscreenCtx.getImageData(0, 0, size, size); const pixelData = new Uint8Array(imageData.data.buffer); // RGBA // Generate BMP data (XOR mask) and AND mask (for transparency) const { bmpData, andMaskData } = generateBmpAndAndMask(pixelData, size, size); // Combine BMP header + XOR mask + AND mask const imageBuffer = new Uint8Array(BITMAPINFOHEADER_SIZE + bmpData.length + andMaskData.length); imageBuffer.set(bmpData, 0); // BMP header + XOR mask imageBuffer.set(andMaskData, BITMAPINFOHEADER_SIZE + bmpData.length); // AND mask after XOR mask imageBuffers.push(imageBuffer); // Write ICONDIRENTRY for this size const widthByte = size === 256 ? 0 : size; // 256 is represented as 0 in ICO header const heightByte = size === 256 ? 0 : size; iconDirEntriesView.setUint8(entryIndex * ICONDIRENTRY_SIZE + ICONDIRENTRY_WIDTH_OFFSET, widthByte); iconDirEntriesView.setUint8(entryIndex * ICONDIRENTRY_SIZE + ICONDIRENTRY_HEIGHT_OFFSET, heightByte); iconDirEntriesView.setUint8(entryIndex * ICONDIRENTRY_SIZE + ICONDIRENTRY_COLORS_OFFSET, 0); // No color palette iconDirEntriesView.setUint8(entryIndex * ICONDIRENTRY_SIZE + ICONDIRENTRY_RESERVED_OFFSET, 0); iconDirEntriesView.setUint16(entryIndex * ICONDIRENTRY_SIZE + ICONDIRENTRY_PLANES_OFFSET, 1, true); // Color planes (1) iconDirEntriesView.setUint16(entryIndex * ICONDIRENTRY_SIZE + ICONDIRENTRY_BITCOUNT_OFFSET, 32, true); // 32 BPP (RGBA) iconDirEntriesView.setUint32(entryIndex * ICONDIRENTRY_SIZE + ICONDIRENTRY_BYTESINRES_OFFSET, imageBuffer.length, true); iconDirEntriesView.setUint32(entryIndex * ICONDIRENTRY_SIZE + ICONDIRENTRY_IMAGEOFFSET_OFFSET, currentOffset, true); currentOffset += imageBuffer.length; entryIndex++; } // Build ICO header (ICONDIR) const icoHeaderBuffer = new ArrayBuffer(ICONDIR_SIZE); const icoHeaderView = new DataView(icoHeaderBuffer); icoHeaderView.setUint16(0, ICONDIR_RESERVED, true); // Reserved icoHeaderView.setUint16(2, ICONDIR_TYPE, true); // Type (1 for ICO) icoHeaderView.setUint16(4, selectedSizes.length, true); // Number of images // Assemble all parts into one ArrayBuffer const totalIcoSize = currentOffset; // Final offset is total size const finalIcoBuffer = new Uint8Array(totalIcoSize); let offset = 0; finalIcoBuffer.set(new Uint8Array(icoHeaderBuffer), offset); offset += icoHeaderBuffer.byteLength; finalIcoBuffer.set(new Uint8Array(iconDirEntriesBuffer), offset); offset += iconDirEntriesBuffer.byteLength; imageBuffers.forEach(buffer => { finalIcoBuffer.set(buffer, offset); offset += buffer.byteLength; }); // Trigger download const outputFileName = originalFileName.replace(/\.png$/i, '.ico'); const blob = new Blob([finalIcoBuffer], { type: 'image/x-icon' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = outputFileName; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); resultArea.style.display = 'block'; resultArea.innerHTML = `

Success! Your PNG has been converted to "${outputFileName}".

Converted sizes: ${selectedSizes.join(', ')}px

`; downloadPdfButton.style.display = 'block'; // Store data for PDF report resultArea.dataset.inputFileName = originalFileName; resultArea.dataset.outputFileName = outputFileName; resultArea.dataset.originalImageDimensions = `${img.width}x${img.height}`; resultArea.dataset.convertedSizes = JSON.stringify(selectedSizes); } catch (error) { console.error('Error during ICO conversion:', error); resultArea.style.display = 'block'; resultArea.innerHTML = `

Conversion failed: ${error.message}. Please try another PNG or fewer sizes.

`; } } /** * Generates 32-bit (BGRA) BMP pixel data (XOR mask) and a 1-bit AND mask for transparency. * Assumes input `pixelData` is RGBA. * @param {Uint8ClampedArray} pixelData RGBA pixel data from CanvasRenderingContext2D.getImageData(). * @param {number} width Image width. * @param {number} height Image height. * @returns {{bmpData: Uint8Array, andMaskData: Uint8Array}} Object containing BMP (XOR) and AND mask data. */ function generateBmpAndAndMask(pixelData, width, height) { const bmpHeaderBuffer = new ArrayBuffer(BITMAPINFOHEADER_SIZE); const bmpHeaderView = new DataView(bmpHeaderBuffer); // BITMAPINFOHEADER bmpHeaderView.setUint32(0, BITMAPINFOHEADER_SIZE, true); // biSize bmpHeaderView.setInt32(4, width, true); // biWidth bmpHeaderView.setInt32(8, height * 2, true); // biHeight (height * 2 for ICO, includes XOR and AND mask) bmpHeaderView.setUint16(12, 1, true); // biPlanes bmpHeaderView.setUint16(14, 32, true); // biBitCount (32-bit RGBA) bmpHeaderView.setUint32(16, BI_RGB, true); // biCompression bmpHeaderView.setUint32(20, width * height * 4, true); // biSizeImage (size of XOR mask) bmpHeaderView.setInt32(24, 0, true); // biXPelsPerMeter bmpHeaderView.setInt32(28, 0, true); // biYPelsPerMeter bmpHeaderView.setUint32(32, 0, true); // biClrUsed bmpHeaderView.setUint32(36, 0, true); // biClrImportant const xorMaskSize = width * height * 4; // BGRA const xorMaskData = new Uint8Array(xorMaskSize); for (let i = 0; i < pixelData.length; i += 4) { const r = pixelData[i]; const g = pixelData[i + 1]; const b = pixelData[i + 2]; const a = pixelData[i + 3]; // ICOs store pixels as BGRA, so swap R and B xorMaskData[i] = b; // Blue xorMaskData[i + 1] = g; // Green xorMaskData[i + 2] = r; // Red xorMaskData[i + 3] = a; // Alpha } // AND mask (1 bit per pixel, 0 = opaque, 1 = transparent) // Rows must be 4-byte aligned (padded with zeros) const rowByteWidth = Math.ceil(width / 8); // Bytes per row for 1-bit mask const paddedRowByteWidth = (rowByteWidth + 3) & ~3; // 4-byte aligned const andMaskSize = paddedRowByteWidth * height; const andMaskData = new Uint8Array(andMaskSize); // Initializes with zeros for (let y = 0; y < height; y++) { for (let x = 0; x < width; x++) { const pixelIndex = (y * width + x) * 4; const alpha = pixelData[pixelIndex + 3]; if (alpha < 255) { // Transparent pixel const byteIndex = y * paddedRowByteWidth + Math.floor(x / 8); const bitIndex = 7 - (x % 8); // Bits are written high-bit first andMaskData[byteIndex] |= (1 << bitIndex); } } } const bmpData = new Uint8Array(bmpHeaderBuffer.byteLength + xorMaskData.byteLength); bmpData.set(new Uint8Array(bmpHeaderBuffer), 0); bmpData.set(xorMaskData, bmpHeaderBuffer.byteLength); return { bmpData, andMaskData }; } /** * Clears all inputs, previews, and results. */ function clearAll() { document.getElementById('pngFileInput').value = ''; // Clear selected file document.getElementById('imagePreview').innerHTML = '

Image preview will appear here.

'; originalFileName = ""; originalImageBase64 = ""; // Reset checkboxes to default (16x16, 32x32 checked) document.querySelectorAll('input[name="iconSize"]').forEach(cb => { if (cb.value === '16' || cb.value === '32') { cb.checked = true; } else { cb.checked = false; } }); clearResultArea(); } /** * Helper function to clear and hide the result area and PDF button. */ function clearResultArea() { const resultArea = document.getElementById('resultArea'); const downloadPdfButton = document.getElementById('downloadPdfButton'); if (resultArea) { resultArea.style.display = 'none'; resultArea.innerHTML = ''; // Clear stored data for PDF delete resultArea.dataset.inputFileName; delete resultArea.dataset.outputFileName; delete resultArea.dataset.originalImageDimensions; delete resultArea.dataset.convertedSizes; } if (downloadPdfButton) { downloadPdfButton.style.display = 'none'; } } /** * Generates a PDF summary of the conversion. */ function generatePdf() { // Check if jsPDF library is loaded if (typeof window.jspdf === 'undefined' || typeof window.jspdf.jsPDF === 'undefined') { console.error('jsPDF library not loaded. Cannot generate PDF.'); const resultArea = document.getElementById('resultArea'); if (resultArea) { resultArea.innerHTML = '

PDF generation failed. Library not loaded.

'; } return; } const { jsPDF } = window.jspdf; const doc = new jsPDF(); const resultArea = document.getElementById('resultArea'); // Retrieve stored data for PDF const inputFileName = resultArea.dataset.inputFileName || 'N/A'; const outputFileName = resultArea.dataset.outputFileName || 'N/A'; const originalImageDimensions = resultArea.dataset.originalImageDimensions || 'N/A'; const convertedSizes = JSON.parse(resultArea.dataset.convertedSizes || '[]'); const generationDate = new Date().toLocaleString(); let yOffset = 20; // Title doc.setFontSize(24); doc.setTextColor(44, 62, 80); doc.text("PNG to ICO Conversion Summary", 105, yOffset, { align: 'center' }); yOffset += 20; // Summary details doc.setFontSize(14); doc.setTextColor(51, 51, 51); doc.text(`Conversion Date: ${generationDate}`, 20, yOffset); yOffset += 10; doc.text(`Input File: ${inputFileName}`, 20, yOffset); yOffset += 10; doc.text(`Original Dimensions: ${originalImageDimensions}`, 20, yOffset); yOffset += 10; doc.text(`Output ICO File: ${outputFileName}`, 20, yOffset); yOffset += 10; doc.text(`Converted Sizes Included: ${convertedSizes.length > 0 ? convertedSizes.join('x, ') + 'x' : 'None'}`, 20, yOffset); yOffset += 20; // Conversion Notes doc.setFontSize(12); doc.setTextColor(80, 80, 80); doc.text("Important Notes on ICO Conversion:", 20, yOffset); yOffset += 7; const notes = [ "• This tool creates an ICO file containing multiple uncompressed 32-bit (BGRA) images.", "• Each selected size is a separate image embedded within the ICO file.", "• Transparency (alpha channel) from the PNG is preserved using an XOR/AND mask approach for compatibility.", "• For optimal favicon performance and smallest file size, ensure your original PNG has appropriate dimensions and transparent background if desired." ]; notes.forEach(note => { const splitNote = doc.splitTextToSize(note, 170); doc.text(splitNote, 25, yOffset); yOffset += (splitNote.length * 7) + 5; }); yOffset += 15; // Footer doc.setFontSize(9); doc.setTextColor(150, 150, 150); doc.text(`Report Generated by PNG to ICO Converter Tool`, 20, doc.internal.pageSize.height - 15); doc.save("PNG_to_ICO_Conversion_Summary.pdf"); }
Scroll to Top