Skip to content
This repository was archived by the owner on Dec 4, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ manifest.json:
"blockSize": 2,
"prefix": "img",
"seed": 72411,
"restoreFileName": false
"preserveName": false
},
"images": [
{
Expand Down
10 changes: 5 additions & 5 deletions packages/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ image-shield shuffle <images...> -o <output_directory> [options]
| `-b, --block-size <size>` | Pixel block size (positive integer) | ❌ | 10 |
| `-p, --prefix <prefix>` | Prefix for fragment files | ❌ | "fragment" |
| `-s, --seed <seed>` | Random seed (integer) | ❌ | auto-generated |
| `--restore-filename` | Restore original file names when restoring | ❌ | false |
| `--preserve-name` | Preserve original file names | ❌ | false |

#### Examples

Expand All @@ -60,7 +60,7 @@ image-shield shuffle image1.jpg image2.png -o ./fragments

**Custom configuration:**
```bash
image-shield shuffle *.jpg -o ./output -b 20 -p "my_fragment" --restore-filename
image-shield shuffle *.jpg -o ./output -b 20 -p "my_fragment" --preserve-name
```

**With seed for reproducible results:**
Expand Down Expand Up @@ -127,7 +127,7 @@ The CLI provides clear error messages for common issues:

2. **Fragment images:**
```bash
image-shield shuffle images/photo1.jpg images/photo2.png -o ./backup --restore-filename
image-shield shuffle images/photo1.jpg images/photo2.png -o ./backup --preserve-name
```
```
🔀 Starting image fragmentation...
Expand Down Expand Up @@ -166,7 +166,7 @@ image-shield shuffle sensitive/*.jpg \
-b 5 \
-p "secure_chunk" \
-s 42 \
--restore-filename
--preserve-name

# The output will use smaller blocks (5x5 pixels) and custom naming
```
Expand All @@ -186,7 +186,7 @@ BACKUP_DIR="./backup"
echo "Creating backup..."
image-shield shuffle "$IMAGES_DIR"/*.{jpg,png} \
-o "$BACKUP_DIR" \
--restore-filename
--preserve-name

if [ $? -eq 0 ]; then
echo "✅ Backup completed successfully"
Expand Down
4 changes: 2 additions & 2 deletions packages/cli/src/commands/shuffle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export function registerShuffleCommand(program: Command): void {
}
return num;
})
.option("--restore-filename", "Restore original file names")
.option("--preserve-name", "Preserve original file names")
.action(handleShuffleCommand);
}

Expand All @@ -52,7 +52,7 @@ async function handleShuffleCommand(
if (options.blockSize !== undefined) config.blockSize = options.blockSize;
if (options.prefix !== undefined) config.prefix = options.prefix;
if (options.seed !== undefined) config.seed = options.seed;
if (options.restoreFilename) config.restoreFileName = true;
if (options.preserveName) config.preserveName = true;

await ImageShield.shuffle({
imagePaths,
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ export interface ShuffleOptions {
blockSize?: number;
prefix?: string;
seed?: number;
restoreFilename?: boolean;
preserveName?: boolean;
}

export interface RestoreOptions {
Expand Down
1 change: 0 additions & 1 deletion packages/cli/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
"noEmit": true,
"module": "ESNext",
"moduleResolution": "bundler",
"rootDir": "./src",
"baseUrl": ".",
"paths": {
"@image-shield/core": ["../core/src"],
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@ export const MANIFEST_FILE_NAME = "manifest.json";
export const DEFAULT_FRAGMENTATION_CONFIG = {
BLOCK_SIZE: 2,
PREFIX: "img",
RESTORE_FILE_NAME: false,
PRESERVE_NAME: false,
};
40 changes: 38 additions & 2 deletions packages/core/src/helpers.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import {
decodeFileName,
encodeFileName,
generateFileName,
generateFragmentFileName,
generateRestoredFileName,
Expand Down Expand Up @@ -95,9 +97,43 @@ describe("generateRestoredFileName", () => {
});
});

describe("encodeFileName / decodeFileName", () => {
test("should encode and decode file names correctly", () => {
const originalName = "test-image.png";
const encoded = encodeFileName(originalName);
expect(encoded).toBeTruthy();
expect(encoded).not.toBe(originalName);
expect(decodeFileName(encoded)).toBe(originalName);
});

test("should handle special characters", () => {
const specialNames = [
"画像_テスト.png",
"test-image-日本語.png",
"file with spaces.png",
"file@#$%^&.png",
];
for (const name of specialNames) {
const encoded = encodeFileName(name);
expect(decodeFileName(encoded)).toBe(name);
}
});
});

describe("generateRestoredOriginalFileName", () => {
test("should generate original file name when name exists", () => {
const imageInfo = { name: "original" } as ManifestData["images"][number];
test("should generate original file name when name exists (base64 encoded)", () => {
const originalName = "original";
const encodedName = encodeFileName(originalName);
const imageInfo = {
name: encodedName,
} as ManifestData["images"][number];
expect(generateRestoredOriginalFileName(imageInfo)).toBe("original.png");
});

test("should handle backward compatibility (non-encoded names)", () => {
const imageInfo = {
name: "b3JpZ2luYWw=",
} as ManifestData["images"][number];
expect(generateRestoredOriginalFileName(imageInfo)).toBe("original.png");
});

Expand Down
45 changes: 44 additions & 1 deletion packages/core/src/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,39 @@
import type { ManifestData } from "./types";

/**
* Encode file name to base64 for safe storage (cross-platform)
* @param name - Original file name
* @returns Base64 encoded file name
*/
export function encodeFileName(name: string): string {
// Use TextEncoder for UTF-8 encoding (available in both browser and Node.js)
const encoder = new TextEncoder();
const bytes = encoder.encode(name);
// Convert bytes to binary string for btoa (cross-platform)
// Use loop to avoid stack overflow with large arrays
let binaryString = "";
for (let i = 0; i < bytes.length; i++) {
const byte = bytes[i];
binaryString += String.fromCharCode(byte);
}
return btoa(binaryString);
}

/**
* Decode file name from base64 (cross-platform)
* @param encodedName - Base64 encoded file name
* @returns Decoded original file name
*/
export function decodeFileName(encodedName: string): string {
// Use atob for base64 decoding (cross-platform)
const binaryString = atob(encodedName);
// Convert binary string to bytes
const bytes = Uint8Array.from(binaryString, (c) => c.charCodeAt(0));
// Use TextDecoder for UTF-8 decoding (available in both browser and Node.js)
const decoder = new TextDecoder();
return decoder.decode(bytes);
}

/**
* Generate a file name with prefix, 1-based zero-padded index, and extension
* @param manifest - Manifest data
Expand Down Expand Up @@ -65,5 +99,14 @@ export function generateRestoredFileName(
export function generateRestoredOriginalFileName(
imageInfo: ManifestData["images"][number],
): string | undefined {
return imageInfo.name ? `${imageInfo.name}.png` : undefined;
if (!imageInfo.name) {
return undefined;
}
try {
const decodedName = decodeFileName(imageInfo.name);
return decodedName ? `${decodedName}.png` : undefined;
} catch {
// Fallback: if decoding fails, treat as already decoded (backward compatibility)
return `${imageInfo.name}.png`;
}
}
6 changes: 3 additions & 3 deletions packages/core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ export interface FragmentationConfig {
prefix?: string;
/** Random seed (auto-generated if not specified) */
seed?: number | string;
/** Restore original file name (optional, default: false) */
restoreFileName?: boolean;
/** Preserve original file name (optional, default: false) */
preserveName?: boolean;
}

/**
Expand All @@ -42,7 +42,7 @@ export interface ImageInfo {
x: number;
/** Number of blocks Y */
y: number;
/** Original file name (optional) */
/** Original file name in base64 encoding (optional) */
name?: string;
}

