-
-
Notifications
You must be signed in to change notification settings - Fork 16
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
5 changed files
with
91 additions
and
86 deletions.
There are no files selected for viewing
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,83 +1,99 @@ | ||
type Input = string | HTMLImageElement; | ||
type Output = HTMLImageElement; | ||
type Attributes = Record<string, string>; | ||
|
||
type ImagePromise = Promise<HTMLImageElement> & { | ||
image: HTMLImageElement; | ||
}; | ||
type Result<Image = HTMLImageElement> = Readonly<{ | ||
image: Image; | ||
loaded: () => Promise<Image>; | ||
metadata: () => Promise<Image>; | ||
}> ; | ||
|
||
function isArrayLike(input: any): input is ArrayLike<Input> { | ||
return input.length !== undefined; | ||
function setAttributes(image: HTMLImageElement, attributes: Attributes): void { | ||
for (const [attribute, value] of Object.entries(attributes)) { | ||
image.setAttribute(attribute, value); | ||
} | ||
} | ||
|
||
function loadSingleImage(image: HTMLImageElement): ImagePromise { | ||
const promise = new Promise<HTMLImageElement>((resolve, reject) => { | ||
if (image.naturalWidth) { | ||
// If the browser can determine the naturalWidth the image is already loaded successfully | ||
resolve(image); | ||
} else if (image.complete) { | ||
// If the image is complete but the naturalWidth is 0px it is probably broken | ||
reject(image); | ||
} else { | ||
image.addEventListener('load', fulfill); | ||
image.addEventListener('error', fulfill); | ||
function lazyPromise<Image = HTMLImageElement>(fn: () => Promise<Image>): () => Promise<Image> { | ||
let promise: Promise<Image> | undefined; | ||
return () => { | ||
if (!promise) { | ||
promise = fn(); | ||
} | ||
|
||
function fulfill(): void { | ||
if (image.naturalWidth) { | ||
resolve(image); | ||
} else { | ||
reject(image); | ||
return promise; | ||
}; | ||
} | ||
|
||
function load(image: HTMLImageElement): Result { | ||
const result = { | ||
image, | ||
loaded: lazyPromise(async () => { | ||
await image.decode(); | ||
return image; | ||
}), | ||
metadata: lazyPromise(async () => new Promise<HTMLImageElement>((resolve, reject) => { | ||
const check = () => { | ||
if (image.naturalWidth) { | ||
resolve(image); | ||
clearInterval(interval); | ||
return true; | ||
} | ||
|
||
if (image.complete) { | ||
// If the image is complete but the naturalWidth is 0px it's probably broken | ||
reject(image); | ||
clearInterval(interval); | ||
return true; | ||
} | ||
|
||
return false; | ||
}; | ||
|
||
if (check()) { | ||
return; | ||
} | ||
|
||
image.removeEventListener('load', fulfill); | ||
image.removeEventListener('error', fulfill); | ||
} | ||
}); | ||
const interval = setInterval(check, 100); | ||
|
||
return Object.assign(promise, {image}); | ||
} | ||
result.loaded().then(resolve, reject); | ||
})), | ||
}; | ||
|
||
function loadImages(input: Input, attributes?: Attributes): ImagePromise; | ||
function loadImages(input: ArrayLike<Input>, attributes?: Attributes): Promise<Output[]>; | ||
function loadImages(input: Input | ArrayLike<Input>, attributes: Attributes = {}): ImagePromise | Promise<Output[]> { | ||
if (input instanceof HTMLImageElement) { | ||
return loadSingleImage(input); | ||
} | ||
return Object.freeze(result); | ||
} | ||
|
||
export function loadImage(input: Input, attributes: Attributes = {}): Result { | ||
if (typeof input === 'string') { | ||
/* Create a <img> from a string */ | ||
const src = input; | ||
// Create a <img> from a string | ||
const image = new Image(); | ||
Object.keys(attributes).forEach( | ||
name => image.setAttribute(name, attributes[name]) | ||
); | ||
image.src = src; | ||
return loadSingleImage(image); | ||
// Set attributes before `src` | ||
setAttributes(image, attributes); | ||
|
||
image.src = input; | ||
return load(image); | ||
} | ||
|
||
if (isArrayLike(input)) { | ||
// Momentarily ignore errors | ||
const reflect = (img: Input): Promise<HTMLImageElement | Error> => loadImages(img, attributes).catch((error: Error) => error); | ||
const reflected = [].map.call(input, reflect) as Array<HTMLImageElement | Error>; | ||
const tsFix = Promise.all(reflected).then((results: Array<HTMLImageElement | Error>) => { | ||
const loaded = results.filter((x: any): x is HTMLImageElement => x.naturalWidth); | ||
if (loaded.length === results.length) { | ||
return loaded; | ||
} | ||
if (input instanceof HTMLImageElement) { | ||
setAttributes(input, attributes); | ||
return load(input); | ||
} | ||
|
||
return Promise.reject({ | ||
loaded, | ||
errored: results.filter((x: any): x is Error => !x.naturalWidth) | ||
}); | ||
}); | ||
throw new TypeError('Expected image or string, got ' + typeof input); | ||
} | ||
|
||
// Variables named `tsFix` are only here because TypeScript hates Promise-returning functions. | ||
return tsFix; | ||
export function loadImages(inputs: Input[], attributes: Attributes = {}): Readonly<{ | ||
results: Result[]; | ||
loaded: () => Promise<HTMLImageElement[]>; | ||
metadata: () => Promise<HTMLImageElement[]>; | ||
}> { | ||
if (Array.isArray(inputs)) { | ||
const results = inputs.map(input => loadImage(input, attributes)); | ||
return Object.freeze({ | ||
results, | ||
loaded: lazyPromise(async () => Promise.all(results.map(result => result.loaded()))), | ||
metadata: lazyPromise(async () => Promise.all(results.map(result => result.metadata()))), | ||
}); | ||
} | ||
|
||
const tsFix = Promise.reject(new TypeError('input is not an image, a URL string, or an array of them.')); | ||
return tsFix; | ||
throw new TypeError('Expected array; got ' + typeof inputs); | ||
} | ||
|
||
export default loadImages; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -17,10 +17,12 @@ | |
"vanilla" | ||
], | ||
"repository": "fregante/image-promise", | ||
"funding": "https://github.com/sponsors/fregante", | ||
"license": "MIT", | ||
"author": "Federico Brigante <[email protected]> (bfred.it)", | ||
"author": "Federico Brigante <[email protected]> (https://fregante.com)", | ||
"type": "module", | ||
"module": "index.js", | ||
"exports": "./index.js", | ||
"main": "./index.js", | ||
"types": "./index.d.ts", | ||
"files": [ | ||
"index.js", | ||
|
@@ -43,9 +45,9 @@ | |
} | ||
}, | ||
"devDependencies": { | ||
"@sindresorhus/tsconfig": "^0.7.0", | ||
"type-fest": "^0.13.1", | ||
"typescript": "^3.8.3", | ||
"xo": "^0.30.0" | ||
"@sindresorhus/tsconfig": "^3.0.1", | ||
"type-fest": "^3.10.0", | ||
"typescript": "^5.0.4", | ||
"xo": "^0.54.2" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters