From fddec2dfeb0c6c88c74a6873720d35a3e2d2afc8 Mon Sep 17 00:00:00 2001 From: Bob Ward Date: Sun, 25 Oct 2026 06:23:59 -0500 Subject: [PATCH] Keep media builders local and simplify figures documentation Publish only the finished book media and concise descriptions. Stop tracking the media generation scripts, ignore local builders and previews, and retain Microsoft icon attribution. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .gitignore | 5 + figures/README.md | 180 +------ tools/make-book-chapters.py | 1003 ----------------------------------- tools/make-book-video.ps1 | 52 -- 4 files changed, 17 insertions(+), 1223 deletions(-) delete mode 100644 tools/make-book-chapters.py delete mode 100644 tools/make-book-video.ps1 diff --git a/.gitignore b/.gitignore index 1a2b92c..7fccef1 100644 --- a/.gitignore +++ b/.gitignore @@ -100,3 +100,8 @@ TestResults/ # Author-supplied full manuscript; publish only the extracted front cover. /figures/book cover.pdf + +# Local book-media build scripts and previews; publish only the finished media. +/tools/make-book-*.py +/tools/make-book-*.ps1 +/figures/opening-*.mp4 diff --git a/figures/README.md b/figures/README.md index 5ff87d7..97c4572 100644 --- a/figures/README.md +++ b/figures/README.md @@ -7,175 +7,19 @@ zoom in. Figures are reproduced as printed. Back to the [repository README](../README.md). -## Book promotional animation - -### Chapter-by-chapter page-turn version - -[View or download the chapter flip-through GIF](book-chapters.gif): an animated -book with a cover, a page for each of the eleven chapters, page-turn transitions, -a summary with an attributed excerpt from Arun Ulag's Chapter 11 quote, -and a closing invitation. Each chapter uses its book title and highlights its -key concepts, from the architecture and SQL engine to applications, AI, -observability, security, resiliency, performance, and the Azure platform. -Each chapter has a distinct illustration and accent color: a story timeline, -deployment terminal, architecture topology, real app preview, in-engine RAG -diagram, approval workflow, telemetry chart, security shield, geo-replication -map, compute-stage chart, and Azure ecosystem hub. Charts are illustrative, -not measured performance results. -Chapter 1 honors Project Socrates and the engineering of a new distributed, -cloud-native SQL architecture, tracing SQL Server through Socrates to Hyperscale. -The actual front cover opens the animation, followed by an inside title page. -That title page emphasizes the book and its subject, with a smaller note -explicitly identifying the book examples as AI-assisted creation and guided -learning, not describing Hyperscale itself as built with AI. -The cover note and Chapter 2 illustrate both roles of AI: AI helped build the -examples, and readers use GitHub Copilot and skills to explore and run them, -including guided provisioning, connection, and schema setup. -Chapter pages omit repetitive fine-print notes to keep the animation readable; -the book and repository documentation provide the synthetic-data and -illustrative-chart context. -The architecture page distinguishes HA and named replicas. Brown arrows show -the primary writing to the log service and both the log service and page servers -writing to Azure Storage. Teal arrows show log data flowing from the log service -to HA replicas, named replicas, and page servers; blue arrows show pages flowing -from page servers to the primary and replicas. Read arrows follow the returned -data, not the requests. Shared read lanes keep these paths separate from writes. -Chapter 5 shows -vectors, DiskANN retrieval, and RAG orchestration inside the SQL engine, with -the external model request and response routed through the APIM AI gateway -to and from Foundry. -A separate, blue embeddings connection goes directly between SQL and Foundry, -bypassing APIM. -Chapter 10 highlights serverless, elastic pools, optimized locking, and automatic -index compaction alongside JSON indexing, memory grant feedback, and illustrative vCore scaling -through 8, 16, 40, and 192. The 192-vCore point is not a measured demo stage; -the repository's compute demo remains 8, 16, and 40 vCores. -Chapter 11 surrounds Azure SQL Hyperscale with six talking points: Azure, -Microsoft Foundry, Migrate, Azure App Service, Change Event Streaming, and -Fabric Mirroring. -Chapter 9 separates built-in HA, geo-replication, and point-in-time restore -into three illustrations, with failover and application retries called out. -The PITR caption emphasizes recovery: "Recover from a mistake quickly." -Chapter 7 includes Database Hub, DMVs, and database agents alongside Azure Monitor, -Query Store, and engine events. -Chapter 6 places the handoff agent inside the Ward General app, connected to an -external LLM for orchestration and to SQL MCP for database tool execution. -Human approval gates saving the handoff. -The inside title page and closing page show both the book and code links. -Square format, 640 x 640 pixels, about 69 seconds, looping, no audio. -The GIF uses 96 colors and fewer intermediate turn frames to stay below 5 MB. -It starts slowly, then moves through the chapters faster; the 4K video is unchanged. -The actual front cover remains visible for six seconds and opens smoothly over -2.4 seconds; the inside title spread remains visible for six seconds. -Each chapter remains visible for 2.5 seconds before the page turns. -The summary page remains visible for seven seconds so readers can read the -quote excerpt, attributed to Arun Ulag, Vice President of Azure (title updated -from the book at the author's request). -Its subtitle is "Hyperscale is your database now and for the future." -The camera pulls back as the actual front cover opens against black, then stays -on the open two-page book. Pages lift toward the viewer and turn right to left -around the center spine. The final open-book version is the primary animation; -older chapter animations and opening-preview videos have been removed. -Chapter titles and prose use Georgia serif typography for a printed-book feel; -diagram labels retain sans-serif type for clarity. - -The editable generator is [make-book-chapters.py](../tools/make-book-chapters.py). -On Windows, with Python, Pillow, and the Segoe UI and Georgia fonts available, -regenerate it from the repository root: - -```powershell -python .\tools\make-book-chapters.py -``` - -Pass `--preview-dir` with a local directory to also save a storyboard and -page-turn preview outside the repository. - -### Book cover - -The actual front cover, including the foreword credit, is available as a -[300-dpi PNG](book-cover.png), 2100 x 3000 pixels, extracted from the author's -supplied PDF. The generator uses this PNG directly; neither the cover PDF nor -the full manuscript is distributed with these examples. The older short -promotional GIF has been removed in favor of the chapter animation above. - -### Event-screen video - -[Download the 4K chapter video](book-chapters-4k.mp4): a silent H.264 MP4 at -3840 x 2160, 25 fps, lasting approximately 146 seconds. It follows the same -chapter sequence as the GIF, with longer holds and page turns for presentations. -The open book and turning pages are rendered natively at 4K -against black, not upscaled from the GIF. Enable repeat/loop in the event's -video player; MP4 does not carry -the GIF's infinite-loop setting. Test playback on the actual event screen -beforehand, since older players may not support 4K H.264. - -With FFmpeg and the chapter generator's Python dependencies installed, regenerate using -[make-book-video.ps1](../tools/make-book-video.ps1): - -```powershell -.\tools\make-book-video.ps1 -``` - -Use `-FfmpegPath` for an encoder outside PATH and `-OutputPath` for a different -destination. The script renders temporary widescreen PNG frames, removes them -after encoding, refuses to overwrite existing output, and decodes the entire -resulting video to check for errors. It does not transcode the square GIF. - -### Camera pullback and open-book spreads - -The primary [chapter GIF](book-chapters.gif) and -[4K video](book-chapters-4k.mp4) begin close to the actual cover against black. -The camera pulls back as -the cover opens, then stays on the open two-page book throughout the chapter -sequence. The first inside left page contains the book's exact dedication to the -Microsoft team who brought Project Socrates to life, the technical-reviewer credit -for Kendal Van Dyke, and thanks to quote contributors Shireesh Thota, Slava Oks, -Krystyna Reisteter, Anna Hoffman, Kendal Van Dyke, Naveen Prakash, and Arun Ulag. -The right page retains the cloud-native database theme, SQL Server and AI -highlights, AI-assisted book examples, and book/code links. The full manuscript is -not included in the repository. There is no separate invitation page. -Each chapter has its title and takeaways at the top and a large illustration -across the spread below. Pages lift toward the viewer and turn right to left -around the center spine; the book never transforms into a full-screen slide. -After Arun's summary and the closing links, the cover closes and the camera -returns to its starting view. - -The video lasts about 146 seconds. The cover opens for 2.4 seconds; each -chapter turn lasts 1.8 seconds. The first inside spread remains fourteen seconds. -Chapter holds remain six seconds and Arun's summary remains fourteen seconds. -The video runs at 25 fps. The square GIF lasts 68.8 seconds, uses approximately -17 fps for motion, loops continuously, and has no audio. It holds the cover for -six seconds and opens it over 2.4 seconds before accelerating to 0.9-second -chapter turns and 2.5-second chapter holds. The final summary holds for seven -seconds. The closing spread holds for four seconds, the cover closes over -1.2 seconds, and the final cover holds for three seconds. Video-export -timings remain unchanged. -All text stays inside the book; the black background has no captions or overlays. -The book URL, `aka.ms/sqlhyperscaleunveiled`, is printed at the bottom of every -inside page, including both sides of each spread. The introduction and closing -spreads also retain the prominent book and code links. The original cover art -is unchanged. - -The 4K video renders the book, typography, diagrams, and rotating sheets from -high-resolution page artwork, rather than switching from low-resolution turns -to high-resolution held pages. The cover uses the high-resolution PNG. -The Unit board uses the original screenshot crop, not the earlier thumbnail; -its detail is still limited by that source image. The shareable GIF is 640 x 640 -with a 96-color palette; the generator validates that it stays below 5,000,000 bytes. - -### Microsoft product icons - -Chapters 2 and 11 use Microsoft's official Azure SQL Database icon, labeled -"Azure SQL Hyperscale" beneath it. The unmodified [source SVG](icons/azure-sql-database.svg) -is `10130-icon-service-SQL-Database.svg` from the official Azure architecture -icons V24 package. Its [transparent 1024-pixel PNG](icons/azure-sql-database.png) -preserves the original colors and proportions and keeps the generator's existing -Python dependencies unchanged. - -These Microsoft icons are used under the +## Book media + +| File | Description | +| --- | --- | +| [Chapter flip-through GIF](book-chapters.gif) | A looping, silent tour of all eleven chapters. 640 x 640, about 69 seconds, under 5 MB. | +| [4K chapter video](book-chapters-4k.mp4) | A silent book flip-through for presentations and event screens. 3840 x 2160, about 2 minutes 26 seconds. | +| [Book cover](book-cover.png) | The high-resolution front cover, including the foreword credit. 2100 x 3000 PNG. | + +Microsoft Azure icons used in the media are Microsoft-owned and subject to the [Azure architecture icon terms](https://learn.microsoft.com/azure/architecture/icons/#icon-terms), -which permit use in architecture diagrams, training materials, and documentation. -They are Microsoft assets, not artwork licensed under this repository's MIT license. +not this repository's MIT license. + +## Chapter figures | Chapter | Figures | | --- | --- | diff --git a/tools/make-book-chapters.py b/tools/make-book-chapters.py deleted file mode 100644 index 5c9d4e0..0000000 --- a/tools/make-book-chapters.py +++ /dev/null @@ -1,1003 +0,0 @@ -from pathlib import Path -import argparse -from functools import lru_cache -import math -import re -import sys - -from PIL import Image, ImageChops, ImageDraw, ImageEnhance, ImageFont, ImageOps - -parser = argparse.ArgumentParser(description="Render the chapter-by-chapter book GIF.") -parser.add_argument("--preview-dir", type=Path, help="Optional directory for preview PNGs.") -parser.add_argument("--landscape", action="store_true", help="Show the physical book on a 16:9 canvas.") -parser.add_argument("--frames-dir", type=Path, help="Export PNG frames and a video timing manifest.") -# Accept the former version flag while making the approved open-book layout the default. -parser.add_argument("--v2", action="store_true", default=True, help=argparse.SUPPRESS) -args = parser.parse_args() -if args.landscape and not args.frames_dir: - parser.error("--landscape requires --frames-dir") -ROOT = Path(__file__).resolve().parents[1] -S = 768 -W = 1366 if args.landscape else S -PAPER = (255, 255, 255) -INK = (22, 39, 56) -MUTED = (80, 99, 112) -GOLD = (255, 198, 28) -TEAL = (0, 118, 126) -BG = (14, 22, 34) -BOOK_URL = "aka.ms/sqlhyperscaleunveiled" -GIF_SIZE = 640 -GIF_COLORS = 96 -GIF_MAX_BYTES = 5_000_000 -SUMMARY_SUBTITLE = "Hyperscale is your database now and for the future." -BOOK_DEDICATION = ( - "This book is dedicated to all at Microsoft who spent countless hours " - "to take Project Socrates from a dream to the reality that is " - "Azure SQL Hyperscale today." -) -QUOTE_CONTRIBUTORS = ( - "Shireesh Thota", "Slava Oks", "Krystyna Reisteter", "Anna Hoffman", - "Kendal Van Dyke", "Naveen Prakash", "Arun Ulag", -) -ARUN_QUOTE = ( - '"Customers can begin with what they need today and expand over time ' - 'without redesigning the application or moving to a different platform. ' - 'That freedom to build once and evolve with confidence makes Hyperscale ' - 'a critical part of Microsoft\u2019s database strategy."' -) - - -def font(size, bold=False, serif=False): - family = ("georgiab.ttf" if bold else "georgia.ttf") if serif else ( - "segoeuib.ttf" if bold else "segoeui.ttf" - ) - return ImageFont.truetype( - str(Path(r"C:\Windows\Fonts") / family), size - ) - - -class ScaledDraw: - """Draw in layout coordinates without raster-upscaling text or shapes.""" - - def __init__(self, image, scale=1): - self.draw = ImageDraw.Draw(image) - self.scale = scale - self.width = image.width / scale - - def coordinates(self, values): - return [self.coordinates(value) if isinstance(value, (tuple, list)) - else value * self.scale for value in values] - - def text(self, xy, value, font, **kwargs): - self.draw.text(self.coordinates(xy), value, - font=font.font_variant(size=round(font.size * self.scale)), **kwargs) - - def textlength(self, value, font): - return self.draw.textlength( - value, font=font.font_variant(size=round(font.size * self.scale)) - ) / self.scale - - def shape(self, name, xy, **kwargs): - if "width" in kwargs: - kwargs["width"] = max(1, round(kwargs["width"] * self.scale)) - getattr(self.draw, name)(self.coordinates(xy), **kwargs) - - def line(self, xy, **kwargs): - self.shape("line", xy, **kwargs) - - def rectangle(self, xy, **kwargs): - self.shape("rectangle", xy, **kwargs) - - def ellipse(self, xy, **kwargs): - self.shape("ellipse", xy, **kwargs) - - def polygon(self, xy, **kwargs): - self.shape("polygon", xy, **kwargs) - - def rounded_rectangle(self, xy, radius, **kwargs): - self.shape("rounded_rectangle", xy, radius=radius * self.scale, **kwargs) - - -def text(d, xy, value, size=24, fill=INK, bold=False, serif=False): - assert xy[0] + d.textlength(value, font=font(size, bold, serif)) <= getattr(d, "width", W) - 28, value - d.text(xy, value, font=font(size, bold, serif), fill=fill) - - -def wrapped(d, xy, value, size, width, fill=INK, bold=False, serif=False): - lines = [] - for word in value.split(): - if not lines or d.textlength(lines[-1] + " " + word, font=font(size, bold, serif)) > width: - lines.append(word) - else: - lines[-1] += " " + word - for i, line in enumerate(lines): - text(d, (xy[0], xy[1] + i * (size + 8)), line, size, fill, bold, serif) - return xy[1] + len(lines) * (size + 8) - - -titles = {} -guide = (ROOT / "README.md").read_text(encoding="utf-8").split("## Chapter guide\n", 1)[1] -guide = guide.split("\n## ", 1)[0] -for line in guide.splitlines(): - match = re.match(r"\| (\d+) \| ([^|]+) \|", line) - if match: - titles[int(match[1])] = match[2].strip() -assert set(titles) == set(range(1, 12)) -assert titles[1] == "Is It Hyper or Scale? Why Not Both?" -assert titles[10] == "Turbocharging and Optimizing Performance in Hyperscale" - -# Short editorial highlights, grounded in the repository's chapter guide. -chapters = [ - ("Honoring Project Socrates.", ["SQL SERVER", "PROJECT SOCRATES", "HYPERSCALE"], - ["Proven engine", "Distributed design", "Cloud-native SQL"], - "How we built a new distributed, cloud-native SQL architecture."), - ("Start small. Think big.", ["DEPLOY", "CONNECT", "BUILD"], - ["Azure SQL", "Passwordless", "Synthetic data"], - "Provision Hyperscale and bring Ward General to life."), - ("Go inside the architecture.", ["COMPUTE", "LOG SERVICE", "PAGE SERVERS"], - ["SQL + cache", "Log distribution", "Data pages"], - "SQL Server engine + RBPEX. HA and named replicas."), - ("Build something real.", ["HYPERSCALE", "BLAZOR", "PATIENT CHART"], - ["SQL procedures", "Application", "Ward General"], - "One hospital. One database. A working application."), - ("Bring AI to your data.", ["EMBEDDINGS", "VECTOR SEARCH", "AI ASSISTANCE"], - ["Meaning", "DiskANN index", "In-engine AI"], - "Vectors + RAG in the SQL engine; governed calls via APIM."), - ("Turn insight into action.", ["SQL MCP TOOLS", "HANDOFF AGENT", "APPROVAL"], - ["Data API Builder", "Prepare handoff", "Gated save"], - "An app-hosted agent uses an LLM and SQL MCP; you approve the save."), - ("See what the engine sees.", ["AZURE MONITOR", "QUERY STORE", "ENGINE EVENTS"], - ["Service metrics", "Plans + runtime", "Extended Events"], - "Database Hub, DMVs, and database agents."), - ("Protect what matters.", ["ROW SECURITY", "AUDITING", "ENCRYPTION"], - ["App identity", "Access evidence", "Optional TDE CMK"], - "Apply security to the same Ward General application."), - ("Prepare for the unexpected.", ["FAILOVER", "RESTORE", "GEO-REPLICA"], - ["App retries", "Point in time", "Across regions"], - "Built-in HA, geo-replication, and point-in-time restore."), - ("Unleash the engine.", ["SCALE COMPUTE", "GROW DATA", "TUNE QUERIES"], - ["8 / 16 / 40 / 192 vCores", "Write workload", "JSON + feedback"], - "Serverless. Elastic pools. Smarter scaling and tuning."), - ("Azure is the best cloud for SQL.", ["AZURE", "MICROSOFT FOUNDRY", "MIGRATE"], - ["Azure App Service", "Change Event Streaming", "Fabric Mirroring"], - "Azure. Microsoft Foundry. Migrate. Azure App Service. " - "Change Event Streaming. Fabric Mirroring."), -] - -with Image.open(ROOT / "figures" / "book-cover.png") as image: - cover = image.convert("RGB") - - -@lru_cache(maxsize=8) -def azure_sql_database_icon(size): - with Image.open(ROOT / "figures" / "icons" / "azure-sql-database.png") as icon: - return icon.convert("RGBA").resize((size, size), Image.Resampling.LANCZOS) - - -def background(scale=1): - im = Image.new("RGB", (round(W * scale), round(S * scale)), PAPER) - d = ScaledDraw(im, scale) - text(d, (44, 24), "AZURE SQL HYPERSCALE UNVEILED", 18, MUTED, serif=True) - d.line((44, 60, W - 44, 60), fill=(217, 222, 224), width=1) - text(d, (44, 730), "BOB WARD / APRESS", 15, MUTED) - text(d, (W - 262, 730), "TODAY + YOUR FUTURE", 15, MUTED) - return im - - -def cover_page(closing=False): - im = background() - d = ImageDraw.Draw(im) - if args.landscape: - im.paste(cover.resize((330, 477), Image.Resampling.LANCZOS), (100, 110)) - text(d, (500, 105), "HYPERSCALE:", 36, TEAL, True, serif=True) - wrapped(d, (500, 166), "The cloud-native database for today and your future.", - 42, 745, INK, True, serif=True) - if closing: - text(d, (500, 317), "Your next chapter starts here.", 32, TEAL, serif=True) - else: - text(d, (500, 317), "Powered by SQL Server.", 32, TEAL, serif=True) - text(d, (500, 367), "Cloud-native scale. AI built in.", 28, INK, serif=True) - text(d, (500, 440), "Architecture. Engine. Applications. AI. Azure.", 27, INK, serif=True) - text(d, (500, 486), "Book examples: AI-assisted creation and guided learning.", 20, MUTED) - text(d, (500, 549), "BOOK", 18, TEAL, True) - text(d, (595, 539), "aka.ms/sqlhyperscaleunveiled", 31, INK, True) - text(d, (500, 610), "CODE", 18, TEAL, True) - text(d, (595, 600), "aka.ms/sqlhyperscalebook", 31, INK, True) - text(d, (595, 646), "github.com/rgward/sqlhyperscalebook", 19, MUTED) - return im - thumb = cover.resize((265, 383), Image.Resampling.LANCZOS) - im.paste(thumb, (70, 112)) - text(d, (366, 114), "HYPERSCALE:", 30, TEAL, True, serif=True) - for i, line in enumerate(["The cloud-native", "database for today", "and your future."]): - text(d, (366, 169 + 42 * i), line, 27, INK, True, serif=True) - if closing: - for i, line in enumerate(["Your next chapter", "starts here."]): - text(d, (366, 354 + i * 40), line, 23, TEAL, serif=True) - else: - wrapped(d, (366, 342), "Powered by SQL Server.", - 23, 326, TEAL, True, serif=True) - wrapped(d, (366, 418), "Cloud-native scale. AI built in.", - 21, 326, INK, serif=True) - text(d, (70, 506), "Architecture. Engine. Applications. AI. Azure.", 24, INK, serif=True) - text(d, (70, 536), "Book examples: AI-assisted creation and guided learning.", 17, MUTED) - d.rounded_rectangle((70, 563, 700, 613), 10, fill=GOLD) - text(d, (85, 575), "BOOK", 18, INK, True) - text(d, (176, 568), "aka.ms/sqlhyperscaleunveiled", 28, INK, True) - d.rounded_rectangle((70, 622, 700, 684), 10, outline=(207, 218, 220), width=1) - text(d, (85, 634), "CODE", 18, TEAL, True) - text(d, (176, 623), "aka.ms/sqlhyperscalebook", 26, INK, True) - text(d, (176, 660), "github.com/rgward/sqlhyperscalebook", 16, MUTED) - return im - - -ACCENTS = [ - (0, 118, 126), (0, 100, 170), (142, 78, 23), (0, 120, 142), - (112, 66, 161), (67, 84, 163), (0, 110, 100), (41, 111, 65), - (36, 96, 160), (166, 75, 25), (0, 112, 183), -] - - -def architecture_art(accent, scale=1): - pale = tuple(round(c * 0.10 + 255 * 0.90) for c in accent) - im = Image.new("RGB", (1200 * scale, 360 * scale), PAPER) - d = ScaledDraw(im, scale) - d.rounded_rectangle((0, 0, 1199, 359), 18, fill=pale) - log_color = (0, 117, 103) - read_color = (0, 111, 173) - - def arrow(points, color): - d.line(points, fill=color, width=4) - end, previous = points[-1], points[-2] - angle = math.atan2(end[1] - previous[1], end[0] - previous[0]) - d.polygon([end] + [ - (end[0] - 12 * math.cos(angle + a), end[1] - 12 * math.sin(angle + a)) - for a in (-0.5, 0.5) - ], fill=color) - - for bounds, label in [ - ((40, 10, 310, 65), "PRIMARY"), - ((460, 10, 730, 65), "HA REPLICAS"), - ((880, 10, 1150, 65), "NAMED REPLICAS"), - ((40, 150, 310, 210), "LOG SERVICE"), - ((700, 150, 970, 210), "PAGE SERVERS"), - ((440, 280, 750, 335), "AZURE STORAGE"), - ]: - d.rounded_rectangle(bounds, 10, fill=accent) - d.text(((bounds[0] + bounds[2]) / 2, (bounds[1] + bounds[3]) / 2), - label, font=font(25, True), fill=PAPER, anchor="mm") - - # Arrows follow data: log and page reads point toward their consumers. - arrow([(100, 65), (100, 150)], accent) - d.text((116, 100), "Write log", font=font(19), fill=accent) - d.line([(835, 150), (835, 80), (175, 80)], fill=read_color, width=4) - d.line([(835, 80), (1100, 80)], fill=read_color, width=4) - for x in (175, 650, 1100): - arrow([(x, 80), (x, 65)], read_color) - - d.line([(240, 150), (240, 125), (1000, 125)], fill=log_color, width=4) - # White gaps distinguish crossings from shared read-lane junctions. - d.line([(835, 118), (835, 132)], fill=pale, width=10) - d.line([(835, 118), (835, 132)], fill=read_color, width=4) - for x in (550, 1000): - d.line([(x, 73), (x, 87)], fill=pale, width=10) - arrow([(x, 125), (x, 65)], log_color) - arrow([(310, 180), (700, 180)], log_color) - d.text((410, 149), "Read log", font=font(20), fill=log_color) - arrow([(175, 210), (175, 305), (440, 305)], accent) - arrow([(835, 210), (835, 305), (750, 305)], accent) - for x, label, color in [ - (190, "Brown: writes", accent), - (595, "Teal: log reads", log_color), - (1000, "Blue: page reads", read_color), - ]: - d.text((x, 346), label, font=font(19, True), fill=color, anchor="mm") - return im - - -def chapter_art(im, number, accent, scale=1): - d = ScaledDraw(im, scale) - pale = tuple(round(c * 0.10 + 255 * 0.90) for c in accent) - d.rounded_rectangle((80, 342, 690, 512), 12, fill=pale) - - def centered(x, y, value, size=18, color=INK, bold=True): - width = d.textlength(value, font=font(size, bold)) - text(d, (x - width / 2, y), value, size, color, bold) - - def box(bounds, value, size=17, color=None): - color = color or accent - d.rounded_rectangle(bounds, 8, fill=color) - centered((bounds[0] + bounds[2]) / 2, (bounds[1] + bounds[3]) / 2 - size * 0.7, - value, size, PAPER) - - def arrow(points, color=None): - color = color or accent - d.line(points, fill=color, width=3) - end, previous = points[-1], points[-2] - angle = math.atan2(end[1] - previous[1], end[0] - previous[0]) - d.polygon([end] + [ - (end[0] - 9 * math.cos(angle + a), end[1] - 9 * math.sin(angle + a)) - for a in (-0.5, 0.5) - ], fill=color) - - def sql_icon(bounds): - side = round(min(bounds[2] - bounds[0], bounds[3] - bounds[1]) * scale) - icon = azure_sql_database_icon(side) - position = (round((bounds[0] + bounds[2]) * scale / 2 - side / 2), - round((bounds[1] + bounds[3]) * scale / 2 - side / 2)) - im.paste(icon, position, icon) - - if number == 1: - d.line((125, 404, 642, 404), fill=accent, width=4) - for x, top, bottom in [(170, "SQL SERVER", "Proven engine"), - (385, "PROJECT SOCRATES", "Distributed design"), - (600, "HYPERSCALE", "Cloud-native SQL")]: - d.ellipse((x - 12, 392, x + 12, 416), fill=accent) - centered(x, 361, top, 17, accent) - centered(x, 440, bottom, 17, INK, False) - centered(385, 478, "Research. Engineering. A new architecture.", 17, accent) - elif number == 2: - d.rounded_rectangle((100, 356, 472, 496), 8, fill=INK) - for i, value in enumerate(["> GitHub Copilot + skills", - "> Provision Hyperscale", "> Connect with Entra ID", - "> Build schema + seed data"]): - text(d, (116, 363 + 31 * i), value, 19, (125, 235, 210)) - sql_icon((535, 360, 660, 478)) - centered(598, 484, "Azure SQL Hyperscale", 15, accent) - arrow([(480, 424), (523, 424)]) - elif number == 3: - im.paste(architecture_art(accent, scale).resize((610 * scale, 180 * scale), - Image.Resampling.LANCZOS), (80 * scale, 342 * scale)) - elif number == 4: - with Image.open(ROOT / "figures" / "ch04-first-app" / - "ch04-fig-01-the-digital-chart-application.png") as screenshot: - # Focus on the Unit board rather than spending thumbnail space on navigation. - left = round(screenshot.width * 0.17) - top = round(screenshot.height * 0.07) - right = round(screenshot.width * 0.99) - bottom = top + round((right - left) * 154 / 390) - preview = screenshot.crop((left, top, right, bottom)) - preview.thumbnail((390 * scale, 154 * scale), Image.Resampling.LANCZOS) - im.paste(preview, (92 * scale, 350 * scale)) - box((480, 366, 674, 411), "BLAZOR + SQL") - text(d, (493, 430), "Ward General", 22, accent, True) - text(d, (493, 463), "The working app", 17, MUTED) - elif number == 5: - d.rounded_rectangle((97, 350, 524, 503), 10, outline=accent, width=2) - text(d, (112, 357), "INSIDE THE SQL ENGINE", 18, accent, True) - box((112, 391, 283, 431), "VECTORS + DiskANN", 15) - box((323, 391, 507, 431), "RETRIEVE + PROMPT", 15) - arrow([(283, 411), (323, 411)]) - box((550, 391, 676, 431), "APIM", 18) - arrow([(507, 411), (550, 411)]) - box((550, 456, 676, 493), "FOUNDRY", 16) - arrow([(613, 431), (613, 456)]) - arrow([(613, 456), (613, 431)]) - arrow([(550, 411), (507, 411)]) - box((344, 456, 507, 493), "AI ASSISTANCE", 15) - arrow([(415, 431), (415, 456)]) - embedding_color = (0, 111, 173) - embedding_path = [(198, 431), (198, 448), (535, 448), (535, 487), (550, 487)] - arrow(embedding_path, embedding_color) - arrow(list(reversed(embedding_path)), embedding_color) - text(d, (211, 432), "Embeddings: direct", 12, embedding_color, True) - text(d, (112, 461), "In-engine RAG", 19, accent, True) - text(d, (112, 485), "Models stay external", 14, MUTED) - centered(613, 358, "AI GATEWAY", 15, accent) - elif number == 6: - d.rounded_rectangle((92, 349, 366, 497), 8, outline=accent, width=2) - centered(229, 355, "WARD GENERAL APP", 17, accent) - box((106, 386, 351, 421), "HANDOFF AGENT", 17) - box((405, 350, 515, 385), "LLM", 19) - arrow([(351, 398), (383, 398), (383, 362), (405, 362)]) - arrow([(405, 375), (394, 375), (394, 411), (351, 411)]) - centered(229, 443, "LLM-led orchestration", 16, accent) - centered(229, 473, "SQL MCP tool execution", 15, MUTED, False) - box((405, 410, 515, 450), "SQL MCP", 17) - box((557, 410, 677, 450), "HYPERSCALE", 14) - arrow([(366, 431), (405, 431)]) - arrow([(405, 443), (366, 443)]) - arrow([(515, 424), (557, 424)]) - arrow([(557, 438), (515, 438)]) - arrow([(366, 486), (394, 486)]) - box((394, 477, 535, 510), "HUMAN APPROVAL", 12) - arrow([(535, 493), (557, 493)]) - box((557, 477, 677, 510), "SAVE HANDOFF", 12) - elif number == 7: - d.line((110, 363, 110, 478, 430, 478), fill=MUTED, width=2) - d.line([(110, 463), (150, 449), (182, 456), (219, 387), (244, 420), - (278, 371), (314, 433), (355, 414), (395, 453), (430, 445)], fill=accent, width=4) - for i, label in enumerate([ - "DATABASE HUB", "AZURE MONITOR", "DMVs", "QUERY STORE", - "ENGINE EVENTS", "DATABASE AGENTS", - ]): - y = 350 + 27 * i - box((457, y, 674, y + 24), label, 14) - elif number == 8: - d.polygon([(192, 355), (264, 377), (257, 444), (227, 478), - (192, 499), (157, 478), (127, 444), (120, 377)], fill=accent) - d.line((157, 424, 183, 451, 228, 398), fill=PAPER, width=9) - for i, label in enumerate(["ROW-LEVEL SECURITY", "AUDIT ACCESS", "ENCRYPT DATA"]): - y = 357 + i * 50 - d.ellipse((318, y + 8, 337, y + 27), fill=accent) - text(d, (355, y + 2), label, 21, accent, True) - elif number == 9: - for x in (283, 486): - d.line((x, 361, x, 491), fill=(187, 205, 222), width=1) - centered(182, 355, "BUILT-IN HA", 18, accent) - box((99, 396, 178, 431), "PRIMARY", 13) - box((207, 396, 264, 431), "HA", 16) - arrow([(178, 413), (207, 413)]) - centered(182, 451, "Primary + HA replicas", 15, accent, False) - centered(182, 479, "Failover + app retries", 14, MUTED, False) - centered(385, 355, "GEO-REPLICATION", 17, accent) - for x in (329, 439): - d.ellipse((x - 24, 389, x + 24, 437), outline=accent, width=2) - d.ellipse((x - 12, 389, x + 12, 437), outline=accent, width=1) - d.line((x - 24, 413, x + 24, 413), fill=accent, width=1) - arrow([(358, 413), (410, 413)]) - centered(385, 451, "Across regions", 15, accent, False) - centered(385, 479, "Geo-secondary", 14, MUTED, False) - centered(588, 355, "PITR", 18, accent) - d.ellipse((557, 382, 619, 444), outline=accent, width=3) - d.line((588, 392, 588, 413, 571, 422), fill=accent, width=3) - for angle in range(0, 360, 90): - radians = math.radians(angle) - x, y = 588 + 25 * math.cos(radians), 413 + 25 * math.sin(radians) - d.ellipse((x - 2, y - 2, x + 2, y + 2), fill=accent) - centered(588, 451, "Point-in-time restore", 15, accent, False) - centered(588, 479, "Recover from a mistake quickly", 14, MUTED, False) - elif number == 10: - for x, height, label in [(105, 24, "8"), (173, 48, "16"), - (241, 74, "40"), (309, 115, "192")]: - d.rounded_rectangle((x, 480 - height, x + 48, 480), 5, fill=accent) - centered(x + 24, 483, label, 16, accent) - centered(232, 354, "vCORE SCALING", 16, accent) - for i, label in enumerate([ - "SERVERLESS", "ELASTIC POOLS", "OPTIMIZED LOCKING", - "AUTOMATIC INDEX COMPACTION", "JSON INDEXES", "MEMORY GRANT FEEDBACK", - ]): - y = 350 + i * 27 - box((390, y, 675, y + 24), label, 14) - elif number == 11: - center = (385, 425) - for bounds, label in [((90, 352, 300, 394), "AZURE"), - ((470, 352, 680, 394), "MICROSOFT FOUNDRY"), - ((90, 405, 300, 447), "MIGRATE"), - ((470, 405, 680, 447), "AZURE APP SERVICE"), - ((90, 458, 300, 500), "CHANGE EVENT STREAMING"), - ((470, 458, 680, 500), "FABRIC MIRRORING")]: - mid = ((bounds[0] + bounds[2]) / 2, (bounds[1] + bounds[3]) / 2) - d.line((center, mid), fill=accent, width=3) - box(bounds, label, 14) - d.rectangle((302, 450, 468, 478), fill=pale) - sql_icon((335, 350, 435, 450)) - centered(385, 456, "AZURE SQL HYPERSCALE", 13, accent) - - -def chapter_illustration(number, accent, target_width): - if number == 3: - return architecture_art(accent, max(1, math.ceil(target_width / 1200))) - scale = max(1, math.ceil(target_width / 610)) - artwork = Image.new("RGB", (S * scale, S * scale), PAPER) - chapter_art(artwork, number, accent, scale) - return artwork.crop(tuple(value * scale for value in (80, 342, 690, 522))) - - -def chapter_page(number, scale=1): - im = background(scale) - d = ScaledDraw(im, scale) - accent = ACCENTS[number - 1] - if args.v2: - margin = 85 if args.landscape else 44 - available = W - margin * 2 - text(d, (margin, 92), f"CHAPTER {number:02d}", 22, accent, True) - end = wrapped(d, (margin, 133), titles[number], - 36 if args.landscape else 30, available, - bold=True, serif=True) - assert end <= 250, (number, end) - hero, _, _, takeaway = chapters[number - 1] - end = wrapped(d, (margin, 225 if args.landscape else 253), hero, - 27, available, accent, serif=True) - assert end <= 323, (number, end) - artwork = chapter_illustration(number, accent, available * scale) - height = round(available * artwork.height / artwork.width) - top = 280 if args.landscape else 345 - im.paste(artwork.resize((round(available * scale), round(height * scale)), - Image.Resampling.LANCZOS), (round(margin * scale), round(top * scale))) - end = wrapped(d, (margin, top + height + 23), takeaway, - 24, available, serif=True) - assert end <= 696, (number, end) - d.line((margin, 705, W - margin, 705), fill=(217, 222, 224)) - text(d, (W - margin - 90, 709), f"{number:02d} / 11", 16, accent) - im.info["chapter_number"] = number - return im - if args.landscape: - text(d, (85, 93), f"CHAPTER {number:02d}", 22, accent, True) - end = wrapped(d, (85, 138), titles[number], 40, W - 170, bold=True, serif=True) - assert end <= 250, (number, end) - hero, labels, details, takeaway = chapters[number - 1] - text(d, (85, 265), hero, 30, accent, serif=True) - artwork = Image.new("RGB", (S, S), PAPER) - chapter_art(artwork, number, accent) - im.paste(artwork.crop((80, 342, 690, 522)).resize((780, 230), - Image.Resampling.LANCZOS), (85, 345)) - end = wrapped(d, (925, 345), takeaway, 28, 350, serif=True) - assert end < 650, (number, end) - d.line((85, 665, W - 85, 665), fill=(217, 222, 224), width=1) - text(d, (85, 680), "AZURE SQL HYPERSCALE UNVEILED", 16, MUTED, serif=True) - text(d, (W - 173, 680), f"{number:02d} / 11", 18, accent) - return im - d.rectangle((80, 108, 147, 113), fill=accent) - text(d, (164, 96), f"CHAPTER {number:02d}", 21, accent, True) - end = wrapped(d, (80, 143), titles[number], 32, 602, bold=True, serif=True) - assert end <= 280, (number, end) - hero, labels, details, takeaway = chapters[number - 1] - wrapped(d, (80, 286), hero, 26, 602, accent, serif=True) - chapter_art(im, number, accent) - wrapped(d, (80, 535), takeaway, 24, 602, serif=True) - d.line((80, 646, 690, 646), fill=(200, 204, 199), width=1) - text(d, (80, 660), "AZURE SQL HYPERSCALE UNVEILED", 15, MUTED, True) - text(d, (603, 657), f"{number:02d} / 11", 18, TEAL, True) - return im - - -def turn(old, new, progress): - im = new.copy() - im.info.pop("chapter_number", None) - page_width, page_height = old.size - if args.v2 and args.landscape and old.size == (W, S): - im.info["page_turn"] = (old, new, progress) - unit = page_height / S if page_width >= W else 1 - # A slanted, bowed fold exposes the next page; the translucent reverse - # retains a hint of mirrored ink instead of becoming an opaque wipe. - bend = math.sin(math.pi * progress) - edge = page_width * (0.5 + 0.5 * math.cos(math.pi * progress)) - reverse = ImageOps.mirror(old) - reverse = Image.blend(reverse, Image.new("RGB", old.size, PAPER), 0.92) - for y in range(page_height): - fraction = y / page_height - bow = math.sin(math.pi * fraction) - left = round(max(0, min(page_width, edge + bend * unit * (46 * (1 - 2 * fraction) - 28 * bow)))) - width = max(1, round(bend * (110 + 44 * bow) * page_width / page_height * unit)) - right = min(page_width, left + width) - im.paste(old.crop((0, y, left, y + 1)), (0, y)) - if right <= left: - continue - # The shadow falls on the exposed page, following the paper's curve. - shadow_end = min(page_width, right + round(25 * bend * unit)) - if shadow_end > right: - shadow = im.crop((right, y, shadow_end, y + 1)) - shade = Image.new("RGB", shadow.size) - sd = ImageDraw.Draw(shade) - for x in range(shadow.width): - value = round(160 + 95 * x / shadow.width) - sd.point((x, 0), fill=(value, value, value)) - im.paste(ImageChops.multiply(shadow, shade), (right, y)) - strip = reverse.crop((max(0, left - round(150 * unit)), y, max(1, left), y + 1)) - strip = strip.resize((right - left, 1), Image.Resampling.BICUBIC) - light = Image.new("RGB", strip.size) - ld = ImageDraw.Draw(light) - for x in range(strip.width): - position = x / max(1, strip.width - 1) - value = round(192 + 63 * math.sin(math.pi * position) ** 0.7) - ld.point((x, 0), fill=(value, value, value)) - im.paste(ImageChops.multiply(strip, light), (left, y)) - return im - - -def summary_page(): - im = background() - d = ImageDraw.Draw(im) - d.rectangle((80, 108, 147, 113), fill=TEAL) - text(d, (164, 96), "THE STORY CONTINUES", 21, TEAL, True) - wrapped(d, (80, 143), "Build once. Evolve with confidence.", 36, 590, - bold=True, serif=True) - end = wrapped(d, (80, 254), SUMMARY_SUBTITLE, 23, 602, TEAL, serif=True) - assert end <= 330, end - text(d, (80, 337), "FROM CHAPTER 11 / QUOTE EXCERPT", 16, MUTED, True) - quote = ARUN_QUOTE - if args.landscape: - im = background() - d = ImageDraw.Draw(im) - text(d, (90, 100), "THE STORY CONTINUES", 22, TEAL, True) - text(d, (90, 158), "Build once. Evolve with confidence.", 44, INK, True, serif=True) - text(d, (90, 237), SUMMARY_SUBTITLE, 29, TEAL, serif=True) - text(d, (110, 310), "FROM CHAPTER 11 / QUOTE EXCERPT", 17, MUTED, True) - end = wrapped(d, (110, 355), quote, 31, 1130, serif=True) - assert end <= 610, end - d.line((88, 360, 88, end - 5), fill=TEAL, width=3) - text(d, (110, 631), "Arun Ulag", 26, INK, True, serif=True) - text(d, (110, 671), "Vice President of Azure", 20, MUTED) - return im - end = wrapped(d, (96, 371), quote, 23, 574, serif=True) - assert end <= 588, end - d.line((80, 376, 80, end - 6), fill=TEAL, width=3) - text(d, (96, 594), "Arun Ulag", 22, INK, True, serif=True) - text(d, (96, 625), "Vice President of Azure", 17, MUTED) - d.line((80, 657, 690, 657), fill=(200, 204, 199), width=1) - text(d, (80, 668), "YOUR DATABASE NOW AND FOR THE FUTURE", 15, TEAL, True) - return im - - -def book_sheet(number=None, closing=False): - """Typeset one portrait sheet at the front cover's proportions.""" - page = Image.new("RGB", (540, 780), PAPER) - d = ImageDraw.Draw(page) - text(d, (40, 28), "AZURE SQL HYPERSCALE UNVEILED", 14, MUTED, serif=True) - d.line((40, 58, 500, 58), fill=(218, 220, 218)) - if number is not None: - accent = ACCENTS[number - 1] - text(d, (40, 83), f"CHAPTER {number:02d}", 18, accent, True) - end = wrapped(d, (40, 125), titles[number], 27, 460, bold=True, serif=True) - assert end <= 265, (number, end) - hero, _, _, takeaway = chapters[number - 1] - end = wrapped(d, (40, 281), hero, 23, 460, accent, serif=True) - assert end <= 345, (number, end) - artwork = Image.new("RGB", (768, 768), PAPER) - chapter_art(artwork, number, accent) - page.paste(artwork.crop((80, 342, 690, 522)).resize((460, 136), - Image.Resampling.LANCZOS), (40, 365)) - wrapped(d, (40, 537), takeaway, 23, 460, serif=True) - text(d, (439, 732), f"{number:02d} / 11", 16, accent) - elif closing: - wrapped(d, (40, 105), "Your next chapter starts here.", 37, 455, - TEAL, True, serif=True) - wrapped(d, (40, 264), "Powered by SQL Server. Cloud-native scale. AI built in.", - 28, 450, serif=True) - text(d, (40, 444), "BOOK", 17, TEAL, True) - text(d, (40, 480), "aka.ms/sqlhyperscaleunveiled", 25, INK, True) - text(d, (40, 550), "CODE", 17, TEAL, True) - text(d, (40, 586), "aka.ms/sqlhyperscalebook", 25, INK, True) - text(d, (40, 632), "github.com/rgward/sqlhyperscalebook", 17, MUTED) - else: - wrapped(d, (40, 94), "Build once. Evolve with confidence.", 33, 460, - bold=True, serif=True) - end = wrapped(d, (40, 233), SUMMARY_SUBTITLE, 23, 460, TEAL, serif=True) - assert end <= 342, end - text(d, (40, 352), "CHAPTER 11 / QUOTE EXCERPT", 15, MUTED, True) - quote = ARUN_QUOTE - end = wrapped(d, (40, 389), quote, 22, 460, serif=True) - assert end <= 660, end - text(d, (40, 675), "Arun Ulag", 23, INK, True, serif=True) - text(d, (40, 711), "Vice President of Azure", 17, MUTED) - if number is not None: - d.line((40, 714, 500, 714), fill=(218, 220, 218)) - text(d, (40, 735), "Bob Ward", 15, MUTED, serif=True) - return page - - -def opening_sheet(): - page = book_sheet(closing=True) - d = ImageDraw.Draw(page) - d.rectangle((0, 80, 540, 425), fill=PAPER) - text(d, (40, 96), "HYPERSCALE:", 30, TEAL, True, serif=True) - wrapped(d, (40, 154), "The cloud-native database for today and your future.", - 32, 460, bold=True, serif=True) - text(d, (40, 320), "Powered by SQL Server.", 25, TEAL, serif=True) - text(d, (40, 365), "Cloud-native scale. AI built in.", 23, INK, serif=True) - text(d, (40, 414), "Book examples: AI-assisted creation and learning.", 16, MUTED) - return page - - -def mount_book(sheet, turned): - stage = Image.new("RGB", (W, S), (0, 0, 0) if args.v2 else (240, 238, 233)) - d = ImageDraw.Draw(stage) - left = (W - 470) // 2 + 12 - top = 40 - # Unread sheets sit on the right; a smaller turned stack collects at the spine. - stack = 2 + min(turned, 14) - unread = max(3, 28 - min(turned, 14) * 2) - if not args.v2: - d.rounded_rectangle((left - stack - 13, top - 6, left + 476 + unread, top + 691), - 5, fill=(197, 191, 181)) - d.rectangle((left - stack - 11, top - 4, left + 474 + unread, top + 686), fill=(65, 48, 36)) - for edge in range(stack, 0, -2): - x = left - edge - d.rectangle((x, top + edge // 3, left + 471, top + 682), - fill=(254, 252, 246), outline=(202, 198, 188)) - d.rectangle((left - stack - 7, top, left - stack - 3, top + 680), fill=(102, 77, 53)) - for edge in range(unread, 0, -2): - d.rectangle((left, top + edge // 3, left + 470 + edge, top + 680 + edge // 3), - fill=(254, 252, 246), outline=(193, 188, 177)) - stage.paste(sheet.resize((470, 680), Image.Resampling.LANCZOS), (left, top)) - for x in range(6): - shade = 215 + x * 7 - d.line((left + x, top, left + x, top + 679), fill=(shade, shade, shade - 2)) - return stage - - -class OpenBook: - def __init__(self, size, detail=1): - self.size = size - self.detail = detail - - @lru_cache(maxsize=1) - def front_cover(self): - return cover.resize((540 * self.detail, 780 * self.detail), - Image.Resampling.LANCZOS) - - @lru_cache(maxsize=4) - def spread(self, kind): - scale = self.detail - page = Image.new("RGB", (1080 * scale, 780 * scale), PAPER) - d = ScaledDraw(page, scale) - if isinstance(kind, int): - accent = ACCENTS[kind - 1] - text(d, (40, 35), f"CHAPTER {kind:02d}", 20, accent, True) - end = wrapped(d, (40, 95), titles[kind], 30, 455, - INK, True, serif=True) - assert end <= 290, (kind, end) - hero, _, _, takeaway = chapters[kind - 1] - end = wrapped(d, (585, 95), hero, 27, 450, accent, serif=True) - end = wrapped(d, (585, end + 25), takeaway, 23, 450, serif=True) - assert end <= 300, (kind, end) - artwork = chapter_illustration(kind, accent, 1000 * scale) - height = round(1000 * artwork.height / artwork.width) - page.paste(artwork.resize((1000 * scale, height * scale), - Image.Resampling.LANCZOS), (40 * scale, 320 * scale)) - text(d, (40, 726), "AZURE SQL HYPERSCALE UNVEILED", 15, TEAL, serif=True) - text(d, (960, 726), f"{kind:02d} / 11", 17, accent) - elif kind == "summary": - text(d, (40, 45), "THE STORY CONTINUES", 20, TEAL, True) - end = wrapped(d, (40, 110), "Build once. Evolve with confidence.", - 34, 450, INK, True, serif=True) - assert end <= 250, end - end = wrapped(d, (40, 280), SUMMARY_SUBTITLE, 27, 450, TEAL, serif=True) - assert end <= 490, end - text(d, (585, 50), "CHAPTER 11 / QUOTE EXCERPT", 16, MUTED, True) - end = wrapped(d, (585, 110), ARUN_QUOTE, 23, 450, serif=True) - assert end <= 630, end - text(d, (585, 670), "Arun Ulag", 25, INK, True, serif=True) - text(d, (585, 714), "Vice President of Azure", 19, MUTED) - else: - assert kind in ("intro", "closing"), kind - text(d, (40, 30), "AZURE SQL HYPERSCALE UNVEILED", 15, MUTED, serif=True) - if kind == "intro": - text(d, (40, 85), "DEDICATION", 18, TEAL, True) - end = wrapped(d, (40, 124), BOOK_DEDICATION, 23, 450, serif=True) - assert end <= 330, end - text(d, (40, 350), "TECHNICAL REVIEWER", 16, MUTED, True) - text(d, (40, 384), "Kendal Van Dyke", 26, INK, True, serif=True) - text(d, (40, 458), "QUOTE CONTRIBUTORS", 16, MUTED, True) - for index, name in enumerate(QUOTE_CONTRIBUTORS): - end = wrapped(d, (40, 495 + index * 29), name, 21, 450, serif=True) - assert end <= 730, (name, end) - else: - page.paste(self.front_cover().resize((330 * scale, 477 * scale), - Image.Resampling.LANCZOS), (100 * scale, 110 * scale)) - text(d, (585, 70), "HYPERSCALE:", 30, TEAL, True, serif=True) - end = wrapped(d, (585, 125), - "The cloud-native database for today and your future.", - 31, 450, INK, True, serif=True) - assert end <= 310, end - if kind == "closing": - end = wrapped(d, (585, 335), "Your next chapter starts here.", - 27, 450, TEAL, serif=True) - assert end <= 420, end - else: - text(d, (585, 335), "Powered by SQL Server.", 25, TEAL, serif=True) - text(d, (585, 380), "Cloud-native scale. AI built in.", 23, INK, serif=True) - end = wrapped(d, (585, 435), "Architecture. Engine. Applications. AI. Azure.", - 20, 450, serif=True) - assert end <= 495, end - end = wrapped(d, (585, 502), "Book examples: AI-assisted creation and guided learning.", - 17, 450, MUTED) - assert end <= 560, end - text(d, (585, 575), "BOOK", 16, TEAL, True) - text(d, (585, 606), BOOK_URL, 23, INK, True) - text(d, (585, 658), "CODE", 16, TEAL, True) - text(d, (585, 689), "aka.ms/sqlhyperscalebook", 23, INK, True) - text(d, (585, 720), "github.com/rgward/sqlhyperscalebook", 14, MUTED) - footer_width = d.textlength(BOOK_URL, font=font(16)) - for left in (0, 540): - text(d, (left + (540 - footer_width) / 2, 750), BOOK_URL, 16, TEAL) - return (page.crop((0, 0, 540 * scale, 780 * scale)), - page.crop((540 * scale, 0, 1080 * scale, 780 * scale))) - - def base(self, left, right): - scale = self.detail - world = Image.new("RGB", (2400 * scale, 1400 * scale), (0, 0, 0)) - d = ScaledDraw(world, scale) - for edge in (10, 7, 4): - d.rectangle((1200, 310 + edge // 2, 1740 + edge, 1090 + edge // 2), - fill=(243, 241, 235), outline=(172, 169, 160), width=1) - if left is not None: - d.rectangle((660 - edge, 310 + edge // 2, 1200, 1090 + edge // 2), - fill=(243, 241, 235), outline=(172, 169, 160), width=1) - if left is not None: - world.paste(left, (660 * scale, 310 * scale)) - world.paste(right, (1200 * scale, 310 * scale)) - return world - - def gutter(self, world): - scale = self.detail - for offset in range(-5, 6): - shade = 203 + abs(offset) * 10 - bounds = ((1200 + offset) * scale, 310 * scale, - (1201 + offset) * scale, 1090 * scale) - band = world.crop(bounds) - tint = Image.new("RGB", band.size, (shade, shade, shade)) - world.paste(ImageChops.multiply(band, tint), bounds[:2]) - - def leaf(self, world, front, back, progress): - scale = self.detail - cosine, sine = math.cos(math.pi * progress), math.sin(math.pi * progress) - if abs(cosine) < 0.002: - ScaledDraw(world, scale).line((1200, 310, 1200, 1090), - fill=(203, 201, 194), width=2) - return - texture = front if cosine > 0 else ImageOps.mirror(back) - texture = ImageEnhance.Brightness(texture).enhance(0.79 + 0.21 * abs(cosine)) - for x in range(540 * scale): - fraction = (x + 0.5) / scale - perspective = 1 / (1 - fraction * sine / 4000) - a = round((1200 + (x / scale) * cosine / - (1 - (x / scale) * sine / 4000)) * scale) - b = round((1200 + ((x + 1) / scale) * cosine / - (1 - ((x + 1) / scale) * sine / 4000)) * scale) - if a == b: - continue - height = round(780 * scale * perspective) - strip = texture.crop((x, 0, x + 1, 780 * scale)).resize( - (abs(b - a), height), Image.Resampling.BICUBIC) - world.paste(strip, (min(a, b), round(700 * scale - height / 2))) - perspective = 1 / (1 - 540 * sine / 4000) - edge_x = 1200 + 540 * cosine * perspective - ScaledDraw(world, scale).line( - [(1200, 310), (edge_x, 700 - 390 * perspective), - (edge_x, 700 + 390 * perspective), (1200, 1090)], - fill=(185, 181, 170), width=1) - - def render(self, descriptor): - mode, kind, previous, progress = descriptor - left, right = self.spread(kind) - if mode == "cover": - world = self.base(left if progress == 1 else None, right) - self.leaf(world, self.front_cover(), left, progress) - if progress == 1: - self.gutter(world) - camera = progress - elif mode == "turn": - old_left, old_right = self.spread(previous) - world = self.base(old_left, right) - self.leaf(world, old_right, left, progress) - self.gutter(world) - camera = 1 - else: - assert mode == "spread", mode - world = self.base(left, right) - self.gutter(world) - camera = 1 - width, height = self.size - view_height = (820 + (max(900, 1160 * height / width) - 820) * camera - + 45 * math.sin(math.pi * camera)) - view_width = view_height * width / height - center_x = 1200 + 270 * (1 - camera) - box = tuple(value * self.detail for value in ( - center_x - view_width / 2, 700 - view_height / 2, - center_x + view_width / 2, 700 + view_height / 2)) - frame = world.resize(self.size, Image.Resampling.LANCZOS, box=box) - return frame - - -def v2_sequence(): - renderer = OpenBook((W, S)) - result, timing = [], [] - gif_output = args.frames_dir is None - - def append(descriptor, delay): - frame = (Image.new("RGB", (1, 1)) if args.frames_dir - else renderer.render(descriptor)) - frame.info["open_book"] = descriptor - result.append(frame) - timing.append(delay) - - delay = 40 if args.landscape else 60 - cover_steps = 60 if args.landscape else 40 - turn_steps = 15 if gif_output else (45 if args.landscape else 30) - closing_steps = 20 if gif_output else cover_steps - append(("cover", "intro", None, 0), 6000 if gif_output else 12000) - for step in range(1, cover_steps + 1): - t = step / cover_steps - t = t * t * (3 - 2 * t) - append(("cover", "intro", None, t), delay) - append(("spread", "intro", None, 1), 6000 if gif_output else 14000) - previous = "intro" - for kind in list(range(1, 12)) + ["summary", "closing"]: - for step in range(1, turn_steps + 1): - t = step / turn_steps - t = t * t * (3 - 2 * t) - append(("turn", kind, previous, t), delay) - if gif_output: - hold = 7000 if kind == "summary" else 4000 if kind == "closing" else 2500 - else: - hold = 14000 if kind == "summary" else 6000 - append(("spread", kind, None, 1), hold) - previous = kind - for step in range(1, closing_steps + 1): - t = step / closing_steps - t = t * t * (3 - 2 * t) - append(("cover", "closing", None, 1 - t), delay) - append(("cover", "closing", None, 0), 3000 if gif_output else 6000) - pages = [renderer.render(("cover", "intro", None, 0))] - pages += [renderer.render(("spread", kind, None, 1)) - for kind in ["intro"] + list(range(1, 12)) + ["summary", "closing"]] - return result, timing, pages - - -if args.v2: - frames, delays, pages = v2_sequence() -else: - sheets = ([cover.resize((540, 780), Image.Resampling.LANCZOS), opening_sheet()] - + [book_sheet(i) for i in range(1, 12)] - + [book_sheet(), book_sheet(closing=True)]) - pages = [mount_book(sheet, i) for i, sheet in enumerate(sheets)] - frames = [pages[0]] - delays = [12000] - for index in range(1, len(pages)): - for step in range(1, 13): - frames.append(mount_book(turn(sheets[index - 1], sheets[index], step / 13), index)) - delays.append(80) - frames.append(pages[index]) - delays.append(14000 if index == len(pages) - 2 else 6000) -if args.preview_dir: - args.preview_dir.mkdir(parents=True, exist_ok=True) - thumb_width = 512 if args.landscape else 384 - thumb_height = round(S * thumb_width / W) - sheet = Image.new("RGB", (thumb_width * 4, thumb_height * 4), PAPER) - for i, page in enumerate(pages): - sheet.paste(page.resize((thumb_width, thumb_height)), - ((i % 4) * thumb_width, (i // 4) * thumb_height)) - suffix = "landscape" if args.landscape else "square" - sheet.save(args.preview_dir / f"chapter-storyboard-{suffix}.png") - preview = frames[6] - if "open_book" in preview.info: - preview = OpenBook((W, S)).render(preview.info["open_book"]) - preview.save(args.preview_dir / f"page-turn-{suffix}.png") - -if args.frames_dir: - args.frames_dir.mkdir(parents=True, exist_ok=True) - manifest = ["ffconcat version 1.0"] - native_book = (OpenBook((3840, 2160), detail=3) if args.landscape - else OpenBook((W, S))) if args.v2 else None - for i, (frame, delay) in enumerate(zip(frames, delays)): - name = f"frame-{i:03d}.png" - if args.v2: - frame = native_book.render(frame.info["open_book"]) - frame.save(args.frames_dir / name) - manifest.extend([f"file '{name}'", f"duration {delay / 1000:.3f}"]) - manifest.append(f"file 'frame-{len(frames) - 1:03d}.png'") - (args.frames_dir / "frames.ffconcat").write_text("\n".join(manifest) + "\n", encoding="ascii") - (args.frames_dir / "duration.txt").write_text(f"{sum(delays) / 1000:.3f}", encoding="ascii") - size = "3840 x 2160" if args.v2 and args.landscape else f"{W} x {S}" - print(f"Exported {len(frames)} {size} frames; duration {sum(delays) / 1000:.2f}s.") - sys.exit(0) - -palette_sheet = Image.new("RGB", (GIF_SIZE * len(pages), GIF_SIZE)) -for i, page in enumerate(pages): - palette_sheet.paste(page.resize((GIF_SIZE, GIF_SIZE), Image.Resampling.LANCZOS), - (i * GIF_SIZE, 0)) -palette = palette_sheet.quantize(colors=GIF_COLORS, method=Image.Quantize.MEDIANCUT) -indexed = [frame.resize((GIF_SIZE, GIF_SIZE), Image.Resampling.LANCZOS).quantize( - palette=palette, dither=Image.Dither.NONE) for frame in frames] -output = ROOT / "figures" / "book-chapters.gif" -indexed[0].save(output, save_all=True, append_images=indexed[1:], duration=delays, - loop=0, optimize=True, disposal=1) -with Image.open(output) as gif: - total = 0 - for frame in range(gif.n_frames): - gif.seek(frame) - total += gif.info["duration"] - gif.load() - assert gif.size == (GIF_SIZE, GIF_SIZE) and gif.info["loop"] == 0 - assert total == sum(delays) - if args.v2: - assert total == 68800 - assert delays[0] == 6000 and delays[-1] == 3000 - else: - assert delays[13::13] == [6000] * 12 + [14000, 6000] - assert total == 117440 - assert output.stat().st_size < GIF_MAX_BYTES - print(f"Verified: 11 chapters, {gif.n_frames} frames, {total / 1000:.2f}s, " - f"{output.stat().st_size:,} bytes, infinite loop; all frames decoded.") -print(output) diff --git a/tools/make-book-video.ps1 b/tools/make-book-video.ps1 deleted file mode 100644 index ed3292c..0000000 --- a/tools/make-book-video.ps1 +++ /dev/null @@ -1,52 +0,0 @@ -#Requires -Version 7.0 -[CmdletBinding()] -param( - [string] $FfmpegPath = 'ffmpeg', - [string] $OutputPath, - # Retained for compatibility; the open-book layout is now the default. - [switch] $V2 -) - -$ErrorActionPreference = 'Stop' -if (-not $OutputPath) { - $OutputPath = Join-Path $PSScriptRoot '..\figures\book-chapters-4k.mp4' -} -$generator = Join-Path $PSScriptRoot 'make-book-chapters.py' -if (-not (Get-Command $FfmpegPath -ErrorAction SilentlyContinue)) { - throw "FFmpeg not found: $FfmpegPath. Install FFmpeg or pass -FfmpegPath." -} -if (Test-Path -LiteralPath $OutputPath) { - throw "Output already exists: $OutputPath. Choose another -OutputPath or remove it explicitly." -} - -$frameDirectory = Join-Path ([System.IO.Path]::GetTempPath()) ("book-video-" + [guid]::NewGuid()) -New-Item -ItemType Directory -Path $frameDirectory | Out-Null -try { - $renderArguments = @($generator, '--landscape', '--frames-dir', $frameDirectory) - & python @renderArguments - if ($LASTEXITCODE -ne 0) { - throw "Landscape page rendering failed with exit code $LASTEXITCODE." - } - $duration = (Get-Content -LiteralPath (Join-Path $frameDirectory 'duration.txt') -Raw).Trim() - & $FfmpegPath -hide_banner -loglevel warning -nostdin -n ` - -f concat -safe 0 -i (Join-Path $frameDirectory 'frames.ffconcat') ` - -map 0:v:0 -an -t $duration ` - -vf 'fps=25,scale=3840:2160:flags=lanczos,setsar=1' ` - -c:v libx264 -preset fast -crf 18 -pix_fmt yuv420p -profile:v high -level:v 5.1 ` - -movflags +faststart $OutputPath - if ($LASTEXITCODE -ne 0) { - throw "FFmpeg encoding failed with exit code $LASTEXITCODE." - } -} -finally { - Get-ChildItem -LiteralPath $frameDirectory -File | ForEach-Object { - Remove-Item -LiteralPath $_.FullName - } - Remove-Item -LiteralPath $frameDirectory -} - -& $FfmpegPath -hide_banner -nostdin -v error -xerror -i $OutputPath -map 0:v:0 -f null - -if ($LASTEXITCODE -ne 0) { - throw "MP4 decode validation failed with exit code $LASTEXITCODE." -} -Write-Output "Created and decode-validated silent 3840 x 2160 MP4 at 25 fps: $OutputPath"