image icon indicating copy to clipboard operation
image copied to clipboard

Add support provider "imgproxy"

Open adinvadim opened this issue 3 years ago • 13 comments

It's free, fast and opensource stadealone server for resizising images.

https://imgproxy.net/

adinvadim avatar Aug 02 '21 13:08 adinvadim

I'm currently testing my custom version of the provider, maybe it will help the developers. @pi0

$ yarn add create-hmac

nuxt.config.js

export default {
  // ....
  image: {
    providers: {
      customProvider: {
        name: 'imgproxy',
        provider: './src/providers/imgproxy.js',
        options: {
          baseURL: 'http://localhost:4000',
          key: 'xxxxxxxxxxxxxx',
          salt: 'xxxxxxxxxxxxxx',
        }
      }
    },
    provider: 'imgproxy',
  },
 // ....
}

imgproxy.js

import {joinURL} from 'ufo';
import createHmac from 'create-hmac';
import {createOperationsGenerator} from '~image'

const operationsGenerator = createOperationsGenerator({
  keyMap: {
    resize: 'rs',
    size: 's',
    fit: 'rt',
    width: 'w',
    height: 'h',
    dpr: 'dpr',
    enlarge: 'el',
    extend: 'ex',
    gravity: 'g',
    crop: 'c',
    padding: 'pd',
    trim: 't',
    rotate: 'rot',
    quality: 'q',
    maxBytes: 'mb',
    background: 'bg',
    backgroundAlpha: 'bga',
    blur: 'bl',
    sharpen: 'sh',
    watermark: 'wm',
    preset: 'pr',
    cacheBuster: 'cb',
    stripMetadata: 'sm',
    stripColorProfile: 'scp',
    autoRotate: 'ar',
    filename: 'fn',
    format: 'f',
  },
  formatter: (key, value) => `${key}:${value}`
})

