xpict 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/README.md +1 -0
  2. package/dev-scripts/generate-exports.cjs +57 -0
  3. package/dev-scripts/index.ts +149 -0
  4. package/dev-scripts/prepare-package-json.js +31 -0
  5. package/dist/cjs/actions.js +232 -0
  6. package/dist/cjs/constants.js +26 -0
  7. package/dist/cjs/index.js +77 -0
  8. package/dist/cjs/package.json +1 -0
  9. package/dist/cjs/utils/clone-image.util.ts.js +12 -0
  10. package/dist/cjs/utils/color.util.js +21 -0
  11. package/dist/cjs/utils/create-image.util.js +18 -0
  12. package/dist/cjs/utils/index.js +14 -0
  13. package/dist/cjs/utils/open-image.util.js +10 -0
  14. package/dist/esm/actions.js +214 -0
  15. package/dist/esm/constants.js +20 -0
  16. package/dist/esm/index.js +71 -0
  17. package/dist/esm/package.json +1 -0
  18. package/dist/esm/utils/clone-image.util.ts.js +7 -0
  19. package/dist/esm/utils/color.util.js +17 -0
  20. package/dist/esm/utils/create-image.util.js +13 -0
  21. package/dist/esm/utils/index.js +4 -0
  22. package/dist/esm/utils/open-image.util.js +5 -0
  23. package/dist/tsconfig.cjs.tsbuildinfo +1 -0
  24. package/dist/tsconfig.esm.tsbuildinfo +1 -0
  25. package/dist/tsconfig.types.tsbuildinfo +1 -0
  26. package/dist/types/actions.d.ts +77 -0
  27. package/dist/types/constants.d.ts +19 -0
  28. package/dist/types/index.d.ts +31 -0
  29. package/dist/types/utils/clone-image.util.ts.d.ts +4 -0
  30. package/dist/types/utils/color.util.d.ts +4 -0
  31. package/dist/types/utils/create-image.util.d.ts +12 -0
  32. package/dist/types/utils/index.d.ts +4 -0
  33. package/dist/types/utils/open-image.util.d.ts +3 -0
  34. package/fonts/Curse Casual.ttf +0 -0
  35. package/fonts/Poppins-Black.ttf +0 -0
  36. package/fonts/Poppins-BlackItalic.ttf +0 -0
  37. package/fonts/Poppins-Bold.ttf +0 -0
  38. package/fonts/Poppins-BoldItalic.ttf +0 -0
  39. package/fonts/Poppins-ExtraBold.ttf +0 -0
  40. package/fonts/Poppins-ExtraBoldItalic.ttf +0 -0
  41. package/fonts/Poppins-ExtraLight.ttf +0 -0
  42. package/fonts/Poppins-ExtraLightItalic.ttf +0 -0
  43. package/fonts/Poppins-Italic.ttf +0 -0
  44. package/fonts/Poppins-Light.ttf +0 -0
  45. package/fonts/Poppins-LightItalic.ttf +0 -0
  46. package/fonts/Poppins-Medium.ttf +0 -0
  47. package/fonts/Poppins-MediumItalic.ttf +0 -0
  48. package/fonts/Poppins-Regular.ttf +0 -0
  49. package/fonts/Poppins-SemiBold.ttf +0 -0
  50. package/fonts/Poppins-SemiBoldItalic.ttf +0 -0
  51. package/fonts/Poppins-Thin.ttf +0 -0
  52. package/fonts/Poppins-ThinItalic.ttf +0 -0
  53. package/package.json +78 -0
  54. package/src/actions.ts +390 -0
  55. package/src/constants.ts +30 -0
  56. package/src/index.ts +124 -0
  57. package/src/utils/clone-image.util.ts.ts +11 -0
  58. package/src/utils/color.util.ts +25 -0
  59. package/src/utils/create-image.util.ts +34 -0
  60. package/src/utils/index.ts +11 -0
  61. package/src/utils/open-image.util.ts +9 -0
  62. package/tsconfig.cjs.json +7 -0
  63. package/tsconfig.esm.json +7 -0
  64. package/tsconfig.json +15 -0
  65. package/tsconfig.types.json +8 -0
