1 Commits
Author SHA1 Message Date
git 6ddda00eb3 Prepare Lildra 0.3.1 Web Store submission 2026-07-27 21:16:04 +10:00
21 changed files with 502 additions and 2 deletions
+3
View File
@@ -19,3 +19,6 @@ Thumbs.db
# Local browser test profiles
.chrome-profile/
.edge-profile/
# Local source captures may contain private Canvas identifiers
screenshots/
+3
View File
@@ -14,6 +14,9 @@ Do not add frameworks, build tools, dependencies, analytics, telemetry, remote s
- `background.js` handles the toolbar and command, then injects `lildra.js` into the active tab.
- `lildra.js` validates the URL and Canvas DOM, injects scoped layout CSS and two inert UI elements, watches for dynamic title changes, and owns complete toggle cleanup.
- `icons/` contains the packaged toolbar/store icons.
- `tools/build-release.ps1` creates and validates the runtime-only Web Store ZIP.
- `tools/build-store-assets.ps1` deterministically composites local source captures into store artwork.
- `store-assets/ready-to-upload/` contains final listing artwork; local `screenshots/` inputs are ignored because they may contain private Canvas identifiers.
There is intentionally no content script registered for persistent execution. Re-injecting `lildra.js` toggles the current page off through its page-scoped state object.
+6
View File
@@ -4,6 +4,12 @@ All notable changes to Lildra are documented here. Versions follow Semantic Vers
## Unreleased
## 0.3.1 - 2026-07-27
### Fixed
- Normalised packaged icons with transparent padding for Chrome Web Store presentation.
## 0.3.0 - 2026-07-27
### Changed
+1 -1
View File
@@ -29,4 +29,4 @@ Follow the manual checklist in [AGENTS.md](AGENTS.md). There is no build step an
## Privacy and security
Lildra runs only after an explicit toolbar click or keyboard command. It does not collect, store, or transmit data. The package contains no remote scripts.
Lildra runs only after an explicit toolbar click or keyboard command. It processes the active supported page locally to provide its focused layout, but does not store or transmit page content. The package contains no analytics or remote scripts.
+185
View File
@@ -0,0 +1,185 @@
# Chrome Web Store submission: Lildra 0.3.1
Prepared: 27 July 2026
This document is a drafting and validation aid. Compare all privacy answers with the current dashboard wording before certification. Do not upload reviewer credentials to the repository or package.
## Product details
- Product name: Lildra
- Version: 0.3.1
- Display version: 0.3.1 (no separate `version_name`)
- Recommended primary category: Productivity
- Language: English (Australia)
- Package: `Lildra-v0.3.1-chrome-web-store.zip`
- Support URL: https://napper.au/lildra/support
- Privacy-policy URL: https://napper.au/lildra/privacy
- Homepage: https://napper.au/lildra
### Short description
> A distraction-free reading layout for Canvas and QLearn lesson pages.
The description is 70 characters and is within the 132-character limit.
### Detailed description
> Lildra gives supported Canvas and QLearn lesson pages a cleaner, distraction-free reading layout while preserving the original lesson content.
>
> - Hides Canvas global and course navigation after you activate it.
> - Keeps the existing lesson centred at a bounded, responsive reading width.
> - Shows a compact, single-line title that follows dynamically rendered Canvas page titles.
> - Toggles from the extension toolbar or with Alt+L.
> - Restores the normal page when toggled off.
> - Contains no analytics, remote code, settings, or persistent site access.
>
> Lildra works only on supported HTTPS Canvas course page and wiki content routes with the expected Canvas lesson structure. It does not support dashboards, module lists, assignments, discussions, quizzes, or arbitrary websites. It does not move, clone, reconstruct, or otherwise edit lesson content.
>
> Lildra is an independent Napper.au project and is not affiliated with or endorsed by Instructure, Google, the Queensland Department of Education, or any educational institution.
### Single-purpose statement
> Lildra's single purpose is to provide a user-invoked, distraction-free reading layout on supported Canvas and QLearn lesson pages while preserving the existing lesson content.
## Permission justifications
### `activeTab`
Lildra uses `activeTab` to receive temporary access to the current tab only after the user clicks the toolbar action or invokes the Alt+L extension command. It needs this access to check the selected tab's HTTPS Canvas route and page structure and to apply or remove the focused layout. A narrower persistent host pattern would not provide user-invoked access across independently hosted Canvas installations; persistent host access would also grant broader access than Lildra needs. Page information remains on the device and is not transmitted by Lildra.
### `scripting`
Lildra uses `chrome.scripting.executeScript()` to inject the packaged `lildra.js` file into the temporarily authorised active tab. This is required because the extension is intentionally inactive until the user chooses to run it and does not register a persistent content script. The injected script contains no remote code and transmits no accessed information.
### Host permissions
Lildra requests no `host_permissions`. Temporary page access comes only from `activeTab` after an explicit user gesture.
## Manifest and code audit
- Manifest V3: confirmed.
- Name/version: `Lildra` / `0.3.1`.
- Manifest description: 70 characters.
- Permissions: only `activeTab` and `scripting`, both used by `background.js`.
- Host permissions: none.
- Remote executable code: none.
- Credentials/API keys: none found.
- Storage APIs: none.
- Network APIs in extension code: none (`fetch`, `XMLHttpRequest`, `WebSocket`, and `sendBeacon` absent).
- Analytics/telemetry SDKs: none.
- Runtime references: validated during package build.
- ZIP contents: limited to the manifest, two scripts, and four referenced icons.
## Privacy-practices dashboard draft
Use the following as a conservative code-based draft, then compare each answer with the dashboard's current definitions.
| Category | Draft answer | Reason |
| --- | --- | --- |
| Personally identifiable information | No as an intended category; review wording | Lildra does not seek identity data, but a lesson title could incidentally contain a person's name and is processed locally. |
| Health information | No | No code seeks or uses health data. Incidental lesson content is not extracted or transmitted. |
| Financial or payment information | No | No payment or financial APIs or fields are accessed. |
| Authentication information | No | Lildra does not access passwords, cookies, tokens, or authentication records. |
| Personal communications | No | Lildra does not access email, chat, messages, or communications APIs. |
| Location | No | No location API or location data is accessed. |
| Web browsing activity | Review dashboard wording; disclose current active URL handling if included | The active supported tab URL is checked locally after a user gesture. Lildra does not collect history, monitor other tabs, retain URLs, or transmit them. |
| Website content | Yes | Canvas DOM structure and the visible page title are processed locally to validate the page and provide the layout. |
| User-generated content | Review dashboard wording; likely Yes if lesson pages qualify | A lesson or title may be authored by a teacher or another user. It is processed only in the active page and is not retained or transmitted. |
| Form data | No | Lildra does not read, intercept, or submit form fields. |
Data-use declarations:
- Sold: No.
- Used for advertising or profiling: No.
- Used for creditworthiness or lending: No.
- Transmitted off-device by Lildra: No.
- Shared with third parties by Lildra: No.
- Used only for the disclosed single purpose: Yes.
- Human access to page information: No.
- Retention: No persistent extension retention; temporary state ends when Lildra is disabled or the page closes.
Limited Use statement:
> The use of information received from Google APIs will adhere to the Chrome Web Store User Data Policy, including the Limited Use requirements.
## Screenshot sequence
All screenshots are 1280×800 PNG, square-cornered, and use deterministic crops of the supplied real interface.
1. `01-original-canvas-page.png` — the normal Canvas lesson with global and course navigation visible.
2. `02-lildra-focused-reading.png` — Lildra's primary focused reading layout.
3. `03-sticky-dynamic-title.png` — the compact title bar above the lesson.
4. `04-original-content-preserved.png` — representative headings, text, learning goals, and imagery preserved in the lesson.
5. `05-before-and-after.png` — the same lesson before and after Lildra activation.
Browser chrome was cropped to remove the production hostname, course identifiers, and unrelated extension icons. The private sandbox course name was covered in the remaining Canvas breadcrumb. No Lildra UI was generated or reconstructed.
## Store icon validation
The 0.3.0 packaged icon was fully opaque and had an unintended white square background. For 0.3.1, the established mark was technically normalised without redesigning it:
- 128×128 PNG: confirmed.
- Alpha transparency and transparent corners: confirmed.
- Visible artwork: centred at approximately 96×96 pixels.
- Packaged and promotional icon: derived from the same normalised asset.
- 16, 32, 48, and 128 sizes: generated and require final visual confirmation in Chrome on both light and dark themes.
## Reviewer instructions
> Lildra has one purpose: it provides a user-invoked, distraction-free reading layout on supported HTTPS Canvas and QLearn course page/wiki lesson routes.
>
> To test, install the extension and open an HTTPS Canvas page matching `/courses/{course}/pages/{page}` or `/courses/{course}/wiki/{page}` that contains the standard Canvas `#application #wrapper` and `#wiki_page_show` structure. Click the Lildra toolbar icon or invoke Alt+L. Canvas global navigation and the left course navigation should hide, the existing lesson should remain centred without content reconstruction, and a compact title bar should appear. Click the icon or invoke Alt+L again to restore the normal page.
>
> On unsupported routes such as dashboards, module lists, assignments, discussions, or quizzes, Lildra performs no layout change. It also performs no change when the expected Canvas page structure is absent.
>
> A Canvas/QLearn account and suitable lesson URL are required because the extension does not include or control a public test environment. If reviewer access is required, provide credentials only through the Chrome Web Store dashboard's secure reviewer-credentials field; never place credentials in the extension ZIP or repository.
## Public website alignment
Confirmed strengths:
- `/lildra`, `/lildra/privacy`, `/lildra/support`, and `/lildra/terms` are live.
- The public pages accurately describe explicit activation, supported page types, lack of persistent host access, local processing, permissions, and independence.
- The public privacy page discloses local access to the active URL, title, and Canvas structure.
Required before submission:
1. Update visible version references from `0.3.0` to `0.3.1`.
2. On the homepage privacy card, replace **“No data collection”** with **“Local processing only”** and use: “The selected lesson is processed in your browser and is not stored or transmitted by Lildra.” This avoids ambiguity under Chrome's definition of data handling.
3. After Web Store publication, replace release-download installation calls to action with the Chrome Web Store listing. The support page may retain a source/release link as a secondary option.
4. Update support installation instructions after publication so normal users install from the store rather than enabling Developer mode.
## Manual submission and testing checklist
- [ ] Install from `Lildra-v0.3.1-chrome-web-store.zip` in current stable Chrome.
- [ ] Confirm Chrome reports no manifest or service-worker errors.
- [ ] Activate using both the toolbar button and Alt+L on representative QLearn and generic Canvas wiki/page lessons.
- [ ] Toggle off and confirm all navigation, layout, styles, observers, and injected elements are restored/removed.
- [ ] Confirm unsupported dashboards, modules, assignments, discussions, quizzes, HTTP pages, and unrelated sites do not change.
- [ ] Test a supported route with missing or renamed Canvas DOM elements; Lildra should make no change.
- [ ] Test long and dynamically rendered titles, including one-line ellipsis.
- [ ] Test keyboard focus and screen-reader output for duplicate titles or inaccessible content.
- [ ] Test narrow/mobile-width viewport behaviour.
- [ ] Test long lessons and unusual heading structures.
- [ ] Test images, video, embeds, captions, transcripts, links, and wide tables.
- [ ] Refresh and restart Chrome; confirm Lildra does not persist without another explicit activation.
- [ ] Confirm there are no preference-storage behaviours to test or clear.
- [ ] Confirm the only permission explanation/prompt is consistent with `activeTab` and `scripting` behaviour.
- [ ] Inspect DevTools Network while toggling; confirm Lildra adds no unexpected requests.
- [ ] Check the 16/32/48/128 icons on light and dark browser themes.
- [ ] Recheck all five screenshots for private names, identifiers, URLs, unrelated extensions, and accurate captions.
- [ ] Upload the privacy-policy URL and compare every dashboard privacy answer against its current wording.
- [ ] Provide any reviewer credentials only through the dashboard's secure field.
## Distribution recommendation
Use **Unlisted** for the first controlled beta. The implementation and submission package are narrowly scoped, but representative live QLearn testing, the 0.3.1 public-site wording updates, icon checks in Chrome, and dashboard privacy-category review remain manual. Move to **Public** after those checks pass and the store/support links are aligned.
## Official references
- [Supplying Images](https://developer.chrome.com/docs/webstore/images)
- [Creating a great listing page](https://developer.chrome.com/docs/webstore/best-listing)
- [Privacy Policies](https://developer.chrome.com/docs/webstore/program-policies/privacy)
- [User Data FAQ](https://developer.chrome.com/docs/webstore/program-policies/user-data-faq)
- [Limited Use](https://developer.chrome.com/docs/webstore/program-policies/limited-use)
- [Disclosure Requirements](https://developer.chrome.com/docs/webstore/program-policies/disclosure-requirements)
+53
View File
@@ -0,0 +1,53 @@
# Lildra Privacy Policy
Effective date: 27 July 2026
Lildra is a browser extension that provides a distraction-free reading layout on supported Canvas and QLearn lesson pages. This policy explains how the extension handles information.
## Information handled by Lildra
After the user clicks Lildra's toolbar button or invokes its keyboard shortcut, Lildra temporarily accesses the selected tab. It processes:
- the active tab address, to confirm that it is an HTTPS Canvas course page or wiki route;
- Canvas page structure, to confirm that the page is supported and hide specified navigation elements; and
- the visible lesson title, to display and update Lildra's compact title bar.
The active lesson may contain website content or user-generated educational content. Lildra processes the active supported page locally in the user's browser solely to provide its disclosed focus-mode functionality. Page content is not transmitted to the developer or third parties.
Lildra does not seek passwords, authentication cookies, health or financial information, private communications, precise location, or information from unrelated tabs.
## Use, storage and retention
Information from the selected tab is used only to validate the page, apply the requested focused layout, display its current title, and restore the normal layout when Lildra is disabled.
Lildra does not request Chrome's storage permission and does not use `chrome.storage`, `localStorage`, or `sessionStorage`. It does not retain lesson content, titles, addresses, settings, or usage records. Temporary in-page state exists only while the page remains open or Lildra remains active.
## Transmission and sharing
Lildra does not transmit page content, page titles, addresses, or usage information to Napper.au or another service. It contains no analytics, telemetry, advertising, tracking, or remote executable code. Lildra does not sell user information or share it with advertisers, data brokers, or unrelated third parties.
Canvas, QLearn, and embedded lesson resources continue to communicate with their existing providers independently of Lildra.
## Chrome Web Store Limited Use
The use of information received from Google APIs will adhere to the Chrome Web Store User Data Policy, including the Limited Use requirements. Information is used only to provide Lildra's single user-facing purpose and is not used for advertising, creditworthiness, lending, or unrelated purposes.
## Support correspondence
If a user voluntarily emails support, the message is handled by the relevant email providers and may contain information the user chooses to include. Support messages are retained only as reasonably needed to respond, maintain security, and meet legal obligations. Users should not send passwords, student records, authentication information, or private lesson content.
## User choices and deletion
Users can stop local processing by toggling Lildra off, closing the page, disabling the extension, or uninstalling it. Because Lildra stores no extension dataset, there is no separate local data to clear. Users may contact support to request deletion of information they voluntarily sent by email.
## Website
The public Lildra website may receive ordinary server information needed to deliver pages, such as an IP address, browser type, requested page, and request time. This may appear in short-term operational or security logs. The website cannot access Canvas or QLearn lesson content.
## Changes
This policy may be updated if Lildra's features, permissions, or data practices change. The effective date will be revised, and material changes will be communicated through appropriate website, listing, extension, or release channels.
## Contact
Privacy questions and deletion requests can be sent to [support@napper.au](mailto:support@napper.au). Lildra is an independent Napper.au project based in Queensland, Australia.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 627 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

+1 -1
View File
@@ -1,7 +1,7 @@
{
"manifest_version": 3,
"name": "Lildra",
"version": "0.3.0",
"version": "0.3.1",
"description": "A distraction-free reading layout for Canvas and QLearn lesson pages.",
"permissions": [
"activeTab",
Binary file not shown.

After

Width:  |  Height:  |  Size: 313 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 227 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 231 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 396 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 219 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

+50
View File
@@ -0,0 +1,50 @@
param(
[string]$Root = (Split-Path -Parent $PSScriptRoot)
)
$ErrorActionPreference = "Stop"
$manifest = Get-Content -Raw -LiteralPath (Join-Path $Root "manifest.json") | ConvertFrom-Json
$version = $manifest.version
$outputDir = Join-Path $Root "store-assets\ready-to-upload"
$package = Join-Path $outputDir "Lildra-v$version-chrome-web-store.zip"
$stage = Join-Path $Root ".store-package-staging"
if (Test-Path -LiteralPath $stage) {
throw "Staging directory already exists: $stage"
}
New-Item -ItemType Directory -Force -Path (Join-Path $stage "icons") | Out-Null
try {
Copy-Item -LiteralPath (Join-Path $Root "manifest.json"), (Join-Path $Root "background.js"), (Join-Path $Root "lildra.js") -Destination $stage
foreach ($size in 16, 32, 48, 128) {
Copy-Item -LiteralPath (Join-Path $Root "icons\LildraLogo$size.png") -Destination (Join-Path $stage "icons")
}
if (Test-Path -LiteralPath $package) { Remove-Item -LiteralPath $package }
Compress-Archive -Path (Join-Path $stage "*") -DestinationPath $package -CompressionLevel Optimal
} finally {
if (Test-Path -LiteralPath $stage) { Remove-Item -LiteralPath $stage -Recurse -Force }
}
Add-Type -AssemblyName System.IO.Compression.FileSystem
$zip = [System.IO.Compression.ZipFile]::OpenRead($package)
try {
$inventory = @($zip.Entries | ForEach-Object { $_.FullName.Replace("\", "/") })
if ($inventory -notcontains "manifest.json") { throw "manifest.json is not at the ZIP root" }
$expected = @(
"manifest.json", "background.js", "lildra.js",
"icons/LildraLogo16.png", "icons/LildraLogo32.png",
"icons/LildraLogo48.png", "icons/LildraLogo128.png"
)
if (Compare-Object $expected $inventory) { throw "Unexpected ZIP inventory" }
} finally {
$zip.Dispose()
}
$hash = Get-FileHash -Algorithm SHA256 -LiteralPath $package
[pscustomobject]@{
Path = $package
Size = (Get-Item -LiteralPath $package).Length
SHA256 = $hash.Hash
Inventory = $inventory -join ", "
}
+200
View File
@@ -0,0 +1,200 @@
param(
[string]$Root = (Split-Path -Parent $PSScriptRoot)
)
$ErrorActionPreference = "Stop"
Add-Type -AssemblyName System.Drawing
$sourceDir = Join-Path $Root "screenshots"
$outputDir = Join-Path $Root "store-assets\ready-to-upload"
New-Item -ItemType Directory -Force -Path $outputDir | Out-Null
$purpleDark = [System.Drawing.ColorTranslator]::FromHtml("#281052")
$purple = [System.Drawing.ColorTranslator]::FromHtml("#6D35D8")
$purpleBright = [System.Drawing.ColorTranslator]::FromHtml("#8250E8")
$white = [System.Drawing.Color]::White
$ink = [System.Drawing.ColorTranslator]::FromHtml("#171429")
function New-Canvas([int]$Width, [int]$Height) {
$bitmap = New-Object System.Drawing.Bitmap($Width, $Height, [System.Drawing.Imaging.PixelFormat]::Format32bppArgb)
$bitmap.SetResolution(96, 96)
return $bitmap
}
function Save-Png($Bitmap, [string]$Name) {
$path = Join-Path $outputDir $Name
$Bitmap.Save($path, [System.Drawing.Imaging.ImageFormat]::Png)
return $path
}
function Set-Quality($Graphics) {
$Graphics.SmoothingMode = [System.Drawing.Drawing2D.SmoothingMode]::HighQuality
$Graphics.InterpolationMode = [System.Drawing.Drawing2D.InterpolationMode]::HighQualityBicubic
$Graphics.PixelOffsetMode = [System.Drawing.Drawing2D.PixelOffsetMode]::HighQuality
$Graphics.TextRenderingHint = [System.Drawing.Text.TextRenderingHint]::ClearTypeGridFit
}
function Draw-Cover($Graphics, $Image, [System.Drawing.Rectangle]$Destination, [System.Drawing.Rectangle]$Source) {
$sourceRatio = $Source.Width / $Source.Height
$destinationRatio = $Destination.Width / $Destination.Height
if ($sourceRatio -gt $destinationRatio) {
$cropWidth = [int]($Source.Height * $destinationRatio)
$Source.X += [int](($Source.Width - $cropWidth) / 2)
$Source.Width = $cropWidth
} else {
$cropHeight = [int]($Source.Width / $destinationRatio)
$Source.Y += [int](($Source.Height - $cropHeight) / 2)
$Source.Height = $cropHeight
}
$Graphics.DrawImage($Image, $Destination, $Source, [System.Drawing.GraphicsUnit]::Pixel)
}
function Draw-Header($Graphics, [string]$Title, [string]$Subtitle) {
$brush = New-Object System.Drawing.Drawing2D.LinearGradientBrush(
(New-Object System.Drawing.Rectangle(0, 0, 1280, 112)),
$purpleBright,
$purpleDark,
0
)
$titleFont = New-Object System.Drawing.Font("Segoe UI", 27, [System.Drawing.FontStyle]::Bold)
$subtitleFont = New-Object System.Drawing.Font("Segoe UI", 15, [System.Drawing.FontStyle]::Regular)
try {
$Graphics.FillRectangle($brush, 0, 0, 1280, 112)
$Graphics.DrawString($Title, $titleFont, [System.Drawing.Brushes]::White, 38, 20)
$Graphics.DrawString($Subtitle, $subtitleFont, [System.Drawing.Brushes]::White, 40, 67)
} finally {
$brush.Dispose(); $titleFont.Dispose(); $subtitleFont.Dispose()
}
}
function Redact-SandboxName($Graphics) {
$cover = New-Object System.Drawing.SolidBrush([System.Drawing.Color]::White)
try { $Graphics.FillRectangle($cover, 128, 124, 150, 34) } finally { $cover.Dispose() }
}
function Build-Screenshot([string]$Name, [string]$SourceName, [string]$Title, [string]$Subtitle, [System.Drawing.Rectangle]$Crop, [bool]$Redact) {
$source = [System.Drawing.Image]::FromFile((Join-Path $sourceDir $SourceName))
$canvas = New-Canvas 1280 800
$graphics = [System.Drawing.Graphics]::FromImage($canvas)
try {
Set-Quality $graphics
Draw-Header $graphics $Title $Subtitle
Draw-Cover $graphics $source (New-Object System.Drawing.Rectangle(0, 112, 1280, 688)) $Crop
if ($Redact) { Redact-SandboxName $graphics }
Save-Png $canvas $Name | Out-Null
} finally {
$graphics.Dispose(); $canvas.Dispose(); $source.Dispose()
}
}
$viewportCrop = New-Object System.Drawing.Rectangle(0, 100, 1920, 1040)
Build-Screenshot "01-original-canvas-page.png" "original.png" "A normal Canvas lesson page" "Global and course navigation surround the lesson." $viewportCrop $true
Build-Screenshot "02-lildra-focused-reading.png" "lildra.png" "A calmer reading view" "Lildra hides surrounding navigation and centres the lesson." $viewportCrop $false
Build-Screenshot "03-sticky-dynamic-title.png" "lildra.png" "The lesson title stays visible" "A compact title bar follows the current Canvas page title." (New-Object System.Drawing.Rectangle(170, 100, 1580, 850)) $false
Build-Screenshot "04-original-content-preserved.png" "lildra.png" "Original lesson content remains" "Headings, text and images remain in the original page." (New-Object System.Drawing.Rectangle(270, 240, 1500, 850)) $false
$before = [System.Drawing.Image]::FromFile((Join-Path $sourceDir "original.png"))
$after = [System.Drawing.Image]::FromFile((Join-Path $sourceDir "lildra.png"))
$comparison = New-Canvas 1280 800
$graphics = [System.Drawing.Graphics]::FromImage($comparison)
try {
Set-Quality $graphics
Draw-Header $graphics "Before and after" "The same lesson, with surrounding Canvas navigation hidden."
Draw-Cover $graphics $before (New-Object System.Drawing.Rectangle(0, 112, 632, 688)) (New-Object System.Drawing.Rectangle(0, 100, 1050, 1040))
Draw-Cover $graphics $after (New-Object System.Drawing.Rectangle(648, 112, 632, 688)) (New-Object System.Drawing.Rectangle(250, 100, 1050, 1040))
$divider = New-Object System.Drawing.SolidBrush($purple)
$labelFont = New-Object System.Drawing.Font("Segoe UI", 15, [System.Drawing.FontStyle]::Bold)
try {
$graphics.FillRectangle($divider, 632, 112, 16, 688)
$graphics.FillRectangle([System.Drawing.Brushes]::White, 18, 130, 92, 34)
$graphics.FillRectangle([System.Drawing.Brushes]::White, 666, 130, 92, 34)
$graphics.DrawString("Before", $labelFont, (New-Object System.Drawing.SolidBrush($ink)), 28, 134)
$graphics.DrawString("Lildra", $labelFont, (New-Object System.Drawing.SolidBrush($purple)), 676, 134)
# Remove the private sandbox course name from the before panel.
$graphics.FillRectangle([System.Drawing.Brushes]::White, 102, 124, 145, 36)
} finally { $divider.Dispose(); $labelFont.Dispose() }
Save-Png $comparison "05-before-and-after.png" | Out-Null
} finally {
$graphics.Dispose(); $comparison.Dispose(); $before.Dispose(); $after.Dispose()
}
# Create a store icon candidate from the established logo. Only edge-connected
# near-white canvas pixels are removed; the white L inside the purple mark remains.
$originalIcon = [System.Drawing.Image]::FromFile((Join-Path $Root "icons\LildraLogoHiRes.png"))
$sourceIcon = New-Canvas $originalIcon.Width $originalIcon.Height
$sourceGraphics = [System.Drawing.Graphics]::FromImage($sourceIcon)
try {
$sourceGraphics.DrawImageUnscaled($originalIcon, 0, 0)
} finally {
$sourceGraphics.Dispose(); $originalIcon.Dispose()
}
$visited = New-Object 'bool[,]' $sourceIcon.Width, $sourceIcon.Height
$queue = New-Object 'System.Collections.Generic.Queue[System.Drawing.Point]'
for ($x = 0; $x -lt $sourceIcon.Width; $x++) {
$queue.Enqueue([System.Drawing.Point]::new($x, 0))
$queue.Enqueue([System.Drawing.Point]::new($x, ($sourceIcon.Height - 1)))
}
for ($y = 0; $y -lt $sourceIcon.Height; $y++) {
$queue.Enqueue([System.Drawing.Point]::new(0, $y))
$queue.Enqueue([System.Drawing.Point]::new(($sourceIcon.Width - 1), $y))
}
while ($queue.Count -gt 0) {
$point = $queue.Dequeue()
if ($point.X -lt 0 -or $point.Y -lt 0 -or $point.X -ge $sourceIcon.Width -or $point.Y -ge $sourceIcon.Height -or $visited[$point.X, $point.Y]) { continue }
$visited[$point.X, $point.Y] = $true
$pixel = $sourceIcon.GetPixel($point.X, $point.Y)
if ($pixel.R -lt 215 -or $pixel.G -lt 215 -or $pixel.B -lt 215) { continue }
$sourceIcon.SetPixel($point.X, $point.Y, [System.Drawing.Color]::Transparent)
$queue.Enqueue([System.Drawing.Point]::new(($point.X + 1), $point.Y))
$queue.Enqueue([System.Drawing.Point]::new(($point.X - 1), $point.Y))
$queue.Enqueue([System.Drawing.Point]::new($point.X, ($point.Y + 1)))
$queue.Enqueue([System.Drawing.Point]::new($point.X, ($point.Y - 1)))
}
$storeIcon = New-Canvas 128 128
$graphics = [System.Drawing.Graphics]::FromImage($storeIcon)
try {
Set-Quality $graphics
$graphics.Clear([System.Drawing.Color]::Transparent)
$graphics.DrawImage($sourceIcon, (New-Object System.Drawing.Rectangle(16, 16, 96, 96)))
Save-Png $storeIcon "store-icon-128.png" | Out-Null
} finally { $graphics.Dispose(); $storeIcon.Dispose(); $sourceIcon.Dispose() }
$icon = [System.Drawing.Image]::FromFile((Join-Path $outputDir "store-icon-128.png"))
$focused = [System.Drawing.Image]::FromFile((Join-Path $sourceDir "lildra.png"))
foreach ($promo in @(
@{Name="small-promo-440x280.png"; Width=440; Height=280; Icon=112; Screenshot=$false},
@{Name="marquee-promo-1400x560.png"; Width=1400; Height=560; Icon=170; Screenshot=$true}
)) {
$canvas = New-Canvas $promo.Width $promo.Height
$graphics = [System.Drawing.Graphics]::FromImage($canvas)
$gradient = New-Object System.Drawing.Drawing2D.LinearGradientBrush(
(New-Object System.Drawing.Rectangle(0, 0, $promo.Width, $promo.Height)),
$purpleBright,
$purpleDark,
12
)
$nameFont = New-Object System.Drawing.Font("Segoe UI", $(if($promo.Width -gt 500){58}else{34}), [System.Drawing.FontStyle]::Bold)
$tagFont = New-Object System.Drawing.Font("Segoe UI", $(if($promo.Width -gt 500){24}else{15}), [System.Drawing.FontStyle]::Regular)
try {
Set-Quality $graphics
$graphics.FillRectangle($gradient, 0, 0, $promo.Width, $promo.Height)
$iconX = $(if($promo.Width -gt 500){80}else{34})
$iconY = [int](($promo.Height - $promo.Icon) / 2)
$graphics.DrawImage($icon, $iconX, $iconY, $promo.Icon, $promo.Icon)
$textX = $iconX + $promo.Icon + $(if($promo.Width -gt 500){42}else{22})
$graphics.DrawString("Lildra", $nameFont, [System.Drawing.Brushes]::White, $textX, $(if($promo.Width -gt 500){170}else{105}))
if ($promo.Width -gt 500) {
$graphics.DrawString("Distraction-free lesson reading", $tagFont, [System.Drawing.Brushes]::White, $textX + 2, 250)
}
if ($promo.Screenshot) {
$panel = New-Object System.Drawing.Rectangle(785, 55, 570, 450)
Draw-Cover $graphics $focused $panel $viewportCrop
}
Save-Png $canvas $promo.Name | Out-Null
} finally {
$gradient.Dispose(); $nameFont.Dispose(); $tagFont.Dispose(); $graphics.Dispose(); $canvas.Dispose()
}
}
$icon.Dispose(); $focused.Dispose()
Get-ChildItem -LiteralPath $outputDir -File | Sort-Object Name | Select-Object Name, Length