Skip to content
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
6 changes: 6 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,12 @@ jobs:
- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Install evidence replay tools
run: |
sudo apt-get update
sudo apt-get install --yes --no-install-recommends ffmpeg
ffmpeg -version | head -n 1

- name: Verify package
run: pnpm verify

Expand Down
127 changes: 120 additions & 7 deletions .github/workflows/demo-evidence.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ on:
- "src/**"
- "package.json"
- "scripts/capture-android-demo.sh"
- "scripts/create-economic-resilience-environment.mjs"
- "scripts/create-economic-resilience-evidence.mjs"
- "scripts/benchmark-core.mjs"
- "scripts/benchmark-comparison-core.mjs"
- "scripts/create-benchmark-comparison-evidence.mjs"
Expand All @@ -20,7 +22,11 @@ on:
- "scripts/demo-visual-agreement-core.mjs"
- "scripts/measure-demo-visual-agreement.mjs"
- "scripts/demo-evidence-core.mjs"
- "scripts/economic-resilience-evidence-core.mjs"
- "scripts/verify-economic-resilience-evidence.mjs"
- "scripts/guided-demo-core.mjs"
- "scripts/inspect-ios-simulator-metadata.mjs"
- "scripts/ios-simulator-metadata-core.mjs"
- "scripts/normalize-demo-recording.mjs"
- "scripts/verify-benchmark-comparison-evidence.mjs"
workflow_dispatch:
Expand Down Expand Up @@ -48,6 +54,14 @@ jobs:
with:
ref: ${{ inputs.source_sha || github.event.pull_request.head.sha || github.sha }}

- name: Verify exact source checkout
env:
EXPECTED_SOURCE_SHA: ${{ inputs.source_sha || github.event.pull_request.head.sha || github.sha }}
run: |
set -euo pipefail
[[ "$EXPECTED_SOURCE_SHA" =~ ^[0-9a-f]{40}$ ]]
test "$(git rev-parse HEAD)" = "$EXPECTED_SOURCE_SHA"

- name: Setup Java
uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5
with:
Expand Down Expand Up @@ -127,6 +141,14 @@ jobs:
with:
ref: ${{ inputs.source_sha || github.event.pull_request.head.sha || github.sha }}

- name: Verify exact source checkout
env:
EXPECTED_SOURCE_SHA: ${{ inputs.source_sha || github.event.pull_request.head.sha || github.sha }}
run: |
set -euo pipefail
[[ "$EXPECTED_SOURCE_SHA" =~ ^[0-9a-f]{40}$ ]]
test "$(git rev-parse HEAD)" = "$EXPECTED_SOURCE_SHA"

- name: Setup pnpm
uses: ./.github/actions/setup-pnpm

