init
Some checks failed
CI / build (16.x) (push) Has been cancelled
CI / build (18.x) (push) Has been cancelled
CI / build (20.x) (push) Has been cancelled

This commit is contained in:
xlarp 2025-01-25 07:08:20 +02:00
commit bb18ffc49b
9 changed files with 4880 additions and 0 deletions

45
.forgejo/workflows/ci.yml Normal file
View file

@ -0,0 +1,45 @@
name: CI
on:
push:
branches: [main, master]
pull_request:
branches: [main, master]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [16.x, 18.x, 20.x]
steps:
- uses: actions/checkout@v3
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
- name: Install dependencies
run: npm ci
- name: Lint
run: npm run lint
- name: Build
run: npm run build
- name: Run tests
run: npm test
- name: Generate coverage
run: npm run test:coverage
- name: Archive coverage
uses: actions/upload-artifact@v3
with:
name: coverage-${{ matrix.node-version }}
path: coverage
retention-days: 5

65
.gitignore vendored Normal file
View file

@ -0,0 +1,65 @@
# Dependency directories
node_modules/
# Build output
dist/
build/
out/
# Environment files
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Coverage directory
coverage/
# IDE and editor files
.idea/
.vscode/
*.swp
*.swo
.DS_Store
# Temporary files
*.tmp
*.bak
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Compiled binary addons
build/Release
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
# Temporary test outputs
__tests__/output/

105
README.md Normal file
View file

@ -0,0 +1,105 @@
# Image Speech Bubble Transformer
A TypeScript library for applying speech bubble effects to images with various transformation options.
## Installation
```bash
npm install image-speech-bubble-transformer
```
## Usage
```typescript
import {
createSpeechBubbleTransformer,
Orientation,
} from "image-speech-bubble-transformer";
import fs from "fs/promises";
async function example() {
// Create transformer instance
const transformer = createSpeechBubbleTransformer();
// Read image buffer
const inputBuffer = await fs.readFile("input.png");
// Process image with options
const processedBuffer = await transformer.processSpeechBubble(inputBuffer, {
mirror: true,
orientation: Orientation.LEFT,
});
// Save processed image
await transformer.processAndSave(inputBuffer, "output.png", {
mirror: true,
orientation: Orientation.LEFT,
});
}
```
## API
### `createSpeechBubbleTransformer(assetsDir?: string)`
- Creates a transformer instance
- `assetsDir`: Optional path to custom assets directory
### `processSpeechBubble(inputBuffer, options)`
- Applies speech bubble effect to image
- Options:
- `mirror`: Flip horizontally
- `orientation`: Rotate speech bubble
- `speechBubblePath`: Custom speech bubble image
### `processAndSave(inputBuffer, outputPath, options)`
- Processes and saves image in one step
## Supported Formats
- .png, .jpg, .jpeg, .gif, .bmp, .webp, .tiff
## Development
### Setup
1. Clone the repository
2. Install dependencies:
```bash
npm install
```
### Scripts
- `npm run build`: Compile TypeScript
- `npm run lint`: Run ESLint
- `npm test`: Run Jest tests
- `npm run test:coverage`: Run tests with coverage report
### Publishing to NPM
1. Ensure you're logged into NPM:
```bash
npm login
```
2. Update version (use appropriate semver):
```bash
npm version patch # or minor/major
```
3. Publish:
```bash
npm publish
```
### Continuous Integration
- Ensure `.github/workflows/ci.yml` is set up for automated testing
- Configure NPM token in repository secrets

29
eslint.config.json Normal file
View file

@ -0,0 +1,29 @@
{
"root": true,
"parser": "@typescript-eslint/parser",
"plugins": [
"@typescript-eslint",
"jest"
],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:jest/recommended",
"prettier"
],
"rules": {
"no-console": "warn",
"@typescript-eslint/explicit-function-return-type": "error",
"@typescript-eslint/no-explicit-any": "error",
"jest/no-disabled-tests": "warn",
"jest/no-focused-tests": "error",
"jest/no-identical-title": "error",
"jest/prefer-to-have-length": "warn",
"jest/valid-expect": "error"
},
"env": {
"node": true,
"es6": true,
"jest/globals": true
}
}

10
eslint.config.mjs Normal file
View file

@ -0,0 +1,10 @@
import globals from "globals";
import tseslint from "typescript-eslint";
/** @type {import('eslint').Linter.Config[]} */
export default [
{files: ["**/*.{js,mjs,cjs,ts}"]},
{languageOptions: { globals: globals.browser }},
...tseslint.configs.recommended,
];

66
package.json Normal file
View file