package/src/actions.ts ADDED
@@ -0,0 +1,390 @@
1
+ import { createCanvas, registerFont } from "canvas"
2
+ import sharp from "sharp"
3
+
4
+ import { colors, insertTextDefaultOptions } from "./constants"
5
+ import { Image } from "."
6
+
7
+ function toGrayScale(grayscale: boolean = true) {
8
+ return (image: Image) => image.grayscale(grayscale)
9
+ }
10
+
11
+ export type CropImageOptions = sharp.Region
12
+
13
+ function cropImage(options: CropImageOptions) {
14
+ return (image: Image) => image.extract(options)
15
+ }
16
+
17
+ export type ResizeImageOptions = {
18
+ width: number
19
+ height: number
20
+ fit: keyof sharp.FitEnum
21
+ }
22
+
23
+ function resizeImage({
24
+ width,
25
+ height,
26
+ fit = "inside"
27
+ }: ResizeImageOptions) {
28
+ return (image: Image) => image.resize(
29
+ width,
30
+ height,
31
+ { fit: fit }
32
+ )
33
+ }
34
+
35
+ function rotateImage(angle: number) {
36
+ return (image: Image) => image.rotate(angle)
37
+ }
38
+
39
+ function blurImage(sigma: number = 1) {
40
+ return (image: Image) => image.blur(sigma)
41
+ }
42
+
43
+ function modulateSaturation(saturation: number) {
44
+ return (image: Image) => image.modulate({ saturation })
45
+ }
46
+
47
+ function modulateBrightness(brightness: number) {
48
+ return (image: Image) => image.modulate({ brightness })
49
+ }
50
+
51
+ function invertColors() {
52
+ return (image: Image) => image.negate()
53
+ }
54
+
55
+ export type AddBorderOptions = {
56
+ size: number
57
+ color?: string
58
+ }
59
+
60
+ function addBorder({
61
+ size,
62
+ color = colors.black
63
+ }: AddBorderOptions) {
64
+ return (image: Image) =>
65
+ image.extend({
66
+ top: size,
67
+ bottom: size,
68
+ left: size,
69
+ right: size,
70
+ background: color
71
+ })
72
+ }
73
+
74
+ function adjustContrast(contrast: number) {
75
+ return (image: Image) => {
76
+ const factor = (259 * (contrast + 255)) / (255 * (259 - contrast))
77
+ return image.linear(factor, -(128 * factor) + 128)
78
+ }
79
+ }
80
+
81
+ function modulateOpacity(opacity: number) {
82
+ return (image: Image) => image.flatten({ background: { alpha: opacity } })
83
+ }
84
+
85
+ function flipImage() {
86
+ return (image: Image) => image.flip()
87
+ }
88
+
89
+ function flopImage() {
90
+ return (image: Image) => image.flop()
91
+ }
92
+
93
+ export type FontOptions = {
94
+ size: number
95
+ color?: string
96
+ name?: string
97
+ filePath?: string
98
+ }
99
+
100
+ export type TextAnchor = "top-left" | "top-center" | "top-right" | "middle-left" | "middle-center" | "middle-right" | "bottom-left" | "bottom-center" | "bottom-right"
101
+
102
+ export type Stroke = {
103
+ fill: string
104
+ width: number
105
+ }
106
+
107
+ export type InsertTextOptions = {
108
+ text: string
109
+ font: FontOptions
110
+ x: number
111
+ y: number
112
+ backgroundColor?: string
113
+ anchor?: TextAnchor
114
+ stroke?: Stroke
115
+ rotation?: number
116
+ }
117
+
118
+ export type AnchorOffsets = Record<TextAnchor, { x: number, y: number }>
119
+
120
+ function insertText({
121
+ text,
122
+ font,
123
+ x,
124
+ y,
125
+ backgroundColor = insertTextDefaultOptions.backgroundColor,
126
+ anchor = insertTextDefaultOptions.anchor as TextAnchor,
127
+ stroke,
128
+ rotation = insertTextDefaultOptions.rotation
129
+ }: InsertTextOptions) {
130
+ return async (image: Image) => {
131
+ const imageMetadata = await image.metadata()
132
+ const width = imageMetadata.width!
133
+ const height = imageMetadata.height!
134
+
135
+ const canvas = createCanvas(width, height)
136
+ const context = canvas.getContext("2d")
137
+
138
+ if (backgroundColor !== "transparent") {
139
+ context.fillStyle = backgroundColor
140
+ context.fillRect(0, 0, width, height)
141
+ }
142
+
143
+ if (font.filePath) {
144
+ registerFont(
145
+ font.filePath,
146
+ { family: font.name! }
147
+ )
148
+ }
149
+
150
+ context.font = `${font.size}px ${font.name ?? insertTextDefaultOptions.font.name}`
151
+ context.fillStyle = font.color ?? colors.black
152
+
153
+ const textMetrics = context.measureText(text)
154
+ const textWidth = textMetrics.width
155
+ const textHeight = font.size
156
+
157
+ const anchorOffsets: AnchorOffsets = {
158
+ "top-left": {
159
+ x: 0,
160
+ y: 0
161
+ },
162
+ "top-center": {
163
+ x: -textWidth / 2,
164
+ y: 0
165
+ },
166
+ "top-right": {
167
+ x: -textWidth,
168
+ y: 0
169
+ },
170
+ "middle-left": {
171
+ x: 0,
172
+ y: -textHeight / 2
173
+ },
174
+ "middle-center": {
175
+ x: -textWidth / 2,
176
+ y: -textHeight / 2
177
+ },
178
+ "middle-right": {
179
+ x: -textWidth,
180
+ y: -textHeight / 2
181
+ },
182
+ "bottom-left": {
183
+ x: 0,
184
+ y: -textHeight
185
+ },
186
+ "bottom-center": {
187
+ x: -textWidth / 2,
188
+ y: -textHeight
189
+ },
190
+ "bottom-right": {
191
+ x: -textWidth,
192
+ y: -textHeight
193
+ }
194
+ }
195
+
196
+ const {
197
+ x: offsetX,
198
+ y: offsetY
199
+ } = anchorOffsets[anchor] || { x: 0, y: 0 }
200
+
201
+ const adjustedX = x + offsetX
202
+ const adjustedY = y + offsetY
203
+ const adjustedRotation = rotation ?? 0
204
+
205
+ context.save()
206
+ context.translate(adjustedX, adjustedY)
207
+ context.rotate((adjustedRotation * Math.PI) / 180)
208
+
209
+ if (stroke) {
210
+ context.strokeStyle = stroke.fill
211
+ context.lineWidth = stroke.width
212
+ context.lineJoin = "round"
213
+ context.strokeText(text, 0, 0)
214
+ }
215
+
216
+ context.fillStyle = font.color ?? colors.black
217
+ context.fillText(text, 0, 0)
218
+
219
+ context.restore()
220
+
221
+ const textBuffer = canvas.toBuffer()
222
+
223
+ return image.composite([
224
+ {
225
+ input: textBuffer,
226
+ top: 0,
227
+ left: 0
228
+ }
229
+ ])
230
+ }
231
+ }
232
+
233
+ export type InsertCircleOptions = {
234
+ x: number
235
+ y: number
236
+ radius: number
237
+ fill: string
238
+ }
239
+
240
+ function insertCircle({
241
+ x,
242
+ y,
243
+ radius,
244
+ fill
245
+ }: InsertCircleOptions) {
246
+ return async (image: Image) => {
247
+ const imageMetadata = await image.metadata()
248
+ const width = imageMetadata.width!
249
+ const height = imageMetadata.height!
250
+
251
+ const canvas = createCanvas(width, height)
252
+ const context = canvas.getContext("2d")
253
+
254
+ context.beginPath()
255
+ context.arc(x, y, radius, 0, Math.PI * 2, true)
256
+ context.closePath()
257
+ context.fillStyle = fill
258
+ context.fill()
259
+
260
+ const circleBuffer = canvas.toBuffer()
261
+
262
+ return image.composite([
263
+ {
264
+ input: circleBuffer,
265
+ top: 0,
266
+ left: 0
267
+ }
268
+ ])
269
+ }
270
+ }
271
+
272
+ export type InsertRectangleOptions = {
273
+ x: number
274
+ y: number
275
+ width: number
276
+ height: number
277
+ fill: string
278
+ borderRadius?: number
279
+ }
280
+
281
+ function insertRectangle({
282
+ x,
283
+ y,
284
+ width,
285
+ height,
286
+ fill,
287
+ borderRadius
288
+ }: InsertRectangleOptions) {
289
+ return async (image: Image) => {
290
+ const imageMetadata = await image.metadata()
291
+ const canvasWidth = imageMetadata.width!
292
+ const canvasHeight = imageMetadata.height!
293
+
294
+ const canvas = createCanvas(canvasWidth, canvasHeight)
295
+ const context = canvas.getContext("2d")
296
+
297
+ context.fillStyle = fill
298
+
299
+ if (borderRadius && borderRadius > 0) {
300
+ context.beginPath()
301
+ context.moveTo(x + borderRadius, y)
302
+ context.lineTo(x + width - borderRadius, y)
303
+ context.arcTo(x + width, y, x + width, y + height, borderRadius)
304
+ context.lineTo(x + width, y + height - borderRadius)
305
+ context.arcTo(x + width, y + height, x, y + height, borderRadius)
306
+ context.lineTo(x + borderRadius, y + height)
307
+ context.arcTo(x, y + height, x, y, borderRadius)
308
+ context.lineTo(x, y + borderRadius)
309
+ context.arcTo(x, y, x + width, y, borderRadius)
310
+ context.closePath()
311
+ context.fill()
312
+ } else {
313
+ context.fillRect(x, y, width, height)
314
+ }
315
+
316
+ const rectangleBuffer = canvas.toBuffer()
317
+
318
+ return image.composite([
319
+ {
320
+ input: rectangleBuffer,
321
+ top: 0,
322
+ left: 0
323
+ }
324
+ ])
325
+ }
326
+ }
327
+
328
+ export type InsertLineOptions = {
329
+ x1: number
330
+ y1: number
331
+ x2: number
332
+ y2: number
333
+ color: string
334
+ width: number
335
+ }
336
+
337
+ function insertLine({
338
+ x1,
339
+ y1,
340
+ x2,
341
+ y2,
342
+ color,
343
+ width
344
+ }: InsertLineOptions) {
345
+ return async (image: Image) => {
346
+ const imageMetadata = await image.metadata()
347
+ const canvasWidth = imageMetadata.width!
348
+ const canvasHeight = imageMetadata.height!
349
+
350
+ const canvas = createCanvas(canvasWidth, canvasHeight)
351
+ const context = canvas.getContext("2d")
352
+
353
+ context.strokeStyle = color
354
+ context.lineWidth = width
355
+ context.beginPath()
356
+ context.moveTo(x1, y1)
357
+ context.lineTo(x2, y2)
358
+ context.stroke()
359
+
360
+ const lineBuffer = canvas.toBuffer()
361
+
362
+ return image.composite([
363
+ {
364
+ input: lineBuffer,
365
+ top: 0,
366
+ left: 0
367
+ }
368
+ ])
369
+ }
370
+ }
371
+
372
+ export {
373
+ toGrayScale,
374
+ cropImage,
375
+ resizeImage,
376
+ rotateImage,
377
+ blurImage,
378
+ modulateSaturation,
379
+ modulateBrightness,
380
+ invertColors,
381
+ addBorder,
382
+ adjustContrast,
383
+ modulateOpacity,
384
+ flipImage,
385
+ flopImage,
386
+ insertText,
387
+ insertCircle,
388
+ insertRectangle,
389
+ insertLine
390
+ }
@@ -0,0 +1,30 @@
1
+ import { rgba } from "./utils/color.util"
2
+
3
+ const colors = {
4
+ black: "#000000",
5
+ white: "#FFFFFF"
6
+ }
7
+
8
+ const defautltTemplateAcitonType = "beforeLayersProccess"
9
+
10
+ const createImageDefaultOptions = {
11
+ chnannels: 4,
12
+ fill: rgba(0, 0, 0, 0),
13
+ format: "png"
14
+ }
15
+
16
+ const insertTextDefaultOptions = {
17
+ font: {
18
+ name: "sans-serif",
19
+ },
20
+ anchor: "top-left",
21
+ backgroundColor: "transparent",
22
+ rotation: 0
23
+ }
24
+
25
+ export {
26
+ colors,
27
+ defautltTemplateAcitonType,
28
+ createImageDefaultOptions,
29
+ insertTextDefaultOptions
30
+ }
package/src/index.ts ADDED
@@ -0,0 +1,124 @@
1
+ import sharp, { Sharp } from "sharp"
2
+
3
+ import { defautltTemplateAcitonType } from "./constants"
4
+ import cloneImage from "./utils/clone-image.util.ts"
5
+
6
+ export type Image = Sharp
7
+
8
+ export type ImageTemplateLayer = {
9
+ x: number
10
+ y: number
11
+ template: ImageTemplate
12
+ }
13
+
14
+ export type ImageTemplateActionFunction = (image: Image) => Image | Promise<Image>
15
+
16
+ export type ImageTemplateAction = ImageTemplateActionFunction | {
17
+ type: "beforeLayersProccess" | "afterLayersProccess"
18
+ func: ImageTemplateActionFunction
19
+ }
20
+
21
+ export type ImageTemplate = {
22
+ image: Image
23
+ actions?: ImageTemplateAction[]
24
+ layers?: ImageTemplateLayer[]
25
+ }
26
+
27
+ export type ExtractActionsResult = {
28
+ before: ImageTemplateActionFunction[]
29
+ after: ImageTemplateActionFunction[]
30
+ }
31
+
32
+ function extractActions(actions: ImageTemplateAction[] | undefined): ExtractActionsResult {
33
+ const actionsLayersProccess: ImageTemplateActionFunction[] = []
34
+ const actionsAfterLayersProccess: ImageTemplateActionFunction[] = []
35
+
36
+ if (!actions) {
37
+ actions = []
38
+ }
39
+
40
+ for (const action of actions) {
41
+ const actionType = typeof action === "function" ? defautltTemplateAcitonType : action.type
42
+ const actionFunc = typeof action === "function" ? action : action.func
43
+
44
+ if (actionType === "beforeLayersProccess") {
45
+ actionsLayersProccess.push(actionFunc)
46
+ } else {
47
+ actionsAfterLayersProccess.push(actionFunc)
48
+ }
49
+ }
50
+
51
+ return {
52
+ before: actionsLayersProccess,
53
+ after: actionsAfterLayersProccess
54
+ }
55
+ }
56
+
57
+ async function processImageTemplate(template: ImageTemplate): Promise<Image> {
58
+ const actions = extractActions(template.actions)
59
+ let image = template.image
60
+
61
+ for (const action of actions.before) {
62
+ const imageResult = await action(image)
63
+ image = await cloneImage(imageResult)
64
+ }
65
+
66
+ if (template.layers) {
67
+ const layerImages: sharp.OverlayOptions[] = []
68
+
69
+ for (const layer of template.layers) {
70
+ const currentImage = await processImageTemplate(layer.template)
71
+ const currentImageBuffer = await currentImage.png().toBuffer()
72
+
73
+ layerImages.push({
74
+ input: currentImageBuffer,
75
+ top: layer.y,
76
+ left: layer.x
77
+ })
78
+ }
79
+
80
+ image = image.composite(layerImages)
81
+ }
82
+
83
+ for (const action of actions.after) {
84
+ const imageResult = await action(image)
85
+ image = await cloneImage(imageResult)
86
+ }
87
+
88
+ return image
89
+ }
90
+
91
+ function extendImageTemplate(
92
+ template: ImageTemplate,
93
+ extendTemplate: Omit<ImageTemplate, "image">
94
+ ) {
95
+ const actions = []
96
+ const layers = []
97
+
98
+ if (template.layers) {
99
+ layers.push(...template.layers)
100
+ }
101
+
102
+ if (extendTemplate.layers) {
103
+ layers.push(...extendTemplate.layers)
104
+ }
105
+
106
+ if (template.actions) {
107
+ actions.push(...template.actions)
108
+ }
109
+
110
+ if (extendTemplate.actions) {
111
+ actions.push(...extendTemplate.actions)
112
+ }
113
+
114
+ return {
115
+ image: template.image,
116
+ actions: actions,
117
+ layers: layers
118
+ }
119
+ }
120
+
121
+ export {
122
+ processImageTemplate,
123
+ extendImageTemplate
124
+ }
@@ -0,0 +1,11 @@
1
+ import sharp from "sharp"
2
+
3
+ import { Image } from ".."
4
+
5
+ async function cloneImage(image: Image) {
6
+ const imageBuffer = await image.toBuffer()
7
+ const clonedImage = sharp(imageBuffer)
8
+ return clonedImage
9
+ }
10
+
11
+ export default cloneImage
@@ -0,0 +1,25 @@
1
+ function componentToHex(component: number): string {
2
+ const hex = component.toString(16)
3
+ return hex.padStart(2, "0")
4
+ }
5
+
6
+ function rgb(red: number, green: number, blue: number): string {
7
+ return `#${componentToHex(red)}${componentToHex(green)}${componentToHex(blue)}`
8
+ }
9
+
10
+ function rgba(red: number, green: number, blue: number, alpha: number): string {
11
+ const alphaHex = componentToHex(Math.round(alpha * 255))
12
+ return `#${componentToHex(red)}${componentToHex(green)}${componentToHex(blue)}${alphaHex}`
13
+ }
14
+
15
+ function hex(value: number) {
16
+ const hexadecimalValue = value.toString(16)
17
+ const fromatedHexadecimal = `#${hexadecimalValue.padStart(6, "0")}`
18
+ return fromatedHexadecimal
19
+ }
20
+
21
+ export {
22
+ rgb,
23
+ rgba,
24
+ hex
25
+ }
@@ -0,0 +1,34 @@
1
+ import sharp from "sharp"
2
+
3
+ import { createImageDefaultOptions } from "../constants"
4
+
5
+ export type ImageChannels = 3 | 4
6
+
7
+ export type ImageFormat = "png" | "jpeg" | "webp" | "avif" | "gif"
8
+
9
+ export type CreateImageOptions = {
10
+ width: number
11
+ height: number
12
+ fill?: sharp.Color
13
+ channels?: ImageChannels
14
+ format?: ImageFormat
15
+ }
16
+
17
+ function createImage({
18
+ width,
19
+ height,
20
+ fill = createImageDefaultOptions.fill,
21
+ channels = createImageDefaultOptions.chnannels as ImageChannels,
22
+ format = createImageDefaultOptions.format as ImageFormat,
23
+ }: CreateImageOptions) {
24
+ return sharp({
25
+ create: {
26
+ width: width,
27
+ height: height,
28
+ channels: channels,
29
+ background: fill,
30
+ },
31
+ })[format]()
32
+ }
33
+
34
+ export default createImage
@@ -0,0 +1,11 @@
1
+ import createImage from "./create-image.util"
2
+ import { rgb, rgba, hex } from "./color.util"
3
+ import openImage from "./open-image.util"
4
+
5
+ export {
6
+ createImage,
7
+ openImage,
8
+ rgba,
9
+ rgb,
10
+ hex
11
+ }
@@ -0,0 +1,9 @@
1
+ import sharp from "sharp"
2
+
3
+ import { Image } from ".."
4
+
5
+ function openImage(filePath: string): Image {
6
+ return sharp(filePath)
7
+ }
8
+
9
+ export default openImage
@@ -0,0 +1,7 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "compilerOptions": {
4
+ "outDir": "./dist/cjs",
5
+ "module": "commonjs"
6
+ }
7
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "compilerOptions": {
4
+ "outDir": "./dist/esm",
5
+ "module": "esnext"
6
+ }
7
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,15 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2021",
4
+ "module": "commonjs",
5
+ "outDir": "./dist",
6
+ "rootDir": "./src",
7
+ "moduleResolution": "node",
8
+ "esModuleInterop": true,
9
+ "forceConsistentCasingInFileNames": true,
10
+ "strict": true,
11
+ "skipLibCheck": true
12
+ },
13
+ "include": ["src/**/*.ts"],
14
+ "exclude": ["node_modules"]
15
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "compilerOptions": {
4
+ "outDir": "./dist/types",
5
+ "declaration": true,
6
+ "emitDeclarationOnly": true
7
+ }
8
+ }