Expand Down Expand Up @@ -158,8 +180,23 @@ jobs:
shell: bash
run: |
set -euo pipefail
udid=$(xcrun simctl list devices booted --json | node -e "let s='';process.stdin.on('data',d=>s+=d).on('end',()=>{const d=Object.values(JSON.parse(s).devices).flat()[0];if(!d)process.exit(1);process.stdout.write(d.udid)})")
app=example/ios/build/Build/Products/Debug-iphonesimulator/ImageCompressionKitExample.app
app_executable="$app/ImageCompressionKitExample"
test -f "$app_executable"
test ! -L "$app_executable"
app_architectures=$(xcrun lipo -archs "$app_executable" | tr -d '\r')
xcrun simctl list devices booted --json > /tmp/rnick-sim-devices.json
xcrun simctl list runtimes --json > /tmp/rnick-sim-runtimes.json
node scripts/inspect-ios-simulator-metadata.mjs \
--devices /tmp/rnick-sim-devices.json \
--runtimes /tmp/rnick-sim-runtimes.json \
--app-architectures "$app_architectures" \
--runner-arch "$RUNNER_ARCH" > /tmp/rnick-sim-metadata.json
udid=$(node -e "process.stdout.write(require('/tmp/rnick-sim-metadata.json').udid)")
runtime=$(node -e "process.stdout.write(require('/tmp/rnick-sim-metadata.json').runtime)")
device=$(node -e "process.stdout.write(require('/tmp/rnick-sim-metadata.json').device)")
os_build=$(node -e "process.stdout.write(require('/tmp/rnick-sim-metadata.json').osBuild)")
abi=$(node -e "process.stdout.write(require('/tmp/rnick-sim-metadata.json').abi)")
xcrun simctl install "$udid" "$app"
pnpm --filter image-compression-kit-example exec react-native start --port 8081 > /tmp/rnick-metro.log 2>&1 &
metro_pid=$!
Expand Down Expand Up @@ -189,7 +226,10 @@ jobs:
sleep 2
SIMCTL_CHILD_RNICK_DEMO_CAPTURE=1 xcrun simctl launch --terminate-running-process "$udid" com.imagecompressionkit.example --rnick-demo-capture
capture_native_log() {
xcrun simctl spawn "$udid" log show --style compact --last 3m --predicate 'eventMessage CONTAINS "RNICK_DEMO_" OR eventMessage CONTAINS "RNICK_GUIDED_DEMO_" OR eventMessage CONTAINS "RNICK_BENCHMARK_"' > /tmp/rnick-demo-raw/native.log
# The simulator is fresh for this job. A 15-minute window exceeds
# every bounded capture poll while avoiding fragile device-local
# date parsing in `log show --start`.
xcrun simctl spawn "$udid" log show --style compact --last 15m --predicate 'eventMessage CONTAINS "RNICK_DEMO_" OR eventMessage CONTAINS "RNICK_GUIDED_DEMO_" OR eventMessage CONTAINS "RNICK_BENCHMARK_" OR eventMessage CONTAINS "RNICK_ECONOMIC_RESILIENCE_"' > /tmp/rnick-demo-raw/native.log
}
for attempt in $(seq 1 60); do
capture_native_log
Expand Down Expand Up @@ -219,14 +259,26 @@ jobs:
test "$attempt" != "60"
sleep 1
done
for attempt in $(seq 1 120); do
for attempt in $(seq 1 300); do
capture_native_log
if grep -q 'RNICK_DEMO_PASS' /tmp/rnick-demo-raw/native.log && \
grep -q 'RNICK_BENCHMARK_PASS' /tmp/rnick-demo-raw/native.log && \
grep -q 'RNICK_BENCHMARK_COMPARISON_PASS' /tmp/rnick-demo-raw/native.log; then
grep -q 'RNICK_BENCHMARK_COMPARISON_PASS' /tmp/rnick-demo-raw/native.log && \
grep -q 'RNICK_ECONOMIC_RESILIENCE_PASS' /tmp/rnick-demo-raw/native.log; then
break
fi
test "$attempt" != "120"
if grep -q 'RNICK_DEMO_FAIL' /tmp/rnick-demo-raw/native.log; then
echo 'Native demo reported failure before all evidence markers passed.' >&2
tail -n 200 /tmp/rnick-demo-raw/native.log >&2 || true
tail -n 200 /tmp/rnick-metro.log >&2 || true
exit 1
fi
if [ "$attempt" = "300" ]; then
echo 'Timed out waiting for all native evidence markers.' >&2
tail -n 200 /tmp/rnick-demo-raw/native.log >&2 || true
tail -n 200 /tmp/rnick-metro.log >&2 || true
fi
test "$attempt" != "300"
sleep 1
done
sleep 2
Expand All @@ -242,8 +294,6 @@ jobs:
NODE
cp "$(sed -n '1p' /tmp/rnick-demo-raw/uris.txt)" /tmp/rnick-demo-raw/source.jpg
cp "$(sed -n '2p' /tmp/rnick-demo-raw/uris.txt)" /tmp/rnick-demo-raw/output.jpg
runtime=$(xcrun simctl list devices booted --json | node -e "let s='';process.stdin.on('data',d=>s+=d).on('end',()=>{const p=JSON.parse(s);const e=Object.entries(p.devices).find(([,v])=>v.length);process.stdout.write(e[0].replace('com.apple.CoreSimulator.SimRuntime.','').replaceAll('-','.'))})")
device=$(xcrun simctl list devices booted --json | node -e "let s='';process.stdin.on('data',d=>s+=d).on('end',()=>{const d=Object.values(JSON.parse(s).devices).flat()[0];process.stdout.write(d.name)})")
node scripts/normalize-demo-recording.mjs \
--input /tmp/rnick-demo-raw/recording-raw.mp4 \
--output /tmp/rnick-demo-raw/recording.mp4 \
Expand Down Expand Up @@ -293,6 +343,69 @@ jobs:
--destination demo-evidence/ios \
--run-url "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
node scripts/verify-benchmark-comparison-evidence.mjs demo-evidence/ios
mkdir -p /tmp/rnick-economic-raw
node --input-type=module - /tmp/rnick-demo-raw/native.log > /tmp/rnick-economic-raw/uris.txt <<'NODE'
import { readFileSync } from 'node:fs';
import { parseNativeEconomicResiliencePayload } from './scripts/economic-resilience-evidence-core.mjs';
const payload = parseNativeEconomicResiliencePayload(readFileSync(process.argv[2], 'utf8'));
console.log(new URL(payload.fixture.sourceUri).pathname);
console.log(new URL(payload.representative.stagedOutputUri).pathname);
NODE
cp "$(sed -n '1p' /tmp/rnick-economic-raw/uris.txt)" /tmp/rnick-economic-raw/source.jpg
cp "$(sed -n '2p' /tmp/rnick-economic-raw/uris.txt)" /tmp/rnick-economic-raw/output.jpg
node scripts/measure-demo-visual-agreement.mjs \
--source /tmp/rnick-economic-raw/source.jpg \
--output /tmp/rnick-economic-raw/output.jpg \
--resize-mode contain \
--max-width 1600 \
--max-height 1200 \
--comparison-profile jpeg-full-range-to-limited-yuv444p-v1 \
--report /tmp/rnick-economic-raw/visual-agreement.json
react_native_version=$(node -e "process.stdout.write(require('./example/package.json').dependencies['react-native'])")
node_version=$(node --version)
ffmpeg_version=$(ffmpeg -version | head -n 1)
ffprobe_version=$(ffprobe -version | head -n 1)
xcode_version=$(xcodebuild -version | tr '\n' ' ' | sed 's/[[:space:]]*$//')
simulator_sdk=$(xcrun --sdk iphonesimulator --show-sdk-version)
node scripts/create-economic-resilience-environment.mjs \
--platform ios \
--runtime "$runtime" \
--os-build "$os_build" \
--device "$device" \
--device-kind simulator \
--abi "$abi" \
--react-native-version "$react_native_version" \
--native-log /tmp/rnick-demo-raw/native.log \
--build-type debug \
--runner-label macos-latest \
--runner-os "$RUNNER_OS" \
--runner-arch "$RUNNER_ARCH" \
--runner-name "$RUNNER_NAME" \
--image-os "$ImageOS" \
--image-version "$ImageVersion" \
--node "$node_version" \
--ffmpeg "$ffmpeg_version" \
--ffprobe "$ffprobe_version" \
--primary-toolchain "$xcode_version" \
--platform-sdk "iOS Simulator $simulator_sdk" \
--output /tmp/rnick-economic-raw/environment.json
node scripts/create-economic-resilience-evidence.mjs \
--platform ios \
--package-version "${{ steps.package.outputs.version }}" \
--source-sha "${{ inputs.source_sha || github.event.pull_request.head.sha || github.sha }}" \
--run-id "$GITHUB_RUN_ID" \
--run-attempt "$GITHUB_RUN_ATTEMPT" \
--run-url "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" \
--log /tmp/rnick-demo-raw/native.log \
--source /tmp/rnick-economic-raw/source.jpg \
--output /tmp/rnick-economic-raw/output.jpg \
--fixture-manifest example/fixtures/kit-only-12mp-v1.json \
--visual-agreement /tmp/rnick-economic-raw/visual-agreement.json \
--environment /tmp/rnick-economic-raw/environment.json \
--destination demo-evidence/ios
node scripts/verify-economic-resilience-evidence.mjs \
--artifact-dir demo-evidence/ios/economic-resilience \
--report-file /tmp/rnick-economic-raw/verification.json

