watermark-find-rect
v0.1.1
Published
Calcula posição e tamanho do retângulo de uma marca d'água sobre uma imagem — função pura, sem I/O
Downloads
185
Maintainers
Readme
watermark-find-rect
Calcula onde e com que tamanho desenhar uma marca d'água sobre uma
imagem. Recebe dois pares de dimensões, devolve [x, y, width, height] em
pixels inteiros — prontos para sharp.composite(), canvas.drawImage() ou CSS.
Função pura, sem dependência de runtime. Não abre, não lê e não escreve imagem nenhuma: só faz a conta. Quem decide o que fazer com os números é você.
pnpm add watermark-find-rectESM e CJS no mesmo pacote, com tipos inclusos. Node >= 18.
Início rápido
import { getRect } from "watermark-find-rect";
const [x, y, width, height] = getRect(
{ width: 1920, height: 1080 }, // a imagem
{ width: 300, height: 80 }, // o logo
{ gravity: "bottom-right", margin: 4, size: 16 },
);
// → [1228, 864, 648, 172]Sem o terceiro argumento, o padrão é bottom-right com 4% de margem e 20% de
tamanho:
getRect({ width: 100, height: 100 }, { width: 10, height: 10 });
// → [76, 76, 20, 20]API
getRect(image, logo, options?): Rect
| Parâmetro | Tipo | Obrigatório | Descrição |
| --------- | --------- | ----------- | ------------------------------------- |
| image | Logo | sim | Dimensões da imagem de fundo |
| logo | Logo | sim | Dimensões originais da marca d'água |
| options | Options | não | Posição, margem e tamanho |
Devolve um Rect: [x, y, width, height], todos inteiros, todos dentro da
imagem.
O primeiro parâmetro é a imagem, não o logo — apesar de os dois usarem o tipo
Logo, que só descreve a forma{ width, height }.
Options
| Opção | Tipo | Padrão | Descrição |
| --------- | ---------- | ---------------- | -------------------------------------------------- |
| gravity | Gravity | "bottom-right" | Onde ancorar a marca d'água |
| margin | number | 4 | Distância das bordas, em % do menor lado da imagem |
| size | number | 20 | Espessura da marca d'água, em % do menor lado da imagem |
Tipos exportados
import type { Logo, Options, Rect } from "watermark-find-rect";
type Logo = { width: number; height: number };
type Rect = [x: number, y: number, width: number, height: number];
type Options = {
gravity?:
| "center"
| "top"
| "top-right"
| "right"
| "bottom-right"
| "bottom"
| "bottom-left"
| "left"
| "top-left";
margin?: number;
size?: number;
};gravity — as nove âncoras
┌─────────────────────────────────────┐
│ top-left top top-right │
│ │
│ left center right │
│ │
│ bottom-left bottom bottom-right │
└─────────────────────────────────────┘Numa imagem 1920×1080 com logo 300×80, margin: 4, size: 16 — o tamanho é
sempre o mesmo, só a posição muda:
| gravity | Rect |
| -------------- | ---------------------- |
| top-left | [43, 43, 648, 172] |
| top | [636, 43, 648, 172] |
| top-right | [1228, 43, 648, 172] |
| left | [43, 453, 648, 172] |
| center | [636, 453, 648, 172] |
| right | [1228, 453, 648, 172] |
| bottom-left | [43, 864, 648, 172] |
| bottom | [636, 864, 648, 172] |
| bottom-right | [1228, 864, 648, 172] |
center ignora a margin — margem é distância de uma borda, e o centro não
tem borda de referência. Passar as duas juntas não é erro, é no-op.
margin e size — percentuais do menor lado
Nenhum dos dois é pixel. Ambos são percentuais do menor lado da imagem, e é isso que faz a marca d'água ter o mesmo peso visual em paisagem e em retrato:
const logo = { width: 100, height: 100 };
getRect({ width: 1000, height: 500 }, logo, { gravity: "center", size: 20 });
// → [450, 200, 100, 100]
getRect({ width: 500, height: 1000 }, logo, { gravity: "center", size: 20 });
// → [200, 450, 100, 100] ← mesma marca de 100×100Menor lado = 500 nos dois casos, então 20% dele = 100px. A mesma foto girada recebe a mesma marca d'água.
size é espessura, não largura
size é aplicado ao menor lado do logo; o outro lado cresce pela proporção
original. Isso mantém a "grossura" percebida igual entre formatos diferentes:
const image = { width: 1920, height: 1080 }; // menor lado = 1080
const opts = { size: 16 }; // 16% de 1080 = 172px
getRect(image, { width: 100, height: 100 }, opts); // → [..., 172, 172]
getRect(image, { width: 300, height: 80 }, opts); // → [..., 648, 172]
getRect(image, { width: 80, height: 300 }, opts); // → [..., 172, 648]Os três têm 172px de espessura. A faixa fica com 648px de largura porque a
proporção 3.75:1 pediu — size não é um limite de bounding box. Se o
resultado estourar a imagem, ele encolhe mantendo o aspect ratio.
Receitas
sharp — compor a marca d'água
import sharp from "sharp";
import { getRect } from "watermark-find-rect";
const image = sharp(imageBuffer);
const { width, height } = await image.metadata();
const logoMeta = await sharp(logoBuffer).metadata();
const [left, top, w, h] = getRect(
{ width: width!, height: height! },
{ width: logoMeta.width!, height: logoMeta.height! },
{ gravity: "bottom-right", margin: 4, size: 16 },
);
const output = await image
.composite([
{ input: await sharp(logoBuffer).resize(w, h).toBuffer(), left, top },
])
.toBuffer();O sharp rejeita rect fora dos limites da imagem — por isso o retorno é
truncado com Math.floor e garantidamente dentro. Veja
Garantias.
sharp — com opacidade
const alpha = Math.round(Math.min(Math.max(opacity / 100, 0), 1) * 255);
const logo = await sharp(logoBuffer)
.resize(w, h)
.ensureAlpha()
.composite([
{
input: Buffer.from([255, 255, 255, alpha]),
raw: { width: 1, height: 1, channels: 4 },
tile: true,
blend: "dest-in",
},
])
.toBuffer();
await image.composite([{ input: logo, left, top }]).toBuffer();Canvas (browser ou node-canvas)
const [x, y, w, h] = getRect(
{ width: canvas.width, height: canvas.height },
{ width: logoImage.naturalWidth, height: logoImage.naturalHeight },
{ gravity: "bottom-right" },
);
ctx.drawImage(logoImage, x, y, w, h);CSS — posicionar por cima com position: absolute
const [x, y, w, h] = getRect(image, logo, { gravity: "top-right", size: 12 });
const style = {
position: "absolute",
left: `${(x / image.width) * 100}%`,
top: `${(y / image.height) * 100}%`,
width: `${(w / image.width) * 100}%`,
height: `${(h / image.height) * 100}%`,
};Convertendo para percentual, a marca d'água acompanha a imagem responsiva.
Garantias
Valem para qualquer entrada, sem exceção:
| Invariante | Significado |
| ----------------------------- | -------------------------------------------------- |
| x + width <= image.width | nunca vaza pela direita |
| y + height <= image.height | nunca vaza por baixo |
| min(x, y, width, height) >= 0 | nunca devolve coordenada negativa |
| todos inteiros | truncados com Math.floor — seguro para sharp |
| aspect ratio preservado | a marca d'água nunca distorce |
A suíte verifica as quatro contra 6.480 combinações de gravity × formato de imagem × formato de logo × margem × tamanho.
Comportamentos conhecidos
size: 0 cai no default de 20%. margin: 0 é respeitado, mas size: 0
não — assimetria herdada da implementação original e preservada por
compatibilidade. Para uma marca d'água invisível, não chame getRect.
margin negativa volta ao default de 4%. size negativo, por outro lado,
passa direto.
gravity inválida degrada para a origem. O tipo cobre as 9 opções, mas um
consumidor JavaScript pode furar isso — nesse caso o retorno é [0, 0, w, h],
sem throw e sem NaN.
Sem validação de entrada. width: 0 devolve [0, 0, 0, 0]; NaN propaga.
Valide antes de chamar se a origem dos dados não for confiável.
Sem rotação nem repetição. O rect é sempre alinhado aos eixos. Marca d'água diagonal ou em padrão repetido é outro algoritmo.
Por dentro
Como o cálculo funciona, por que a régua é o menor lado, a ordem dos clamps e o
histórico das decisões: docs/ARCHITECTURE.md.
Para experimentar as opções visualmente, o playground do monorepo desenha o rect sobre a imagem em tempo real:
pnpm --filter @picture-algorithms/playground devLicença
MIT © Anderson D. Rosa
