예제
KO

이미지 처리

Hwaro는 빌드 중에 리사이즈된 이미지 변형을 자동으로 생성할 수 있습니다. 반응형 이미지, 썸네일, 성능 최적화에 유용합니다. 외부 도구는 필요 없습니다. 이미지 처리는 stb 라이브러리 기반으로 바이너리에 내장되어 있습니다.

지원 포맷

포맷 읽기 쓰기
JPEG (.jpg, .jpeg) 가능 가능
PNG (.png) 가능 가능
BMP (.bmp) 가능 가능

설정

config.toml에서 이미지 처리를 활성화합니다:

[image_processing]
enabled = true
widths = [320, 640, 1024, 1280]
quality = 85
키 타입 기본값 설명
enabled bool false 이미지 리사이즈 활성화
widths array [] 생성할 대상 너비(픽셀)
quality int 85 JPEG 출력 품질 (1-100)
dimensions bool false 콘텐츠 이미지에 원본 width/height 추가 (enabled 없이도 동작)

LQIP (저화질 이미지 플레이스홀더)

LQIP를 활성화하면 빌드 시점에 작은 base64 인코딩 플레이스홀더 이미지를 만들고 대표 색상을 추출합니다. CLS(Cumulative Layout Shift)를 없애고, 원본 이미지가 로드되는 동안 즉시 시각적 피드백을 보여줍니다.

[image_processing.lqip]
enabled = true
width = 32
quality = 20
키 타입 기본값 설명
enabled bool false LQIP 생성 활성화
width int 32 플레이스홀더 이미지 너비(픽셀, 8-128)
quality int 20 플레이스홀더 JPEG 품질 (1-100, 낮을수록 작음)

너비 32, 품질 20이면 이미지당 보통 400-800바이트 정도의 base64 문자열이 나옵니다. HTML에 바로 인라인해도 될 만큼 작습니다.

이미지 크기 속성

dimensions = true를 설정하면 렌더링된 페이지 콘텐츠의 로컬 이미지마다 원본 크기를 붙여, 파일이 도착하기 전에 브라우저가 알맞은 자리를 잡아 둡니다(레이아웃 이동 없음):

[image_processing]
dimensions = true   # enabled = true가 필요 없습니다
<!-- ![Logo](/hwaro.png)는 이렇게 렌더링됩니다 -->
<img width="512" height="512" src="/hwaro.png" alt="Logo" />

동작 방식

  1. 빌드 중 Hwaro는 세 위치의 이미지를 스캔합니다:
    • 페이지 번들 에셋 (index.md와 함께 둔 이미지)
    • 콘텐츠 파일 ([content.files] 설정으로 게시되는 이미지)
    • 정적 파일 (static/ 디렉터리의 이미지)
  2. 각 이미지마다 설정된 모든 너비의 리사이즈 변형을 생성합니다
  3. 종횡비는 항상 유지됩니다
  4. 대상 너비가 원본보다 크면 업스케일 없이 원본을 그대로 복사합니다
  5. 원본 이미지는 한 번만 디코딩한 뒤 모든 너비로 리사이즈합니다 (효율적)
  6. EXIF 방향 정보가 있는 JPEG 사진(휴대폰·카메라 촬영본)은 먼저 바로 세워서 처리하므로, 변형·LQIP 플레이스홀더·fill/crop 결과가 브라우저가 보여 주는 원본과 일치합니다. dimensions = true는 화면에 표시되는 크기를 기록합니다

출력 파일 이름

리사이즈된 이미지에는 {name}_{width}w.{ext} 규칙으로 이름이 붙습니다. 예를 들어 static/hwaro.png라면:

static/hwaro.png
  -> public/hwaro_320w.png
  -> public/hwaro_640w.png
  -> public/hwaro_1024w.png
  -> public/hwaro_1280w.png

직접 게시하는 파일이 이 이름 중 하나와 같으면(예: 직접 만든 static/hwaro_320w.png) 덮어쓰지 않습니다. 해당 너비는 생성하지 않고 srcset에서도 제외합니다.

템플릿에서 사용

resize_image() 함수로 리사이즈 변형의 URL을 얻습니다:

{% set img = resize_image(path="/hwaro.png", width=640) %}
<img src="{{ img.url }}" width="{{ img.width }}">

srcset을 사용하는 반응형 이미지:

{% set sm = resize_image(path="/hwaro.png", width=320) %}
{% set md = resize_image(path="/hwaro.png", width=640) %}
{% set lg = resize_image(path="/hwaro.png", width=1024) %}
<img
  src="{{ md.url }}"
  srcset="{{ sm.url }} 320w, {{ md.url }} 640w, {{ lg.url }} 1024w"
  sizes="(max-width: 640px) 320px, (max-width: 1024px) 640px, 1024px"
  alt="Hwaro logo"
>

이 함수는 가장 가까운 너비를 선택합니다. width=500을 요청했는데 설정된 너비가 [320, 640, 1024]라면 640px 변형(요청값 이상 중 가장 작은 너비)을 반환합니다. 충분히 큰 변형이 없으면 가장 큰 변형으로 폴백합니다.

LQIP 플레이스홀더 사용

LQIP가 활성화되면 resize_image()가 두 속성을 추가로 반환합니다: lqip(base64 데이터 URI)와 dominant_color(16진수 색상 문자열)입니다. 블러업(blur-up) 효과나 단색 플레이스홀더에 사용합니다:

블러업 효과:

{% set img = resize_image(path="/images/hero.jpg", width=1024) %}
<img
  src="{{ img.url }}"
  style="background-image: url({{ img.lqip }}); background-size: cover;"
  loading="lazy"
  alt="Hero image"