- name: Upload iOS evidence
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
Expand Down
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ and patch releases remain backward compatible within the current minor line.
arbitrary files, traversal, symlinks, and directories.
- An iOS SDK privacy manifest declaring no tracking or collected data and the
C617.1 reason used to inspect package-owned cache output metadata.
- A reproducible kit-only 12 MP JPEG evidence harness with exact environment,
byte, geometry, explicit color-range visual agreement, latency, portable
offline replay, and output-cleanup verification.

### Changed

Expand All @@ -22,6 +25,10 @@ and patch releases remain backward compatible within the current minor line.

### Fixed

- iOS `metadata: 'strip'` now removes encoder-generated JPEG APP1, APP13, and
COM segments after ImageIO encoding. The marker parser rejects malformed,
truncated, or trailing JPEG output through the existing `ERR_ENCODE_FAILED`
path while leaving `safe` and `preserve` encoding unchanged.
- iOS no longer vertically inverts pixels after ImageIO has normalized an
orientation-bearing input. The default pipeline now verifies EXIF
orientations 1–8 through decode, transform, and encode, and native demo
Expand Down
1 change: 1 addition & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ Requirements:
- pnpm 11.8.0 through Corepack
- Ruby/CocoaPods and Xcode for iOS validation
- Java 21 and Android SDK 36 for Android executable validation
- ffmpeg and ffprobe for decode, geometry, and SSIM evidence replay

