diff --git a/tradein-mvp/backend/app/services/exporters/fonts/OFL_PTSans.txt b/tradein-mvp/backend/app/services/exporters/fonts/OFL_PTSans.txt
new file mode 100644
index 00000000..297566b7
--- /dev/null
+++ b/tradein-mvp/backend/app/services/exporters/fonts/OFL_PTSans.txt
@@ -0,0 +1,93 @@
+Copyright (c) 2010, ParaType Ltd. (http://www.paratype.com/public),
+with Reserved Font Names "PT Sans" and "ParaType".
+
+This Font Software is licensed under the SIL Open Font License, Version 1.1
+This license is copied below, and is also available with a FAQ at:
+http://scripts.sil.org/OFL
+
+-----------------------------------------------------------
+SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
+-----------------------------------------------------------
+
+PREAMBLE
+The goals of the Open Font License (OFL) are to stimulate worldwide
+development of collaborative font projects, to support the font creation
+efforts of academic and linguistic communities, and to provide a free and
+open framework in which fonts may be shared and improved in partnership
+with others.
+
+The OFL allows the licensed fonts to be used, studied, modified and
+redistributed freely as long as they are not sold by themselves. The
+fonts, including any derivative works, can be bundled, embedded,
+redistributed and/or sold with any software provided that any reserved
+names are not used by derivative works. The fonts and derivatives,
+however, cannot be released under any other type of license. The
+requirement for fonts to remain under this license does not apply
+to any document created using the fonts or their derivatives.
+
+DEFINITIONS
+"Font Software" refers to the set of files released by the Copyright
+Holder(s) under this license and clearly marked as such. This may
+include source files, build scripts and documentation.
+
+"Reserved Font Name" refers to any names specified as such after the
+copyright statement(s).
+
+"Original Version" refers to the collection of Font Software components as
+distributed by the Copyright Holder(s).
+
+"Modified Version" refers to any derivative made by adding to, deleting,
+or substituting -- in part or in whole -- any of the components of the
+Original Version, by changing formats or by porting the Font Software to a
+new environment.
+
+"Author" refers to any designer, engineer, programmer, technical
+writer or other person who contributed to the Font Software.
+
+PERMISSION & CONDITIONS
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of the Font Software, to use, study, copy, merge, embed, modify,
+redistribute, and sell modified and unmodified copies of the Font
+Software, subject to the following conditions:
+
+1) Neither the Font Software nor any of its individual components,
+in Original or Modified Versions, may be sold by itself.
+
+2) Original or Modified Versions of the Font Software may be bundled,
+redistributed and/or sold with any software, provided that each copy
+contains the above copyright notice and this license. These can be
+included either as stand-alone text files, human-readable headers or
+in the appropriate machine-readable metadata fields within text or
+binary files as long as those fields can be easily viewed by the user.
+
+3) No Modified Version of the Font Software may use the Reserved Font
+Name(s) unless explicit written permission is granted by the corresponding
+Copyright Holder. This restriction only applies to the primary font name as
+presented to the users.
+
+4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
+Software shall not be used to promote, endorse or advertise any
+Modified Version, except to acknowledge the contribution(s) of the
+Copyright Holder(s) and the Author(s) or with their explicit written
+permission.
+
+5) The Font Software, modified or unmodified, in part or in whole,
+must be distributed entirely under this license, and must not be
+distributed under any other license. The requirement for fonts to
+remain under this license does not apply to any document created
+using the Font Software.
+
+TERMINATION
+This license becomes null and void if any of the above conditions are
+not met.
+
+DISCLAIMER
+THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
+OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
+COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
+DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
+OTHER DEALINGS IN THE FONT SOFTWARE.
diff --git a/tradein-mvp/backend/app/services/exporters/fonts/OFL_PTSerif.txt b/tradein-mvp/backend/app/services/exporters/fonts/OFL_PTSerif.txt
new file mode 100644
index 00000000..0a30c669
--- /dev/null
+++ b/tradein-mvp/backend/app/services/exporters/fonts/OFL_PTSerif.txt
@@ -0,0 +1,93 @@
+Copyright (c) 2010, ParaType Ltd. (http://www.paratype.com/public),
+with Reserved Font Names "PT Sans", "PT Serif" and "ParaType".
+
+This Font Software is licensed under the SIL Open Font License, Version 1.1
+This license is copied below, and is also available with a FAQ at:
+http://scripts.sil.org/OFL
+
+-----------------------------------------------------------
+SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
+-----------------------------------------------------------
+
+PREAMBLE
+The goals of the Open Font License (OFL) are to stimulate worldwide
+development of collaborative font projects, to support the font creation
+efforts of academic and linguistic communities, and to provide a free and
+open framework in which fonts may be shared and improved in partnership
+with others.
+
+The OFL allows the licensed fonts to be used, studied, modified and
+redistributed freely as long as they are not sold by themselves. The
+fonts, including any derivative works, can be bundled, embedded,
+redistributed and/or sold with any software provided that any reserved
+names are not used by derivative works. The fonts and derivatives,
+however, cannot be released under any other type of license. The
+requirement for fonts to remain under this license does not apply
+to any document created using the fonts or their derivatives.
+
+DEFINITIONS
+"Font Software" refers to the set of files released by the Copyright
+Holder(s) under this license and clearly marked as such. This may
+include source files, build scripts and documentation.
+
+"Reserved Font Name" refers to any names specified as such after the
+copyright statement(s).
+
+"Original Version" refers to the collection of Font Software components as
+distributed by the Copyright Holder(s).
+
+"Modified Version" refers to any derivative made by adding to, deleting,
+or substituting -- in part or in whole -- any of the components of the
+Original Version, by changing formats or by porting the Font Software to a
+new environment.
+
+"Author" refers to any designer, engineer, programmer, technical
+writer or other person who contributed to the Font Software.
+
+PERMISSION & CONDITIONS
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of the Font Software, to use, study, copy, merge, embed, modify,
+redistribute, and sell modified and unmodified copies of the Font
+Software, subject to the following conditions:
+
+1) Neither the Font Software nor any of its individual components,
+in Original or Modified Versions, may be sold by itself.
+
+2) Original or Modified Versions of the Font Software may be bundled,
+redistributed and/or sold with any software, provided that each copy
+contains the above copyright notice and this license. These can be
+included either as stand-alone text files, human-readable headers or
+in the appropriate machine-readable metadata fields within text or
+binary files as long as those fields can be easily viewed by the user.
+
+3) No Modified Version of the Font Software may use the Reserved Font
+Name(s) unless explicit written permission is granted by the corresponding
+Copyright Holder. This restriction only applies to the primary font name as
+presented to the users.
+
+4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
+Software shall not be used to promote, endorse or advertise any
+Modified Version, except to acknowledge the contribution(s) of the
+Copyright Holder(s) and the Author(s) or with their explicit written
+permission.
+
+5) The Font Software, modified or unmodified, in part or in whole,
+must be distributed entirely under this license, and must not be
+distributed under any other license. The requirement for fonts to
+remain under this license does not apply to any document created
+using the Font Software.
+
+TERMINATION
+This license becomes null and void if any of the above conditions are
+not met.
+
+DISCLAIMER
+THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
+OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
+COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
+DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
+OTHER DEALINGS IN THE FONT SOFTWARE.
diff --git a/tradein-mvp/backend/app/services/exporters/fonts/PTSans-Bold.ttf b/tradein-mvp/backend/app/services/exporters/fonts/PTSans-Bold.ttf
new file mode 100644
index 00000000..3d4e6fe2
Binary files /dev/null and b/tradein-mvp/backend/app/services/exporters/fonts/PTSans-Bold.ttf differ
diff --git a/tradein-mvp/backend/app/services/exporters/fonts/PTSans-Regular.ttf b/tradein-mvp/backend/app/services/exporters/fonts/PTSans-Regular.ttf
new file mode 100644
index 00000000..83a21b72
Binary files /dev/null and b/tradein-mvp/backend/app/services/exporters/fonts/PTSans-Regular.ttf differ
diff --git a/tradein-mvp/backend/app/services/exporters/fonts/PTSerif-Bold.ttf b/tradein-mvp/backend/app/services/exporters/fonts/PTSerif-Bold.ttf
new file mode 100644
index 00000000..0483e59f
Binary files /dev/null and b/tradein-mvp/backend/app/services/exporters/fonts/PTSerif-Bold.ttf differ
diff --git a/tradein-mvp/backend/app/services/exporters/trade_in_pdf.py b/tradein-mvp/backend/app/services/exporters/trade_in_pdf.py
index 539b0ebe..cb5db27a 100644
--- a/tradein-mvp/backend/app/services/exporters/trade_in_pdf.py
+++ b/tradein-mvp/backend/app/services/exporters/trade_in_pdf.py
@@ -2,10 +2,13 @@
Дизайн опирается на живой веб-продукт (tradein-mvp/frontend/.../trade-in.css):
цветовая палитра сконвертирована из OKLCH-токенов веб-приложения в hex через
-точную OKLab→sRGB матрицу (Ottosson), типографика — DejaVu Serif (заголовки
-страниц), DejaVu Sans (текст/таблицы), DejaVu Sans Mono (все числовые значения,
-tabular-nums) — три семейства из пакета fonts-dejavu-core, гарантированно
-установленного в Docker-образе (без внешних зависимостей и риска font-embedding).
+точную OKLab→sRGB матрицу (Ottosson), типографика — PT Serif (заголовки страниц,
+только жирное начертание), PT Sans (текст/таблицы) — оба встроены в PDF как
+base64 @font-face (SIL OFL 1.1, см. app/services/exporters/fonts/), с DejaVu
+Serif/Sans как fallback-цепочкой; DejaVu Sans Mono (все числовые значения,
+tabular-nums) остаётся системным — из пакета fonts-dejavu-core, гарантированно
+установленного в Docker-образе. Диапазоны цен (_price_range_chart_svg) — реальный
+matplotlib-график (Agg, SVG), не декоративный CSS-div.
Документ — деловой/утилитарный отчёт (ближе к банковской оценке, чем к
маркетинговому лендингу), НЕ 1:1 копия конкурентного референса.
@@ -26,11 +29,19 @@ import datetime as dt
import html as _html
import io
import logging
+import pathlib
import re
from urllib.parse import urlparse
from uuid import UUID
+import matplotlib
+
+matplotlib.use("Agg") # headless-safe backend — ОБЯЗАТЕЛЬНО до любого рендера: в Docker-образе
+# нет GUI toolkit (Tkinter/Qt/GTK), импорт matplotlib без явного backend может тянуть их/падать.
+import matplotlib.font_manager as _font_manager
import segno
+from matplotlib.figure import Figure # object API, НЕ pyplot — см. _price_range_chart_svg
+from matplotlib.patches import Rectangle
from app.core.config import settings
from app.schemas.trade_in import AggregatedEstimate, AnalogLot
@@ -155,8 +166,7 @@ def _make_safe_url_fetcher(): # type: ignore[no-untyped-def]
if p.scheme in ("data", "https"):
return default_url_fetcher(url, timeout=timeout, ssl_context=ssl_context)
raise ValueError(
- f"PDF resource scheme '{p.scheme}' is not allowed (only data: and https:). "
- f"URL: {url!r}"
+ f"PDF resource scheme '{p.scheme}' is not allowed (only data: and https:). URL: {url!r}"
)
return fetcher
@@ -186,11 +196,37 @@ _CARD_BG = "#f4f7f9" # --surface-2: oklch(97.5% 0.004 240) — карточка
_FS_XS = "8pt" # футеры, дисклеймеры, source badges, sub-captions
_FS_SM = "9pt" # labels, заголовки таблиц, explainer-текст
_FS_MD = "10.5pt" # базовый текст (body), значения в таблицах
-_FS_LG = "13pt" # заголовки страниц (h2, DejaVu Serif)
-_FS_XL = "16pt" # главный заголовок cover (h1, DejaVu Serif)
+_FS_LG = "13pt" # заголовки страниц (h2, PT Serif)
+_FS_XL = "16pt" # главный заголовок cover (h1, PT Serif)
_FS_XXL = "22pt" # крупные ценовые цифры (dual-price блок)
+# ── Embedded fonts (PT Sans / PT Serif, ParaType, SIL OFL 1.1) ──────────────
+# Раньше CSS декларировал 'DejaVu Serif'/'DejaVu Sans' — это системные fallback-шрифты
+# без гарантированного визуального качества (только моноширинный 'DejaVu Sans Mono'
+# для цифр остаётся — не часть этой замены, см. _mono()). Теперь настоящие PT Sans/
+# PT Serif встроены как base64 @font-face (см. _build_css) — файлы читаются и
+# кодируются ОДИН раз при импорте модуля, не на каждый рендер PDF.
+_FONTS_DIR = pathlib.Path(__file__).parent / "fonts"
+
+
+def _font_data_url(filename: str) -> str:
+ """Читает .ttf из fonts/ (рядом с этим модулем) и возвращает data:font/ttf;base64 URI."""
+ data = (_FONTS_DIR / filename).read_bytes()
+ return f"data:font/ttf;base64,{base64.b64encode(data).decode('ascii')}"
+
+
+_PT_SANS_REGULAR_URL = _font_data_url("PTSans-Regular.ttf")
+_PT_SANS_BOLD_URL = _font_data_url("PTSans-Bold.ttf")
+_PT_SERIF_BOLD_URL = _font_data_url("PTSerif-Bold.ttf")
+
+# Регистрируем те же файлы в matplotlib (chart endpoint-labels используют тот же
+# PT Sans, что и остальной документ — единая типографика, tabular-figure цифры).
+_font_manager.fontManager.addfont(str(_FONTS_DIR / "PTSans-Regular.ttf"))
+_font_manager.fontManager.addfont(str(_FONTS_DIR / "PTSans-Bold.ttf"))
+matplotlib.rcParams["font.family"] = "PT Sans"
+
+
# ── Source pseudo-logos (текстовые pill-badges с брендовыми цветами источников) ─
# Неизвестный/снятый source (напр. историческое inactive 'n1', #2204) рендерится
# через безопасный fallback в _source_pill: серый фон + source.title() — код на
@@ -317,10 +353,18 @@ def _page_header(brand) -> str: # type: ignore[no-untyped-def]
)
-# ── Price range bar — единый CSS-блок (rounded fill), без faux-3D table-хака ──
+# ── Price range chart — matplotlib SVG bullet/range chart (real chart, не div) ──
+# Раньше рендерилась одним div с фиксированной 64%-заливкой/14%-отступом — числа
+# НЕ определяли позицию заливки (чисто декоративный "прогресс-бар", не привязанный
+# к range_low/range_high). Теперь — настоящий график: серый трек на всю область
+# построения + акцентная заливка ровно на [range_low, range_high] (в млн ₽) + подписи-
+# концы (tabular figures, тот же PT Sans, что и весь документ) + опционально срок
+# экспозиции. Рендерится headless (Agg backend, см. import matplotlib.use выше) в
+# SVG и встраивается как base64 — WeasyPrint рендерит SVG только через image
+# pipeline (не как inline-разметку), см. reference prototype build_report.py.
-def _price_range_bar(
+def _price_range_chart_svg(
range_low: int,
range_high: int,
*,
@@ -330,45 +374,127 @@ def _price_range_bar(
days_min: int | None = None,
days_max: int | None = None,
show_days: bool = False,
+ accent: str = _ACCENT,
) -> str:
- """Полоска диапазона цен — один div с закруглённой заливкой в акцентном цвете.
+ """Bullet/range-chart диапазона цен (matplotlib Agg → SVG → base64
).
- Раньше рендерилась 4-колоночной table с border-хаком под faux-3D + generic
- красной заливкой; теперь — чистый div (та же информация: подписи диапазона,
- срок экспозиции при show_days=True)."""
+ Подложка-трек (светло-серый) + закрашенная акцентная зона на реальном диапазоне
+ [range_low, range_high] + акцентные end-tick'и + подписи-концы + опционально
+ срок экспозиции (show_days=True) — та же информация, что раньше показывал
+ _price_range_bar (удалена), но настоящий график, а не декоративный div."""
if range_high <= range_low:
return ""
label_left = label_left or _fmt_rub_m(range_low)
label_right = label_right or _fmt_rub_m(range_high)
- days_html = ""
- if show_days and days_min is not None and days_max is not None:
- days_html = (
- f'