Skip to content

Commit

Permalink
Merge pull request #513 from uploadcare/feature/implement-image-shrink
Browse files Browse the repository at this point in the history
feat: implemented stable image shrink
  • Loading branch information
Egor Didenko authored Feb 15, 2024
2 parents 36b5583 + de4850d commit 6c690b3
Show file tree
Hide file tree
Showing 42 changed files with 909 additions and 47 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ jobs:
- 'packages/api-client-utils/**'
signed-uploads:
- 'packages/signed-uploads/**'
image-shrink:
- 'packages/image-shrink/**'
- name: Install dependencies
working-directory: ./
run: npm i
Expand Down
53 changes: 7 additions & 46 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
"packages/api-client-utils",
"packages/upload-client",
"packages/rest-client",
"packages/signed-uploads"
"packages/signed-uploads",
"packages/image-shrink"
],
"engines": {
"node": ">=16"
Expand All @@ -17,6 +18,7 @@
"build:docs:upload-client": "typedoc --out docs/upload-client packages/upload-client/src/index.ts --readme packages/upload-client/README.md --options ./typedoc.json",
"build:docs:rest-client": "typedoc --out docs/rest-client packages/rest-client/src/index.ts --readme packages/rest-client/README.md --options ./typedoc.json",
"build:docs:signed-uploads": "typedoc --out docs/signed-uploads packages/signed-uploads/src/index.ts --readme packages/signed-uploads/README.md --options ./typedoc.json",
"build:docs:image-shrink": "typedoc --out docs/image-shrink packages/image-shrink/src/index.ts --readme packages/image-shrink/README.md --options ./typedoc.json",
"build:docs": "rimraf docs && run-s build:docs:* && touch docs/.nojekyll",
"build": "npm run build --workspaces",
"lint": "run-s eslint prettier",
Expand Down
1 change: 1 addition & 0 deletions packages/image-shrink/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
LICENSE
Empty file added packages/image-shrink/README.md
Empty file.
5 changes: 5 additions & 0 deletions packages/image-shrink/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import baseConfig from '../../jest.config.js'

export default {
...baseConfig
}
49 changes: 49 additions & 0 deletions packages/image-shrink/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
{
"name": "@uploadcare/image-shrink",
"version": "6.10.0",
"description": "Library for work with Uploadcare image shrink",
"type": "module",
"main": "./dist/cjs/index.browser.cjs",
"module": "./dist/esm/index.browser.mjs",
"types": "./dist/index.d.ts",
"exports": {
".": {
"import": "./dist/esm/index.browser.mjs",
"require": "./dist/cjs/index.browser.cjs"
}
},
"sideEffects": false,
"files": [
"dist/*"
],
"scripts": {
"prepack": "cp ../../LICENSE ./LICENSE",
"clean": "rimraf dist",
"test": "node --experimental-vm-modules ../../node_modules/jest/bin/jest.js",
"prebuild": "npm run clean",
"build": "npm run build:types && npm run build:compile",
"build:types": "dts-bundle-generator --project tsconfig.dts.json -o dist/index.d.ts src/index.ts",
"build:compile": "rollup -c"
},
"repository": {
"type": "git",
"url": "git+https://github.com/uploadcare/uploadcare-js-api-clients.git"
},
"author": "Uploadcare",
"license": "MIT",
"bugs": {
"url": "https://github.com/uploadcare/uploadcare-js-api-clients/issues"
},
"homepage": "https://github.com/uploadcare/uploadcare-js-api-clients#readme",
"keywords": [
"uploadcare",
"signed",
"uploads",
"secure",
"signature"
],
"devDependencies": {
"ts-node": "^10.8.1"
},
"dependencies": {}
}
12 changes: 12 additions & 0 deletions packages/image-shrink/rollup.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import {
createRollupConfig,
RollupTargetEnv
} from '../../createRollupConfig.js'
import * as url from 'url'

const __dirname = url.fileURLToPath(new URL('.', import.meta.url))

const config = ({ targetEnv }) =>
createRollupConfig({ targetEnv, cwd: __dirname })

export default [...config({ targetEnv: RollupTargetEnv.BROWSER })]
3 changes: 3 additions & 0 deletions packages/image-shrink/src/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"env": {"es6": true, "node": true, "browser": true}
}
4 changes: 4 additions & 0 deletions packages/image-shrink/src/constants/allowLayers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export const allowLayers = [
1, // L (black-white)
3 // RGB
]
3 changes: 3 additions & 0 deletions packages/image-shrink/src/constants/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export { allowLayers } from './allowLayers'
export { markers } from './markers'
export { sizes } from './sizes'
15 changes: 15 additions & 0 deletions packages/image-shrink/src/constants/markers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export const markers = [
0xc0, // ("SOF0", "Baseline DCT", SOF)
0xc1, // ("SOF1", "Extended Sequential DCT", SOF)
0xc2, // ("SOF2", "Progressive DCT", SOF)
0xc3, // ("SOF3", "Spatial lossless", SOF)
0xc5, // ("SOF5", "Differential sequential DCT", SOF)
0xc6, // ("SOF6", "Differential progressive DCT", SOF)
0xc7, // ("SOF7", "Differential spatial", SOF)
0xc9, // ("SOF9", "Extended sequential DCT (AC)", SOF)
0xca, // ("SOF10", "Progressive DCT (AC)", SOF)
0xcb, // ("SOF11", "Spatial lossless DCT (AC)", SOF)
0xcd, // ("SOF13", "Differential sequential DCT (AC)", SOF)
0xce, // ("SOF14", "Differential progressive DCT (AC)", SOF)
0xcf // ("SOF15", "Differential spatial (AC)", SOF)
]
45 changes: 45 additions & 0 deletions packages/image-shrink/src/constants/sizes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
export const sizes = {
squareSide: [
// Safari (iOS < 9, ram >= 256)
// We are supported mobile safari < 9 since widget v2, by 5 Mpx limit
// so it's better to continue support despite the absence of this browser in the support table
Math.floor(Math.sqrt(5 * 1000 * 1000)),
// IE Mobile (Windows Phone 8.x)
// Safari (iOS >= 9)
4096,
// IE 9 (Win)
8192,
// Firefox 63 (Mac, Win)
11180,
// Chrome 68 (Android 6)
10836,
// Chrome 68 (Android 5)
11402,
// Chrome 68 (Android 7.1-9)
14188,
// Chrome 70 (Mac, Win)
// Chrome 68 (Android 4.4)
// Edge 17 (Win)
// Safari 7-12 (Mac)
16384
],
dimension: [
// IE Mobile (Windows Phone 8.x)
4096,
// IE 9 (Win)
8192,
// Edge 17 (Win)
// IE11 (Win)
16384,
// Chrome 70 (Mac, Win)
// Chrome 68 (Android 4.4-9)
// Firefox 63 (Mac, Win)
32767,
// Chrome 83 (Mac, Win)
// Safari 7-12 (Mac)
// Safari (iOS 9-12)
// Actually Safari has a much bigger limits - 4194303 of width and 8388607 of height,
// but we will not use them
65535
]
}
38 changes: 38 additions & 0 deletions packages/image-shrink/src/helper/memoize.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
export const memoize = (fn, serializer) => {
const cache = {}
return (...args) => {
const key = serializer(args, cache)
return key in cache ? cache[key] : (cache[key] = fn(...args))
}
}