```bash
corepack enable
Expand Down
3 changes: 3 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,16 @@ RUN set -eux; \
apt-get install -y --no-install-recommends \
ca-certificates \
curl \
ffmpeg \
git \
make \
openssh-client \
python3 \
unzip \
xz-utils \
g++; \
ffmpeg -version | head -n 1; \
ffprobe -version | head -n 1; \
rm -rf /var/lib/apt/lists/*

RUN set -eux; \
Expand Down
19 changes: 16 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ owned-file cleanup.
| Large photos | Decode downsampling, pixel limits, two-operation scheduling | 48 MP → 1.92 MP planned decode; this is not measured peak memory |
| Cancellation | `ERR_CANCELLED` without publishing partial output | JS and native suites assert zero residual output at representative boundaries |
| Output lifecycle | Narrow `removeCompressionOutput(uri)` ownership check | 0.4.1 candidate tests owned deletion and foreign/path/directory rejection |
| Metadata | Explicit `preserve`, `safe`, and `strip` | Android safe retained 0/7 named sensitive fields; iOS safe/strip copy no source metadata |
| Metadata | Explicit `preserve`, `safe`, and `strip` | Android safe retained 0/7 named sensitive fields; iOS safe copies no source metadata and JPEG strip also removes APP1/APP13/COM segments |
| Integration | Packed tarball installed by fresh consumers | 8/8 release-target platform builds passed for v0.4.0 |

</details>
Expand Down Expand Up @@ -352,8 +352,9 @@ Important limitations:
- HEIC, HEIF, and AVIF output reject with `ERR_NOT_IMPLEMENTED`.
- GIF output and animation preservation for GIF/WebP/AVIF are not implemented.
- `metadata: 'preserve'` is supported only for JPEG source to JPEG output.
- Android `safe` copies a privacy-filtered JPEG EXIF allowlist. iOS `safe` and
`strip` re-encode without copying source metadata.
- Android `safe` copies a privacy-filtered JPEG EXIF allowlist. iOS `safe`
re-encodes without copying source metadata; for JPEG output, iOS `strip`
additionally removes encoder-generated APP1, APP13, and COM segments.
- The iOS SDK ships a namespaced privacy manifest declaring no tracking or
collected data and C617.1 for package-cache file metadata validation.
- JPEG orientation is rendered into pixels before resize/encode; preserved
Expand All @@ -372,6 +373,10 @@ Important limitations:

## Development verification

The full repository gate requires `ffmpeg` and `ffprobe` so retained and
newly generated native image evidence can be decoded and visually replayed.
The pinned Docker lane includes both tools.

```bash
pnpm test:coverage
pnpm verify
Expand All @@ -384,6 +389,7 @@ pnpm example:ios:output-test
pnpm example:ios:pipeline-test
pnpm example:ios:large-image-test
pnpm example:ios:metadata-test
pnpm example:ios:jpeg-sanitizer-test
pnpm example:ios:transformer-test
pnpm docs:check
pnpm site:check
Expand Down Expand Up @@ -416,6 +422,13 @@ movie header.
The same runs emit
versioned baseline and exact-plan comparison evidence with raw samples, fixture
and plan digests, balanced execution positions, and median/p95 summaries.
They also create a kit-only 12 MP JPEG source-tree evidence bundle that binds source and
output bytes, environment, capabilities, latency samples, visual agreement,
and package-output cleanup. Its visual replay pins the JPEG color-range
conversion and allows only a 0.001 SSIM implementation tolerance after both
measurements independently pass the quality and orientation gates. The bundle
is an environment-specific observation, not a speed ranking, cost-savings
claim, or real-device benchmark.
Comparison dependencies remain inside the private example application and
outside the published package. See the
[benchmark methodology](docs/benchmarks/README.md) for its timing boundary,
Expand Down
51 changes: 50 additions & 1 deletion docs/benchmarks/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,61 @@ Do not compare Android measurements with iOS measurements. Native codecs,
device classes, simulator behavior, filesystem caches, thermals, and runner
load differ. A result describes only its captured environment.

