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
113 changes: 113 additions & 0 deletions .github/workflows/test-layer.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
name: Test layer

on:
pull_request:
push:
branches: [main]
workflow_dispatch:

permissions:
contents: read

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}

env:
SAM_CLI_TELEMETRY: 0
LAMBDA_IMAGE: public.ecr.aws/lambda/nodejs:24

jobs:
syntax-check:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: 24
- run: npm test

# Decides whether the layer needs to be rebuilt. The workflow itself always runs because its jobs
# are required checks; a job skipped via `if` counts as passing, a workflow that never starts does not.
changes:
runs-on: ubuntu-latest
timeout-minutes: 5
outputs:
layer: ${{ steps.filter.outputs.layer }}
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
- id: filter
env:
BASE_SHA: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || github.event.before }}
run: |
if [ "$GITHUB_EVENT_NAME" = workflow_dispatch ] || ! git cat-file -e "${BASE_SHA}^{commit}" 2>/dev/null; then
echo "layer=true" >> "$GITHUB_OUTPUT"
exit 0
fi
git diff --name-only "$BASE_SHA" "$GITHUB_SHA" | tee changed-files.txt
if grep -qE '^(layer/|test/|examples/src/|template\.yaml$|\.github/workflows/test-layer\.yml$)' changed-files.txt; then
echo "layer=true" >> "$GITHUB_OUTPUT"
else
echo "layer=false" >> "$GITHUB_OUTPUT"
fi

# The compiled layer must not be published (codec licensing), so it is never uploaded as an
# artifact or cached; it only exists on the ephemeral runner for the duration of this job.
build-and-test:
needs: changes
if: needs.changes.outputs.layer == 'true'
runs-on: ubuntu-latest
timeout-minutes: 120
steps:
- uses: actions/checkout@v7

- uses: aws-actions/setup-sam@v3
with:
use-installer: true

- name: Read pinned versions
run: |
echo "EXPECTED_SHARP_VERSION=$(sed -n 's/^SHARP_VERSION=//p' layer/Makefile)" >> "$GITHUB_ENV"
echo "EXPECTED_VIPS_VERSION=$(sed -n 's/^VIPS_VERSION=//p' layer/Makefile)" >> "$GITHUB_ENV"

# Same build image as the documented CodeBuild/SAM path. GitHub runners have 4 vCPUs / 16 GB,
# so the CodeBuild-oriented parallelism limits can be raised.
- name: Build layer
run: sam build --use-container --container-env-var BUILD_JOBS=4 --container-env-var LIBAOM_BUILD_JOBS=2

# The Lambda runtime image has no build toolchain or -devel packages, so any shared library
# missing from the layer's lib/ directory fails here.
- name: Test layer in Lambda runtime image
run: |
docker run --rm \
--entrypoint /var/lang/bin/node \
-v "$PWD/.aws-sam/build/SharpHEICLayer:/opt:ro" \
-v "$PWD:/work:ro" \
-e NODE_PATH=/opt/nodejs/node_modules \
-e LD_LIBRARY_PATH=/var/lang/lib:/lib64:/usr/lib64:/var/runtime:/var/runtime/lib:/var/task:/var/task/lib:/opt/lib \
-e EXPECTED_SHARP_VERSION -e EXPECTED_VIPS_VERSION \
"$LAMBDA_IMAGE" --test /work/test/layer.test.js

- name: Invoke example function via Lambda Runtime Interface Emulator
run: |
docker run -d --name lambda -p 9000:8080 \
-v "$PWD/.aws-sam/build/SharpHEICLayer:/opt:ro" \
-v "$PWD/examples/src:/var/task:ro" \
"$LAMBDA_IMAGE" index.handler
curl --silent --show-error --fail --retry 10 --retry-connrefused --retry-delay 1 \
-d '{}' http://localhost:9000/2015-03-31/functions/function/invocations > response.json
node -e '
const res = JSON.parse(require("fs").readFileSync("response.json", "utf8"));
if (res.statusCode !== 200) throw new Error("unexpected response: " + JSON.stringify(res).slice(0, 500));
if (res.headers["content-type"] !== "image/webp") throw new Error("unexpected content-type " + res.headers["content-type"]);
const body = Buffer.from(res.body, "base64");
if (body.toString("ascii", 0, 4) !== "RIFF" || body.toString("ascii", 8, 12) !== "WEBP") throw new Error("body is not a WebP image");
console.log("Lambda returned a WebP image of " + body.length + " bytes");
'

- name: Lambda container logs
if: always()
run: docker logs lambda 2>&1 || true
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ sam deploy --guided
### Lambda Layer
- Add the lambda layer with ARN `arn:aws:lambda:us-east-1:${AWS:AccountId}:layer:sharp-heic:${LAYER_VERSION}` to any lambda function (replace `${LAYER_VERSION}` with the appropriate version and `${AWS:AccountId}` if you're not using a layer from the same account as the function). You can also import the layer ARN using `!ImportValue SharpHEICLayerArn`.
- Remove sharp from the dependencies in the function code (it will otherwise conflict with the one provided through the layer)
- When encoding HEIC (`heif({ compression: 'hevc' })`), pass `tune: 'ssim'` or `tune: 'psnr'`. sharp's default `tune: 'auto'` is rejected by the x265 encoder with `heif: Invalid parameter value (5.2006)`, see [lovell/sharp#4621](https://github.com/lovell/sharp/issues/4621). AVIF (`compression: 'av1'`) is not affected.
- See [example template](examples/sam-template.yaml) for a complete sample template.

### Environment Variables for build
Expand All @@ -58,6 +59,9 @@ The environment variables are used to create a `samconfig.toml` file that config
### Note regarding build process
Previously, some custom docker images were needed to build this layer. AWS now publishes managed SAM build images for current Lambda runtimes, including `public.ecr.aws/sam/build-nodejs24.x`.

### Testing
The [Test layer](.github/workflows/test-layer.yml) workflow builds the layer with `sam build --use-container` and mounts the result at `/opt` in the `public.ecr.aws/lambda/nodejs:24` runtime image. There it runs [test/layer.test.js](test/layer.test.js), which checks shared library resolution, the libvips and sharp versions, HEIC decoding, WebP resizing, and HEIF encoding with both HEVC and AV1. Pixel colours are verified against [test/fixtures/quadrants.heic](test/fixtures/quadrants.heic), a synthetic image encoded with macOS `sips` (regenerate with `node test/fixtures/make-quadrants-heic.js`). The layer is only rebuilt when `layer/`, `test/`, `examples/src/`, `template.yaml` or the workflow itself change; for other changes (e.g. documentation) the build job is skipped. It also invokes the [example function](examples/src/index.js) through the Lambda Runtime Interface Emulator.

## Background
This repo exists as it is rather painful to compile all libraries required to get sharp to work with HEIC/HEIF files in an AWS Lambda environment. The sharp repository has several [issues](https://github.com/lovell/sharp/issues) related to this.

Expand Down
4 changes: 2 additions & 2 deletions buildspec.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ phases:
- NODE_ENV=development npm ci
- 'echo "==> Pre-build phase: audit dependencies"'
- npm audit --audit-level=moderate
- 'echo "==> Pre-build phase: lint"'
- npm run lint
- 'echo "==> Pre-build phase: syntax check"'
- npm test
build:
commands:
- 'echo "==> Build phase: sam build"'
Expand Down
8 changes: 0 additions & 8 deletions eslint.config.js

This file was deleted.

Loading
Loading