diff --git a/example/README.md b/example/README.md
index f981602..b20b37d 100644
--- a/example/README.md
+++ b/example/README.md
@@ -3,5 +3,5 @@
Чтобы запустить этот пример,
нужно предварительно запустить imgproxy локально на 8080 порту командой
`docker run -e IMGPROXY_MAX_SRC_RESOLUTION=40 -p 8080:8080 -it darthsim/imgproxy`,
-затем выполнить `npm run run-example` из корня проекта
+затем выполнить `npm run dev` из корня проекта
и открыть в браузере http://localhost:8081/.
diff --git a/example/react-entry.tsx b/example/react-entry.tsx
index bd3d2ae..4bbfa6c 100644
--- a/example/react-entry.tsx
+++ b/example/react-entry.tsx
@@ -2,35 +2,58 @@ import React from 'react';
import ReactDOM from 'react-dom';
import { PictureSmart } from '../src/react';
import { backgroundCssSmart } from '../src/utils/backgroundCss';
+import { getOriginal } from '../src/utils';
// Adds 'webp' class to html element if the browser supports webp.
-import '../src/utils/webpDetector'
+import '../src/utils/webpDetector';
const oneImageForAllBreakpoints = require.context('./images/oneImageForAllBreakpoints');
const differentBreakpoints = require.context('./images/differentBreakpoints');
+const imageWithoutResize = require.context('./images/oneImageForAllBreakpoints?dontresize');
+
+const myImageData = require('./images/oneImageForAllBreakpoints/all.png');
+const myImageWithoutResizeData = require('./images/oneImageForAllBreakpoints/all.png?dontresize');
+
+// Usage example getOriginal ()
+console.log(getOriginal(myImageData));
+console.log(getOriginal(myImageWithoutResizeData));
+
ReactDOM.render(
Example usage of csssr.images
As picture tag
+
One image for all resolutions
+
Image with different breakpoints
+
Image without resize
+
As background css
+
One image for all resolutions
One image for all resolutions on background
+
Different breakpoints
Image with different breakpoints on background
+
+
Image without resize
+
+
+ Image without resize on background
+
+
,
document.getElementById('app'),
);
diff --git a/example/webpack.config.ts b/example/webpack.config.ts
index de0be20..5b96552 100644
--- a/example/webpack.config.ts
+++ b/example/webpack.config.ts
@@ -1,6 +1,52 @@
import path from 'path';
import webpack from 'webpack';
+import ip from 'ip';
import { Plugin } from '../src/webpack';
+import { Dpr } from '../src/types';
+
+const handleImagesForOriginalPixelRatio = (originalPixelRatio: Dpr) => {
+ return {
+ use: [
+ {
+ loader: path.resolve(__dirname, '../src/index.ts'),
+ options: {
+ breakpoints: [
+ {
+ name: 'mobile',
+ maxWidth: 767,
+ },
+ {
+ name: 'tablet',
+ minWidth: 768,
+ maxWidth: 1279,
+ },
+ {
+ name: 'desktop',
+ minWidth: 1280,
+ },
+ ],
+ imgproxy: {
+ disable: false,
+ imagesHost: process.env.HOST || `http://${ip.address()}:8081`,
+ host: process.env.IMGPROXY_HOST || 'http://localhost:8080',
+ },
+ originalPixelRatio,
+ },
+ },
+ {
+ loader: 'file-loader',
+ options: {
+ publicPath: '/build',
+ name:
+ process.env.NODE_ENV === 'development'
+ ? '[path][name].[ext]'
+ : '[path][name]-[hash:8].[ext]',
+ esModule: false,
+ },
+ },
+ ],
+ };
+};
const config: webpack.Configuration = {
mode: 'production',
@@ -23,42 +69,13 @@ const config: webpack.Configuration = {
},
{
test: /\.(jpe?g|png|gif)$/,
- use: [
+ oneOf: [
{
- loader: path.resolve(__dirname, '../src/index.ts'),
- options: {
- breakpoints: [
- {
- name: 'mobile',
- maxWidth: 767,
- },
- {
- name: 'tablet',
- minWidth: 768,
- maxWidth: 1279,
- },
- {
- name: 'desktop',
- minWidth: 1280,
- },
- ],
- imgproxy: {
- disable: false,
- imagesHost: process.env.HOST || 'http://192.168.1.134:8081',
- host: process.env.IMGPROXY_HOST || 'http://localhost:8080',
- },
- },
+ resourceQuery: /dontresize/,
+ ...handleImagesForOriginalPixelRatio('1x'),
},
{
- loader: 'file-loader',
- options: {
- publicPath: '/build',
- name:
- process.env.NODE_ENV === 'development'
- ? '[path][name].[ext]'
- : '[path][name]-[hash:8].[ext]',
- esModule: false,
- },
+ ...handleImagesForOriginalPixelRatio('3x'),
},
],
},
diff --git a/package-lock.json b/package-lock.json
index 171267a..f9fb08a 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1181,6 +1181,14 @@
"@types/node": "*"
}
},
+ "@types/ip": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@types/ip/-/ip-1.1.0.tgz",
+ "integrity": "sha512-dwNe8gOoF70VdL6WJBwVHtQmAX4RMd62M+mAB9HQFjG1/qiCLM/meRy95Pd14FYBbEDwCq7jgJs89cHpLBu4HQ==",
+ "requires": {
+ "@types/node": "*"
+ }
+ },
"@types/istanbul-lib-coverage": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.2.tgz",
@@ -1235,8 +1243,7 @@
"@types/node": {
"version": "14.0.23",
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.0.23.tgz",
- "integrity": "sha512-Z4U8yDAl5TFkmYsZdFPdjeMa57NOvnaf1tljHzhouaPEp7LCj2JKkejpI1ODviIAQuW4CcQmxkQ77rnLsOOoKw==",
- "dev": true
+ "integrity": "sha512-Z4U8yDAl5TFkmYsZdFPdjeMa57NOvnaf1tljHzhouaPEp7LCj2JKkejpI1ODviIAQuW4CcQmxkQ77rnLsOOoKw=="
},
"@types/normalize-package-data": {
"version": "2.4.0",
@@ -4728,6 +4735,12 @@
"integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==",
"dev": true
},
+ "ip": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz",
+ "integrity": "sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=",
+ "dev": true
+ },
"ip-regex": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-2.1.0.tgz",
diff --git a/package.json b/package.json
index 2e9e2e8..cfcfd64 100644
--- a/package.json
+++ b/package.json
@@ -38,7 +38,8 @@
"eslint-plugin-react": "^7.20.3",
"file-loader": "^6.0.0",
"http-server": "^0.12.3",
- "jest": "^26.1.0",
+ "ip": "^1.1.5",
+ "jest": "^26.0.1",
"loader-utils": "^2.0.0",
"prettier": "^2.0.5",
"ts-jest": "^26.1.3",
@@ -49,6 +50,7 @@
"webpack-cli": "^3.3.12"
},
"dependencies": {
+ "@types/ip": "^1.1.0",
"imgproxy": "^0.1.2",
"react": "^16.13.1",
"react-dom": "^16.13.1"
diff --git a/src/types.ts b/src/types.ts
index 90fcd8a..68b2552 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -1,7 +1,15 @@
export type Dpr = '1x' | '2x' | '3x';
+export type CompressionRatio = {
+ '1x': number;
+ '2x'?: number;
+ '3x'?: number;
+};
+
export type SrcSet = {
- [dpr in Dpr]: string;
+ '1x': string;
+ '2x'?: string;
+ '3x'?: string;
};
export type ExtensionSrcSet = {
diff --git a/src/utils/__tests__/backgroundCss.ts b/src/utils/__tests__/backgroundCss.ts
index ca6c704..3ca4b86 100644
--- a/src/utils/__tests__/backgroundCss.ts
+++ b/src/utils/__tests__/backgroundCss.ts
@@ -397,3 +397,34 @@ test('backgroundCss multiple breakpoints, png and webp', () => {
}
}`);
});
+
+test('backgroundCss all breakpoints, png and webp, one pixel ratio', () => {
+ expect(
+ backgroundCss('.my-selector', [
+ {
+ breakpointMedia: null,
+ srcSets: [
+ {
+ extension: 'png',
+ srcSet: {
+ '1x': '/mobile.all.1x.png',
+ },
+ },
+ {
+ extension: 'webp',
+ srcSet: {
+ '1x': '/mobile.all.1x.webp',
+ },
+ },
+ ],
+ },
+ ]),
+ ).toBeSameCss(`
+ .my-selector {
+ background-image: url(/mobile.all.1x.png);
+ }
+ html.webp .my-selector {
+ background-image: url(/mobile.all.1x.webp);
+ }
+`);
+});
diff --git a/src/utils/__tests__/getCompressionRatio.ts b/src/utils/__tests__/getCompressionRatio.ts
new file mode 100644
index 0000000..591e5ef
--- /dev/null
+++ b/src/utils/__tests__/getCompressionRatio.ts
@@ -0,0 +1,17 @@
+import { getCompressionRatio } from '../index';
+
+test('Pixel ratios 1x', () => {
+ expect(getCompressionRatio(['1x'])).toStrictEqual({ '1x': 0 });
+});
+
+test('Pixel ratios 1x, 2x', () => {
+ expect(getCompressionRatio(['1x', '2x'])).toStrictEqual({ '1x': 0.5, '2x': 0 });
+});
+
+test('Pixel ratios 1x, 2x, 3x', () => {
+ expect(getCompressionRatio(['1x', '2x', '3x'])).toStrictEqual({
+ '1x': 0.33333,
+ '2x': 0.66667,
+ '3x': 0,
+ });
+});
diff --git a/src/utils/backgroundCss.ts b/src/utils/backgroundCss.ts
index d492e3a..19e04dd 100644
--- a/src/utils/backgroundCss.ts
+++ b/src/utils/backgroundCss.ts
@@ -17,17 +17,31 @@ const srcSetCss = (selector: string, sources: ExtensionSrcSet[]): string => {
(acc, source) => {
const finalSelector = getSelector(selector, source.extension);
acc['1x'].push(`${finalSelector} { background-image: url(${source.srcSet['1x']}); }`);
- acc['2x'].push(`${finalSelector} { background-image: url(${source.srcSet['2x']}); }`);
- acc['3x'].push(`${finalSelector} { background-image: url(${source.srcSet['3x']}); }`);
+ source.srcSet['2x'] &&
+ acc['2x'].push(`${finalSelector} { background-image: url(${source.srcSet['2x']}); }`);
+ source.srcSet['3x'] &&
+ acc['3x'].push(`${finalSelector} { background-image: url(${source.srcSet['3x']}); }`);
return acc;
},
{ '1x': [], '2x': [], '3x': [] },
);
return `
-${result['1x'].join(' ')}
-@media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi) { ${result['2x'].join(' ')} }
-@media (-webkit-min-device-pixel-ratio: 3), (min-resolution: 288dpi) { ${result['3x'].join(' ')} }
+ ${result['1x'].join(' ')}
+ ${
+ result['2x'].length
+ ? `@media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi) { ${result['2x'].join(
+ ' ',
+ )} }`
+ : ''
+ }
+ ${
+ result['3x'].length
+ ? ` @media (-webkit-min-device-pixel-ratio: 3), (min-resolution: 288dpi) { ${result[
+ '3x'
+ ].join(' ')} }`
+ : ''
+ }
`;
};
diff --git a/src/utils/getCompressionRatio.ts b/src/utils/getCompressionRatio.ts
new file mode 100644
index 0000000..cc122db
--- /dev/null
+++ b/src/utils/getCompressionRatio.ts
@@ -0,0 +1,15 @@
+import { Dpr, CompressionRatio } from '../types';
+
+export const getCompressionRatio = (pixelRatios: Dpr[]): CompressionRatio => {
+ const length = pixelRatios.length;
+
+ return pixelRatios.reduce((acc, item, index) => {
+ if (index + 1 === length) {
+ acc[item] = 0;
+ return acc;
+ }
+
+ acc[item] = Number(((index + 1) / length).toFixed(5));
+ return acc;
+ }, {} as CompressionRatio);
+};
diff --git a/src/utils/getOriginal.ts b/src/utils/getOriginal.ts
index c904e46..03fa628 100644
--- a/src/utils/getOriginal.ts
+++ b/src/utils/getOriginal.ts
@@ -1,4 +1,7 @@
import { BreakpointSource } from '../types';
-export const getOriginal = (source: BreakpointSource): string =>
- source.srcSets[source.srcSets.length - 1].srcSet['3x'];
+export const getOriginal = (source: BreakpointSource): string => {
+ const srcSet = source.srcSets[source.srcSets.length - 1].srcSet;
+
+ return srcSet['3x'] || srcSet['2x'] || srcSet['1x'];
+};
diff --git a/src/utils/getPixelRatios.ts b/src/utils/getPixelRatios.ts
new file mode 100644
index 0000000..0d4dacd
--- /dev/null
+++ b/src/utils/getPixelRatios.ts
@@ -0,0 +1,12 @@
+import { Dpr } from '../types';
+
+export const getPixelRatios = (originalPixelRatio: Dpr): Dpr[] => {
+ switch (originalPixelRatio) {
+ case '1x':
+ return ['1x'];
+ case '2x':
+ return ['1x', '2x'];
+ case '3x':
+ return ['1x', '2x', '3x'];
+ }
+};
diff --git a/src/utils/index.ts b/src/utils/index.ts
index ed31173..9f6e826 100644
--- a/src/utils/index.ts
+++ b/src/utils/index.ts
@@ -3,3 +3,5 @@ export { getBreakpointMedia } from './getBreakpointMedia';
export { getSrcSetString } from './getSrcSetString';
export { getSources } from './getSources';
export { getOriginal } from './getOriginal';
+export { getPixelRatios } from './getPixelRatios';
+export { getCompressionRatio } from './getCompressionRatio';
diff --git a/src/utils/webpDetector.ts b/src/utils/webpDetector.ts
index 43282ba..6cdabe8 100644
--- a/src/utils/webpDetector.ts
+++ b/src/utils/webpDetector.ts
@@ -1,13 +1,15 @@
// https://stackoverflow.com/a/27232658
const canUseWebp = (): boolean => {
- const canvas = document.createElement('canvas')
- canvas.width = canvas.height = 1
- return Boolean(canvas.toDataURL &&
- canvas.toDataURL('image/webp') &&
- canvas.toDataURL('image/webp').indexOf('image/webp') === 5)
-}
+ const canvas = document.createElement('canvas');
+ canvas.width = canvas.height = 1;
+ return Boolean(
+ canvas.toDataURL &&
+ canvas.toDataURL('image/webp') &&
+ canvas.toDataURL('image/webp').indexOf('image/webp') === 5,
+ );
+};
if (canUseWebp()) {
- document.documentElement.classList.add('webp')
+ document.documentElement.classList.add('webp');
}
diff --git a/src/webpack/imgproxyUrlBuilder.ts b/src/webpack/imgproxyUrlBuilder.ts
index b93e055..f0dd5fc 100644
--- a/src/webpack/imgproxyUrlBuilder.ts
+++ b/src/webpack/imgproxyUrlBuilder.ts
@@ -1,17 +1,22 @@
import Imgproxy from 'imgproxy';
-import { SrcSet } from '../types';
+import { Dpr, SrcSet } from '../types';
+import { getCompressionRatio } from '../utils';
type ImgproxyUrlBuilderConfig = {
imagesHost: string;
host: string;
};
-export type BuildUrlsForAllPixelRatios = (imagePath: string, extension: string) => SrcSet;
+export type BuildUrlsForPixelRatios = (
+ pixelRatios: Dpr[],
+ imagePath: string,
+ extension: string,
+) => SrcSet;
export const getImgproxyUrlBuilder = ({
imagesHost,
host,
-}: ImgproxyUrlBuilderConfig): BuildUrlsForAllPixelRatios => {
+}: ImgproxyUrlBuilderConfig): BuildUrlsForPixelRatios => {
const imgproxy = new Imgproxy({
baseUrl: host,
encode: false,
@@ -24,10 +29,15 @@ export const getImgproxyUrlBuilder = ({
.generateUrl(imagesHost + imgPath, extension);
};
- return (imagePath: string, extension: string): SrcSet => ({
- '1x': buildImgproxyUrl(imagePath, 0.3333, extension),
- '2x': buildImgproxyUrl(imagePath, 0.6666, extension),
- // 0 здесь означает, что не будет никакого изменения размеров картинки
- '3x': buildImgproxyUrl(imagePath, 0, extension),
- });
+ return (pixelRatios: Dpr[], imagePath: string, extension: string): SrcSet => {
+ const compressionsRatio = getCompressionRatio(pixelRatios);
+
+ return pixelRatios.reduce((acc, item) => {
+ const dprResize = compressionsRatio[item];
+ if (dprResize !== undefined) {
+ acc[item] = buildImgproxyUrl(imagePath, dprResize, extension);
+ }
+ return acc;
+ }, {} as SrcSet);
+ };
};
diff --git a/src/webpack/loader.ts b/src/webpack/loader.ts
index deccada..28ff2c9 100644
--- a/src/webpack/loader.ts
+++ b/src/webpack/loader.ts
@@ -3,10 +3,10 @@ import path from 'path';
import loaderUtils from 'loader-utils';
import validateOptions from 'schema-utils';
import { getImgproxyUrlBuilder } from './imgproxyUrlBuilder';
-import { Breakpoint, OrderedBreakpointSource, SrcSet } from '../types';
+import { Breakpoint, OrderedBreakpointSource, SrcSet, Dpr } from '../types';
import { imageUrls } from './plugin';
import { schema } from './loaderOptionsSchema';
-import { getBreakpointMedia } from '../utils';
+import { getBreakpointMedia, getPixelRatios } from '../utils';
// Такое имя используется, если нужна одна картинка для всех разрешений
// В таком случаем не будут сгенерированы медиа выражения для разных breakpoint'ов
@@ -19,18 +19,19 @@ export type LoaderOptions = {
imagesHost: string;
host: string;
};
+ originalPixelRatio: Dpr;
};
// Каждый импорт картинки проходит через этот лоадер и на выходе
// для каждой картинки получится массив с двумя значениями –
// srcset'ы для webp и srcset для оригинального расширения изображения
export const loader = function (this: webpack.loader.LoaderContext, source: string): string {
- const options = loaderUtils.getOptions(this) as unknown as LoaderOptions;
+ const options = (loaderUtils.getOptions(this) as unknown) as LoaderOptions;
validateOptions(schema, options, { name: 'Imgproxy responsive loader', baseDataPath: 'options' });
+ const pixelRatios: Dpr[] = getPixelRatios(options.originalPixelRatio);
const breakpoints: Breakpoint[] = options.breakpoints;
-
// Такой результат приходит от file-loader 'module.exports = "/build/myImage/mobile.all-4b767a7b.png";'
// Получаем оригинальное имя файла изображения (originalImageFileName = mobile.all.png)
const originalImageFileName = path.relative(this.context, this.resourcePath);
@@ -61,30 +62,36 @@ export const loader = function (this: webpack.loader.LoaderContext, source: stri
const breakpointMedia = breakpointName === all ? null : getBreakpointMedia(breakpoints[order]);
// Получаем путь до картинки (outputImagePath = '/build/myImage/mobile.all-4b767a7b.png')
- const outputImagePath = source.replace(/^module.exports = (__webpack_public_path__ \+ )?"(.+)";$/, (a, b, imagePath) => imagePath);
+ const outputImagePath = source.replace(
+ /^module.exports = (__webpack_public_path__ \+ )?"(.+)";$/,
+ (a, b, imagePath) => imagePath,
+ );
let webpSrcSet: SrcSet, originalExtensionSrcSet: SrcSet, data: OrderedBreakpointSource;
// Отключает процессинг картинок, генерируется srcSet только для оригинального типа изображения
if (options.imgproxy.disable) {
- originalExtensionSrcSet = {
- '1x': outputImagePath,
- '2x': outputImagePath,
- '3x': outputImagePath,
- };
data = {
order,
breakpointMedia,
srcSets: [
{
extension: originalExtension,
- srcSet: originalExtensionSrcSet,
+ srcSet: pixelRatios.reduce((acc, item): SrcSet => {
+ acc[item] = outputImagePath;
+ return acc;
+ }, {} as SrcSet),
},
],
};
} else {
- const buildUrlsForAllPixelRatios = getImgproxyUrlBuilder(options.imgproxy);
- webpSrcSet = buildUrlsForAllPixelRatios(outputImagePath, 'webp');
- originalExtensionSrcSet = buildUrlsForAllPixelRatios(outputImagePath, originalExtension);
+ const buildUrlsForPixelRatios = getImgproxyUrlBuilder(options.imgproxy);
+ webpSrcSet = buildUrlsForPixelRatios(pixelRatios, outputImagePath, 'webp');
+ originalExtensionSrcSet = buildUrlsForPixelRatios(
+ pixelRatios,
+ outputImagePath,
+ originalExtension,
+ );
+
data = {
order,
breakpointMedia,
@@ -100,7 +107,10 @@ export const loader = function (this: webpack.loader.LoaderContext, source: stri
],
};
// Добавляем ссылки на картинки через imgproxy в глобальный объект
- imageUrls.push(...Object.values(webpSrcSet), ...Object.values(originalExtensionSrcSet));
+ imageUrls.push(
+ ...(Object.values(webpSrcSet) as string[]),
+ ...(Object.values(originalExtensionSrcSet) as string[]),
+ );
}
const result: OrderedBreakpointSource = data;
diff --git a/src/webpack/loaderOptionsSchema.ts b/src/webpack/loaderOptionsSchema.ts
index 94619b8..c3c8326 100644
--- a/src/webpack/loaderOptionsSchema.ts
+++ b/src/webpack/loaderOptionsSchema.ts
@@ -48,7 +48,11 @@ export const schema: JSONSchema7 = {
},
],
},
+ originalPixelRatio: {
+ type: 'string',
+ pattern: '^(1x|2x|3x)$',
+ },
},
- required: ['breakpoints', 'imgproxy'],
+ required: ['breakpoints', 'imgproxy', 'originalPixelRatio'],
additionalProperties: false,
};