## 12 MP kit-only economic resilience

The same hosted workflow runs a separate large-photo case for this package
only. Its repository-generated, non-personal JPEG is exactly 4,000 × 3,000,
1,721,333 bytes, and SHA-256
`bdcf4e083f1860d8829898211e4b1c428a80dfd53dceca697c6f7e4a4901bfcc`.
Android and iOS bundle those same fixture bytes. The request is fixed to
`contain` within 1,600 × 1,200, JPEG quality 90, `maxBytes: 500_000`, and
`metadata: "strip"`.

Each platform runs two warmups and ten sequential measured calls. The timer
surrounds only `compressImage(options)`; option construction, inspection,
staging, and cleanup are outside it. Measured iteration 10 is copied to an
example-owned staging file before all 12 package-owned outputs are removed.
Acceptance requires a decodable 1,600 × 1,200 JPEG at or below the byte target,
exact native/file byte and SHA-256 agreement, no APP1/APP13/comment metadata,
upright SSIM of at least 0.90, an upright-over-vertical-flip margin of at least
0.02, the unchanged source, and zero package-output residuals. The visual
profile requires full-range JPEG inputs and explicitly converts both sides to
limited-range `yuv444p` with Lanczos scaling before comparison, avoiding
version-dependent implicit YUVJ range negotiation.

The artifact records the exact checked-out source commit, package source-tree
version, workflow run and attempt, runtime, OS build, simulator/emulator,
architecture, JS engine, React Native version, runner image, toolchains,
capabilities, raw warmup/measured samples, and signed source-minus-output byte
difference. `sourceToOutputByteDifference` is an observation, not avoided
transfer or storage. The source remains, no matched transfer baseline exists,
and no cost-savings claim is made.

This case is not a competitor comparison, speed ranking, production workload,
physical-device benchmark, peak-RSS measurement, or universal resilience
rate. Verify a downloaded platform bundle with the locally installed ffmpeg
and ffprobe tools:

```bash
pnpm verify:economic-resilience-evidence -- \
--artifact-dir path/to/native-demo-platform-artifact/economic-resilience
```

The replay reports both captured and local ffmpeg versions and gates on the
recalculated decode, geometry, hashes, SSIM, and flip-control report. Both the
captured and replayed reports must independently pass the 0.90 quality and 0.02
orientation gates; all profile, geometry, check, and digest fields must match.
Only the two six-decimal SSIM values may differ, by at most 0.001. That narrow
tolerance absorbs decoder/scaler implementation drift for this exact bound
fixture; it is not extra quality slack or a claim about other images.

## Capture and verify

The [Native Demo Evidence workflow](https://github.com/GGULBAE/react-native-image-compression-kit/actions/workflows/demo-evidence.yml)
runs the benchmark after the visible demo result on both platforms. Each
platform artifact contains `benchmark.json`, the exact source fixture, the demo
manifest, input/output images, screenshot, and native log.
manifest, input/output images, screenshot, native log, and an independently
scoped `economic-resilience/` directory.

After downloading one platform artifact, verify it without network access:

Expand Down
Loading
Loading