p = 'backend/app/routers/articles.py'
s = open(p).read()

# Factor out what both figure paths do, so a raster figure and a drawn one
# arrive in an article identically.
helper = '''

#: What a teaching image may be. SVG comes in through the spec endpoint, which
#: renders it here; anything arriving as a file is a photograph, a scan or a
#: rendering from an image model, and those are raster.
FIGURE_IMAGE_TYPES = {"image/png", "image/jpeg", "image/webp"}
MAX_FIGURE_BYTES = 8 * 1024 * 1024


def _figure_library(db: Session):
    """The one library article figures are filed in, made if it is missing."""
    from app.models.media import MediaLibrary

    library = db.query(MediaLibrary).filter(MediaLibrary.name == FIGURE_LIBRARY).first()
    if not library:
        library = MediaLibrary(name=FIGURE_LIBRARY, user_id=None,
                               description="Diagrams and teaching images for article sections.")
        db.add(library)
        db.flush()
    return library


def _attach_figure(db, article, index: int, section_id: str, asset, alt: str,
                   user_id: int, replace: bool):
    """Hang a stored asset on a section: the line, the link, a revision."""
    from app.models.media import ArticleMedia
    from app.services import illustrate

    sections = list(article.sections or [])
    section = sections[index]
    body = section.get("content") or ""
    article_service.snapshot(db, article, user_id, note="figure added")
    if replace:
        body = illustrate.strip_figures(body)
    sections[index] = {**section, "content": body.rstrip() + illustrate.figure_line(alt, asset.path)}
    article.sections = sections
    db.add(ArticleMedia(article_id=article.id, media_id=asset.id, section_id=section_id))
    db.commit()
    article_service.reindex(db, article)


def _section_index(article, section_id: str) -> int:
    index = next((i for i, s in enumerate(article.sections or [])
                  if str(s.get("id")) == section_id), None)
    if index is None:
        raise HTTPException(404, "No section with that id in this article")
    return index


@router.post("/{article_id}/figures/image", status_code=201)
def add_article_image(
    article_id: int,
    section_id: str = Form(...),
    file: UploadFile = File(...),
    title: str | None = Form(None),
    caption: str | None = Form(None),
    replace: bool = Form(False),
    db: Session = Depends(get_db),
    current_user: User = Depends(require_moderator),
):
    """Attach a teaching image to a section — the raster twin of the spec route.

    A drawing this site generates is an SVG rendered from a spec; a clinical
    sign, an exanthem or an anatomical plate is a picture, and no amount of
    geometry will produce one. Both end up in an article the same way, so both
    go through the same attach: the same library, the same link table, the
    same figure line, the same revision.

    The caption becomes the alt text, so a figure with no caption is refused
    rather than reaching a screen reader as nothing.
    """
    from app.models.media import MediaAsset
    from app.services import illustrate, storage_service

    article = db.get(Article, article_id)
    if not article:
        raise HTTPException(404, "Article not found")
    index = _section_index(article, section_id)

    if file.content_type not in FIGURE_IMAGE_TYPES:
        raise HTTPException(400, "A figure image must be PNG, JPEG or WebP")
    data = file.file.read(MAX_FIGURE_BYTES + 1)
    if not data:
        raise HTTPException(400, "That file is empty")
    if len(data) > MAX_FIGURE_BYTES:
        raise HTTPException(413, f"Keep a figure under {MAX_FIGURE_BYTES // (1024 * 1024)} MB")
    # The bytes decide, not the header. A content type is a claim the caller
    # makes about its own file.
    if not (data[:8] == b"\\x89PNG\\r\\n\\x1a\\n" or data[:3] == b"\\xff\\xd8\\xff"
            or (data[:4] == b"RIFF" and data[8:12] == b"WEBP")):
        raise HTTPException(400, "That file is not a PNG, JPEG or WebP")

    alt = " ".join((caption or "").split())
    if not alt:
        raise HTTPException(422, "A figure needs a caption — it is the alt text")

    body = (article.sections or [])[index].get("content") or ""
    if illustrate.already_illustrated(body) and not replace:
        raise HTTPException(409, "That section already has a figure; pass replace to swap it")

    suffix = {"image/png": "png", "image/jpeg": "jpg", "image/webp": "webp"}[file.content_type]
    key = illustrate.key_for().rsplit(".", 1)[0] + f".{suffix}"
    storage_service.save(key, data, file.content_type)
    asset = MediaAsset(
        path=key,
        title=(" ".join((title or "").split()) or (article.sections or [])[index].get("title"))[:300],
        caption=alt[:300], alt_text=alt[:300], kind="image", user_id=None,
        library_id=_figure_library(db).id,
        storage="s3" if storage_service.using_s3() else "local",
        byte_size=len(data),
    )
    db.add(asset)
    db.flush()
    _attach_figure(db, article, index, section_id, asset, alt, current_user.id, replace)
    return {"asset_id": asset.id, "path": key, "url": f"/uploads/{key}",
            "article_id": article.id, "section_id": section_id,
            "title": asset.title, "caption": asset.caption}
'''
s = s + helper
open(p, 'w').write(s)
print('written')