@ -0,0 +1,66 @@
{
"name": "image-speech-bubble-transformer",
"version": "1.0.0",
"description": "TypeScript library for applying speech bubble effects to images",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"files": [
"dist",
"README.md",
"LICENSE"
],
"engines": {
"node": ">=16.0.0"
},
"scripts": {
"clean": "rimraf dist",
"build": "npm run clean && tsc",
"build:watch": "tsc -w",
"lint": "eslint src",
"lint:fix": "eslint src --fix",
"prepare": "npm run build",
"prepublishOnly": "npm test && npm run lint",
"version": "git add -A src",
"postversion": "git push && git push --tags"
},
"repository": {
"type": "git",
"url": "https://f0rk.systems/reso/packages_speech_bubble.git"
},
"keywords": [
"image",
"processing",
"speech-bubble",
"transformer",
"sharp",
"image-manipulation"
],
"author": "proto <proto@throwing.lol>",
"license": "MIT",
"bugs": {
"url": "https://f0rk.systems/reso/packages_speech_bubble/issues"
},
"homepage": "https://f0rk.systems/reso/packages_speech_bubble#readme",
"dependencies": {
"sharp": "^0.32.6"
},
"devDependencies": {
"@types/jest": "^29.5.3",
"@types/node": "^20.17.16",
"@types/sharp": "^0.31.1",
"@typescript-eslint/eslint-plugin": "^6.1.0",
"@typescript-eslint/parser": "^6.1.0",
"eslint": "^8.57.1",
"eslint-config-prettier": "^8.8.0",
"eslint-plugin-jest": "^27.2.3",
"globals": "^15.14.0",
"prettier": "^3.0.0",
"rimraf": "^5.0.1",
"ts-node": "^10.9.1",
"typescript": "^5.1.6",
"typescript-eslint": "^8.21.0"
},
"publishConfig": {
"access": "public"
}
}

4387
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load diff

155
src/index.ts Normal file
View file

@ -0,0 +1,155 @@
import { PathLike } from "fs";
import fs from "fs/promises";
import path from "path";
import sharp from "sharp";
const SUPPORTED_FORMATS = [
".png",
".jpg",
".jpeg",
".gif",
".bmp",
".webp",
".tiff",
];
export enum Orientation {
TOP = 1,
LEFT = 2,
BOTTOM = 3,
RIGHT = 4,
}
export interface SpeechBubbleOptions {
mirror?: boolean;
orientation?: Orientation;
speechBubblePath?: string;
}
export class ImageSpeechBubbleTransformer {
private defaultSpeechBubblePath: string;
constructor(assetsDir: string = path.join(__dirname, "assets")) {
this.defaultSpeechBubblePath = path.join(assetsDir, "speech_bubble.png");
}
/**
* Check if the file format is supported
*/
private checkFileFormat(filePath: string): void {
const ext = path.extname(filePath).toLowerCase();
if (!SUPPORTED_FORMATS.includes(ext)) {
throw new Error(
`Unsupported file extension ${ext}. Only ${SUPPORTED_FORMATS.join(", ")} are supported.`
);
}
}
/**
* Transform image with optional mirroring and rotation
*/
private async transformImage(
inputBuffer: Buffer,
options: { mirror?: boolean; orientation?: Orientation } = {}
): Promise<Buffer> {
let pipeline = sharp(inputBuffer);
// Mirror (flip horizontally)
if (options.mirror) {
pipeline = pipeline.flop();
}
// Rotate based on orientation
switch (options.orientation) {
case Orientation.LEFT:
pipeline = pipeline.rotate(270);
break;
case Orientation.BOTTOM:
pipeline = pipeline.rotate(180);
break;
case Orientation.RIGHT:
pipeline = pipeline.rotate(90);
break;
default:
// No rotation for TOP or default
break;
}
return pipeline.toBuffer();
}
/**
* Process image with speech bubble
*/
async processSpeechBubble(
inputBuffer: Buffer,
options: SpeechBubbleOptions = {}
): Promise<Buffer> {
if (!inputBuffer) {
throw new Error("Input buffer is required");
}
const speechBubblePath =
options.speechBubblePath || this.defaultSpeechBubblePath;
this.checkFileFormat(speechBubblePath);
const speechBubbleBuffer = await sharp(speechBubblePath).toBuffer();
const processedImage = await sharp(inputBuffer)
.ensureAlpha()
.metadata()
.then(async (metadata) => {
// match image size
const resizedSpeechBubble = await sharp(speechBubbleBuffer)
.resize(metadata.width, metadata.height)
.toBuffer();
const transformedSpeechBubble = await this.transformImage(
resizedSpeechBubble,
{
mirror: options.mirror,
orientation: options.orientation,
}
);
// subtract speech bubble
return sharp(inputBuffer)
.composite([
{
input: transformedSpeechBubble,
blend: "difference",
},
])
.toBuffer();
});
return processedImage;
}
/**
* Convenience method to process and save image
*/
async processAndSave(
inputBuffer: Buffer,
outputPath: PathLike,
options: SpeechBubbleOptions = {}
): Promise<void> {
this.checkFileFormat(outputPath.toString());
// Process image
const processedBuffer = await this.processSpeechBubble(
inputBuffer,
options
);
const outputDir = path.dirname(outputPath.toString());
await fs.mkdir(outputDir, { recursive: true });
await sharp(processedBuffer).toFile(outputPath.toString());
}
}
export function createSpeechBubbleTransformer(assetsDir?: string) {
return new ImageSpeechBubbleTransformer(assetsDir);
}

18
tsconfig.json Normal file
View file

@ -0,0 +1,18 @@
{
"compilerOptions": {
"target": "es2020",
"module": "commonjs",
"lib": ["es2020"],
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "**/*.spec.ts"]
}