npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

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

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-rect

ESM 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×100

Menor 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 dev

Licença

MIT © Anderson D. Rosa