function urlSafeBase64(string) {
  return Buffer.from(string)
    .toString('base64')
    .replace(/=/g, '')
    .replace(/\+/g, '-')
    .replace(/\//g, '_')
}

const hexDecode = (hex) => Buffer.from(hex, 'hex')

function sign(salt, target, secret) {
  const hmac = createHmac('sha256', hexDecode(secret))

  hmac.update(hexDecode(salt))
  hmac.update(target)

  return urlSafeBase64(hmac.digest())
}

const defaultModifiers = {
  fit: 'fill',
  width: 0,
  height: 0,
  gravity: 'no',
  enlarge: 1,
  format: 'webp',
}

export function getImage(src, {modifiers, baseURL, key, salt} = {}) {
  const mergeModifiers = {...defaultModifiers, ...modifiers}
  const encodedUrl = urlSafeBase64(src)
  const path = joinURL('/', operationsGenerator(mergeModifiers), encodedUrl)
  const signature = sign(salt, path, key)
  
  return {
    url: joinURL(baseURL, signature, path)
  }
}

Example of usage

<template>
  <div class="md:container md:mx-auto md:max-w-screen-lg">
    <nuxt-img src="https://cc.cz/wp-content/uploads/2021/08/messi-psg.jpg" />
  </div>
</template>

<script>
export default {
  name: 'Homepage'
}
</script>

Cheers! 🍷 ⚽

misaon avatar Aug 13 '21 10:08 misaon

Thanks, but what is createOperationsGenerator? How does this function look like?

adinvadim avatar Aug 29 '21 16:08 adinvadim

I found

import { createOperationsGenerator } from '@nuxt/image/dist/runtime/utils/index';

adinvadim avatar Aug 29 '21 16:08 adinvadim

Thanks a lot!

adinvadim avatar Aug 29 '21 17:08 adinvadim

Update for nuxt 3:

create-hmac doesnt work fine with vite, thats why easiest way it is create-hmac → hash.js

function sign(salt: string, target: string, secret: string) {
  const hmac = hash.hmac(hash.sha256, hexDecode(secret));
  hmac.update(hexDecode(salt));
  hmac.update(target);

  return urlSafeBase64(hmac.digest());
}

And you should yarn add buffer and import { Buffer } from 'buffer' for isomorph Buffer

adinvadim avatar Oct 23 '22 16:10 adinvadim

Full version of imageproxy provider for nuxt3 and nuxt/image v1

import { joinURL } from "ufo";
import { createOperationsGenerator } from "@nuxt/image-edge/dist/runtime/utils/index";
import { ProviderGetImage } from "@nuxt/image-edge";
import { Buffer } from "buffer";
import * as hash from "hash.js";

const operationsGenerator = createOperationsGenerator({
  keyMap: {
    resize: "rs",
    size: "s",
    fit: "rt",
    width: "w",
    height: "h",
    dpr: "dpr",
    enlarge: "el",
    extend: "ex",
    gravity: "g",
    crop: "c",
    padding: "pd",
    trim: "t",
    rotate: "rot",
    quality: "q",
    maxBytes: "mb",
    background: "bg",
    backgroundAlpha: "bga",
    blur: "bl",
    sharpen: "sh",
    watermark: "wm",
    preset: "pr",
    cacheBuster: "cb",
    stripMetadata: "sm",
    stripColorProfile: "scp",
    autoRotate: "ar",
    filename: "fn",
    format: "f",
  },
  formatter: (key, value) => `${key}:${value}`,
});

function urlSafeBase64(string) {
  return Buffer.from(string, "utf8")
    .toString("base64")
    .replace(/=/g, "")
    .replace(/\+/g, "-")
    .replace(/\//g, "_");
}

const hexDecode = (hex: string) => Buffer.from(hex, "hex");

function sign(salt: string, target: string, secret: string) {
  const hmac = hash.hmac(hash.sha256, hexDecode(secret));
  hmac.update(hexDecode(salt));
  hmac.update(target);

  return urlSafeBase64(hmac.digest());
}

const defaultModifiers = {
  fit: "fill",
  width: 0,
  height: 0,
  gravity: "no",
  enlarge: 1,
  format: "webp",
};

export const getImage: ProviderGetImage = (src, options) => {
  const { modifiers, imgProxyUrl, imgProxySalt, imgProxyKey } = options;
  const mergeModifiers = { ...defaultModifiers, ...modifiers };
  const encodedUrl = urlSafeBase64(src);
  const path = joinURL("/", operationsGenerator(mergeModifiers), encodedUrl);
  const signature = sign(imgProxySalt, path, imgProxyKey);

  return {
    url: joinURL(imgProxyUrl, signature, path),
  };
};

adinvadim avatar Oct 23 '22 16:10 adinvadim

Hello @adinvadim, Where should I add the config that you have shared ? Is it still working?

casualmatt avatar Feb 05 '23 18:02 casualmatt

Hi! @casualmatt

Add this file to any directory and add this configuration to nuxt.config.ts

  image: {
    providers: {
      imgproxy: {
        name: "imgproxy",
        provider: "~~/utils/nuxt-image/imgproxy.provider",
        options: {
          imgProxyUrl: ...,
          imgProxyKey: ...,
          imgProxySalt: ...,
        },
      },
    },
  },
image

adinvadim avatar May 08 '23 05:05 adinvadim

Hi @adinvadim. How does getSafeConfigEnv look like?

Sevochka avatar Jul 09 '23 09:07 Sevochka

@Sevochka it is just my simple utils for working with env variables. May be is not great, because I am using this code from my nuxt 2 codebase.

export const getSafeEnv = (key: string, defaultValue?: string) => {
  const result = process.env[key];

  if (result != null) {
    return result;
  } else if (defaultValue !== undefined) {
    return defaultValue;
  } else {
    throw new Error(`Env variable "${key}" is required`);
  }
};

export const getSafeConfigEnv = (key: string, defaultValue?: string) => {
  try {
    return getSafeEnv(key, defaultValue);
  } catch (e) {
    if (isBuild) {
      return null;
    } else {
      throw e;
    }
  }
};

adinvadim avatar Apr 21 '24 13:04 adinvadim

I would welcome a PR to add an imgproxy provider. 🙏

Given the code provided above, I imagine it would be a straightforward implementation...

danielroe avatar Apr 22 '24 12:04 danielroe

@danielroe I have mine working as the custom provider; I can open a PR; the only question I have is how to sign the URL without publishing the key so that imgproxy can't be exploited, right now I allow on imgproxy just my domain as source but I don't think is a clean way to do it.

casualmatt avatar Apr 22 '24 13:04 casualmatt

Perhaps we can run a docker container within the github actions workflow?

danielroe avatar Apr 22 '24 13:04 danielroe