>

대표 색상 플레이스홀더:

{% set img = resize_image(path="/images/hero.jpg", width=1024) %}
<img
  src="{{ img.url }}"
  style="background-color: {{ img.dominant_color }}"
  loading="lazy"
  alt="Hero image"
>

결합 방식 (색상 → 블러 → 원본 이미지 순서):

{% set img = resize_image(path="/images/hero.jpg", width=1024) %}
<div style="background-color: {{ img.dominant_color }}">
  <img
    src="{{ img.url }}"
    style="background-image: url({{ img.lqip }}); background-size: cover;"
    loading="lazy"
    alt="Hero image"
  >
</div>

LQIP가 비활성화 상태면 lqip와 dominant_color는 빈 문자열을 반환하므로 템플릿을 고치지 않아도 됩니다.

투명도가 있는 이미지(PNG 로고, 아이콘)에서는 dominant_color가 불투명도로 가중 평균되므로 투명 픽셀 때문에 색이 어두워지지 않습니다. 알파 채널이 없는 lqip JPEG는 투명한 영역을 흰색으로 표시합니다.

자르기와 채우기

resize_image()는 썸네일이나 고정 크기 카드를 위해 op와 anchor를 받습니다:

{# 400x300 상자를 덮도록 맞춘 뒤 넘치는 부분을 가운데 기준으로 자릅니다 #}
{% set card = resize_image(path="/images/hero.jpg", width=400, height=300, op="fill") %}
<img src="{{ card.url }}" width="{{ card.width }}" height="{{ card.height }}">

{# 크기를 바꾸지 않고 왼쪽 위에서 200x200 영역을 잘라냅니다 #}
{% set corner = resize_image(path="/images/hero.jpg", width=200, height=200, op="crop", anchor="top_left") %}
op 동작
fit 기본값. 미리 생성된 너비 변형 중 가장 가까운 것을 고릅니다(위 참고). 확대하지 않습니다.
fill widthxheight를 덮도록 크기를 맞춘 뒤 anchor 기준으로 정확히 그 크기로 자릅니다.
crop 크기를 바꾸지 않습니다. anchor 위치에서 widthxheight 영역을 잘라냅니다(이미지 크기로 제한).

anchor는 center(기본값), top, bottom, left, right, top_left, top_right, bottom_left, bottom_right 중 하나입니다.

라이브 데모

리사이즈 데모

이 문서 사이트는 widths = [128, 256, 512]와 LQIP를 켠 상태로 이미지 처리를 사용합니다. 아래 이미지들은 static/hwaro.png에서 자동 생성된 리사이즈 변형입니다:

원본 (hwaro.png):

Hwaro 로고 - 원본

128px (hwaro_128w.png):

Hwaro 로고 - 가로 128px

256px (hwaro_256w.png):

Hwaro 로고 - 가로 256px

512px (hwaro_512w.png):

Hwaro 로고 - 가로 512px

이 파일들은 빌드 시점에 생성되므로 런타임 리사이즈나 외부 서비스가 필요 없습니다. 템플릿에서는 resize_image()로 참조합니다:

{% set img = resize_image(path="/hwaro.png", width=256) %}
<img src="{{ img.url }}">
{# renders as: <img src="https://example.com/hwaro_256w.png"> (img.url is absolute, under base_url) #}

LQIP 데모

resize_image() 함수는 LQIP 데이터도 제공합니다. hwaro.png의 실제 출력은 다음과 같습니다:

Dominant color: #b35454
LQIP placeholder (the tiny base64 image, shown at 256px):
LQIP placeholder
Blur-up effect (LQIP as background, full image on top):
Full image with LQIP background

성능

간단한 예시

사이트에 static/hwaro.png(Hwaro 로고)가 있고 이를 반응형 이미지로 표시하려는 경우입니다:

config.toml:

[image_processing]
enabled = true
widths = [128, 256, 512]
quality = 90

[image_processing.lqip]
enabled = true
width = 32
quality = 20

템플릿:

{% set logo_sm = resize_image(path="/hwaro.png", width=128) %}
{% set logo_md = resize_image(path="/hwaro.png", width=256) %}
{% set logo_lg = resize_image(path="/hwaro.png", width=512) %}
<img
  src="{{ logo_md.url }}"
  srcset="{{ logo_sm.url }} 128w, {{ logo_md.url }} 256w, {{ logo_lg.url }} 512w"
  sizes="(max-width: 480px) 128px, (max-width: 768px) 256px, 512px"
  style="background-color: {{ logo_md.dominant_color }}"
  loading="lazy"
  alt="Hwaro"
>

빌드 출력:

public/
  hwaro.png           (original, copied by static files)
  hwaro_128w.png      (128px wide)
  hwaro_256w.png      (256px wide)
  hwaro_512w.png      (512px wide)

블로그 글 이미지

프론트 매터에 히어로 이미지를 지정한 블로그 글이라면:

[image_processing]
enabled = true
widths = [320, 640, 1024]
quality = 85

[image_processing.lqip]
enabled = true

[content.files]
allow_extensions = ["jpg", "jpeg", "png"]
{% if page.image %}
  {% set hero = resize_image(path=page.image, width=1024) %}
  {% set thumb = resize_image(path=page.image, width=320) %}
  <picture>
    <source media="(min-width: 768px)" srcset="{{ hero.url }}">
    <img
      src="{{ thumb.url }}"
      style="background-color: {{ thumb.dominant_color }}"
      loading="lazy"
      alt="{{ page.title }}"
    >
  </picture>
{% endif %}

함께 보기