UNPKG

@crowdin/crowdin-apps-functions

Version:

Utility library to easily and quickly develop Crowdin App

130 lines (129 loc) 6.62 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; Object.defineProperty(exports, "__esModule", { value: true }); const Jimp = require("jimp"); /** * Provides tools for adding annotations to images using the Jimp library. * Capable of resizing images, drawing rectangles, and placing text labels with backgrounds. */ class ImageAnnotator { constructor(imageBuffer) { this.imageBuffer = imageBuffer; this.borderColor = Jimp.cssColorToHex('rgba(255, 0, 0, 0.75)'); this.backgroundColor = Jimp.cssColorToHex('rgba(255, 255, 255, 0.75)'); } initialize() { return __awaiter(this, void 0, void 0, function* () { this.image = yield Jimp.read(this.imageBuffer); this.originalWidth = this.image.bitmap.width; this.originalHeight = this.image.bitmap.height; [this.imageWidth, this.imageHeight] = this.calculateNewDimensions(this.originalWidth, this.originalHeight); this.image.resize(this.imageWidth, this.imageHeight); }); } annotate(tracks) { return __awaiter(this, void 0, void 0, function* () { yield this.initialize(); const font = yield Jimp.loadFont(Jimp.FONT_SANS_12_BLACK); for (const track of tracks) { // Adjust coordinates for resized image const adjustedTrack = this.adjustTrackCoordinates(track); // Draw the rectangle (border) with thickness this.drawRectangle(adjustedTrack.x, adjustedTrack.y, adjustedTrack.w, adjustedTrack.h); // Initial position for text let textX = adjustedTrack.x; let textY = adjustedTrack.y - ImageAnnotator.FONT_SIZE - 4; // Place text slightly above the rectangle // Adjust text position if it goes out of bounds [textX, textY] = this.fitTextWithinBounds(textX, textY, adjustedTrack.text, adjustedTrack.y, adjustedTrack.h); // Draw the text background yield this.drawTextBackground(textX, textY, adjustedTrack.text); // Add the text this.createText(textX, textY, adjustedTrack.text, font); } return yield this.image.getBufferAsync(Jimp.MIME_PNG); }); } calculateNewDimensions(width, height) { const scaleForShortestSide = width < height ? ImageAnnotator.SHORT_SIDE_MAX_SIZE / width : ImageAnnotator.SHORT_SIDE_MAX_SIZE / height; const scaleForLongestSide = width > height ? ImageAnnotator.LONG_SIDE_MAX_SIZE / width : ImageAnnotator.LONG_SIDE_MAX_SIZE / height; const scale = Math.min(scaleForShortestSide, scaleForLongestSide, 1); const downsizedWidth = Math.round(width * scale); const downsizedHeight = Math.round(height * scale); return [downsizedWidth, downsizedHeight]; } adjustTrackCoordinates(track) { const scaleX = this.imageWidth / this.originalWidth; const scaleY = this.imageHeight / this.originalHeight; return { x: Math.round(track.x * scaleX), y: Math.round(track.y * scaleY), w: Math.round(track.w * scaleX), h: Math.round(track.h * scaleY), text: track.text, }; } drawRectangle(x, y, width, height) { this.image.scan(x, y, width, height, (xx, yy, idx) => { if (xx === x || xx === x + width - 1 || yy === y || yy === y + height - 1) { this.image.bitmap.data.writeUInt32BE(this.borderColor, idx); } }); } drawTextBackground(x, y, text) { return __awaiter(this, void 0, void 0, function* () { const textWidth = this.getTextWidth(text); const textHeight = ImageAnnotator.FONT_SIZE; // Create a new background image with additional padding for text. The background extends 4 pixels beyond the text dimensions for padding. const background = new Jimp(textWidth + 4, textHeight + 4, this.backgroundColor); // Position the background centered around the text coordinates, offset by 2 pixels to ensure text is centered within the background. this.image.composite(background, x - 2, y - 2, { mode: Jimp.BLEND_SOURCE_OVER, opacitySource: 0.75, opacityDest: 1, }); }); } createText(x, y, text, font) { this.image.print(font, x, y, { text: text, alignmentX: Jimp.HORIZONTAL_ALIGN_LEFT, alignmentY: Jimp.VERTICAL_ALIGN_TOP, }); } fitTextWithinBounds(x, y, text, rectY, rectHeight) { const textWidth = this.getTextWidth(text); const textHeight = ImageAnnotator.FONT_SIZE; // Adjust position if text goes beyond the right edge if (x + textWidth > this.imageWidth) { x = this.imageWidth - textWidth - ImageAnnotator.PADDING_PIXELS; // 10 pixels padding from the edge } // Adjust position if text goes beyond the top edge if (y - textHeight < 0) { // Try placing text below the rectangle y = rectY + rectHeight + textHeight + ImageAnnotator.PADDING_PIXELS; // 10 pixels padding below the rectangle } // Adjust position if text goes beyond the bottom edge if (y + textHeight > this.imageHeight) { y = this.imageHeight - textHeight - ImageAnnotator.PADDING_PIXELS; // 10 pixels padding from the bottom } return [x, y]; } getTextWidth(text) { // Approximate average width of a character in the font being used return text.length * ImageAnnotator.AVERAGE_CHAR_WIDTH; } } ImageAnnotator.FONT_SIZE = 12; ImageAnnotator.SHORT_SIDE_MAX_SIZE = 768; ImageAnnotator.LONG_SIDE_MAX_SIZE = 2048; ImageAnnotator.AVERAGE_CHAR_WIDTH = ImageAnnotator.FONT_SIZE * 0.6; ImageAnnotator.PADDING_PIXELS = 10; exports.default = ImageAnnotator;