Expand Down
14 changes: 7 additions & 7 deletions packages/node/src/fragmenter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ describe("ImageFragmenter", () => {
blockSize: 8,
prefix: "test",
seed: "test-seed",
restoreFileName: true,
preserveName: true,
};
const fragmenter = new ImageFragmenter(config);
expect(fragmenter).toBeInstanceOf(ImageFragmenter);
Expand Down Expand Up @@ -159,7 +159,7 @@ describe("ImageFragmenter", () => {
blockSize: 2,
prefix: "testprefix",
seed: "test-seed-123",
restoreFileName: true,
preserveName: true,
};
const fragmenter = new ImageFragmenter(config);

Expand All @@ -171,7 +171,7 @@ describe("ImageFragmenter", () => {
expect(result.manifest.config.blockSize).toBe(2);
expect(result.manifest.config.prefix).toBe("testprefix");
expect(result.manifest.config.seed).toBe("test-seed-123");
expect(result.manifest.config.restoreFileName).toBe(true);
expect(result.manifest.config.preserveName).toBe(true);
});

test("creates valid fragment images", async () => {
Expand Down Expand Up @@ -209,7 +209,7 @@ describe("ImageFragmenter", () => {
);
});

test("throws error on duplicate file names when restoreFileName is enabled", async () => {
test("throws error on duplicate file names when preserveName is enabled", async () => {
// Create two files with the same base name in different directories
const tmpDir2 = path.join(tmpdir(), "fragmenter_test_tmp2");
if (!fs.existsSync(tmpDir2)) fs.mkdirSync(tmpDir2);
Expand All @@ -218,7 +218,7 @@ describe("ImageFragmenter", () => {
await fs.promises.copyFile(testImagePath, duplicatePath);

const fragmenter = new ImageFragmenter({
restoreFileName: true,
preserveName: true,
});

try {
Expand All @@ -243,8 +243,8 @@ describe("ImageFragmenter", () => {
expect(result.manifest.config.prefix).toBe(
DEFAULT_FRAGMENTATION_CONFIG.PREFIX,
);
expect(result.manifest.config.restoreFileName).toBe(
DEFAULT_FRAGMENTATION_CONFIG.RESTORE_FILE_NAME,
expect(result.manifest.config.preserveName).toBe(
DEFAULT_FRAGMENTATION_CONFIG.PRESERVE_NAME,
);
expect(result.manifest.config.seed).toBeDefined(); // Should be auto-generated
});
Expand Down
27 changes: 18 additions & 9 deletions packages/node/src/fragmenter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import {
type ImageInfo,
type ManifestData,
calcBlocksPerFragment,
decodeFileName,
encodeFileName,
} from "@image-shield/core";
import { SeededRandom, shuffle } from "@tuki0918/seeded-shuffle";
import { blocksToPngImage, imageFileToBlocks } from "./block";
Expand All @@ -26,9 +28,8 @@ export class ImageFragmenter {
blockSize: config.blockSize ?? DEFAULT_FRAGMENTATION_CONFIG.BLOCK_SIZE,
prefix: config.prefix ?? DEFAULT_FRAGMENTATION_CONFIG.PREFIX,
seed: config.seed || SeededRandom.generateSeed(),
restoreFileName:
config.restoreFileName ??
DEFAULT_FRAGMENTATION_CONFIG.RESTORE_FILE_NAME,
preserveName:
config.preserveName ?? DEFAULT_FRAGMENTATION_CONFIG.PRESERVE_NAME,
};
}

Expand Down Expand Up @@ -98,18 +99,26 @@ export class ImageFragmenter {
}

private _validateFileNames(imageInfos: ImageInfo[]): void {
if (!this.config.restoreFileName || imageInfos.length <= 1) {
if (!this.config.preserveName || imageInfos.length <= 1) {
return;
}

const nameSet = new Set<string>();

for (const info of imageInfos) {
if (info.name !== undefined) {
if (nameSet.has(info.name)) {
throw new Error(`Duplicate file name detected: ${info.name}`);
// Decode base64 to get original name for comparison
let decodedName: string;
try {
decodedName = decodeFileName(info.name);
} catch {
// If decoding fails, treat as already decoded (backward compatibility)
decodedName = info.name;
}
nameSet.add(info.name);
if (nameSet.has(decodedName)) {
throw new Error(`Duplicate file name detected: ${decodedName}`);
}
nameSet.add(decodedName);
}
}
}
Expand Down Expand Up @@ -163,8 +172,8 @@ export class ImageFragmenter {
c: 4, // Always use 4 channels (RGBA) for generated PNG
x: blockCountX,
y: blockCountY,
name: this.config.restoreFileName
? fileNameWithoutExtension(imagePath)
name: this.config.preserveName
? encodeFileName(fileNameWithoutExtension(imagePath))
: undefined,
};

Expand Down
21 changes: 14 additions & 7 deletions packages/node/src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { tmpdir } from "node:os";
import path from "node:path";
import type { ManifestData } from "@image-shield/core";
import {
decodeFileName,
generateFragmentFileName,
generateRestoredFileName,
} from "@image-shield/core";
Expand Down Expand Up @@ -132,8 +133,8 @@ describe("ImageShield (integration)", () => {
});
});

describe("ImageShield (restoreFileName integration)", () => {
const tmpDir = path.join(tmpdir(), "index_test_tmp_restoreFileName");
describe("ImageShield (preserveName integration)", () => {
const tmpDir = path.join(tmpdir(), "index_test_tmp_preserveName");
const originalImages = [
Buffer.from([
// 2x2 RGBA image 1
Expand Down Expand Up @@ -168,10 +169,10 @@ describe("ImageShield (restoreFileName integration)", () => {
await image.write(filePath, JimpMime.png);
imagePaths.push(filePath);
}
// Fragment images using ImageShield.shuffle (with restoreFileName)
// Fragment images using ImageShield.shuffle (with preserveName)
await ImageShield.shuffle({
imagePaths,
config: { blockSize, prefix, restoreFileName: true },
config: { blockSize, prefix, preserveName: true },
outputDir: tmpDir,
});
// Find manifest and fragment files
Expand Down Expand Up @@ -214,13 +215,19 @@ describe("ImageShield (restoreFileName integration)", () => {
if (fs.existsSync(tmpDir)) fs.rmdirSync(tmpDir);
});

test("manifest images[].name contains original file name when restoreFileName=true", () => {
test("manifest images[].name contains original file name when preserveName=true", () => {
expect(manifest).toBeDefined();
expect(manifest?.config.restoreFileName).toBe(true);
expect(manifest?.config.preserveName).toBe(true);
expect(Array.isArray(manifest?.images)).toBe(true);
for (let i = 0; i < imagePaths.length; i++) {
const expectedName = path.parse(imagePaths[i]).name;
expect(manifest?.images[i].name).toBe(expectedName);
const encodedName = manifest?.images[i]?.name;
expect(encodedName).toBeTruthy();
// Decode base64 encoded name
if (encodedName) {
const decodedName = decodeFileName(encodedName);
expect(decodedName).toBe(expectedName);
}
}
});

Expand Down
Loading