feat(worker): sharp pipeline with orientation, variants, exif strip

Task 20: services/worker package (@photo/worker, commonjs) with jest
unit/e2e configs mirroring api conventions (root .env via test/load-env.ts,
moduleNameMapper to @photo/contracts/db-cn/storage TS sources, maxWorkers 1,
test/reset.ts = MySQL TRUNCATE + MinIO photo-live/dev prefix wipe) and the
sharp processing pipeline runPipeline(): metadata whitelist gate
(jpeg/png/webp/avif/heif/tiff, else PipelineError UNSUPPORTED_TYPE),
decode failures mapped to PipelineError DECODE_FAILED, no-arg .rotate()
auto-orientation (EXIF 5-8 swaps stored width/height), thumb <=480w q75
and browse <=1920 q82 JPEG variants with withoutEnlargement, and no
withMetadata so outputs carry no EXIF/GPS at all. sharp 0.35 prebuilt
loads script-free under pnpm 10; its win32 libvips lacks a HEVC encoder,
so the HEVC-HEIF unit case self-skips with a console note (G2 verifies
with real iPhone samples) while the AV1-HEIF case covers the same
libheif decode path / 'heif' format bucket.

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-09-17 23:16:20 +08:00
parent f5f1855b58
commit 64a2cec788
10 changed files with 820 additions and 6 deletions
+353 -6
View File
@@ -46,7 +46,7 @@ importers:
version: 7.13.0(typescript@5.9.3)
ts-jest:
specifier: ^29.2.0
version: 29.4.12(@babel/core@7.29.7)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.29.7))(jest-util@29.7.0)(jest@29.7.0(@types/node@22.20.3))(typescript@5.9.3)
version: 29.4.12(@babel/core@7.29.7)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.29.7))(jest-util@29.7.0)(jest@29.7.0(@types/node@22.20.3)(ts-node@10.9.2(@types/node@22.20.3)(typescript@5.9.3)))(typescript@5.9.3)
typescript:
specifier: ^5.6.0
version: 5.9.3
@@ -90,7 +90,7 @@ importers:
version: 29.7.0(@types/node@22.20.3)(ts-node@10.9.2(@types/node@22.20.3)(typescript@5.9.3))
ts-jest:
specifier: ^29.2.5
version: 29.4.12(@babel/core@7.29.7)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.29.7))(jest-util@29.7.0)(jest@29.7.0(@types/node@22.20.3))(typescript@5.9.3)
version: 29.4.12(@babel/core@7.29.7)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.29.7))(jest-util@29.7.0)(jest@29.7.0(@types/node@22.20.3)(ts-node@10.9.2(@types/node@22.20.3)(typescript@5.9.3)))(typescript@5.9.3)
typescript:
specifier: ^5.6.0
version: 5.9.3
@@ -172,7 +172,7 @@ importers:
version: 7.2.2
ts-jest:
specifier: ^29.2.5
version: 29.4.12(@babel/core@7.29.7)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.29.7))(jest-util@29.7.0)(jest@29.7.0(@types/node@22.20.3))(typescript@5.9.3)
version: 29.4.12(@babel/core@7.29.7)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.29.7))(jest-util@29.7.0)(jest@29.7.0(@types/node@22.20.3)(ts-node@10.9.2(@types/node@22.20.3)(typescript@5.9.3)))(typescript@5.9.3)
ts-node:
specifier: ^10.9.2
version: 10.9.2(@types/node@22.20.3)(typescript@5.9.3)
@@ -180,6 +180,52 @@ importers:
specifier: ^5.6.0
version: 5.9.3
services/worker:
dependencies:
'@photo/contracts':
specifier: workspace:*
version: link:../../packages/contracts
'@photo/db-cn':
specifier: workspace:*
version: link:../../packages/db-cn
'@photo/storage':
specifier: workspace:^
version: link:../../packages/storage
bullmq:
specifier: ^6.3.6
version: 6.3.6(ioredis@6.0.0)
ioredis:
specifier: ^6.0.0
version: 6.0.0
pino:
specifier: ^10.0.0
version: 10.3.1
sharp:
specifier: ^0.35.0
version: 0.35.4(@types/node@22.20.3)
devDependencies:
'@aws-sdk/client-s3':
specifier: ^3.700.0
version: 3.1134.0
'@types/jest':
specifier: ^29.5.13
version: 29.5.14
'@types/node':
specifier: ^22.10.0
version: 22.20.3
dotenv:
specifier: ^16.4.5
version: 16.6.1
jest:
specifier: ^29.7.0
version: 29.7.0(@types/node@22.20.3)(ts-node@10.9.2(@types/node@22.20.3)(typescript@5.9.3))
ts-jest:
specifier: ^29.2.5
version: 29.4.12(@babel/core@7.29.7)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.29.7))(jest-util@29.7.0)(jest@29.7.0(@types/node@22.20.3)(ts-node@10.9.2(@types/node@22.20.3)(typescript@5.9.3)))(typescript@5.9.3)
typescript:
specifier: ^5.6.0
version: 5.9.3
packages:
'@aws-sdk/checksums@3.1001.0':
@@ -434,6 +480,9 @@ packages:
resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==}
engines: {node: '>=12'}
'@emnapi/runtime@1.11.3':
resolution: {integrity: sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==}
'@emotion/is-prop-valid@1.4.0':
resolution: {integrity: sha512-QgD4fyscGcbbKwJmqNvUMSE02OsHUa+lAWKdEUIJKgqe5IwRSKd7+KhibEWdaKwgjLj0DRSHA9biAIqGBk05lw==}
@@ -509,6 +558,152 @@ packages:
resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==}
engines: {node: '>=18.18'}
'@img/colour@1.1.0':
resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==}
engines: {node: '>=18'}
'@img/sharp-darwin-arm64@0.35.4':
resolution: {integrity: sha512-Uhfl4V4lhP2nbUVF9+hyH1+luj86f1gUFeo8ALYxFoULoU+G87D43BfeMP8XHsk9boxAnCY/bf2EHwhA7MuGsA==}
engines: {node: '>=20.9.0'}
cpu: [arm64]
os: [darwin]
'@img/sharp-darwin-x64@0.35.4':
resolution: {integrity: sha512-hWniXY3bG5qKpkKrAwPe4y+VTPmf086YQAnkxWh7uA1YrlRouWGa0M0Mxj3ZjnXFkv7/TD1bTy9lGUK26vRvWw==}
engines: {node: '>=20.9.0'}
cpu: [x64]
os: [darwin]
'@img/sharp-freebsd-wasm32@0.35.4':
resolution: {integrity: sha512-lIsKw/BU+kjB4eZjxrYrZmwOJYi3Ajrv66iAlBmUPyKc3HpnloevB1g3wxGD9P/5BbQ1brBGl65VRRrCvQDEqA==}
engines: {node: '>=20.9.0'}
os: [freebsd]
'@img/sharp-libvips-darwin-arm64@1.3.3':
resolution: {integrity: sha512-suTBPTDGrI9WodccaDdwZItTSaBYASlBk1NSfElSHrUfzu3szG6lvIF58+WiFvnfzuK8ZBFS5zE00PxqxnRiPg==}
cpu: [arm64]
os: [darwin]
'@img/sharp-libvips-darwin-x64@1.3.3':
resolution: {integrity: sha512-FVJZ5mITMobmXIz/hPDTw0EintTW5H3WfrxwLqEqjiIihlu+hVRyGrFQ60xl0Lxn7Bt3zdpevPaQi0HEzqz9fw==}
cpu: [x64]
os: [darwin]
'@img/sharp-libvips-linux-arm64@1.3.3':
resolution: {integrity: sha512-0DaL0A6Xu6sQSQFwe4iVCrKWU2cCTItnRsYsCdxAMm9NF6twAA9BKnoqy4hqz4+azQ0JHuA26qiUKsf1XJ/v5A==}
cpu: [arm64]
os: [linux]
'@img/sharp-libvips-linux-arm@1.3.3':
resolution: {integrity: sha512-3rbU4vqXXc3hY/OiXdl52xZvT0F1yEngWfvqudtPJg/KkyiaQw2DRsFrNzpmLvfavbwOq3qXn36GP8obHRULQA==}
cpu: [arm]
os: [linux]
'@img/sharp-libvips-linux-ppc64@1.3.3':
resolution: {integrity: sha512-cdn1OvUBwsXhbC0zSzJnNzf5MZ/mTrobawDvNXBTxe8VtqKAm0sRuEY2Evzovb/w9JMk4TvRxqt1mekSuJz64w==}
cpu: [ppc64]
os: [linux]
'@img/sharp-libvips-linux-riscv64@1.3.3':
resolution: {integrity: sha512-HjPVx7yKz+0lqdhDlTw1tt90wamBoxhiXpvl1XZpJLiHH4RCJ5yDTqH+VlYPv2fwFs89JFw4c1IexYOcQUi4IQ==}
cpu: [riscv64]
os: [linux]
'@img/sharp-libvips-linux-s390x@1.3.3':
resolution: {integrity: sha512-neWLh+3yCNThxnfy3c4BbVBeGgt9aftno+XbT56iK28RgeDs3UOFWviLWlUu0bArYVYJaFDK+RRohbicUNCm8Q==}
cpu: [s390x]
os: [linux]
'@img/sharp-libvips-linux-x64@1.3.3':
resolution: {integrity: sha512-4vKmvAst9nrowcqquKFAyZJUDolUaIp8uRiN0mWFguJ1IplC9/pitXtlnnlU4aa/eJw3J7i67V+pwUL+wZGdsA==}
cpu: [x64]
os: [linux]
'@img/sharp-libvips-linuxmusl-arm64@1.3.3':
resolution: {integrity: sha512-Y9kQaLMuNoB0bPYOOdcZMaseNrFpPodIWWMrx+CZyydf2xn68j9WYc6sWWRrDwNkzCQjKYfc68L7jKjGlHMibw==}
cpu: [arm64]
os: [linux]
'@img/sharp-libvips-linuxmusl-x64@1.3.3':
resolution: {integrity: sha512-fj8Mv0HHfD1Rr+4I68+3agJynxDWtBFgicTbSOb9Bke6pIwzGcJ+RX/yHjmiEGFMCavY/dxvem7MyNaJF+wDiw==}
cpu: [x64]
os: [linux]
'@img/sharp-linux-arm64@0.35.4':
resolution: {integrity: sha512-De4jpEnAU8Hd5oT0j1G3uL4ZvTuipVMn7YC6vPaJhy6/7EwEae0SVAoBrUMYQbkLGDm85taVWwuPc1a44LTzCQ==}
engines: {node: '>=20.9.0'}
cpu: [arm64]
os: [linux]
'@img/sharp-linux-arm@0.35.4':
resolution: {integrity: sha512-7OAS8gI0EReKGVN2HssHlM6umJgxF5VI3xN0p9FA91p/YO+ou5hiNghLdZ5BEHztwaaK5+bLKRf8x/o2L2nk9A==}
engines: {node: '>=20.9.0'}
cpu: [arm]
os: [linux]
'@img/sharp-linux-ppc64@0.35.4':
resolution: {integrity: sha512-2oYZJeIl4kCcMGk4ouZVjnkCtFrpQFlNEtJ6GbxzhHQchwH0NH/qEb9ykmOl29dqwMq+JhFdZn+1ak2FKhI9fQ==}
engines: {node: '>=20.9.0'}
cpu: [ppc64]
os: [linux]
'@img/sharp-linux-riscv64@0.35.4':
resolution: {integrity: sha512-cPbNChoRURAWdebDIHSenxRpgEdy7JkPydSnUxRm9VvKD7m0/xVaR/8Fzlu81pk5nHEvHH87UZUA7cTtwnbJSA==}
engines: {node: '>=20.9.0'}
cpu: [riscv64]
os: [linux]
'@img/sharp-linux-s390x@0.35.4':
resolution: {integrity: sha512-RY0JFY8Fd6RonCBtHz+DvadaPkXDSI1AUn6yWL9TipqkZ1vY8w8evqdgyDFnkm4/K1ve1TvZiaePP5oSd4+WVQ==}
engines: {node: '>=20.9.0'}
cpu: [s390x]
os: [linux]
'@img/sharp-linux-x64@0.35.4':
resolution: {integrity: sha512-9qvvEAuk8k89TfWUoX2htWjbAMX8p+NxCppjpcg5k6xMsjhBQPTsoIh36h9Qde4WRuGpJeYnOjdosDn/cnv+OA==}
engines: {node: '>=20.9.0'}
cpu: [x64]
os: [linux]
'@img/sharp-linuxmusl-arm64@0.35.4':
resolution: {integrity: sha512-KB5jxpfWQTr0nc3xdHtWChdbifHrBGsd2SM62Eyxrl8afikm+f5qGBU75SJIZBT/S1MC8XyacdlXBMSWq6OURA==}
engines: {node: '>=20.9.0'}
cpu: [arm64]
os: [linux]
'@img/sharp-linuxmusl-x64@0.35.4':
resolution: {integrity: sha512-f+eZJZIQNEEd26RPSW+76chwOf1XtA2Y/O+5ocVyLliHkeih3e+jhLVBdNTd2rS3IbNXK8+ug93Vf5ZXtF5Lxg==}
engines: {node: '>=20.9.0'}
cpu: [x64]
os: [linux]
'@img/sharp-wasm32@0.35.4':
resolution: {integrity: sha512-zQnl4Kwp7Q6NHsENtU2T/00Zi+w3AQNwz3+UaTyVBy2FpXrzXzGjndpK61onhZjRtRpQXxCTeqw19bVyXOh7jA==}
engines: {node: '>=20.9.0'}
'@img/sharp-webcontainers-wasm32@0.35.4':
resolution: {integrity: sha512-ESfNkywmCfPNyaZjxooddJQiQ+l/nTpGEOGthxiLnIHXC/CmcBixnfwUleX9mCz9ovrUUvKMap/pm8RYbzfwaA==}
engines: {node: '>=20.9.0'}
cpu: [wasm32]
'@img/sharp-win32-arm64@0.35.4':
resolution: {integrity: sha512-iNdlBX9gLVvqe2I3uIJSIKTq6wckP/DYxZtcqxm09x5Gi24DnFBmPAWZmr60ZyYMG0xlzo6goG3670ar+RXvRw==}
engines: {node: '>=20.9.0'}
cpu: [arm64]
os: [win32]
'@img/sharp-win32-ia32@0.35.4':
resolution: {integrity: sha512-kqRsbaa5CS6KHlpxnN7WhE6vAAugXyZButpRdvDWetlv6Qv4N9WTcrWzF7tXfB9T7MsoadqdI8hmwLq6UlLvtw==}
engines: {node: ^20.9.0}
cpu: [ia32]
os: [win32]
'@img/sharp-win32-x64@0.35.4':
resolution: {integrity: sha512-XtmnYhBcrORsJ4XJngyzr/EWP0hRZLAZRFaApdKuviyqF78+ylxh2y06ZmtULAMOnObJ3ucpN0AcwSWnMowTRg==}
engines: {node: '>=20.9.0'}
cpu: [x64]
os: [win32]
'@ioredis/commands@2.0.0':
resolution: {integrity: sha512-vrx0AE/T0h7cRZwfo1M39Cr+ZhZrkf0V8mQN75wucKCxCLD9l/VX6no3gFvrLqD1IlG/1LtzWovqEw3t0Vr9zg==}
@@ -2825,6 +3020,15 @@ packages:
setprototypeof@1.2.0:
resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==}
sharp@0.35.4:
resolution: {integrity: sha512-n++8XWcj+jCOr2IOl7h8LbKnGBDY4aPbmprMONBNFdn0ImXqpGVv5zliDs0V9HbmbCQLpbuo2ej9rAoOQTvMDA==}
engines: {node: '>=20.9.0'}
peerDependencies:
'@types/node': '*'
peerDependenciesMeta:
'@types/node':
optional: true
shebang-command@2.0.0:
resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
engines: {node: '>=8'}
@@ -3648,6 +3852,11 @@ snapshots:
dependencies:
'@jridgewell/trace-mapping': 0.3.9
'@emnapi/runtime@1.11.3':
dependencies:
tslib: 2.8.1
optional: true
'@emotion/is-prop-valid@1.4.0':
dependencies:
'@emotion/memoize': 0.9.0
@@ -3722,6 +3931,112 @@ snapshots:
'@humanwhocodes/retry@0.4.3': {}
'@img/colour@1.1.0': {}
'@img/sharp-darwin-arm64@0.35.4':
optionalDependencies:
'@img/sharp-libvips-darwin-arm64': 1.3.3
optional: true
'@img/sharp-darwin-x64@0.35.4':
optionalDependencies:
'@img/sharp-libvips-darwin-x64': 1.3.3
optional: true
'@img/sharp-freebsd-wasm32@0.35.4':
dependencies:
'@img/sharp-wasm32': 0.35.4
optional: true
'@img/sharp-libvips-darwin-arm64@1.3.3':
optional: true
'@img/sharp-libvips-darwin-x64@1.3.3':
optional: true
'@img/sharp-libvips-linux-arm64@1.3.3':
optional: true
'@img/sharp-libvips-linux-arm@1.3.3':
optional: true
'@img/sharp-libvips-linux-ppc64@1.3.3':
optional: true
'@img/sharp-libvips-linux-riscv64@1.3.3':
optional: true
'@img/sharp-libvips-linux-s390x@1.3.3':
optional: true
'@img/sharp-libvips-linux-x64@1.3.3':
optional: true
'@img/sharp-libvips-linuxmusl-arm64@1.3.3':
optional: true
'@img/sharp-libvips-linuxmusl-x64@1.3.3':
optional: true
'@img/sharp-linux-arm64@0.35.4':
optionalDependencies:
'@img/sharp-libvips-linux-arm64': 1.3.3
optional: true
'@img/sharp-linux-arm@0.35.4':
optionalDependencies:
'@img/sharp-libvips-linux-arm': 1.3.3
optional: true
'@img/sharp-linux-ppc64@0.35.4':
optionalDependencies:
'@img/sharp-libvips-linux-ppc64': 1.3.3
optional: true
'@img/sharp-linux-riscv64@0.35.4':
optionalDependencies:
'@img/sharp-libvips-linux-riscv64': 1.3.3
optional: true
'@img/sharp-linux-s390x@0.35.4':
optionalDependencies:
'@img/sharp-libvips-linux-s390x': 1.3.3
optional: true
'@img/sharp-linux-x64@0.35.4':
optionalDependencies:
'@img/sharp-libvips-linux-x64': 1.3.3
optional: true
'@img/sharp-linuxmusl-arm64@0.35.4':
optionalDependencies:
'@img/sharp-libvips-linuxmusl-arm64': 1.3.3
optional: true
'@img/sharp-linuxmusl-x64@0.35.4':
optionalDependencies:
'@img/sharp-libvips-linuxmusl-x64': 1.3.3
optional: true
'@img/sharp-wasm32@0.35.4':
dependencies:
'@emnapi/runtime': 1.11.3
optional: true
'@img/sharp-webcontainers-wasm32@0.35.4':
dependencies:
'@img/sharp-wasm32': 0.35.4
optional: true
'@img/sharp-win32-arm64@0.35.4':
optional: true
'@img/sharp-win32-ia32@0.35.4':
optional: true
'@img/sharp-win32-x64@0.35.4':
optional: true
'@ioredis/commands@2.0.0': {}
'@istanbuljs/load-nyc-config@1.1.0':
@@ -4915,8 +5230,7 @@ snapshots:
destroy@1.2.0: {}
detect-libc@2.1.2:
optional: true
detect-libc@2.1.2: {}
detect-newline@3.1.0: {}
@@ -6387,6 +6701,39 @@ snapshots:
setprototypeof@1.2.0: {}
sharp@0.35.4(@types/node@22.20.3):
dependencies:
'@img/colour': 1.1.0
detect-libc: 2.1.2
semver: 7.8.5
optionalDependencies:
'@img/sharp-darwin-arm64': 0.35.4
'@img/sharp-darwin-x64': 0.35.4
'@img/sharp-freebsd-wasm32': 0.35.4
'@img/sharp-libvips-darwin-arm64': 1.3.3
'@img/sharp-libvips-darwin-x64': 1.3.3
'@img/sharp-libvips-linux-arm': 1.3.3
'@img/sharp-libvips-linux-arm64': 1.3.3
'@img/sharp-libvips-linux-ppc64': 1.3.3
'@img/sharp-libvips-linux-riscv64': 1.3.3
'@img/sharp-libvips-linux-s390x': 1.3.3
'@img/sharp-libvips-linux-x64': 1.3.3
'@img/sharp-libvips-linuxmusl-arm64': 1.3.3
'@img/sharp-libvips-linuxmusl-x64': 1.3.3
'@img/sharp-linux-arm': 0.35.4
'@img/sharp-linux-arm64': 0.35.4
'@img/sharp-linux-ppc64': 0.35.4
'@img/sharp-linux-riscv64': 0.35.4
'@img/sharp-linux-s390x': 0.35.4
'@img/sharp-linux-x64': 0.35.4
'@img/sharp-linuxmusl-arm64': 0.35.4
'@img/sharp-linuxmusl-x64': 0.35.4
'@img/sharp-webcontainers-wasm32': 0.35.4
'@img/sharp-win32-arm64': 0.35.4
'@img/sharp-win32-ia32': 0.35.4
'@img/sharp-win32-x64': 0.35.4
'@types/node': 22.20.3
shebang-command@2.0.0:
dependencies:
shebang-regex: 3.0.0
@@ -6623,7 +6970,7 @@ snapshots:
dependencies:
typescript: 5.9.3
ts-jest@29.4.12(@babel/core@7.29.7)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.29.7))(jest-util@29.7.0)(jest@29.7.0(@types/node@22.20.3))(typescript@5.9.3):
ts-jest@29.4.12(@babel/core@7.29.7)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.29.7))(jest-util@29.7.0)(jest@29.7.0(@types/node@22.20.3)(ts-node@10.9.2(@types/node@22.20.3)(typescript@5.9.3)))(typescript@5.9.3):
dependencies:
bs-logger: 0.2.6
fast-json-stable-stringify: 2.1.0