Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
name: CI/CD

on: [pull_request]

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.9'
- name: Install apt dependencies
run: |
sudo apt-get update
sudo apt-get install -y fontconfig libgl1-mesa-dev libglu1-mesa libopencv-dev
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install -r requirements_dev.txt
pip install ./common_lib/emojilib-1.0.1-cp39-cp39-linux_x86_64.whl
- name: Run tests
run: |
pytest
1 change: 1 addition & 0 deletions .python-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
3.9.23

Large diffs are not rendered by default.

21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2025 K.T

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
92 changes: 91 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,93 @@
# Emoji_API

Python+Falcon+Skiaを使用した絵文字生成API。
Python+FastAPI+Skiaを使用した絵文字生成API。

[![FOSSA Status](https://app.fossa.com/api/projects/custom%2B56475%2Fgithub.com%2FMilix-M%2Femoji_API.svg?type=shield&issueType=license)](https://app.fossa.com/projects/custom%2B56475%2Fgithub.com%2FMilix-M%2Femoji_API?ref=badge_shield&issueType=license)
[![FOSSA Status](https://app.fossa.com/api/projects/custom%2B56475%2Fgithub.com%2FMilix-M%2Femoji_API.svg?type=shield&issueType=security)](https://app.fossa.com/projects/custom%2B56475%2Fgithub.com%2FMilix-M%2Femoji_API?ref=badge_shield&issueType=security)

## 概要 (Overview)

このプロジェクトは、PythonのFastAPIとSkiaライブラリを使用して、テキストからカスタム絵文字画像を生成するAPIを提供します。様々なフォント、色、サイズ、配置オプションをサポートし、動的な画像生成を可能にします。

## 特徴 (Features)

- **テキストから画像生成**: 指定されたテキストから絵文字画像を生成します。
- **カスタマイズ可能なオプション**: 幅、高さ、文字色、背景色、テキスト配置、フォントサイズ固定、画像引き伸ばし無効化などのパラメータをサポートします。
- **多様なフォントサポート**: 複数の日本語フォントを含む、利用可能なフォントを選択できます。
- **FastAPIによる高速API**: 高性能なFastAPIフレームワークを使用しています。
- **Docker対応**: Dockerizedされており、簡単に環境をセットアップ・デプロイできます。

## セットアップ (Setup)

### ローカル環境での実行 (Running Locally)

1. **リポジリのクローン**:

```bash
git clone https://github.com/your-username/emoji_API.git
cd emoji_API
```

2. **Python環境のセットアップ**:
Python 3.9以上が必要です。`venv`などを使用して仮想環境を構築することを推奨します。

```bash
python -m venv venv
source venv/bin/activate # Windows: venv\Scripts\activate
```

3. **依存関係のインストール**:

```bash
pip install -r requirements.txt
pip install -r requirements_dev.txt
pip install ./common_lib/emojilib-1.0.1-cp39-cp39-linux_x86_64.whl
```

4. **アプリケーションの起動**:

```bash
uvicorn src.app:app --host 0.0.0.0 --port 8000 --reload
```

アプリケーションは `http://localhost:8000` で利用可能になります。

### Dockerを使用した実行 (Running with Docker)

DockerとDocker Composeがインストールされていることを確認してください。

1. **Dockerイメージのビルドと起動**:

```bash
docker-compose up --build
```

アプリケーションは `http://localhost:8000` で利用可能になります。

## テスト (Testing)

プロジェクトには`pytest`を使用した単体テストが含まれています。

1. **依存関係のインストール**:
ローカル環境でのセットアップ手順に従い、`requirements_dev.txt`を含むすべての依存関係をインストールしてください。

2. **テストの実行**:

```bash
pytest
```

## GitHub Actions

このプロジェクトはGitHub Actionsを使用してCI/CDパイプラインを自動化しています。

- `.github/workflows/ci.yml`: プルリクエストごとにテストを実行します。

## ライセンス (License)

このプロジェクトはMITライセンスの下で公開されています。詳細については[LICENCE](./LICENSE)ファイルを参照してください。
使用されているフォント・ライブラリには個別のライセンスがあります。詳細は`LICENCEs/`ディレクトリ内の各ライセンスファイルを参照してください。

## 貢献 (Contributing)

貢献を歓迎します!バグ報告、機能リクエスト、プルリクエストなど、お気軽にお寄せください。
4 changes: 1 addition & 3 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
# docker-composeのバージョンを指定
version: '3.8'

services:
# サービス名を「app」と定義
app:
Expand All @@ -23,4 +21,4 @@ services:
# コンテナの標準入力を開き、TTYを割り当てる (インタラクティブな操作に必要)
stdin_open: true
tty: true
command: python -m src.app
command: python -m uvicorn src.app:app --host 0.0.0.0 --port 8000 --reload
2 changes: 2 additions & 0 deletions pytest.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[pytest]
pythonpath = .
3 changes: 2 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
falcon
fastapi
uvicorn[standard]
4 changes: 3 additions & 1 deletion requirements_dev.txt
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
pip-licenses
pytest
httpx
pytest-asyncio
Empty file added src/__init__.py
Empty file.
43 changes: 31 additions & 12 deletions src/app.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,35 @@
import falcon
from wsgiref.simple_server import make_server
from .dto.emoji_parameter import EmojiParamDTO
from .dto.font_resorce import EmojiResource
"""Main application file for the Emoji API.

api = falcon.App(cors_enable=True)
api.add_route("/emoji", EmojiParamDTO())
api.add_route("/fonts", EmojiResource())
This file initializes the FastAPI application, configures logging, sets up CORS middleware,
and includes the API routers.
"""

import logging
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from src.routers import emoji

if __name__ == "__main__":
with make_server("", 8000, api) as httpd:
print("Serving on port 8000...")
# ログ設定
logging.basicConfig(
level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)
logger = logging.getLogger(__name__)

# Serve until process is killed
httpd.serve_forever()
app = FastAPI(
title="Emoji API",
description="API for generating custom emoji images with text.",
version="1.0.0",
)
"""FastAPI application instance."""

# CORS設定
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)

# ルーターの登録
app.include_router(emoji.router)
4 changes: 3 additions & 1 deletion src/common/constants.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
class ConstantMeta(type):
"""Metaclass to prevent reassignment of constants."""
def __setattr__(cls, key, value):
"""Prevents reassignment of attributes once set."""
if key in cls.__dict__:
raise AttributeError("Cannot reassign constant")
super().__setattr__(key, value)


class Fonts(metaclass=ConstantMeta):
"""フォントに関する定数を定義するクラス"""
"""Defines constants related to fonts."""

FONT_NAME_PATH_MAPPING = {
"M+ 1p black": {
Expand Down
133 changes: 18 additions & 115 deletions src/dto/emoji_parameter.py
Original file line number Diff line number Diff line change
@@ -1,119 +1,22 @@
import logging
from dataclasses import dataclass
from pydantic import BaseModel, Field
from typing import Literal

import emojilib
import falcon

from ..common.constants import Fonts

# ログ設定
logging.basicConfig(
level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)
logger = logging.getLogger(__name__)


# 'align'パラメータの選択肢をリテラル型で定義
# これにより、'left', 'center', 'right' 以外は型チェッカーでエラーにできる
AlignOptions = Literal["left", "center", "right"]


@dataclass
class EmojiParamDTO:
"""
テキスト画像生成のためのパラメータを保持するDTOクラス
"""

text: str = "絵文\n字。"
width: int = 128
height: int = 128
color: str = "#000000FF"
background_color: str = "#00000000"
align: AlignOptions = "center"
size_fixed: bool = False
disable_stretch: bool = False
typeface_file: str = Fonts.FONT_NAME_PATH_MAPPING["M+ 1p black"]["path"]
typeface_name: str = "M+ 1p black"
format: str = "png"
quality: int = 100

def emoji_generate(self, req_dto):
"""DTOを基に絵文字画像を生成し、'emoji.png'として保存する。

`typeface_name`からフォントパスを解決し、emojilibで画像を生成する。

Args:
req_dto (EmojiParamDTO): 画像生成用のパラメータを持つDTO。
関数内で`typeface_file`が設定され、`typeface_name`はNoneに更新される。

Raises:
KeyError: 不正なフォント名が指定された場合に発生する。
"""

# フォント名からフォントパスを出す
req_dto.typeface_file = Fonts.FONT_NAME_PATH_MAPPING[req_dto.typeface_name][
"path"
]

# Noneにしないと何故かエラーになる
req_dto.typeface_name = None

try:
logger.info("絵文字生成中...")
# 絵文字生成
emoji_raw = emojilib.generate(
text=req_dto.text,
width=req_dto.width,
height=req_dto.height,
color=req_dto.color,
background_color=req_dto.background_color,
align=req_dto.align,
size_fixed=req_dto.size_fixed,
disable_stretch=req_dto.disable_stretch,
typeface_file=req_dto.typeface_file,
typeface_name=req_dto.typeface_name,
format=req_dto.format,
quality=req_dto.quality,
)
logger.info("絵文字生成完了")
return emoji_raw
except Exception as e:
logger.error(f"絵文字生成でエラーが発生しました: {e}")
raise falcon.HTTPInternalServerError(
title="Image generation error",
description=f"An error occurred while generating the image: {e}",
)

def on_post(self, req, resp):
"""
画像生成のリクエストを受け付け、DTOにマッピングして処理します。
"""
try:
req_param = req.get_media()
dto = EmojiParamDTO(**req_param)

except (TypeError, ValueError) as e:
# TypeError: 必須引数がない場合 (例: textがない)
# ValueError: 型が不正な場合 (例: alignに'top'を指定)
raise falcon.HTTPBadRequest(
title="Invalid parameters",
description=f"Request body is invalid or missing required fields. Error: {e}",
)

logger.info(f"画像生成リクエストを受け付けました: {req_param}")

try:
image_data = self.emoji_generate(dto)

resp.status = falcon.HTTP_200
resp.content_type = falcon.MEDIA_PNG
resp.data = image_data

except Exception as e:
# 画像生成中のエラーをハンドリング
logger.error(f"リクエスト処理中にエラー: {e}")
raise falcon.HTTPInternalServerError(
title="Image Generation Failed",
description="An unexpected error occurred during image generation.",
)
"""Literal type for text alignment options."""


class EmojiParam(BaseModel):
"""Pydantic model for holding parameters for text image generation."""

text: str = Field(default="絵文\n字。", description="画像に表示するテキスト", min_length=1)
width: int = Field(default=128, description="画像の幅")
height: int = Field(default=128, description="画像の高さ")
color: str = Field(default="#000000FF", description="文字色 (RGBA)")
background_color: str = Field(default="#00000000", description="背景色 (RGBA)")
align: AlignOptions = Field(default="center", description="テキストの水平方向の配置")
size_fixed: bool = Field(default=False, description="フォントサイズを固定するかどうか")
disable_stretch: bool = Field(default=False, description="画像の引き伸ばしを無効にするかどうか")
typeface_name: str = Field(default="M+ 1p black", description="使用するフォント名")
format: str = Field(default="png", description="出力画像形式")
quality: int = Field(default=100, description="画質 (JPEGの場合に有効)", ge=0, le=100)
Loading