Actually render emoji in calendar event titles; stack duplicate shared events
Build and push server image / build-and-push (push) Successful in 53s
Build and push server image / build-and-push (push) Successful in 53s
The earlier fix stripped emoji instead of rendering them, which wasn't what was asked for. Event titles now draw with two fonts: the usual default font for text, and a vendored monochrome emoji font (Noto Emoji, OFL-1.1) for actual emoji runs, so they show up as real glyphs instead of a tofu box or nothing at all. Monochrome rather than color, since reliably rendering COLR/CBDT color glyphs depends on how Pillow's FreeType was built -- not something to depend on across deployments. Also: events sharing the exact same title and time across different calendars (e.g. a shared family event synced onto more than one person's calendar) now collapse into one row instead of showing twice, with a color bar split between every contributing calendar so it's still clear whose event it is.
This commit is contained in:
@@ -224,6 +224,20 @@ Pages: `/` (routing hub), `/setup`, `/login`, `/claim`, `/settings`,
|
||||
explicit, informed call by the project owner, not a default -- anyone
|
||||
redistributing this project (vs. just self-hosting it) should
|
||||
re-evaluate that tradeoff for their own situation before doing so.
|
||||
- Calendar event titles can contain emoji, which `ImageFont.load_default()`
|
||||
(used for every other bit of text this project renders) has no glyphs
|
||||
for -- PIL/FreeType substitute a visible ".notdef" tofu box rather than
|
||||
skipping the codepoint. `app/calendar_render.py` draws emoji runs with
|
||||
a vendored monochrome font instead (`app/fonts/NotoEmoji.ttf`, Google's
|
||||
Noto Emoji, OFL-1.1 -- license text alongside it at
|
||||
`app/fonts/NotoEmoji-OFL.txt`), the one deliberate exception to this
|
||||
project's usual "no new font/icon assets" default elsewhere in
|
||||
calendar_render.py -- there's no way to hand-draw arbitrary emoji with
|
||||
primitives the way the weather icons are. Monochrome rather than a
|
||||
color emoji font on purpose: reliably rendering COLR/CBDT color glyphs
|
||||
depends on how Pillow's FreeType was built, which isn't something this
|
||||
project controls in every deployment environment, and solid black
|
||||
reads cleanly against everything else this module draws in black.
|
||||
- The 6-color palette RGB values in `app/image_pipeline.py`
|
||||
(`DEFAULT_PALETTE_RGB`) are approximations, not measured values
|
||||
(Waveshare doesn't publish exact color primaries for this panel).
|
||||
|
||||
@@ -115,8 +115,18 @@ def merge_events(
|
||||
fetch_summary is "" when every source succeeded, else "N of M
|
||||
calendars unavailable" (never *which* source -- naming whose feed is
|
||||
down to everyone who looks at a shared household display is a bigger
|
||||
overshare than the outage itself)."""
|
||||
overshare than the outage itself).
|
||||
|
||||
Events sharing the exact same (summary, start, end, all_day) across
|
||||
different calendars -- e.g. a shared family event synced onto more
|
||||
than one person's calendar -- collapse into one entry rather than
|
||||
showing as duplicate rows. Every merged event carries a "sources"
|
||||
list ([{"owner_display_name", "color_index"}, ...], length 1 for an
|
||||
ordinary non-duplicated event) that calendar_render.py draws a
|
||||
color indicator per entry of, so a collapsed event still visibly
|
||||
shows every calendar it came from."""
|
||||
merged: list[dict] = []
|
||||
by_key: dict[tuple, dict] = {}
|
||||
failures = 0
|
||||
for source in sources:
|
||||
try:
|
||||
@@ -130,9 +140,15 @@ def merge_events(
|
||||
failures += 1
|
||||
continue
|
||||
for event in events:
|
||||
event["owner_display_name"] = source.owner_display_name
|
||||
event["color_index"] = source.color_index
|
||||
merged.append(event)
|
||||
source_entry = {"owner_display_name": source.owner_display_name, "color_index": source.color_index}
|
||||
key = (event["summary"], event["start"], event["end"], event["all_day"])
|
||||
existing = by_key.get(key)
|
||||
if existing is None:
|
||||
event["sources"] = [source_entry]
|
||||
by_key[key] = event
|
||||
merged.append(event)
|
||||
else:
|
||||
existing["sources"].append(source_entry)
|
||||
|
||||
merged.sort(key=lambda e: e["start"])
|
||||
summary = f"{failures} of {len(sources)} calendars unavailable" if failures else ""
|
||||
|
||||
+149
-45
@@ -4,7 +4,10 @@ precedent: build an RGB canvas with ImageDraw/ImageFont, then the same
|
||||
_quantize/_transpose_and_pack every other renderer ends on.
|
||||
|
||||
Event dicts here are calendar_feed.py's shape: {"summary", "start", "end"
|
||||
(ISO 8601 strings), "all_day", "owner_display_name"}.
|
||||
(ISO 8601 strings), "all_day", "sources": [{"owner_display_name",
|
||||
"color_index"}, ...]} -- more than one entry in "sources" means
|
||||
merge_events collapsed several calendars' identical (same title/time)
|
||||
events into one, see _event_colors/_draw_color_bar below.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -13,6 +16,8 @@ import calendar as calendar_module
|
||||
import io
|
||||
import re
|
||||
from datetime import date, datetime, timedelta
|
||||
from functools import lru_cache
|
||||
from pathlib import Path
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
@@ -50,22 +55,54 @@ RULE = (0, 0, 0)
|
||||
OWNER_COLORS = DEFAULT_PALETTE_RGB[2:]
|
||||
|
||||
|
||||
def _event_color(event: dict, owners_seen: list[str], palette_rgb: list | None) -> tuple[int, int, int]:
|
||||
"""A specific calendar's manually pinned color (event["color_index"],
|
||||
set from models.FrameCalendar.color_index -- see
|
||||
routers/api_frames.py's api_calendar_color) resolved against
|
||||
whichever palette this frame actually renders with, so a pinned
|
||||
"Blue" still looks like this frame's blue even if its Advanced
|
||||
configuration has retuned the panel's RGB values. Falls back to the
|
||||
old auto-cycle-by-owner-name when a calendar has no color pinned."""
|
||||
color_index = event.get("color_index")
|
||||
if color_index is not None:
|
||||
palette = palette_rgb or DEFAULT_PALETTE_RGB
|
||||
return tuple(palette[color_index])
|
||||
owner_display_name = event["owner_display_name"]
|
||||
if owner_display_name not in owners_seen:
|
||||
owners_seen.append(owner_display_name)
|
||||
return OWNER_COLORS[owners_seen.index(owner_display_name) % len(OWNER_COLORS)]
|
||||
def _event_colors(event: dict, owners_seen: list[str], palette_rgb: list | None) -> list[tuple[int, int, int]]:
|
||||
"""One color per contributing calendar (event["sources"] -- see
|
||||
calendar_feed.merge_events, which collapses events sharing the exact
|
||||
same title/time across different calendars into one entry with
|
||||
several sources, e.g. a shared family event synced onto more than
|
||||
one person's calendar). Usually just one color; more than one is
|
||||
what tells the "same event, more than one calendar" case apart from
|
||||
an ordinary single-calendar event at render time -- see
|
||||
_draw_color_bar. Each source's own manually pinned color
|
||||
(FrameCalendar.color_index -- see routers/api_frames.py's
|
||||
api_calendar_color) resolves against whichever palette this frame
|
||||
actually renders with, so a pinned "Blue" stays this frame's actual
|
||||
blue; a source with no color pinned falls back to the old
|
||||
auto-cycle-by-owner-name behavior. owners_seen is shared across every
|
||||
event/source in a render so that cycle stays consistent view-wide."""
|
||||
sources = event.get("sources") or [
|
||||
{"owner_display_name": event.get("owner_display_name"), "color_index": event.get("color_index")}
|
||||
]
|
||||
colors = []
|
||||
for source in sources:
|
||||
color_index = source.get("color_index")
|
||||
if color_index is not None:
|
||||
palette = palette_rgb or DEFAULT_PALETTE_RGB
|
||||
colors.append(tuple(palette[color_index]))
|
||||
continue
|
||||
owner_display_name = source.get("owner_display_name")
|
||||
if owner_display_name not in owners_seen:
|
||||
owners_seen.append(owner_display_name)
|
||||
colors.append(OWNER_COLORS[owners_seen.index(owner_display_name) % len(OWNER_COLORS)])
|
||||
return colors
|
||||
|
||||
|
||||
def _draw_color_bar(draw: ImageDraw.ImageDraw, x0: int, y0: int, x1: int, y1: int,
|
||||
colors: list[tuple[int, int, int]], radius: int) -> None:
|
||||
"""One rounded bar for a single-source event, or that same overall
|
||||
footprint split into equal-width side-by-side segments -- one per
|
||||
contributing calendar -- for a deduplicated shared event (see
|
||||
_event_colors/calendar_feed.merge_events). Splitting rather than
|
||||
e.g. concentric rings keeps every color equally "thick and bold" at
|
||||
a glance, the same design goal a single pinned color already has."""
|
||||
if len(colors) == 1:
|
||||
draw.rounded_rectangle([x0, y0, x1, y1], radius=radius, fill=colors[0])
|
||||
return
|
||||
seg_w = (x1 - x0) / len(colors)
|
||||
for i, color in enumerate(colors):
|
||||
seg_x0 = round(x0 + i * seg_w)
|
||||
seg_x1 = round(x0 + (i + 1) * seg_w) - (2 if i < len(colors) - 1 else 0)
|
||||
draw.rectangle([seg_x0, y0, seg_x1, y1], fill=color)
|
||||
|
||||
|
||||
def _event_start(event: dict, tz: ZoneInfo) -> datetime | date:
|
||||
@@ -103,16 +140,30 @@ def _fmt_time(dt: datetime) -> str:
|
||||
return text if text else "12:00 AM"
|
||||
|
||||
|
||||
# ImageFont.load_default() (used everywhere in this module -- see the
|
||||
# module docstring) has no emoji glyphs. PIL/FreeType don't just skip an
|
||||
# unsupported codepoint, they substitute a ".notdef" tofu box (a visible
|
||||
# filled rectangle), which reads as a rendering glitch rather than "emoji
|
||||
# not supported" on an actual panel. Stripped before drawing rather than
|
||||
# left to render as tofu. Covers the standard Unicode emoji blocks (plus
|
||||
# skin-tone modifiers, the zero-width joiner used to combine them into
|
||||
# one glyph, and the variation selector that forces emoji presentation)
|
||||
# -- these block ranges are stable even as new individual emoji get
|
||||
# added within them, so this doesn't need updating as emoji sets grow.
|
||||
# ImageFont.load_default() (used for everything else in this module --
|
||||
# see the module docstring) has no emoji glyphs, and PIL/FreeType don't
|
||||
# skip an unsupported codepoint, they substitute a ".notdef" tofu box (a
|
||||
# visible filled rectangle) -- reads as a rendering glitch, not "emoji
|
||||
# not supported". So event titles get drawn with TWO fonts: this one
|
||||
# (monochrome Noto Emoji, OFL-1.1, vendored at app/fonts/NotoEmoji.ttf --
|
||||
# see app/fonts/NotoEmoji-OFL.txt) for actual emoji runs, and the normal
|
||||
# text font for everything else -- see _draw_mixed_line. Monochrome
|
||||
# rather than a color emoji font on purpose: this is a 6-color dithered
|
||||
# e-ink panel, and reliably rendering COLR/CBDT color glyphs depends on
|
||||
# how Pillow's FreeType was built, which isn't something this project
|
||||
# controls in every deployment environment. Solid black next to solid
|
||||
# black text reads cleanly and matches everything else this module draws.
|
||||
_EMOJI_FONT_PATH = Path(__file__).parent / "fonts" / "NotoEmoji.ttf"
|
||||
|
||||
|
||||
@lru_cache(maxsize=None)
|
||||
def _emoji_font(size: int) -> ImageFont.FreeTypeFont:
|
||||
return ImageFont.truetype(str(_EMOJI_FONT_PATH), size)
|
||||
|
||||
|
||||
# Matches runs of actual emoji base characters (the standard Unicode
|
||||
# emoji blocks -- stable ranges even as new individual emoji get added
|
||||
# within them, so this doesn't need updating as emoji sets grow).
|
||||
_EMOJI_PATTERN = re.compile(
|
||||
"["
|
||||
"\U0001F1E6-\U0001F1FF" # regional indicator symbols (flag emoji)
|
||||
@@ -121,20 +172,59 @@ _EMOJI_PATTERN = re.compile(
|
||||
"\U0001F680-\U0001F6FF" # transport & map symbols
|
||||
"\U0001F900-\U0001F9FF" # supplemental symbols & pictographs
|
||||
"\U0001FA70-\U0001FAFF" # symbols & pictographs extended-A
|
||||
"\U0001F3FB-\U0001F3FF" # skin-tone modifiers
|
||||
"\U00002600-\U000026FF" # misc symbols (☀☂☕ etc.)
|
||||
"\U00002700-\U000027BF" # dingbats (✂✈✉ etc.)
|
||||
"\U0000FE0F" # variation selector-16 (emoji presentation)
|
||||
"\U0000200D" # zero-width joiner
|
||||
"]+"
|
||||
)
|
||||
_EMOJI_SPLIT_PATTERN = re.compile(f"({_EMOJI_PATTERN.pattern})")
|
||||
# Codepoints with no meaningful standalone glyph once color/ligature
|
||||
# context is dropped: skin-tone modifiers (this is monochrome -- no
|
||||
# color to modify), the variation selector that just requests emoji
|
||||
# presentation, and the zero-width joiner used to fuse multiple emoji
|
||||
# into one combined glyph. That fusion (e.g. the "family" emoji from
|
||||
# four base emoji + 3 ZWJs) needs OpenType ligature substitution
|
||||
# (raqm/harfbuzz), which Pillow only does with a specific, non-default
|
||||
# build -- not something to depend on. Stripping the ZWJ instead means a
|
||||
# ZWJ sequence just draws as its individual base glyphs side by side
|
||||
# (four separate people instead of one family glyph) -- a real fallback,
|
||||
# not a crash or tofu.
|
||||
_EMOJI_MODIFIER_PATTERN = re.compile("[\U0001F3FB-\U0001F3FF\U0000FE0F\U0000200D]")
|
||||
|
||||
|
||||
def _strip_emoji(text: str) -> str:
|
||||
"""Removes emoji (see _EMOJI_PATTERN above) and collapses whatever
|
||||
whitespace that leaves behind -- "\U0001F389 Birthday party" becomes
|
||||
"Birthday party", not " Birthday party"."""
|
||||
return re.sub(r"\s+", " ", _EMOJI_PATTERN.sub("", text)).strip()
|
||||
def _split_emoji_runs(text: str) -> list[tuple[str, bool]]:
|
||||
"""text -> [(run, is_emoji), ...], modifier/joiner codepoints
|
||||
dropped first (see _EMOJI_MODIFIER_PATTERN). Consecutive emoji
|
||||
collapse into one run (_EMOJI_PATTERN's own "+"), consecutive
|
||||
plain-text characters into the other."""
|
||||
cleaned = _EMOJI_MODIFIER_PATTERN.sub("", text)
|
||||
parts = [p for p in _EMOJI_SPLIT_PATTERN.split(cleaned) if p]
|
||||
return [(p, bool(_EMOJI_PATTERN.fullmatch(p))) for p in parts]
|
||||
|
||||
|
||||
def _draw_mixed_line(img: Image.Image, draw: ImageDraw.ImageDraw, xy: tuple[int, int], text: str,
|
||||
text_font: ImageFont.ImageFont, emoji_font: ImageFont.ImageFont, max_width: int) -> None:
|
||||
"""Draws `text` left-to-right, switching between text_font (normal
|
||||
characters) and emoji_font (actual emoji glyphs, per
|
||||
_split_emoji_runs) so emoji visibly render instead of a tofu box.
|
||||
Truncates with "..." once max_width is exceeded. Unlike
|
||||
_truncate_to_width this can't binary-search a single font's metrics
|
||||
across mixed fonts, so it works run-by-run instead -- fine for the
|
||||
short single-line strings this draws (event titles), not meant as a
|
||||
general rich-text layout engine."""
|
||||
x, y = xy
|
||||
cursor = x
|
||||
for run_text, is_emoji in _split_emoji_runs(text):
|
||||
font = emoji_font if is_emoji else text_font
|
||||
remaining = max_width - (cursor - x)
|
||||
if remaining <= 0:
|
||||
break
|
||||
run_w = draw.textlength(run_text, font=font)
|
||||
if run_w <= remaining:
|
||||
draw_text(img, (round(cursor), y), run_text, font)
|
||||
cursor += run_w
|
||||
else:
|
||||
draw_text(img, (round(cursor), y), _truncate_to_width(draw, run_text, font, remaining), font)
|
||||
break
|
||||
|
||||
|
||||
def _truncate_to_width(draw: ImageDraw.ImageDraw, text: str, font: ImageFont.ImageFont, max_width: int) -> str:
|
||||
@@ -339,17 +429,21 @@ def _draw_agenda_day(img: Image.Image, draw: ImageDraw.ImageDraw, day: date, eve
|
||||
row_h = body_font.size + 14
|
||||
max_rows = max(0, (y0 + h - MARGIN - y) // row_h)
|
||||
|
||||
emoji_font = _emoji_font(body_font.size)
|
||||
if not day_events:
|
||||
draw_text(img, (text_x0, y), "Nothing scheduled", body_font, MUTED)
|
||||
for i, event in enumerate(day_events):
|
||||
if i >= max_rows:
|
||||
draw_text(img, (text_x0, y), f"+{len(day_events) - max_rows} more", body_font, MUTED)
|
||||
break
|
||||
color = _event_color(event, owners_seen, palette_rgb)
|
||||
draw.rounded_rectangle([text_x0, y + 2, text_x0 + 10, y + row_h - 7], radius=3, fill=color)
|
||||
colors = _event_colors(event, owners_seen, palette_rgb)
|
||||
_draw_color_bar(draw, text_x0, y + 2, text_x0 + 10, y + row_h - 7, colors, radius=3)
|
||||
time_str = "All day" if event["all_day"] else _fmt_time(_event_start(event, tz))
|
||||
line = f"{time_str} {_strip_emoji(event['summary'])}"
|
||||
draw_text(img, (text_x0 + 18, y), _truncate_to_width(draw, line, body_font, text_w - 18), body_font)
|
||||
prefix = f"{time_str} "
|
||||
draw_text(img, (text_x0 + 18, y), prefix, body_font)
|
||||
prefix_w = draw.textlength(prefix, font=body_font)
|
||||
_draw_mixed_line(img, draw, (text_x0 + 18 + prefix_w, y), event["summary"],
|
||||
body_font, emoji_font, text_w - 18 - prefix_w)
|
||||
y += row_h
|
||||
|
||||
|
||||
@@ -457,15 +551,21 @@ def _build_week(events: list[dict], browse_offset: int, orientation: str, tz: Zo
|
||||
row_h = chip_font.size + 10
|
||||
max_rows = max(0, (cy0 + ch - MARGIN - y) // row_h)
|
||||
day_events = _events_on_day(events, day, tz)
|
||||
emoji_font = _emoji_font(chip_font.size)
|
||||
for i, event in enumerate(day_events):
|
||||
if i >= max_rows:
|
||||
draw_text(img, (x0 + 6, y), f"+{len(day_events) - max_rows}", chip_font, MUTED)
|
||||
break
|
||||
color = _event_color(event, owners_seen, palette_rgb)
|
||||
draw.rounded_rectangle([x0 + 4, y + 1, x0 + 11, y + row_h - 5], radius=2, fill=color)
|
||||
summary = _strip_emoji(event["summary"])
|
||||
text = summary if event["all_day"] else f"{_fmt_time(_event_start(event, tz))[:-3]} {summary}"
|
||||
draw_text(img, (x0 + 16, y), _truncate_to_width(draw, text, chip_font, col_w - 20), chip_font)
|
||||
colors = _event_colors(event, owners_seen, palette_rgb)
|
||||
_draw_color_bar(draw, x0 + 4, y + 1, x0 + 11, y + row_h - 5, colors, radius=2)
|
||||
if event["all_day"]:
|
||||
_draw_mixed_line(img, draw, (x0 + 16, y), event["summary"], chip_font, emoji_font, col_w - 20)
|
||||
else:
|
||||
prefix = f"{_fmt_time(_event_start(event, tz))[:-3]} "
|
||||
draw_text(img, (x0 + 16, y), prefix, chip_font)
|
||||
prefix_w = draw.textlength(prefix, font=chip_font)
|
||||
_draw_mixed_line(img, draw, (x0 + 16 + prefix_w, y), event["summary"],
|
||||
chip_font, emoji_font, col_w - 20 - prefix_w)
|
||||
y += row_h
|
||||
|
||||
return img
|
||||
@@ -516,7 +616,11 @@ def _build_month(events: list[dict], browse_offset: int, orientation: str, tz: Z
|
||||
dot_x = x0 + 8
|
||||
dot_y = y0 + row_h - dot_r * 2 - 6
|
||||
for i, event in enumerate(day_events[:4]):
|
||||
event_color = _event_color(event, owners_seen, palette_rgb)
|
||||
# First contributing calendar's color only, even for a
|
||||
# deduplicated shared event -- month view is density
|
||||
# dots, not a place to also show which calendars a
|
||||
# shared event came from (see _event_colors).
|
||||
event_color = _event_colors(event, owners_seen, palette_rgb)[0]
|
||||
draw.ellipse([dot_x, dot_y, dot_x + dot_r * 2, dot_y + dot_r * 2], fill=event_color)
|
||||
dot_x += dot_r * 2 + 5
|
||||
if len(day_events) > 4:
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
Copyright 2013 Google LLC
|
||||
|
||||
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:
|
||||
https://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.
|
||||
Binary file not shown.
Reference in New Issue
Block a user