From 66aacb81ea13c80703a04b7c91da1ea11c4ffc74 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 7 Nov 2025 21:14:10 +0000 Subject: [PATCH 1/8] Initial plan From 5f631b8c1e6a666139c0bfa4f2a9f71464f62e45 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 7 Nov 2025 21:25:26 +0000 Subject: [PATCH 2/8] Add per-image shuffle option - Added `perImageShuffle` option to FragmentationConfig interface - Updated ImageFragmenter to support per-image shuffle mode - Updated ImageRestorer to handle per-image unshuffle correctly - Added CLI option `--per-image-shuffle` to the shuffle command - Added comprehensive tests for per-image shuffle functionality - Default remains cross-image shuffle (false) for security Co-authored-by: tuki0918 <886088+tuki0918@users.noreply.github.com> --- packages/cli/src/commands/shuffle.ts | 5 ++ packages/cli/src/types.ts | 1 + packages/core/src/constants.ts | 1 + packages/core/src/types.ts | 2 + packages/node/src/fragmenter.test.ts | 74 ++++++++++++++++++ packages/node/src/fragmenter.ts | 29 ++++++- packages/node/src/restorer.test.ts | 111 +++++++++++++++++++++++++++ packages/node/src/restorer.ts | 28 ++++++- 8 files changed, 248 insertions(+), 3 deletions(-) diff --git a/packages/cli/src/commands/shuffle.ts b/packages/cli/src/commands/shuffle.ts index 1640fa6..77ecc6e 100644 --- a/packages/cli/src/commands/shuffle.ts +++ b/packages/cli/src/commands/shuffle.ts @@ -30,6 +30,10 @@ export function registerShuffleCommand(program: Command): void { return num; }) .option("--preserve-name", "Preserve original file names") + .option( + "--per-image-shuffle", + "Shuffle blocks within each image independently instead of across all images", + ) .action(handleShuffleCommand); } @@ -53,6 +57,7 @@ async function handleShuffleCommand( if (options.prefix !== undefined) config.prefix = options.prefix; if (options.seed !== undefined) config.seed = options.seed; if (options.preserveName) config.preserveName = true; + if (options.perImageShuffle) config.perImageShuffle = true; await ImageShield.shuffle({ imagePaths, diff --git a/packages/cli/src/types.ts b/packages/cli/src/types.ts index 60d2c47..c188fd1 100644 --- a/packages/cli/src/types.ts +++ b/packages/cli/src/types.ts @@ -4,6 +4,7 @@ export interface ShuffleOptions { prefix?: string; seed?: number; preserveName?: boolean; + perImageShuffle?: boolean; } export interface RestoreOptions { diff --git a/packages/core/src/constants.ts b/packages/core/src/constants.ts index b1d717d..4c62225 100644 --- a/packages/core/src/constants.ts +++ b/packages/core/src/constants.ts @@ -4,4 +4,5 @@ export const DEFAULT_FRAGMENTATION_CONFIG = { BLOCK_SIZE: 2, PREFIX: "img", PRESERVE_NAME: false, + PER_IMAGE_SHUFFLE: false, }; diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index de8de86..7de1b51 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -25,6 +25,8 @@ export interface FragmentationConfig { seed?: number | string; /** Preserve original file name (optional, default: false) */ preserveName?: boolean; + /** Shuffle blocks per image instead of across all images (optional, default: false) */ + perImageShuffle?: boolean; } /** diff --git a/packages/node/src/fragmenter.test.ts b/packages/node/src/fragmenter.test.ts index 190cacc..3c2c18b 100644 --- a/packages/node/src/fragmenter.test.ts +++ b/packages/node/src/fragmenter.test.ts @@ -275,4 +275,78 @@ describe("ImageFragmenter", () => { } }); }); + + describe("perImageShuffle", () => { + test("fragments with per-image shuffle enabled", async () => { + const fragmenter = new ImageFragmenter({ + blockSize: 2, + seed: "test-seed", + perImageShuffle: true, + }); + + const result = await fragmenter.fragmentImages([testImagePath]); + + expect(result.manifest.config.perImageShuffle).toBe(true); + expect(result.fragmentedImages).toHaveLength(1); + }); + + test("fragments multiple images with per-image shuffle", async () => { + const fragmenter = new ImageFragmenter({ + blockSize: 2, + seed: "test-seed", + perImageShuffle: true, + }); + + const result = await fragmenter.fragmentImages([ + testImagePath, + testImagePath, + ]); + + expect(result.manifest.config.perImageShuffle).toBe(true); + expect(result.manifest.images).toHaveLength(2); + expect(result.fragmentedImages).toHaveLength(2); + }); + + test("per-image shuffle produces different results than cross-image shuffle", async () => { + const seed = "test-seed-123"; + + const fragmenterCrossImage = new ImageFragmenter({ + blockSize: 2, + seed, + perImageShuffle: false, + }); + + const fragmenterPerImage = new ImageFragmenter({ + blockSize: 2, + seed, + perImageShuffle: true, + }); + + const resultCrossImage = await fragmenterCrossImage.fragmentImages([ + testImagePath, + testImagePath, + ]); + + const resultPerImage = await fragmenterPerImage.fragmentImages([ + testImagePath, + testImagePath, + ]); + + // With the same seed but different shuffle modes, results should differ + expect(resultCrossImage.fragmentedImages[0]).not.toEqual( + resultPerImage.fragmentedImages[0], + ); + }); + + test("defaults to cross-image shuffle when perImageShuffle is not specified", async () => { + const fragmenter = new ImageFragmenter({ + blockSize: 2, + seed: "test-seed", + }); + + const result = await fragmenter.fragmentImages([testImagePath]); + + expect(result.manifest.config.perImageShuffle).toBe(false); + }); + }); }); diff --git a/packages/node/src/fragmenter.ts b/packages/node/src/fragmenter.ts index 8e265a4..ee3adca 100644 --- a/packages/node/src/fragmenter.ts +++ b/packages/node/src/fragmenter.ts @@ -30,6 +30,9 @@ export class ImageFragmenter { seed: config.seed || SeededRandom.generateSeed(), preserveName: config.preserveName ?? DEFAULT_FRAGMENTATION_CONFIG.PRESERVE_NAME, + perImageShuffle: + config.perImageShuffle ?? + DEFAULT_FRAGMENTATION_CONFIG.PER_IMAGE_SHUFFLE, }; } @@ -37,7 +40,13 @@ export class ImageFragmenter { const { manifest, allBlocks, fragmentBlocksCount } = await this._prepareFragmentData(imagePaths); - const shuffledBlocks = shuffle(allBlocks, manifest.config.seed); + const shuffledBlocks = this.config.perImageShuffle + ? this._shufflePerImage( + allBlocks, + fragmentBlocksCount, + manifest.config.seed, + ) + : shuffle(allBlocks, manifest.config.seed); const fragmentedImages = await this._createFragmentedImages( shuffledBlocks, @@ -71,6 +80,24 @@ export class ImageFragmenter { ); } + private _shufflePerImage( + allBlocks: Buffer[], + fragmentBlocksCount: number[], + seed: number | string, + ): Buffer[] { + const shuffledBlocks: Buffer[] = []; + let offset = 0; + + for (const blockCount of fragmentBlocksCount) { + const imageBlocks = allBlocks.slice(offset, offset + blockCount); + const shuffled = shuffle(imageBlocks, seed); + shuffledBlocks.push(...shuffled); + offset += blockCount; + } + + return shuffledBlocks; + } + private _calculateBlockRange( fragmentBlocksCount: number[], targetIndex: number, diff --git a/packages/node/src/restorer.test.ts b/packages/node/src/restorer.test.ts index 68c3576..2761828 100644 --- a/packages/node/src/restorer.test.ts +++ b/packages/node/src/restorer.test.ts @@ -383,4 +383,115 @@ describe("ImageRestorer", () => { expect(manifest.config.prefix).toBe("custom-prefix"); }); }); + + describe("perImageShuffle restoration", () => { + test("restores single image with per-image shuffle", async () => { + const fragmenter = new ImageFragmenter({ + blockSize: 2, + seed: "test-seed", + perImageShuffle: true, + }); + const { manifest, fragmentedImages } = await fragmenter.fragmentImages([ + testImagePath, + ]); + + const restorer = new ImageRestorer(); + const restoredImages = await restorer.restoreImages( + fragmentedImages, + manifest, + ); + + expect(restoredImages).toHaveLength(1); + expect(manifest.config.perImageShuffle).toBe(true); + + const jimpImage = await Jimp.read(restoredImages[0]); + expect(jimpImage.bitmap.width).toBe(4); + expect(jimpImage.bitmap.height).toBe(4); + }); + + test("restores multiple images with per-image shuffle", async () => { + const fragmenter = new ImageFragmenter({ + blockSize: 2, + seed: "test-seed", + perImageShuffle: true, + }); + const { manifest, fragmentedImages } = await fragmenter.fragmentImages([ + testImagePath, + testImagePath, + ]); + + const restorer = new ImageRestorer(); + const restoredImages = await restorer.restoreImages( + fragmentedImages, + manifest, + ); + + expect(restoredImages).toHaveLength(2); + expect(manifest.config.perImageShuffle).toBe(true); + + for (const restoredImage of restoredImages) { + const jimpImage = await Jimp.read(restoredImage); + expect(jimpImage.bitmap.width).toBe(4); + expect(jimpImage.bitmap.height).toBe(4); + } + }); + + test("round-trip with per-image shuffle preserves image data", async () => { + const fragmenter = new ImageFragmenter({ + blockSize: 1, + seed: "deterministic-seed", + perImageShuffle: true, + }); + + const { manifest, fragmentedImages } = await fragmenter.fragmentImages([ + testImagePath, + ]); + + const restorer = new ImageRestorer(); + const restoredImages = await restorer.restoreImages( + fragmentedImages, + manifest, + ); + + const originalJimp = await Jimp.read(testImagePath); + const restoredJimp = await Jimp.read(restoredImages[0]); + + expect(restoredJimp.bitmap.width).toBe(originalJimp.bitmap.width); + expect(restoredJimp.bitmap.height).toBe(originalJimp.bitmap.height); + expect(restoredJimp.bitmap.data.length).toBe( + originalJimp.bitmap.data.length, + ); + }); + + test("round-trip with multiple images and per-image shuffle", async () => { + const fragmenter = new ImageFragmenter({ + blockSize: 1, + seed: "deterministic-seed", + perImageShuffle: true, + }); + + const { manifest, fragmentedImages } = await fragmenter.fragmentImages([ + testImagePath, + testImagePath, + ]); + + const restorer = new ImageRestorer(); + const restoredImages = await restorer.restoreImages( + fragmentedImages, + manifest, + ); + + expect(restoredImages).toHaveLength(2); + + const originalJimp = await Jimp.read(testImagePath); + for (const restoredImage of restoredImages) { + const restoredJimp = await Jimp.read(restoredImage); + expect(restoredJimp.bitmap.width).toBe(originalJimp.bitmap.width); + expect(restoredJimp.bitmap.height).toBe(originalJimp.bitmap.height); + expect(restoredJimp.bitmap.data.length).toBe( + originalJimp.bitmap.data.length, + ); + } + }); + }); }); diff --git a/packages/node/src/restorer.ts b/packages/node/src/restorer.ts index f02da7e..521f9e0 100644 --- a/packages/node/src/restorer.ts +++ b/packages/node/src/restorer.ts @@ -12,12 +12,18 @@ export class ImageRestorer { fragmentImages: (string | Buffer)[], manifest: ManifestData, ): Promise { - const { allBlocks } = await this._prepareRestoreData( + const { allBlocks, fragmentBlocksCount } = await this._prepareRestoreData( fragmentImages, manifest, ); - const restoredBlocks = unshuffle(allBlocks, manifest.config.seed); + const restoredBlocks = manifest.config.perImageShuffle + ? this._unshufflePerImage( + allBlocks, + fragmentBlocksCount, + manifest.config.seed, + ) + : unshuffle(allBlocks, manifest.config.seed); const reconstructedImages = await this._reconstructAllImages( restoredBlocks, @@ -141,4 +147,22 @@ export class ImageRestorer { const { w, h } = imageInfo; return await blocksToPngImage(blocks, w, h, blockSize); } + + private _unshufflePerImage( + allBlocks: Buffer[], + fragmentBlocksCount: number[], + seed: number | string, + ): Buffer[] { + const unshuffledBlocks: Buffer[] = []; + let offset = 0; + + for (const blockCount of fragmentBlocksCount) { + const imageBlocks = allBlocks.slice(offset, offset + blockCount); + const unshuffled = unshuffle(imageBlocks, seed); + unshuffledBlocks.push(...unshuffled); + offset += blockCount; + } + + return unshuffledBlocks; + } } From 7d432074f2cdf979bb2f2c17e0cca212a063964d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 7 Nov 2025 21:28:49 +0000 Subject: [PATCH 3/8] Fix per-image shuffle to use actual image block counts - Use imageBlockCounts (actual blocks per image) instead of fragmentBlocksCount - fragmentBlocksCount distributes blocks evenly across fragments - imageBlockCounts represents actual block counts per image (x * y) - This ensures blocks are shuffled correctly within each image boundary Co-authored-by: tuki0918 <886088+tuki0918@users.noreply.github.com> --- packages/node/src/fragmenter.ts | 14 +++++++------- packages/node/src/restorer.ts | 19 ++++++++++++------- 2 files changed, 19 insertions(+), 14 deletions(-) diff --git a/packages/node/src/fragmenter.ts b/packages/node/src/fragmenter.ts index ee3adca..1274190 100644 --- a/packages/node/src/fragmenter.ts +++ b/packages/node/src/fragmenter.ts @@ -37,15 +37,11 @@ export class ImageFragmenter { } async fragmentImages(imagePaths: string[]): Promise { - const { manifest, allBlocks, fragmentBlocksCount } = + const { manifest, allBlocks, fragmentBlocksCount, imageBlockCounts } = await this._prepareFragmentData(imagePaths); const shuffledBlocks = this.config.perImageShuffle - ? this._shufflePerImage( - allBlocks, - fragmentBlocksCount, - manifest.config.seed, - ) + ? this._shufflePerImage(allBlocks, imageBlockCounts, manifest.config.seed) : shuffle(allBlocks, manifest.config.seed); const fragmentedImages = await this._createFragmentedImages( @@ -154,6 +150,7 @@ export class ImageFragmenter { manifest: ManifestData; allBlocks: Buffer[]; fragmentBlocksCount: number[]; + imageBlockCounts: number[]; }> { const manifestId = generateManifestId(); @@ -167,7 +164,10 @@ export class ImageFragmenter { imagePaths.length, ); - return { manifest, allBlocks, fragmentBlocksCount }; + // Calculate actual block counts per image for per-image shuffle + const imageBlockCounts = imageInfos.map((info) => info.x * info.y); + + return { manifest, allBlocks, fragmentBlocksCount, imageBlockCounts }; } private async _processSourceImages(imagePaths: string[]): Promise<{ diff --git a/packages/node/src/restorer.ts b/packages/node/src/restorer.ts index 521f9e0..31305b0 100644 --- a/packages/node/src/restorer.ts +++ b/packages/node/src/restorer.ts @@ -12,15 +12,13 @@ export class ImageRestorer { fragmentImages: (string | Buffer)[], manifest: ManifestData, ): Promise { - const { allBlocks, fragmentBlocksCount } = await this._prepareRestoreData( - fragmentImages, - manifest, - ); + const { allBlocks, fragmentBlocksCount, imageBlockCounts } = + await this._prepareRestoreData(fragmentImages, manifest); const restoredBlocks = manifest.config.perImageShuffle ? this._unshufflePerImage( allBlocks, - fragmentBlocksCount, + imageBlockCounts, manifest.config.seed, ) : unshuffle(allBlocks, manifest.config.seed); @@ -69,7 +67,11 @@ export class ImageRestorer { private async _prepareRestoreData( fragmentImages: (string | Buffer)[], manifest: ManifestData, - ): Promise<{ allBlocks: Buffer[]; fragmentBlocksCount: number[] }> { + ): Promise<{ + allBlocks: Buffer[]; + fragmentBlocksCount: number[]; + imageBlockCounts: number[]; + }> { this._validateInputs(fragmentImages, manifest); const totalBlocks = this._calculateTotalBlocks(manifest.images); @@ -84,7 +86,10 @@ export class ImageRestorer { fragmentBlocksCount, ); - return { allBlocks, fragmentBlocksCount }; + // Calculate actual block counts per image for per-image unshuffle + const imageBlockCounts = manifest.images.map((info) => info.x * info.y); + + return { allBlocks, fragmentBlocksCount, imageBlockCounts }; } private _validateInputs( From 628b8696749160461c02b39e2effa544feea6b7b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 7 Nov 2025 21:31:42 +0000 Subject: [PATCH 4/8] Add tests for different-sized images with per-image shuffle - Added test for fragmenting different-sized images with per-image shuffle - Added test for round-trip restoration with different-sized images - Verifies correct handling of different block counts per image - All 52 tests pass Co-authored-by: tuki0918 <886088+tuki0918@users.noreply.github.com> --- packages/node/src/fragmenter.test.ts | 75 ++++++++++++++++++++++++++ packages/node/src/restorer.test.ts | 81 ++++++++++++++++++++++++++++ 2 files changed, 156 insertions(+) diff --git a/packages/node/src/fragmenter.test.ts b/packages/node/src/fragmenter.test.ts index 3c2c18b..681aa9f 100644 --- a/packages/node/src/fragmenter.test.ts +++ b/packages/node/src/fragmenter.test.ts @@ -348,5 +348,80 @@ describe("ImageFragmenter", () => { expect(result.manifest.config.perImageShuffle).toBe(false); }); + + test("handles images of different sizes with per-image shuffle", async () => { + // Create test images of different sizes + const smallImagePath = path.join(tmpDir, "small_test.png"); + const largeImagePath = path.join(tmpDir, "large_test.png"); + + // Create 2x2 image + const smallImageData = Buffer.from([ + 255, + 0, + 0, + 255, // Red + 0, + 255, + 0, + 255, // Green + 0, + 0, + 255, + 255, // Blue + 255, + 255, + 0, + 255, // Yellow + ]); + const smallImage = Jimp.fromBitmap({ + data: smallImageData, + width: 2, + height: 2, + }); + await smallImage.write(smallImagePath, JimpMime.png); + + // Create 6x6 image + const largeImageData = Buffer.alloc(6 * 6 * 4); + for (let i = 0; i < 6 * 6 * 4; i += 4) { + largeImageData[i] = 128; // R + largeImageData[i + 1] = 128; // G + largeImageData[i + 2] = 128; // B + largeImageData[i + 3] = 255; // A + } + const largeImage = Jimp.fromBitmap({ + data: largeImageData, + width: 6, + height: 6, + }); + await largeImage.write(largeImagePath, JimpMime.png); + + try { + const fragmenter = new ImageFragmenter({ + blockSize: 2, + seed: "test-seed", + perImageShuffle: true, + }); + + const result = await fragmenter.fragmentImages([ + smallImagePath, + largeImagePath, + ]); + + // Should successfully fragment both images + expect(result.manifest.images).toHaveLength(2); + expect(result.fragmentedImages).toHaveLength(2); + expect(result.manifest.config.perImageShuffle).toBe(true); + + // Verify different block counts + expect(result.manifest.images[0].x).toBe(1); // 2x2 image with blockSize 2 + expect(result.manifest.images[0].y).toBe(1); + expect(result.manifest.images[1].x).toBe(3); // 6x6 image with blockSize 2 + expect(result.manifest.images[1].y).toBe(3); + } finally { + // Clean up + if (fs.existsSync(smallImagePath)) fs.unlinkSync(smallImagePath); + if (fs.existsSync(largeImagePath)) fs.unlinkSync(largeImagePath); + } + }); }); }); diff --git a/packages/node/src/restorer.test.ts b/packages/node/src/restorer.test.ts index 2761828..d4649b9 100644 --- a/packages/node/src/restorer.test.ts +++ b/packages/node/src/restorer.test.ts @@ -493,5 +493,86 @@ describe("ImageRestorer", () => { ); } }); + + test("round-trip with different-sized images and per-image shuffle", async () => { + // Create test images of different sizes + const smallImagePath = path.join(tmpDir, "small_restore_test.png"); + const largeImagePath = path.join(tmpDir, "large_restore_test.png"); + + // Create 2x2 image + const smallImageData = Buffer.from([ + 255, + 0, + 0, + 255, // Red + 0, + 255, + 0, + 255, // Green + 0, + 0, + 255, + 255, // Blue + 255, + 255, + 0, + 255, // Yellow + ]); + const smallImage = Jimp.fromBitmap({ + data: smallImageData, + width: 2, + height: 2, + }); + await smallImage.write(smallImagePath, JimpMime.png); + + // Create 6x6 image + const largeImageData = Buffer.alloc(6 * 6 * 4); + for (let i = 0; i < 6 * 6 * 4; i += 4) { + largeImageData[i] = 128; // R + largeImageData[i + 1] = 128; // G + largeImageData[i + 2] = 128; // B + largeImageData[i + 3] = 255; // A + } + const largeImage = Jimp.fromBitmap({ + data: largeImageData, + width: 6, + height: 6, + }); + await largeImage.write(largeImagePath, JimpMime.png); + + try { + const fragmenter = new ImageFragmenter({ + blockSize: 2, + seed: "deterministic-seed", + perImageShuffle: true, + }); + + const { manifest, fragmentedImages } = await fragmenter.fragmentImages([ + smallImagePath, + largeImagePath, + ]); + + const restorer = new ImageRestorer(); + const restoredImages = await restorer.restoreImages( + fragmentedImages, + manifest, + ); + + expect(restoredImages).toHaveLength(2); + + // Verify restored dimensions match originals + const smallJimp = await Jimp.read(restoredImages[0]); + const largeJimp = await Jimp.read(restoredImages[1]); + + expect(smallJimp.bitmap.width).toBe(2); + expect(smallJimp.bitmap.height).toBe(2); + expect(largeJimp.bitmap.width).toBe(6); + expect(largeJimp.bitmap.height).toBe(6); + } finally { + // Clean up + if (fs.existsSync(smallImagePath)) fs.unlinkSync(smallImagePath); + if (fs.existsSync(largeImagePath)) fs.unlinkSync(largeImagePath); + } + }); }); }); From f84dd980f3b2d1461369069b42c6697cd9819a34 Mon Sep 17 00:00:00 2001 From: "Y.Yamamoto" Date: Tue, 11 Nov 2025 00:43:25 +0900 Subject: [PATCH 5/8] Rename perImageShuffle to crossImageShuffle for clarity and consistency - Updated the ShuffleOptions and FragmentationConfig interfaces to replace `perImageShuffle` with `crossImageShuffle`. - Modified the shuffle command and related logic to reflect the new naming. - Adjusted tests to ensure they validate the new `crossImageShuffle` functionality. - Ensured backward compatibility by updating references throughout the codebase. --- packages/cli/src/commands/shuffle.ts | 6 +- packages/cli/src/types.ts | 2 +- packages/core/src/constants.ts | 2 +- packages/core/src/types.ts | 4 +- packages/node/src/fragmenter.test.ts | 129 +++++++++-- packages/node/src/fragmenter.ts | 16 +- packages/node/src/restorer.test.ts | 314 ++++++++++++++++++++++++++- packages/node/src/restorer.ts | 8 +- 8 files changed, 441 insertions(+), 40 deletions(-) diff --git a/packages/cli/src/commands/shuffle.ts b/packages/cli/src/commands/shuffle.ts index 77ecc6e..c978c76 100644 --- a/packages/cli/src/commands/shuffle.ts +++ b/packages/cli/src/commands/shuffle.ts @@ -31,8 +31,8 @@ export function registerShuffleCommand(program: Command): void { }) .option("--preserve-name", "Preserve original file names") .option( - "--per-image-shuffle", - "Shuffle blocks within each image independently instead of across all images", + "--cross-image-shuffle", + "Shuffle blocks across all images instead of within each image independently", ) .action(handleShuffleCommand); } @@ -57,7 +57,7 @@ async function handleShuffleCommand( if (options.prefix !== undefined) config.prefix = options.prefix; if (options.seed !== undefined) config.seed = options.seed; if (options.preserveName) config.preserveName = true; - if (options.perImageShuffle) config.perImageShuffle = true; + if (options.crossImageShuffle) config.crossImageShuffle = true; await ImageShield.shuffle({ imagePaths, diff --git a/packages/cli/src/types.ts b/packages/cli/src/types.ts index c188fd1..0bab7d9 100644 --- a/packages/cli/src/types.ts +++ b/packages/cli/src/types.ts @@ -4,7 +4,7 @@ export interface ShuffleOptions { prefix?: string; seed?: number; preserveName?: boolean; - perImageShuffle?: boolean; + crossImageShuffle?: boolean; } export interface RestoreOptions { diff --git a/packages/core/src/constants.ts b/packages/core/src/constants.ts index 4c62225..3706858 100644 --- a/packages/core/src/constants.ts +++ b/packages/core/src/constants.ts @@ -4,5 +4,5 @@ export const DEFAULT_FRAGMENTATION_CONFIG = { BLOCK_SIZE: 2, PREFIX: "img", PRESERVE_NAME: false, - PER_IMAGE_SHUFFLE: false, + CROSS_IMAGE_SHUFFLE: false, }; diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 7de1b51..32d1942 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -25,8 +25,8 @@ export interface FragmentationConfig { seed?: number | string; /** Preserve original file name (optional, default: false) */ preserveName?: boolean; - /** Shuffle blocks per image instead of across all images (optional, default: false) */ - perImageShuffle?: boolean; + /** Shuffle blocks across all images instead of within each image independently (optional, default: false) */ + crossImageShuffle?: boolean; } /** diff --git a/packages/node/src/fragmenter.test.ts b/packages/node/src/fragmenter.test.ts index 681aa9f..5b23e4c 100644 --- a/packages/node/src/fragmenter.test.ts +++ b/packages/node/src/fragmenter.test.ts @@ -276,25 +276,23 @@ describe("ImageFragmenter", () => { }); }); - describe("perImageShuffle", () => { - test("fragments with per-image shuffle enabled", async () => { + describe("crossImageShuffle", () => { + test("fragments with per-image shuffle (default)", async () => { const fragmenter = new ImageFragmenter({ blockSize: 2, seed: "test-seed", - perImageShuffle: true, }); const result = await fragmenter.fragmentImages([testImagePath]); - expect(result.manifest.config.perImageShuffle).toBe(true); + expect(result.manifest.config.crossImageShuffle).toBe(false); expect(result.fragmentedImages).toHaveLength(1); }); - test("fragments multiple images with per-image shuffle", async () => { + test("fragments multiple images with per-image shuffle (default)", async () => { const fragmenter = new ImageFragmenter({ blockSize: 2, seed: "test-seed", - perImageShuffle: true, }); const result = await fragmenter.fragmentImages([ @@ -302,7 +300,7 @@ describe("ImageFragmenter", () => { testImagePath, ]); - expect(result.manifest.config.perImageShuffle).toBe(true); + expect(result.manifest.config.crossImageShuffle).toBe(false); expect(result.manifest.images).toHaveLength(2); expect(result.fragmentedImages).toHaveLength(2); }); @@ -313,13 +311,13 @@ describe("ImageFragmenter", () => { const fragmenterCrossImage = new ImageFragmenter({ blockSize: 2, seed, - perImageShuffle: false, + crossImageShuffle: true, }); const fragmenterPerImage = new ImageFragmenter({ blockSize: 2, seed, - perImageShuffle: true, + crossImageShuffle: false, }); const resultCrossImage = await fragmenterCrossImage.fragmentImages([ @@ -338,7 +336,7 @@ describe("ImageFragmenter", () => { ); }); - test("defaults to cross-image shuffle when perImageShuffle is not specified", async () => { + test("defaults to per-image shuffle when crossImageShuffle is not specified", async () => { const fragmenter = new ImageFragmenter({ blockSize: 2, seed: "test-seed", @@ -346,7 +344,7 @@ describe("ImageFragmenter", () => { const result = await fragmenter.fragmentImages([testImagePath]); - expect(result.manifest.config.perImageShuffle).toBe(false); + expect(result.manifest.config.crossImageShuffle).toBe(false); }); test("handles images of different sizes with per-image shuffle", async () => { @@ -399,7 +397,7 @@ describe("ImageFragmenter", () => { const fragmenter = new ImageFragmenter({ blockSize: 2, seed: "test-seed", - perImageShuffle: true, + crossImageShuffle: false, }); const result = await fragmenter.fragmentImages([ @@ -410,7 +408,112 @@ describe("ImageFragmenter", () => { // Should successfully fragment both images expect(result.manifest.images).toHaveLength(2); expect(result.fragmentedImages).toHaveLength(2); - expect(result.manifest.config.perImageShuffle).toBe(true); + expect(result.manifest.config.crossImageShuffle).toBe(false); + + // Verify different block counts + expect(result.manifest.images[0].x).toBe(1); // 2x2 image with blockSize 2 + expect(result.manifest.images[0].y).toBe(1); + expect(result.manifest.images[1].x).toBe(3); // 6x6 image with blockSize 2 + expect(result.manifest.images[1].y).toBe(3); + } finally { + // Clean up + if (fs.existsSync(smallImagePath)) fs.unlinkSync(smallImagePath); + if (fs.existsSync(largeImagePath)) fs.unlinkSync(largeImagePath); + } + }); + + test("fragments with cross-image shuffle enabled", async () => { + const fragmenter = new ImageFragmenter({ + blockSize: 2, + seed: "test-seed", + crossImageShuffle: true, + }); + + const result = await fragmenter.fragmentImages([testImagePath]); + + expect(result.manifest.config.crossImageShuffle).toBe(true); + expect(result.fragmentedImages).toHaveLength(1); + }); + + test("fragments multiple images with cross-image shuffle", async () => { + const fragmenter = new ImageFragmenter({ + blockSize: 2, + seed: "test-seed", + crossImageShuffle: true, + }); + + const result = await fragmenter.fragmentImages([ + testImagePath, + testImagePath, + ]); + + expect(result.manifest.config.crossImageShuffle).toBe(true); + expect(result.manifest.images).toHaveLength(2); + expect(result.fragmentedImages).toHaveLength(2); + }); + + test("handles images of different sizes with cross-image shuffle", async () => { + // Create test images of different sizes + const smallImagePath = path.join(tmpDir, "small_cross_test.png"); + const largeImagePath = path.join(tmpDir, "large_cross_test.png"); + + // Create 2x2 image + const smallImageData = Buffer.from([ + 255, + 0, + 0, + 255, // Red + 0, + 255, + 0, + 255, // Green + 0, + 0, + 255, + 255, // Blue + 255, + 255, + 0, + 255, // Yellow + ]); + const smallImage = Jimp.fromBitmap({ + data: smallImageData, + width: 2, + height: 2, + }); + await smallImage.write(smallImagePath, JimpMime.png); + + // Create 6x6 image + const largeImageData = Buffer.alloc(6 * 6 * 4); + for (let i = 0; i < 6 * 6 * 4; i += 4) { + largeImageData[i] = 128; // R + largeImageData[i + 1] = 128; // G + largeImageData[i + 2] = 128; // B + largeImageData[i + 3] = 255; // A + } + const largeImage = Jimp.fromBitmap({ + data: largeImageData, + width: 6, + height: 6, + }); + await largeImage.write(largeImagePath, JimpMime.png); + + try { + const fragmenter = new ImageFragmenter({ + blockSize: 2, + seed: "test-seed", + crossImageShuffle: true, + }); + + const result = await fragmenter.fragmentImages([ + smallImagePath, + largeImagePath, + ]); + + // Should successfully fragment both images + expect(result.manifest.images).toHaveLength(2); + expect(result.fragmentedImages).toHaveLength(2); + expect(result.manifest.config.crossImageShuffle).toBe(true); // Verify different block counts expect(result.manifest.images[0].x).toBe(1); // 2x2 image with blockSize 2 diff --git a/packages/node/src/fragmenter.ts b/packages/node/src/fragmenter.ts index 1274190..0c55b51 100644 --- a/packages/node/src/fragmenter.ts +++ b/packages/node/src/fragmenter.ts @@ -30,9 +30,9 @@ export class ImageFragmenter { seed: config.seed || SeededRandom.generateSeed(), preserveName: config.preserveName ?? DEFAULT_FRAGMENTATION_CONFIG.PRESERVE_NAME, - perImageShuffle: - config.perImageShuffle ?? - DEFAULT_FRAGMENTATION_CONFIG.PER_IMAGE_SHUFFLE, + crossImageShuffle: + config.crossImageShuffle ?? + DEFAULT_FRAGMENTATION_CONFIG.CROSS_IMAGE_SHUFFLE, }; } @@ -40,9 +40,13 @@ export class ImageFragmenter { const { manifest, allBlocks, fragmentBlocksCount, imageBlockCounts } = await this._prepareFragmentData(imagePaths); - const shuffledBlocks = this.config.perImageShuffle - ? this._shufflePerImage(allBlocks, imageBlockCounts, manifest.config.seed) - : shuffle(allBlocks, manifest.config.seed); + const shuffledBlocks = this.config.crossImageShuffle + ? shuffle(allBlocks, manifest.config.seed) + : this._shufflePerImage( + allBlocks, + imageBlockCounts, + manifest.config.seed, + ); const fragmentedImages = await this._createFragmentedImages( shuffledBlocks, diff --git a/packages/node/src/restorer.test.ts b/packages/node/src/restorer.test.ts index d4649b9..e8dc019 100644 --- a/packages/node/src/restorer.test.ts +++ b/packages/node/src/restorer.test.ts @@ -282,6 +282,27 @@ describe("ImageRestorer", () => { expect(restoredJimp.bitmap.data.length).toBe( originalJimp.bitmap.data.length, ); + + // Verify pixel RGBA values match + for (let y = 0; y < originalJimp.bitmap.height; y++) { + for (let x = 0; x < originalJimp.bitmap.width; x++) { + const pixelIndex = (y * originalJimp.bitmap.width + x) * 4; + const originalR = originalJimp.bitmap.data[pixelIndex]; + const originalG = originalJimp.bitmap.data[pixelIndex + 1]; + const originalB = originalJimp.bitmap.data[pixelIndex + 2]; + const originalA = originalJimp.bitmap.data[pixelIndex + 3]; + + const restoredR = restoredJimp.bitmap.data[pixelIndex]; + const restoredG = restoredJimp.bitmap.data[pixelIndex + 1]; + const restoredB = restoredJimp.bitmap.data[pixelIndex + 2]; + const restoredA = restoredJimp.bitmap.data[pixelIndex + 3]; + + expect(restoredR).toBe(originalR); + expect(restoredG).toBe(originalG); + expect(restoredB).toBe(originalB); + expect(restoredA).toBe(originalA); + } + } }); }); @@ -384,12 +405,11 @@ describe("ImageRestorer", () => { }); }); - describe("perImageShuffle restoration", () => { - test("restores single image with per-image shuffle", async () => { + describe("crossImageShuffle restoration", () => { + test("restores single image with per-image shuffle (default)", async () => { const fragmenter = new ImageFragmenter({ blockSize: 2, seed: "test-seed", - perImageShuffle: true, }); const { manifest, fragmentedImages } = await fragmenter.fragmentImages([ testImagePath, @@ -402,18 +422,17 @@ describe("ImageRestorer", () => { ); expect(restoredImages).toHaveLength(1); - expect(manifest.config.perImageShuffle).toBe(true); + expect(manifest.config.crossImageShuffle).toBe(false); const jimpImage = await Jimp.read(restoredImages[0]); expect(jimpImage.bitmap.width).toBe(4); expect(jimpImage.bitmap.height).toBe(4); }); - test("restores multiple images with per-image shuffle", async () => { + test("restores multiple images with per-image shuffle (default)", async () => { const fragmenter = new ImageFragmenter({ blockSize: 2, seed: "test-seed", - perImageShuffle: true, }); const { manifest, fragmentedImages } = await fragmenter.fragmentImages([ testImagePath, @@ -427,7 +446,7 @@ describe("ImageRestorer", () => { ); expect(restoredImages).toHaveLength(2); - expect(manifest.config.perImageShuffle).toBe(true); + expect(manifest.config.crossImageShuffle).toBe(false); for (const restoredImage of restoredImages) { const jimpImage = await Jimp.read(restoredImage); @@ -440,7 +459,7 @@ describe("ImageRestorer", () => { const fragmenter = new ImageFragmenter({ blockSize: 1, seed: "deterministic-seed", - perImageShuffle: true, + crossImageShuffle: false, }); const { manifest, fragmentedImages } = await fragmenter.fragmentImages([ @@ -461,13 +480,34 @@ describe("ImageRestorer", () => { expect(restoredJimp.bitmap.data.length).toBe( originalJimp.bitmap.data.length, ); + + // Verify pixel RGBA values match + for (let y = 0; y < originalJimp.bitmap.height; y++) { + for (let x = 0; x < originalJimp.bitmap.width; x++) { + const pixelIndex = (y * originalJimp.bitmap.width + x) * 4; + const originalR = originalJimp.bitmap.data[pixelIndex]; + const originalG = originalJimp.bitmap.data[pixelIndex + 1]; + const originalB = originalJimp.bitmap.data[pixelIndex + 2]; + const originalA = originalJimp.bitmap.data[pixelIndex + 3]; + + const restoredR = restoredJimp.bitmap.data[pixelIndex]; + const restoredG = restoredJimp.bitmap.data[pixelIndex + 1]; + const restoredB = restoredJimp.bitmap.data[pixelIndex + 2]; + const restoredA = restoredJimp.bitmap.data[pixelIndex + 3]; + + expect(restoredR).toBe(originalR); + expect(restoredG).toBe(originalG); + expect(restoredB).toBe(originalB); + expect(restoredA).toBe(originalA); + } + } }); test("round-trip with multiple images and per-image shuffle", async () => { const fragmenter = new ImageFragmenter({ blockSize: 1, seed: "deterministic-seed", - perImageShuffle: true, + crossImageShuffle: false, }); const { manifest, fragmentedImages } = await fragmenter.fragmentImages([ @@ -491,6 +531,27 @@ describe("ImageRestorer", () => { expect(restoredJimp.bitmap.data.length).toBe( originalJimp.bitmap.data.length, ); + + // Verify pixel RGBA values match + for (let y = 0; y < originalJimp.bitmap.height; y++) { + for (let x = 0; x < originalJimp.bitmap.width; x++) { + const pixelIndex = (y * originalJimp.bitmap.width + x) * 4; + const originalR = originalJimp.bitmap.data[pixelIndex]; + const originalG = originalJimp.bitmap.data[pixelIndex + 1]; + const originalB = originalJimp.bitmap.data[pixelIndex + 2]; + const originalA = originalJimp.bitmap.data[pixelIndex + 3]; + + const restoredR = restoredJimp.bitmap.data[pixelIndex]; + const restoredG = restoredJimp.bitmap.data[pixelIndex + 1]; + const restoredB = restoredJimp.bitmap.data[pixelIndex + 2]; + const restoredA = restoredJimp.bitmap.data[pixelIndex + 3]; + + expect(restoredR).toBe(originalR); + expect(restoredG).toBe(originalG); + expect(restoredB).toBe(originalB); + expect(restoredA).toBe(originalA); + } + } } }); @@ -544,7 +605,239 @@ describe("ImageRestorer", () => { const fragmenter = new ImageFragmenter({ blockSize: 2, seed: "deterministic-seed", - perImageShuffle: true, + crossImageShuffle: false, + }); + + const { manifest, fragmentedImages } = await fragmenter.fragmentImages([ + smallImagePath, + largeImagePath, + ]); + + const restorer = new ImageRestorer(); + const restoredImages = await restorer.restoreImages( + fragmentedImages, + manifest, + ); + + expect(restoredImages).toHaveLength(2); + + // Verify restored dimensions match originals + const smallJimp = await Jimp.read(restoredImages[0]); + const largeJimp = await Jimp.read(restoredImages[1]); + + expect(smallJimp.bitmap.width).toBe(2); + expect(smallJimp.bitmap.height).toBe(2); + expect(largeJimp.bitmap.width).toBe(6); + expect(largeJimp.bitmap.height).toBe(6); + } finally { + // Clean up + if (fs.existsSync(smallImagePath)) fs.unlinkSync(smallImagePath); + if (fs.existsSync(largeImagePath)) fs.unlinkSync(largeImagePath); + } + }); + + test("restores single image with cross-image shuffle", async () => { + const fragmenter = new ImageFragmenter({ + blockSize: 2, + seed: "test-seed", + crossImageShuffle: true, + }); + const { manifest, fragmentedImages } = await fragmenter.fragmentImages([ + testImagePath, + ]); + + const restorer = new ImageRestorer(); + const restoredImages = await restorer.restoreImages( + fragmentedImages, + manifest, + ); + + expect(restoredImages).toHaveLength(1); + expect(manifest.config.crossImageShuffle).toBe(true); + + const jimpImage = await Jimp.read(restoredImages[0]); + expect(jimpImage.bitmap.width).toBe(4); + expect(jimpImage.bitmap.height).toBe(4); + }); + + test("restores multiple images with cross-image shuffle", async () => { + const fragmenter = new ImageFragmenter({ + blockSize: 2, + seed: "test-seed", + crossImageShuffle: true, + }); + const { manifest, fragmentedImages } = await fragmenter.fragmentImages([ + testImagePath, + testImagePath, + ]); + + const restorer = new ImageRestorer(); + const restoredImages = await restorer.restoreImages( + fragmentedImages, + manifest, + ); + + expect(restoredImages).toHaveLength(2); + expect(manifest.config.crossImageShuffle).toBe(true); + + for (const restoredImage of restoredImages) { + const jimpImage = await Jimp.read(restoredImage); + expect(jimpImage.bitmap.width).toBe(4); + expect(jimpImage.bitmap.height).toBe(4); + } + }); + + test("round-trip with cross-image shuffle preserves image data", async () => { + const fragmenter = new ImageFragmenter({ + blockSize: 1, + seed: "deterministic-seed", + crossImageShuffle: true, + }); + + const { manifest, fragmentedImages } = await fragmenter.fragmentImages([ + testImagePath, + ]); + + const restorer = new ImageRestorer(); + const restoredImages = await restorer.restoreImages( + fragmentedImages, + manifest, + ); + + const originalJimp = await Jimp.read(testImagePath); + const restoredJimp = await Jimp.read(restoredImages[0]); + + expect(restoredJimp.bitmap.width).toBe(originalJimp.bitmap.width); + expect(restoredJimp.bitmap.height).toBe(originalJimp.bitmap.height); + expect(restoredJimp.bitmap.data.length).toBe( + originalJimp.bitmap.data.length, + ); + + // Verify pixel RGBA values match + for (let y = 0; y < originalJimp.bitmap.height; y++) { + for (let x = 0; x < originalJimp.bitmap.width; x++) { + const pixelIndex = (y * originalJimp.bitmap.width + x) * 4; + const originalR = originalJimp.bitmap.data[pixelIndex]; + const originalG = originalJimp.bitmap.data[pixelIndex + 1]; + const originalB = originalJimp.bitmap.data[pixelIndex + 2]; + const originalA = originalJimp.bitmap.data[pixelIndex + 3]; + + const restoredR = restoredJimp.bitmap.data[pixelIndex]; + const restoredG = restoredJimp.bitmap.data[pixelIndex + 1]; + const restoredB = restoredJimp.bitmap.data[pixelIndex + 2]; + const restoredA = restoredJimp.bitmap.data[pixelIndex + 3]; + + expect(restoredR).toBe(originalR); + expect(restoredG).toBe(originalG); + expect(restoredB).toBe(originalB); + expect(restoredA).toBe(originalA); + } + } + }); + + test("round-trip with multiple images and cross-image shuffle", async () => { + const fragmenter = new ImageFragmenter({ + blockSize: 1, + seed: "deterministic-seed", + crossImageShuffle: true, + }); + + const { manifest, fragmentedImages } = await fragmenter.fragmentImages([ + testImagePath, + testImagePath, + ]); + + const restorer = new ImageRestorer(); + const restoredImages = await restorer.restoreImages( + fragmentedImages, + manifest, + ); + + expect(restoredImages).toHaveLength(2); + + const originalJimp = await Jimp.read(testImagePath); + for (const restoredImage of restoredImages) { + const restoredJimp = await Jimp.read(restoredImage); + expect(restoredJimp.bitmap.width).toBe(originalJimp.bitmap.width); + expect(restoredJimp.bitmap.height).toBe(originalJimp.bitmap.height); + expect(restoredJimp.bitmap.data.length).toBe( + originalJimp.bitmap.data.length, + ); + + // Verify pixel RGBA values match + for (let y = 0; y < originalJimp.bitmap.height; y++) { + for (let x = 0; x < originalJimp.bitmap.width; x++) { + const pixelIndex = (y * originalJimp.bitmap.width + x) * 4; + const originalR = originalJimp.bitmap.data[pixelIndex]; + const originalG = originalJimp.bitmap.data[pixelIndex + 1]; + const originalB = originalJimp.bitmap.data[pixelIndex + 2]; + const originalA = originalJimp.bitmap.data[pixelIndex + 3]; + + const restoredR = restoredJimp.bitmap.data[pixelIndex]; + const restoredG = restoredJimp.bitmap.data[pixelIndex + 1]; + const restoredB = restoredJimp.bitmap.data[pixelIndex + 2]; + const restoredA = restoredJimp.bitmap.data[pixelIndex + 3]; + + expect(restoredR).toBe(originalR); + expect(restoredG).toBe(originalG); + expect(restoredB).toBe(originalB); + expect(restoredA).toBe(originalA); + } + } + } + }); + + test("round-trip with different-sized images and cross-image shuffle", async () => { + // Create test images of different sizes + const smallImagePath = path.join(tmpDir, "small_cross_restore_test.png"); + const largeImagePath = path.join(tmpDir, "large_cross_restore_test.png"); + + // Create 2x2 image + const smallImageData = Buffer.from([ + 255, + 0, + 0, + 255, // Red + 0, + 255, + 0, + 255, // Green + 0, + 0, + 255, + 255, // Blue + 255, + 255, + 0, + 255, // Yellow + ]); + const smallImage = Jimp.fromBitmap({ + data: smallImageData, + width: 2, + height: 2, + }); + await smallImage.write(smallImagePath, JimpMime.png); + + // Create 6x6 image + const largeImageData = Buffer.alloc(6 * 6 * 4); + for (let i = 0; i < 6 * 6 * 4; i += 4) { + largeImageData[i] = 128; // R + largeImageData[i + 1] = 128; // G + largeImageData[i + 2] = 128; // B + largeImageData[i + 3] = 255; // A + } + const largeImage = Jimp.fromBitmap({ + data: largeImageData, + width: 6, + height: 6, + }); + await largeImage.write(largeImagePath, JimpMime.png); + + try { + const fragmenter = new ImageFragmenter({ + blockSize: 2, + seed: "deterministic-seed", + crossImageShuffle: true, }); const { manifest, fragmentedImages } = await fragmenter.fragmentImages([ @@ -559,6 +852,7 @@ describe("ImageRestorer", () => { ); expect(restoredImages).toHaveLength(2); + expect(manifest.config.crossImageShuffle).toBe(true); // Verify restored dimensions match originals const smallJimp = await Jimp.read(restoredImages[0]); diff --git a/packages/node/src/restorer.ts b/packages/node/src/restorer.ts index 31305b0..e1a4b26 100644 --- a/packages/node/src/restorer.ts +++ b/packages/node/src/restorer.ts @@ -15,13 +15,13 @@ export class ImageRestorer { const { allBlocks, fragmentBlocksCount, imageBlockCounts } = await this._prepareRestoreData(fragmentImages, manifest); - const restoredBlocks = manifest.config.perImageShuffle - ? this._unshufflePerImage( + const restoredBlocks = manifest.config.crossImageShuffle + ? unshuffle(allBlocks, manifest.config.seed) + : this._unshufflePerImage( allBlocks, imageBlockCounts, manifest.config.seed, - ) - : unshuffle(allBlocks, manifest.config.seed); + ); const reconstructedImages = await this._reconstructAllImages( restoredBlocks, From b67a7ae2494a479e9a3348378f99e324cc91e628 Mon Sep 17 00:00:00 2001 From: "Y.Yamamoto" Date: Tue, 11 Nov 2025 00:43:43 +0900 Subject: [PATCH 6/8] Update README to include cross-image shuffle details and CLI options - Enhanced the README.md to clarify the `--cross-image-shuffle` option for shuffling blocks across multiple images. - Added example usage for cross-image shuffle in the CLI documentation. - Updated the configuration section to include `crossImageShuffle` in the manifest.json example. --- README.md | 7 +++++-- packages/cli/README.md | 6 ++++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 1bb7028..107e218 100644 --- a/README.md +++ b/README.md @@ -129,7 +129,9 @@ output ### Input multiple images -blockSize: `50` +blockSize: `50` (with `--cross-image-shuffle`) + +When processing multiple images with cross-image shuffle enabled, blocks are shuffled across all images, creating a unified fragmentation pattern. | input 1 | input 2 | input 3 | |:-------:|:---------------:|:---------------:| @@ -152,7 +154,8 @@ manifest.json: "blockSize": 2, "prefix": "img", "seed": 72411, - "preserveName": false + "preserveName": false, + "crossImageShuffle": false }, "images": [ { diff --git a/packages/cli/README.md b/packages/cli/README.md index 289b6ea..3b6fcec 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -50,6 +50,7 @@ image-shield shuffle -o [options] | `-p, --prefix ` | Prefix for fragment files | ❌ | "fragment" | | `-s, --seed ` | Random seed (integer) | ❌ | auto-generated | | `--preserve-name` | Preserve original file names | ❌ | false | +| `--cross-image-shuffle` | Shuffle blocks across all images instead of within each image independently | ❌ | false (per-image shuffle by default) | #### Examples @@ -68,6 +69,11 @@ image-shield shuffle *.jpg -o ./output -b 20 -p "my_fragment" --preserve-name image-shield shuffle image.png -o ./output -s 12345 ``` +**Cross-image shuffle (shuffle blocks across all images):** +```bash +image-shield shuffle image1.png image2.png image3.png -o ./output --cross-image-shuffle +``` + #### Output Structure After fragmentation, the output directory will contain: From 0d2184ad46e76be4b8086a91aadcc5fd3042b1e8 Mon Sep 17 00:00:00 2001 From: "Y.Yamamoto" Date: Tue, 11 Nov 2025 01:00:43 +0900 Subject: [PATCH 7/8] Refactor image block handling for cross-image shuffle - Updated ImageFragmenter and ImageRestorer to use imageBlockCounts when crossImageShuffle is false, ensuring accurate block distribution based on actual image dimensions. - Adjusted logic to maintain compatibility with existing shuffle functionality while enhancing clarity in block count calculations. --- packages/node/src/fragmenter.ts | 2 +- packages/node/src/restorer.ts | 13 +++++++++---- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/packages/node/src/fragmenter.ts b/packages/node/src/fragmenter.ts index 0c55b51..72305a4 100644 --- a/packages/node/src/fragmenter.ts +++ b/packages/node/src/fragmenter.ts @@ -50,7 +50,7 @@ export class ImageFragmenter { const fragmentedImages = await this._createFragmentedImages( shuffledBlocks, - fragmentBlocksCount, + this.config.crossImageShuffle ? fragmentBlocksCount : imageBlockCounts, manifest, ); diff --git a/packages/node/src/restorer.ts b/packages/node/src/restorer.ts index e1a4b26..5469f1b 100644 --- a/packages/node/src/restorer.ts +++ b/packages/node/src/restorer.ts @@ -80,15 +80,20 @@ export class ImageRestorer { fragmentImages.length, ); + // Calculate actual block counts per image for per-image unshuffle + const imageBlockCounts = manifest.images.map((info) => info.x * info.y); + + // Use imageBlockCounts when crossImageShuffle is false + const blocksCounts = manifest.config.crossImageShuffle + ? fragmentBlocksCount + : imageBlockCounts; + const allBlocks = await this._extractBlocksFromFragments( fragmentImages, manifest, - fragmentBlocksCount, + blocksCounts, ); - // Calculate actual block counts per image for per-image unshuffle - const imageBlockCounts = manifest.images.map((info) => info.x * info.y); - return { allBlocks, fragmentBlocksCount, imageBlockCounts }; } From 775f411ae0371e348c109b015ed7177ebceb4f23 Mon Sep 17 00:00:00 2001 From: "Y.Yamamoto" Date: Tue, 11 Nov 2025 08:49:46 +0900 Subject: [PATCH 8/8] Refactor ImageRestorer to remove fragmentBlocksCount - Removed fragmentBlocksCount from the restoration process in ImageRestorer, simplifying the return structure to focus solely on imageBlockCounts. - Adjusted related methods to ensure compatibility and maintain functionality while enhancing clarity in block handling. --- packages/node/src/restorer.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/node/src/restorer.ts b/packages/node/src/restorer.ts index 5469f1b..e3efd16 100644 --- a/packages/node/src/restorer.ts +++ b/packages/node/src/restorer.ts @@ -12,8 +12,10 @@ export class ImageRestorer { fragmentImages: (string | Buffer)[], manifest: ManifestData, ): Promise { - const { allBlocks, fragmentBlocksCount, imageBlockCounts } = - await this._prepareRestoreData(fragmentImages, manifest); + const { allBlocks, imageBlockCounts } = await this._prepareRestoreData( + fragmentImages, + manifest, + ); const restoredBlocks = manifest.config.crossImageShuffle ? unshuffle(allBlocks, manifest.config.seed) @@ -69,7 +71,6 @@ export class ImageRestorer { manifest: ManifestData, ): Promise<{ allBlocks: Buffer[]; - fragmentBlocksCount: number[]; imageBlockCounts: number[]; }> { this._validateInputs(fragmentImages, manifest); @@ -94,7 +95,7 @@ export class ImageRestorer { blocksCounts, ); - return { allBlocks, fragmentBlocksCount, imageBlockCounts }; + return { allBlocks, imageBlockCounts }; } private _validateInputs(