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