review(p6-2): 회차 1 지적 반영

- crates/kebab-config/src/lib.rs:
  • `OcrCfg.endpoint: String` (\"\" sentinel) → `Option<String>` 으로 교체.
    `#[serde(default)]` 적용. `KEBAB_IMAGE_OCR_ENDPOINT=\"\"` (빈 값) 도
    None 으로 매핑하는 분기 추가.
  • 신규 회귀 테스트 `image_ocr_endpoint_empty_env_value_is_none`.
- crates/kebab-parse-image/src/ocr.rs:
  • `OllamaVisionOcr::new` 의 endpoint fallback 로직을 새 `Option<String>`
    스키마에 맞춰 정리 (`as_deref` + match).
  • `OllamaGenerateResponse` 의 dead `_other: HashMap<String, Value>` 필드
    제거. `serde_json::Value` import 도 같이 정리.
  • `OllamaGenerateRequest.images: Vec<&'a str>` → `[&'a str; 1]`
    (호출당 vec! 알로케이션 제거, multi-image 는 OcrEngine trait 가
    단일 이미지를 받으므로 OOS).
  • `downscale_to_long_edge` 단일-디코드로 리팩터. PNG passthrough
    hot path 보존 (header sniff 만으로 분기), 그 외 모든 경로는
    decode 1회 + (필요 시) resize + PNG re-encode 1회로 통일.
  • `pub fn max_pixels(&self) -> u32` accessor 추가 — clamp 결과
    검증 용 (단순 inspector).
- crates/kebab-parse-image/tests/ocr.rs:
  • `cfg_for_endpoint` / 통합 테스트가 `Some(endpoint)` 형태로 갱신.
  • `from_parts_clamps_max_pixels_into_legal_range` 가 새 accessor
    로 실제 클램프 결과 (256 / 4096 / 1024) 를 검증하도록 강화.
  • 통합 테스트가 폰트 부재 시 panic 대신 skip 하도록 분기.
- crates/kebab-parse-image/tests/common/mod.rs:
  • `hello_world_png` 가 `anyhow::Result<Vec<u8>>` 반환하도록 변경.
    expect(\"DejaVu Sans Bold required\") 메시지를 \"only the opt-in
    OCR integration fixture needs this font\" 로 의도 명확화.

cargo test -p kebab-parse-image — 28 pass + 1 ignored.
cargo test -p kebab-config — 21 pass (+1 회귀).
cargo clippy --workspace --all-targets -- -D warnings — pass.

Reviewer-suggested workspace.dependencies 통합 (reqwest / base64) 은
P6-3 와 함께 처리할 수 있도록 follow-up 으로 두고 본 PR scope 에서
제외 (회차 1 본문에서 명시).
This commit is contained in:
2026-05-02 05:45:25 +00:00
parent 4ed5536c92
commit e869710d82
4 changed files with 125 additions and 66 deletions

View File

@@ -60,18 +60,23 @@ pub fn large_blue_4000x3000_png() -> Vec<u8> {
/// `ocr_integration_real_ollama_transcribes_text` integration test —
/// regular hermetic tests never call it.
///
/// Renders with a font shipped by `dejavu` (path under
/// `/usr/share/fonts/truetype/dejavu/`) — common across most Linux dev
/// boxes. Falls back to a tiny built-in glyph map if the font is
/// missing so the helper compiles even without DejaVu installed.
pub fn hello_world_png() -> Vec<u8> {
/// Returns `Err` (not panic) if the DejaVu Sans Bold font is missing
/// from the standard Linux path, so dev boxes without the font can
/// gracefully skip the integration test rather than crashing the
/// process.
pub fn hello_world_png() -> anyhow::Result<Vec<u8>> {
use ab_glyph::{Font, FontRef, ScaleFont};
use anyhow::Context;
let mut img: ImageBuffer<Rgb<u8>, _> =
ImageBuffer::from_fn(400, 100, |_, _| Rgb([255, 255, 255]));
let font_bytes = std::fs::read("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf")
.expect("DejaVu Sans Bold required for OCR integration fixture");
let font = FontRef::try_from_slice(&font_bytes).expect("font parses");
let font_path = "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf";
let font_bytes = std::fs::read(font_path).with_context(|| {
format!(
"{font_path} not found — only the opt-in OCR integration fixture needs this font"
)
})?;
let font = FontRef::try_from_slice(&font_bytes).context("DejaVu font parses")?;
let scaled = font.as_scaled(40.0);
let text = "Hello World 2026";
let mut x = 10.0_f32;
@@ -93,8 +98,8 @@ pub fn hello_world_png() -> Vec<u8> {
}
let mut buf = Cursor::new(Vec::new());
img.write_to(&mut buf, image::ImageFormat::Png)
.expect("encoding hello-world PNG must not fail");
buf.into_inner()
.context("encoding hello-world PNG")?;
Ok(buf.into_inner())
}
/// JPEG with embedded EXIF APP1 segment carrying GPS + Make + Model +