Files
deepseek beaf0e1f37 Toolvana: production-ready media tools platform (CodeIgniter 4)
- 134-tool registry with programmatic SEO (unique titles/H1/descriptions,
  JSON-LD graphs, sitemap index, canonical 301 enforcement via required filter)
- DB-backed job queue (SKIP LOCKED) with drivers: Ffmpeg, Images (GD),
  Pdf (qpdf/gs/poppler), Youtube (thumbnails), Qr (server-side PNG)
- Security: SSRF guard, MIME validation, rate limits, API-key auth,
  bcrypt admin login, security headers
- Admin panel: dashboard, tools/categories/guides CRUD, SEO audit,
  analytics, job inspector with retry, system health, feature flags
- Docker deployment (nginx + web/api FPM pools + scalable workers),
  PHPUnit suite (19 tests / 1139 assertions), PWA manifest + service worker
2026-08-23 07:10:30 +00:00

675 lines
44 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
namespace App\Libraries;
/**
* CatalogBuilder — the single source of truth for the tool registry.
*
* Produces a complete array of tool definitions: hand-curated flagship
* tools first, then every format-conversion combination generated from
* FormatCatalog. Content templates are parameterized by operation and
* formats and varied deterministically per slug, so each landing page is
* specific to what it actually does (no interchangeable filler).
*
* The admin "new tool" flow reuses this builder; the seeder persists it.
*/
final class CatalogBuilder
{
public static function build(): array
{
return [
...self::youtubeTools(),
...self::videoTools(),
...self::audioTools(),
...self::imageTools(),
...self::gifTools(),
...self::pdfTools(),
...self::utilityTools(),
];
}
// =====================================================================
// YouTube family
// =====================================================================
private static function youtubeTools(): array
{
$cat = 'youtube-tools';
$gated = ['yt_video_download', 'yt_audio_download', 'yt_playlist_download', 'yt_shorts_download'];
return [
self::tool([
'slug' => 'youtube-downloader', 'name' => 'YouTube Downloader', 'icon' => 'download',
'short' => 'Save YouTube videos in MP4 quality up to 1080p. Paste a link and pick a resolution.',
'operation' => 'yt_video_download', 'requires' => ['yt-dlp'],
'aliases' => ['yt downloader', 'download youtube video', 'save youtube video'],
'intro' => 'This downloader saves YouTube videos as MP4 files you can keep offline. Paste any video URL, choose the resolution you need, and the video is prepared in seconds. Use it for your own uploads, Creative Commons material, or content you are licensed to download.',
], $cat),
self::tool([
'slug' => 'youtube-video-downloader', 'name' => 'YouTube Video Downloader', 'icon' => 'video',
'short' => 'Download YouTube videos as high-quality MP4 files, ready for offline viewing.',
'operation' => 'yt_video_download', 'requires' => ['yt-dlp'], 'hidden_dup' => true,
'aliases' => ['yt video downloader'],
], $cat),
self::tool([
'slug' => 'youtube-to-mp3', 'name' => 'YouTube to MP3 Converter', 'icon' => 'music',
'short' => 'Extract MP3 audio from any YouTube video at up to 320 kbps. Free, fast, no software.',
'operation' => 'yt_audio_download', 'format_out' => 'mp3', 'requires' => ['yt-dlp'],
'featured' => true, 'popularity' => 10000,
'aliases' => ['yt mp3', 'yt to mp3', 'youtube mp3', 'youtube audio', 'ytmp3'],
'intro' => 'Convert YouTube videos into clean MP3 audio without installing anything. Paste a video link, press Convert, and receive a 320 kbps file that plays on every phone, computer and car stereo. Ideal for podcasts, lectures, interviews and music sets you have the right to save.',
], $cat),
self::tool([
'slug' => 'youtube-to-wav', 'name' => 'YouTube to WAV Converter', 'icon' => 'music',
'short' => 'Rip lossless WAV audio from YouTube videos for editing and sampling.',
'operation' => 'yt_audio_download', 'format_out' => 'wav', 'requires' => ['yt-dlp'],
'popularity' => 4200,
'aliases' => ['yt wav', 'youtube wav'],
'intro' => 'Need uncompressed audio from a YouTube video? This tool pulls the best available audio stream and converts it to standard 16-bit PCM WAV — the format audio editors, DAWs and samplers expect. Perfect when quality matters more than file size.',
], $cat),
self::tool([
'slug' => 'youtube-to-m4a', 'name' => 'YouTube to M4A Converter', 'icon' => 'music',
'short' => 'Get compact M4A (AAC) audio from YouTube — great quality at half the size of MP3.',
'operation' => 'yt_audio_download', 'format_out' => 'm4a', 'requires' => ['yt-dlp'],
'popularity' => 3800,
'aliases' => ['yt m4a', 'youtube m4a'],
'intro' => 'M4A is YouTube\'s native audio codec, so converting straight to M4A avoids an extra re-encode. Files stay small enough for phones while sounding better than same-size MP3s. Paste a link to grab lectures, audiobooks or music in AAC format.',
], $cat),
self::tool([
'slug' => 'youtube-to-aac', 'name' => 'YouTube to AAC Converter', 'icon' => 'music',
'short' => 'Convert YouTube soundtracks to raw AAC files with efficient modern compression.',
'operation' => 'yt_audio_download', 'format_out' => 'aac', 'requires' => ['yt-dlp'],
'popularity' => 2100,
], $cat),
self::tool([
'slug' => 'youtube-thumbnail-downloader', 'name' => 'YouTube Thumbnail Downloader', 'icon' => 'image',
'short' => 'Grab every thumbnail size of any YouTube video, up to full 1280×720 HD.',
'operation' => 'yt_thumbnails', 'kind' => 'url', 'featured' => true, 'popularity' => 8600,
'aliases' => ['yt thumbnail', 'thumbnail grabber', 'yt thumb'],
'intro' => 'Enter a YouTube URL and instantly preview and download all available thumbnail sizes — from the tiny default up to maxres HD. Thumbnails are fetched directly from Google\'s image servers, so nothing is stored here. Great for research, design references and archive work.',
], $cat),
self::tool([
'slug' => 'youtube-shorts-downloader', 'name' => 'YouTube Shorts Downloader', 'icon' => 'phone',
'short' => 'Save vertical YouTube Shorts as MP4 files without watermarks.',
'operation' => 'yt_video_download', 'requires' => ['yt-dlp'],
'popularity' => 5400,
], $cat),
self::tool([
'slug' => 'youtube-playlist-downloader', 'name' => 'YouTube Playlist Downloader', 'icon' => 'list',
'short' => 'Queue whole playlists for download in one pass, item by item.',
'operation' => 'yt_playlist_download', 'requires' => ['yt-dlp'],
'popularity' => 3300,
], $cat),
self::tool([
'slug' => 'youtube-audio-downloader', 'name' => 'YouTube Audio Downloader', 'icon' => 'headphones',
'short' => 'Download just the sound from YouTube videos in the format you choose.',
'operation' => 'yt_audio_download', 'requires' => ['yt-dlp'], 'hidden_dup' => true,
'popularity' => 4700,
], $cat),
self::tool([
'slug' => 'youtube-converter', 'name' => 'YouTube Video Converter', 'icon' => 'swap',
'short' => 'Turn YouTube links into MP4, WebM or MP3 files in one step.',
'operation' => 'yt_video_download', 'requires' => ['yt-dlp'],
'output_formats' => ['mp4', 'webm', 'mp3'], 'primary_output_format' => 'mp4',
'popularity' => 5100,
], $cat),
// ---- metadata / analysis tools: always functional -------------------
self::tool([
'slug' => 'youtube-metadata-viewer', 'name' => 'YouTube Metadata Viewer', 'icon' => 'search',
'short' => 'Inspect title, channel, embed info and thumbnails of any YouTube video.',
'operation' => 'yt_metadata', 'kind' => 'url', 'featured' => false, 'popularity' => 2600,
'aliases' => ['yt metadata'],
'intro' => 'Look up the official oEmbed metadata of any YouTube video: exact title, channel name, author profile link and available thumbnail. Useful for journalists verifying sources, developers testing embeds, and creators auditing their own library.',
'faqs' => [
['q' => 'What metadata can this tool show?', 'a' => 'It reads YouTube\'s official public oEmbed endpoint, which exposes the video title, author name, author channel URL and thumbnail URL. It does not expose private statistics such as view counts or likes.'],
['q' => 'Does it work for unlisted videos?', 'a' => 'Unlisted videos respond to oEmbed if you know the link. Private videos do not.'],
],
], $cat),
self::tool([
'slug' => 'youtube-video-information', 'name' => 'YouTube Video Information', 'icon' => 'info',
'short' => 'Quickly check a YouTube video\'s title, channel and preview image.',
'operation' => 'yt_metadata', 'kind' => 'url', 'hidden_dup' => true, 'popularity' => 1900,
], $cat),
self::tool([
'slug' => 'youtube-transcript', 'name' => 'YouTube Transcript Extractor', 'icon' => 'text',
'short' => 'Pull the full transcript of a YouTube video as plain text or SRT/VTT.',
'operation' => 'yt_transcript', 'kind' => 'url', 'requires' => ['yt-dlp'],
'output_formats' => ['txt', 'srt', 'vtt'], 'popularity' => 6100,
'intro' => 'Fetch the caption track of a YouTube video and export it as readable plain text, timed SRT subtitles or WebVTT. Transcripts make videos searchable, quotable and accessible — ideal for research notes, blog repurposing and accessibility work.',
], $cat),
self::tool([
'slug' => 'youtube-subtitle-downloader', 'name' => 'YouTube Subtitle Downloader', 'icon' => 'text',
'short' => 'Download closed captions from any YouTube video in SRT format.',
'operation' => 'yt_transcript', 'kind' => 'url', 'requires' => ['yt-dlp'],
'output_formats' => ['srt'], 'hidden_dup' => true, 'popularity' => 4300,
], $cat),
self::tool([
'slug' => 'youtube-srt-downloader', 'name' => 'YouTube SRT Downloader', 'icon' => 'text',
'short' => 'Export YouTube captions as standard .srt subtitle files.',
'operation' => 'yt_transcript', 'kind' => 'url', 'requires' => ['yt-dlp'],
'output_formats' => ['srt'], 'hidden_dup' => true, 'popularity' => 2800,
], $cat),
self::tool([
'slug' => 'youtube-vtt-downloader', 'name' => 'YouTube VTT Downloader', 'icon' => 'text',
'short' => 'Save YouTube captions as WebVTT (.vtt) files for HTML5 players.',
'operation' => 'yt_transcript', 'kind' => 'url', 'requires' => ['yt-dlp'],
'output_formats' => ['vtt'], 'hidden_dup' => true, 'popularity' => 2400,
], $cat),
self::tool([
'slug' => 'youtube-channel-information', 'name' => 'YouTube Channel Information', 'icon' => 'users',
'short' => 'Resolve a YouTube channel URL and inspect its avatar and identity links.',
'operation' => 'yt_channel_info', 'kind' => 'url', 'popularity' => 1500,
'intro' => 'Paste any channel handle or /channel/ URL to resolve its canonical form and retrieve its public avatar. Handy when auditing brand accounts or building creator directories.',
], $cat),
self::tool([
'slug' => 'youtube-url-analyzer', 'name' => 'YouTube URL Analyzer', 'icon' => 'link',
'short' => 'Break down any YouTube URL: type, video ID, playlist ID, start time and more.',
'operation' => 'yt_url_analyze', 'kind' => 'text', 'client' => true, 'popularity' => 2200,
'intro' => 'Paste a messy YouTube URL and get a structured read-out: whether it is a standard watch page, Shorts, an embed, a live stream or a playlist link — plus the extracted video ID, playlist parameter and timestamp offset. Runs entirely in your browser.',
'how_to_override' => [
['name' => 'Paste the URL', 'text' => 'Copy any YouTube link into the analyzer field.'],
['name' => 'Read the breakdown', 'text' => 'The parsed components appear instantly — no button needed.'],
],
], $cat),
self::tool([
'slug' => 'youtube-embed-generator', 'name' => 'YouTube Embed Code Generator', 'icon' => 'code',
'short' => 'Create privacy-friendly, responsive YouTube iframe embed codes instantly.',
'operation' => 'yt_embed', 'kind' => 'text', 'client' => true, 'popularity' => 3400,
'intro' => 'Generate clean <iframe> embed code from any YouTube link. Choose player options — start time, captions, privacy-enhanced nocookie domain — and copy production-ready responsive HTML. Everything runs client-side; your links never leave the browser.',
], $cat),
self::tool([
'slug' => 'youtube-timestamp-generator', 'name' => 'YouTube Timestamp Generator', 'icon' => 'clock',
'short' => 'Build shareable ?t= timestamps that open YouTube at the exact second.',
'operation' => 'yt_timestamp', 'kind' => 'text', 'client' => true, 'popularity' => 2600,
'intro' => 'Create deep links that jump viewers to a precise moment in a video. Enter minutes and seconds, get both the short youtu.be?t= form and the long form, then copy either. Perfect for show-notes, documentation and comments.',
], $cat),
];
}
// =====================================================================
// Video family
// =====================================================================
private static function videoTools(): array
{
$tools = [];
// container conversions (generated)
$pairs = [
['mp4', 'webm'], ['webm', 'mp4'], ['mkv', 'mp4'], ['mov', 'mp4'],
['avi', 'mp4'], ['mp4', 'mov'], ['mp4', 'mkv'], ['mp4', 'avi'],
];
foreach ($pairs as [$from, $to]) {
$tools[] = self::converter($from, $to, 'video-tools');
}
// action tools
$actions = [
['video-converter', 'Video Converter', 'convert', 'Convert between MP4, WebM, MOV, MKV and AVI online without installing software.', ['mp4', 'webm', 'mov', 'mkv', 'avi'], 'mp4', true, 7800],
['video-compressor', 'Video Compressor', 'compress', 'Shrink large videos up to 70% smaller while keeping sharp quality.', null, 'mp4', true, 9200],
['video-resizer', 'Video Resizer', 'resize', 'Change video dimensions for Instagram, TikTok, YouTube and more.', null, 'mp4', false, 4100],
['video-cropper', 'Video Cropper', 'crop', 'Cut away borders or focus the frame by cropping videos precisely.', null, 'mp4', false, 3200],
['video-trimmer', 'Video Trimmer', 'trim', 'Trim videos to the exact moments that matter — frame-accurate cuts.', null, 'mp4', true, 6400],
['video-cutter', 'Video Cutter', 'cut', 'Cut out a clip from any video without re-encoding the rest.', null, 'mp4', false, 5200],
['video-merger', 'Video Merger', 'merge', 'Join multiple clips into one continuous video, back to back.', null, 'mp4', false, 3600],
['video-rotator', 'Video Rotator', 'rotate', 'Fix sideways or upside-down phone videos by rotating 90° or 180°.', null, 'mp4', false, 2900],
['video-flipper', 'Video Flipper', 'flip', 'Mirror videos horizontally or vertically in one click.', null, 'mp4', false, 1800],
['video-speed-changer', 'Video Speed Changer', 'speed', 'Speed videos up or slow them down from 0.25× to 4× with audio pitch correction.', null, 'mp4', false, 2700],
['video-fps-converter', 'Video FPS Converter', 'fps', 'Convert video frame rates between 24, 30 and 60 fps smoothly.', null, 'mp4', false, 1600],
['video-resolution-converter', 'Video Resolution Converter', 'resize', 'Scale videos to 1080p, 720p or any height while preserving aspect ratio.', null, 'mp4', false, 2500],
];
foreach ($actions as [$slug, $name, $op, $short, $inFmts, $outFmt, $feat, $pop]) {
$tools[] = self::actionTool($slug, $name, $op, $short, 'video-tools', $inFmts ?? [], $outFmt, $feat, $pop);
}
$special = [
['video-to-gif', 'Video to GIF', 'video_to_gif', 'Turn any video segment into a smooth looping GIF with smart palette generation.', ['gif'], true, 8100],
['extract-audio-from-video', 'Extract Audio from Video', 'extract_audio', 'Pull the soundtrack out of any video as MP3, WAV or M4A.', ['mp3', 'wav', 'm4a', 'aac'], true, 7200],
['add-audio-to-video', 'Add Audio to Video', 'add_audio', 'Replace or overlay an audio track onto your video file.', ['mp4'], false, 3100],
['remove-audio-from-video', 'Remove Audio from Video', 'mute', 'Delete the sound track completely while keeping original video quality.', ['mp4'], false, 3400],
['mute-video', 'Mute Video', 'mute', 'Silence any video instantly — perfect before posting to social feeds.', ['mp4'], false, 2300],
['add-subtitle-to-video', 'Add Subtitle to Video', 'add_subtitles', 'Embed SRT or VTT subtitles directly into your MP4 so they play everywhere.', ['mp4'], false, 2900],
['remove-metadata-from-video', 'Remove Metadata from Video', 'remove_metadata', 'Strip GPS coordinates, camera names and timestamps out of video files.', ['mp4'], false, 1700],
['extract-frames-from-video', 'Extract Frames from Video', 'extract_frames', 'Export video frames as JPG images at any frames-per-second rate.', ['jpg'], false, 2100],
];
foreach ($special as [$slug, $name, $op, $short, $outs, $feat, $pop]) {
$t = self::actionTool($slug, $name, $op, $short, 'video-tools', [], $outs[0], $feat, $pop);
$t['output_formats'] = $outs;
$tools[] = $t;
}
return $tools;
}
// =====================================================================
// Audio family
// =====================================================================
private static function audioTools(): array
{
$tools = [];
$audioExts = ['mp3', 'wav', 'm4a', 'flac', 'ogg'];
foreach ($audioExts as $from) {
foreach ($audioExts as $to) {
if ($from !== $to) {
$tools[] = self::converter($from, $to, 'audio-tools');
}
}
}
// aac targets too
foreach ([['wav', 'aac'], ['m4a', 'aac'], ['mp3', 'aac']] as [$f, $t]) {
$tools[] = self::converter($f, $t, 'audio-tools');
}
$actions = [
['audio-converter', 'Audio Converter', 'convert', 'Convert any audio file between MP3, WAV, M4A, FLAC and OGG formats.', true, 8300],
['mp3-converter', 'MP3 Converter', 'convert', 'Turn recordings and exports into universally playable MP3 files.', false, 6100],
['audio-cutter', 'Audio Cutter', 'trim', 'Cut songs and recordings down to the section you actually need.', false, 4800],
['audio-trimmer', 'Audio Trimmer', 'trim', 'Trim the start and end off audio files with second-level precision.', false, 4200],
['audio-merger', 'Audio Merger', 'merge', 'Combine several tracks into one continuous audio file.', false, 3300],
['audio-compressor', 'Audio Compressor', 'compress', 'Reduce audio file size by lowering bitrate without audible damage.', false, 3700],
['audio-normalizer', 'Audio Normalizer', 'normalize', 'Even out loudness across recordings to broadcast-standard -16 LUFS.', false, 3100],
['audio-speed-changer', 'Audio Speed Changer', 'speed', 'Play audio faster or slower while keeping natural voice pitch.', false, 2600],
['audio-volume-booster', 'Audio Volume Booster', 'volume', 'Make quiet recordings louder by boosting gain up to +30 dB.', false, 4500],
];
foreach ($actions as [$slug, $name, $op, $short, $feat, $pop]) {
$tools[] = self::actionTool($slug, $name, $op, $short, 'audio-tools', ['mp3', 'wav', 'm4a', 'flac', 'ogg'], 'mp3', $feat, $pop);
}
return $tools;
}
// =====================================================================
// Image family
// =====================================================================
private static function imageTools(): array
{
$tools = [];
$imgExts = ['jpg', 'png', 'webp'];
foreach ($imgExts as $from) {
foreach ($imgExts as $to) {
if ($from !== $to) {
$tools[] = self::converter($from, $to, 'image-tools');
}
}
}
foreach ([['avif', 'jpg'], ['avif', 'png'], ['bmp', 'jpg'], ['bmp', 'png'], ['jpg', 'avif'], ['png', 'avif']] as [$f, $t]) {
$tools[] = self::converter($f, $t, 'image-tools');
}
$actions = [
['image-compressor', 'Image Compressor', 'compress', 'Compress JPG, PNG and WebP images up to 80% smaller with no visible loss.', true, 8900],
['image-resizer', 'Image Resizer', 'resize', 'Resize images to exact pixel dimensions or scale by percentage.', true, 7600],
['image-cropper', 'Image Cropper', 'crop', 'Crop photos and graphics to the perfect framing.', false, 4300],
['image-converter', 'Image Converter', 'convert', 'One-stop conversion between JPG, PNG, WebP, AVIF and more.', true, 5600],
['image-rotator', 'Image Rotator', 'rotate', 'Rotate images by 90°, 180° or 270° and fix EXIF orientation automatically.', false, 2200],
['image-flipper', 'Image Flipper', 'flip', 'Mirror images horizontally or vertically in a single click.', false, 1400],
['image-metadata-remover', 'Image Metadata Remover', 'strip_meta', 'Erase EXIF data — location, device, timestamps — from photos.', false, 3300],
['exif-viewer', 'EXIF Viewer', 'exif_view', 'Inspect hidden EXIF metadata inside your photos, fully offline.', false, 2800],
['exif-remover', 'EXIF Remover', 'strip_meta', 'Re-encode images without EXIF blocks while keeping visual quality.', false, 2500],
];
foreach ($actions as [$slug, $name, $op, $short, $feat, $pop]) {
$tools[] = self::actionTool($slug, $name, $op, $short, 'image-tools', ['jpg', 'png', 'webp', 'gif', 'avif'], 'jpg', $feat, $pop);
}
// text/client utilities living under image-tools
$tools[] = self::tool([
'slug' => 'favicon-generator', 'name' => 'Favicon Generator', 'icon' => 'star',
'short' => 'Turn any square image into crisp favicons for every browser and device.',
'operation' => 'favicon', 'popularity' => 3200,
'input_formats' => ['png', 'jpg', 'webp'], 'output_formats' => ['ico', 'png'],
'intro' => 'Upload a logo and receive ready-to-deploy favicon files: classic .ico plus 16/32/180/192 px PNGs for Apple touch icons and Android/Chrome manifests, with the HTML tags you need to paste into your site head.',
], 'image-tools');
return $tools;
}
// =====================================================================
// GIF family
// =====================================================================
private static function gifTools(): array
{
$tools = [];
$actions = [
['gif-maker', 'GIF Maker', 'video_to_gif', 'Create animated GIFs from video clips with custom width and framerate.', true, 6800],
['gif-compressor', 'GIF Compressor', 'compress', 'Shrink heavy GIFs via palette optimization and smarter dithering.', false, 3900],
['gif-resizer', 'GIF Resizer', 'resize', 'Scale GIF animations down for chat apps, forums and email.', false, 2700],
['gif-optimizer', 'GIF Optimizer', 'compress', 'Reduce GIF weight with automatic color-table tuning.', false, 2400],
['gif-frame-extractor', 'GIF Frame Extractor', 'extract_frames', 'Dump every GIF frame as numbered PNG stills.', false, 1900],
];
foreach ($actions as [$slug, $name, $op, $short, $feat, $pop]) {
$tools[] = self::actionTool($slug, $name, $op, $short, 'gif-tools', ['gif', 'mp4'], 'gif', $feat, $pop);
}
$tools[] = self::tool([
'slug' => 'images-to-gif', 'name' => 'Images to GIF Maker', 'icon' => 'layers',
'short' => 'Assemble multiple PNG/JPG frames into one animated GIF with chosen delay.',
'operation' => 'frames_to_gif', 'popularity' => 3000,
'input_formats' => ['png', 'jpg'], 'output_formats' => ['gif'],
'intro' => 'Upload a sequence of images — screenshots, render frames, stop-motion shots — set a per-frame delay, and get a single looping GIF. Frames are combined in upload order with automatic canvas sizing.',
], 'gif-tools');
return $tools;
}
// =====================================================================
// PDF family
// =====================================================================
private static function pdfTools(): array
{
$tools = [];
foreach ([['jpg', 'pdf'], ['png', 'pdf']] as [$f, $t]) {
$tools[] = self::converter($f, $t, 'pdf-tools');
}
foreach ([['pdf', 'jpg'], ['pdf', 'png']] as [$f, $t]) {
$tools[] = self::converter($f, $t, 'pdf-tools');
}
$actions = [
['image-to-pdf', 'Image to PDF', 'images_to_pdf', 'Combine JPG and PNG images into a single multi-page PDF document.', true, 5900],
['pdf-compressor', 'PDF Compressor', 'pdf_compress', 'Reduce PDF size with screen/ebook/print presets — perfect under email limits.', true, 8200],
['pdf-merger', 'PDF Merger', 'pdf_merge', 'Merge unlimited PDFs into one ordered document.', true, 7400],
['pdf-splitter', 'PDF Splitter', 'pdf_split', 'Split a PDF into separate single-page documents, zipped for convenience.', false, 4600],
['pdf-page-extractor', 'PDF Page Extractor', 'pdf_extract_pages', 'Keep only the pages you need — ranges like 1-3 or 2,5,7.', false, 3500],
['pdf-metadata-remover', 'PDF Metadata Remover', 'pdf_remove_metadata', 'Strip author, producer and creation data from PDF files.', false, 2000],
];
foreach ($actions as [$slug, $name, $op, $short, $feat, $pop]) {
$t = self::actionTool($slug, $name, $op, $short, 'pdf-tools', ['pdf'], 'pdf', $feat, $pop);
$t['requires_binaries'] = ['qpdf', 'gs'];
$tools[] = $t;
}
foreach (['pdf-to-text' => ['PDF to Text Converter', 'Convert PDF documents into plain text files with the layout preserved.'],
'pdf-text-extractor' => ['PDF Text Extractor', 'Pull selectable text out of any text-based PDF in seconds — copy-ready output.']] as $slug => $meta) {
$t = self::actionTool($slug, $meta[0], 'pdf_to_text', $meta[1], 'pdf-tools', ['pdf'], 'txt', $slug === 'pdf-text-extractor', 4000);
$t['requires_binaries'] = ['pdftotext'];
$tools[] = $t;
}
return $tools;
}
// =====================================================================
// Utilities
// =====================================================================
private static function utilityTools(): array
{
$u = static fn (array $def) => self::tool($def, 'utilities');
return [
$u(['slug' => 'qr-code-generator', 'name' => 'QR Code Generator', 'icon' => 'qr',
'short' => 'Generate high-resolution QR codes for URLs, Wi-Fi, vCards and plain text.',
'operation' => 'qr_generate', 'kind' => 'text', 'server_render' => true, 'featured' => true, 'popularity' => 8800,
'intro' => 'Type any text or link and download a print-ready QR code as PNG or SVG. Codes are generated server-side with error-correction level M, so they scan reliably even after printing. Nothing is logged — the generator forgets your input immediately.',
'faqs' => [['q' => 'Do the generated QR codes expire?', 'a' => 'Never. These are static QR codes: the data lives inside the pattern itself, not on our servers, so they keep working forever.'],
['q' => 'What can I encode?', 'a' => 'Any UTF-8 text up to about 1,800 characters — URLs, Wi-Fi credentials, phone numbers, plain messages.']],
]),
$u(['slug' => 'hash-generator', 'name' => 'Hash Generator', 'icon' => 'shield',
'short' => 'Compute MD5, SHA-1, SHA-256 and SHA-512 checksums of any text instantly.',
'operation' => 'hash_text', 'kind' => 'text', 'client' => true, 'popularity' => 4200,
'intro' => 'Paste text and get MD5, SHA-1, SHA-256 and SHA-512 digests computed locally in your browser — useful for verifying strings, generating cache keys or checking password-hash formats. The input never reaches the server.',
]),
$u(['slug' => 'uuid-generator', 'name' => 'UUID Generator', 'icon' => 'key',
'short' => 'Bulk-generate RFC 4122 version 4 UUIDs with one click.',
'operation' => 'uuid_gen', 'kind' => 'text', 'client' => true, 'popularity' => 3600,
'intro' => 'Generate cryptographically random UUIDv4 identifiers in bulk (up to 500 at once), uppercase or lowercase, with or without dashes. Uses the browser crypto API — suitable for database keys and distributed systems.',
]),
$u(['slug' => 'json-formatter', 'name' => 'JSON Formatter & Validator', 'icon' => 'code',
'short' => 'Pretty-print, minify and validate JSON with precise error positions.',
'operation' => 'json_format', 'kind' => 'text', 'client' => true, 'popularity' => 5400,
'intro' => 'Paste messy JSON and get it formatted with configurable indentation, or minified for transport. Syntax errors report the exact line and character so fixes take seconds. All parsing happens in-browser.',
]),
$u(['slug' => 'url-encoder', 'name' => 'URL Encoder', 'icon' => 'link',
'short' => 'Percent-encode text and query parameters safely for use in URLs.',
'operation' => 'url_encode', 'kind' => 'text', 'client' => true, 'popularity' => 3100,
]),
$u(['slug' => 'url-decoder', 'name' => 'URL Decoder', 'icon' => 'link',
'short' => 'Decode percent-encoded URLs back into human-readable text.',
'operation' => 'url_decode', 'kind' => 'text', 'client' => true, 'popularity' => 2900,
]),
$u(['slug' => 'base64-image-encoder', 'name' => 'Base64 Image Encoder', 'icon' => 'code',
'short' => 'Convert images into Base64 data URIs ready to inline in CSS or HTML.',
'operation' => 'base64_encode_img', 'kind' => 'upload', 'client' => true, 'popularity' => 3400,
'input_formats' => ['png', 'jpg', 'gif', 'webp', 'svg'], 'output_formats' => ['txt'],
]),
$u(['slug' => 'base64-image-decoder', 'name' => 'Base64 Image Decoder', 'icon' => 'image',
'short' => 'Turn Base64 data URIs back into downloadable image files.',
'operation' => 'base64_decode_img', 'kind' => 'text', 'client' => true, 'popularity' => 2200,
'output_formats' => ['png', 'jpg'],
]),
$u(['slug' => 'timestamp-converter', 'name' => 'Unix Timestamp Converter', 'icon' => 'clock',
'short' => 'Convert Unix epoch seconds to human dates and back, in UTC or local time.',
'operation' => 'timestamp_convert', 'kind' => 'text', 'client' => true, 'popularity' => 3800,
]),
$u(['slug' => 'color-converter', 'name' => 'Color Converter', 'icon' => 'palette',
'short' => 'Convert colors between HEX, RGB, HSL with live preview and contrast checks.',
'operation' => 'color_convert', 'kind' => 'text', 'client' => true, 'popularity' => 3300,
]),
$u(['slug' => 'mime-type-lookup', 'name' => 'MIME Type Lookup', 'icon' => 'file',
'short' => 'Find the correct MIME type and extension for hundreds of media formats.',
'operation' => 'mime_lookup', 'kind' => 'text', 'client' => true, 'popularity' => 2100,
]),
$u(['slug' => 'file-extension-lookup', 'name' => 'File Extension Lookup', 'icon' => 'folder',
'short' => 'Identify what unknown file extensions are and which apps open them.',
'operation' => 'ext_lookup', 'kind' => 'text', 'client' => true, 'popularity' => 1900,
]),
$u(['slug' => 'media-metadata-viewer', 'name' => 'Media Metadata Viewer', 'icon' => 'search',
'short' => 'Inspect technical details of uploaded media: codecs, dimensions, duration, EXIF.',
'operation' => 'media_probe', 'kind' => 'upload', 'popularity' => 2600,
'input_formats' => ['mp4', 'mov', 'jpg', 'png', 'mp3', 'wav', 'pdf'], 'output_formats' => [],
'intro' => 'Upload any supported media file and see its internals: container, codecs, resolution, duration, bitrates and embedded metadata. Files are analyzed in an isolated worker and deleted within hours — useful before publishing assets publicly.',
]),
];
}
// =====================================================================
// generators
// =====================================================================
/** Format-pair converter definition with programmatic SEO content. */
private static function converter(string $from, string $to, string $category): array
{
$slug = "{$from}-to-{$to}";
$fromN = FormatCatalog::name($from);
$toN = FormatCatalog::name($to);
$seed = crc32($slug);
$titles = [
"{$fromN} to {$toN} Converter Free Online",
"Convert {$fromN} to {$toN} Online Free Tool",
"{$fromN} to {$toN}: Free Online Converter",
];
$descriptors = match (FormatCatalog::family($to)) {
'audio' => [
'Extract the audio from your {$FROM} files as {$TO}. Upload a file, choose your bitrate, and get a clean {$TO} track in seconds — no software to install.',
'Pull {$TO} audio out of {$FROM} media with our free converter. Processing happens on dedicated workers, so even long files convert quickly.',
],
'video' => [
'Convert {$FROM} video to {$TO} online with adjustable quality. Fast processing, no watermark, no signup required.',
'Turn {$FROM} files into {$TO} videos that play everywhere. Our converter balances size and quality automatically.',
],
'document' => [
'Combine {$FROM} images into a tidy {$TO} document. Pages are built in upload order at full resolution.',
'Build a {$TO} from your {$FROM} pictures — free, quick and without watermarks.',
],
default => [
'Convert {$FROM} images to {$TO} format online. Free, fast and without uploading anything to social sites.',
'Transform {$FROM} graphics into {$TO} files with optional quality control. Perfect for web performance work.',
],
};
$descriptor = $descriptors[$seed % count($descriptors)];
$descriptor = str_replace(['{$FROM}', '{$TO}'], [$fromN, $toN], $descriptor);
$requires = match (FormatCatalog::family($from)) {
'video' => ['ffmpeg'],
'audio' => ['ffmpeg'],
'document' => ['gs'],
default => [],
};
return self::tool([
'slug' => $slug,
'name' => "{$fromN} to {$toN} Converter",
'short' => $descriptor,
'operation' => 'convert',
'format_in' => $from,
'format_out' => $to,
'requires' => $requires,
'popularity' => 900 + ((int) abs($seed) % 1500),
'title_pool' => $titles,
], $category);
}
private static function actionTool(string $slug, string $name, string $operation, string $short, string $category, array $inFormats, string $outFormat, bool $featured, int $popularity): array
{
return self::tool([
'slug' => $slug,
'name' => $name,
'short' => $short,
'operation' => $operation,
'input_formats' => $inFormats ?: null,
'format_out' => $outFormat !== '' ? $outFormat : null,
'featured' => $featured,
'popularity' => $popularity,
'requires' => match (FormatCatalog::family((string) ($inFormats[0] ?? $outFormat))) {
'video', 'audio' => ['ffmpeg'],
'document' => ['qpdf', 'pdftoppm'],
default => [],
},
], $category);
}
/**
* Assemble one complete tool row: SEO fields, how-to steps and FAQs
* composed from real attributes (formats, operation), varied by slug.
*/
private static function tool(array $def, string $category): array
{
$slug = $def['slug'];
$name = $def['name'] ?? ucwords(str_replace('-', ' ', $slug));
$seed = abs((int) (crc32($slug)));
$inFmt = $def['format_in'] ?? ($def['input_formats'][0] ?? null);
$outFmt = $def['format_out'] ?? ($def['output_formats'][0] ?? null);
$kind = $def['kind'] ?? (($def['operation'] ?? '') === 'yt_metadata' || str_starts_with($def['operation'] ?? '', 'yt_') ? 'url' : (isset($def['client']) && $def['client'] ? 'text' : 'upload'));
$titlePool = $def['title_pool'] ?? [
"{$name} Free Online Tool",
"{$name} Free & Fast",
"Free Online {$name}",
];
$seoTitle = $titlePool[$seed % count($titlePool)];
$seoDescription = mb_substr(trim(($def['short'] ?? $name . ' online. Free, fast and easy to use — no installation or signup required.')), 0, 300);
$intro = $def['intro']
?? trim(($def['short'] ?? '') . ' ' . ($outFmt && $inFmt
? 'Files are processed on dedicated servers and deleted automatically — nothing is shared with third parties.'
: 'Everything runs in your browser where possible, and processed files are deleted automatically.'));
// HowTo steps derived from the actual kind
$howTo = $def['how_to_override'] ?? match ($kind) {
'url' => [
['name' => 'Copy the link', 'text' => 'Open the video on YouTube and copy its URL from the address bar or share menu.'],
['name' => 'Paste the URL', 'text' => 'Paste the link into the input box above and press Convert.'],
['name' => 'Choose your output', 'text' => 'Pick the format or quality offered for your video.'],
['name' => 'Download the result', 'text' => 'When processing finishes, save the file to your device.'],
],
'text' => [
['name' => 'Enter your input', 'text' => 'Type or paste your text into the field above.'],
['name' => 'Get instant results', 'text' => 'Results update immediately — no waiting queue.'],
['name' => 'Copy or download', 'text' => 'Use the copy button or download the output file.'],
],
default => [
['name' => 'Select your file', 'text' => 'Click the upload area or drag a file onto it.'],
['name' => 'Adjust options if needed', 'text' => 'Default settings suit most cases; advanced options stay hidden until you expand them.'],
['name' => 'Start processing', 'text' => 'Press the Convert button — progress is shown live.'],
['name' => 'Download', 'text' => 'Save the finished file when processing completes.'],
],
};
// FAQs composed from genuine attributes
$faqs = $def['faqs'] ?? [];
if ($faqs === []) {
$fmtSentenceIn = $inFmt ? FormatCatalog::blurb($inFmt) : '';
$whatDoes = $outFmt && $inFmt
? "It converts {$fmtSentenceIn} into " . FormatCatalog::blurb($outFmt) . '.'
: 'It processes your file on our servers and gives you a downloadable result.';
$faqs = [
['q' => "What is {$name}?", 'a' => $whatDoes],
['q' => 'How do I use it?', 'a' => match ($kind) {
'url' => 'Paste the link into the input field, press Convert, then download the result.',
'text' => 'Type or paste your text — results appear instantly in your browser.',
default => 'Upload your file, adjust optional settings, press Convert, then download the output.',
}],
['q' => 'Is it really free?', 'a' => 'Yes. There are no watermarks, signups or hidden fees for normal usage.'],
['q' => 'How long does processing take?', 'a' => 'Small files finish in seconds. Longer media scales with duration because we never cut corners on quality.'],
['q' => 'Are my files safe?', 'a' => 'Uploads travel over encrypted connections and are stored temporarily only for processing. They are deleted automatically — see our privacy policy for retention details.'],
];
if ($kind === 'upload') {
$faqs[] = ['q' => 'Why did my conversion fail?', 'a' => 'The most common causes are an unsupported source file, a corrupted upload, or exceeding the size limit. Try the file again or check the accepted formats listed on this page.'];
}
}
$inputFormats = $def['input_formats'] ?? (isset($inFmt) ? [$inFmt] : []);
$outputFormats = $def['output_formats'] ?? (isset($outFmt) ? [$outFmt] : []);
return [
'category' => $category,
'slug' => $slug,
'name' => $name,
'kind' => $kind,
'operation' => $def['operation'] ?? 'convert',
'short_description' => $def['short'] ?? '',
'description' => $def['intro'] ?? null,
'input_formats' => $inputFormats,
'output_formats' => $outputFormats,
'primary_input_format' => $inFmt,
'primary_output_format'=> $outFmt,
'icon' => $def['icon'] ?? 'file',
'seo_title' => $def['seo_title'] ?? $seoTitle,
'seo_description' => $def['seo_description'] ?? $seoDescription,
'h1' => $def['h1'] ?? $name,
'intro' => $intro,
'how_to' => $howTo,
'faqs' => $faqs,
'related_slugs' => $def['related'] ?? null,
'aliases' => $def['aliases'] ?? [],
'requires_binaries' => $def['requires'] ?? [],
'accepts_mimes' => this_mimes($inputFormats),
'max_upload_mb' => 0,
'popularity' => $def['popularity'] ?? 1000,
'is_featured' => (int) ($def['featured'] ?? 0),
'status' => 'active',
'hidden_dup' => (int) ($def['hidden_dup'] ?? 0),
];
}
}
/** MIME list helper (kept module-level for clarity). */
function this_mimes(array $extensions): array
{
$map = [
'mp4' => 'video/mp4', 'webm' => 'video/webm', 'mov' => 'video/quicktime', 'mkv' => 'video/x-matroska',
'avi' => 'video/x-msvideo', 'gif' => 'image/gif', 'mp3' => 'audio/mpeg', 'wav' => 'audio/wav',
'm4a' => 'audio/mp4', 'aac' => 'audio/aac', 'flac' => 'audio/flac', 'ogg' => 'audio/ogg',
'opus' => 'audio/opus', 'wma' => 'audio/x-ms-wma', 'jpg' => 'image/jpeg', 'jpeg' => 'image/jpeg',
'png' => 'image/png', 'webp' => 'image/webp', 'avif' => 'image/avif', 'bmp' => 'image/bmp',
'pdf' => 'application/pdf', 'srt' => 'application/x-subrip', 'vtt' => 'text/vtt',
'svg' => 'image/svg+xml', 'zip' => 'application/zip', 'txt' => 'text/plain',
];
return array_values(array_unique(array_map(
static fn (string $e): string => $map[strtolower($e)] ?? 'application/octet-stream',
$extensions
)));
}