/**
* Memoization key serealizer, that prevents unnecessary canvas tests. No need
* to make test if we know that:
*
* - Browser supports higher canvas size
* - Browser doesn't support lower canvas size
*/
export const memoKeySerializer = (args, cache) => {
const [w] = args
const cachedWidths = Object.keys(cache)
.map((val) => parseInt(val, 10))
.sort((a, b) => a - b)

for (let i = 0; i < cachedWidths.length; i++) {
const cachedWidth = cachedWidths[i]
const isSupported = !!cache[cachedWidth]
// higher supported canvas size, return it
if (cachedWidth > w && isSupported) {
return cachedWidth
}
// lower unsupported canvas size, return it
if (cachedWidth < w && !isSupported) {
return cachedWidth
}
}

// use canvas width as the key,
// because we're doing dimension test by width - [dimension, 1]
return w
}
1 change: 1 addition & 0 deletions packages/image-shrink/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { shrinkFile, type TSetting } from './utils/shrinkFile'
24 changes: 24 additions & 0 deletions packages/image-shrink/src/utils/IccProfile/getIccProfile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { readJpegChunks } from '../image/JPEG/readJpegChunks'

export const getIccProfile = async (file: File) => {
const iccProfile: DataView[] = []
const { promiseReadJpegChunks, stack } = readJpegChunks()

return await promiseReadJpegChunks(file)
.then(() => {
stack.forEach(({ marker, view }) => {
if (marker === 0xe2) {
if (
// check for "ICC_PROFILE\0"
view.getUint32(0) === 0x4943435f &&
view.getUint32(4) === 0x50524f46 &&
view.getUint32(8) === 0x494c4500
) {
iccProfile.push(view)
}
}
})
return iccProfile
})
.catch(() => iccProfile)
}
13 changes: 13 additions & 0 deletions packages/image-shrink/src/utils/IccProfile/replaceIccProfile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { replaceJpegChunk } from '../image/JPEG/replaceJpegChunk'

export const MARKER = 0xe2
export const replaceIccProfile = (
blob: Blob | File,
iccProfiles: DataView[]
) => {
return replaceJpegChunk(
blob,
MARKER,
iccProfiles.map((chunk) => chunk.buffer)
)
}
17 changes: 17 additions & 0 deletions packages/image-shrink/src/utils/IccProfile/stripIccProfile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { replaceIccProfile } from './replaceIccProfile'
import { imageLoader } from '../image/imageLoader'

export const stripIccProfile = async (
inputFile: File
): Promise<HTMLImageElement> => {
try {
const file = await replaceIccProfile(inputFile, [])
const image = await imageLoader(URL.createObjectURL(file as Blob))

URL.revokeObjectURL(image.src)

return image
} catch (e) {
throw new Error(`Failed to strip ICC profile and not image ${e}`)
}
}
Loading

0 comments on commit 6c690b3

Please sign in to comment.