Clean up repository structure and release docs

This commit is contained in:
Akshay Kolli
2026-06-30 00:18:59 -07:00
parent 226c29b565
commit 992f1444e6
66 changed files with 330 additions and 1193 deletions

View File

@@ -1,25 +1,23 @@
---
name: Bug report
about: Report a reproducible problem in I Hate PDFs
title: ""
labels: bug
assignees: ""
---
## Type
Bug report, feature request, documentation issue, or support question?
## Summary
What happened?
## Expected Behavior
What did you expect to happen?
What should maintainers know first?
## Steps To Reproduce
For bugs, list the shortest reproducible path:
1.
2.
3.
## Expected Behavior
What did you expect to happen?
## Environment
- I Hate PDFs version/build:
@@ -27,10 +25,10 @@ What did you expect to happen?
- Install source: GitHub release or Mac App Store
- Mac architecture: Apple Silicon or Intel
## Screenshots Or Recordings
## UI Evidence
Attach screenshots or recordings for UI problems. Each file must be under 1 MB.
## Additional Context
## Size And Privacy Impact
Add any relevant PDF workflow details. Do not attach private PDFs publicly.
For feature requests, note whether this requires new assets, dependencies, network behavior, bundled PDFs, or release-size increases.

View File

@@ -1,27 +0,0 @@
---
name: Feature request
about: Suggest an improvement for I Hate PDFs
title: ""
labels: enhancement
assignees: ""
---
## Problem
What workflow problem should this solve?
## Proposed Solution
What should I Hate PDFs do?
## Alternatives Considered
What workaround or other app behavior have you tried?
## UI Impact
Will this change visible UI? If yes, describe the screen, toolbar, menu, sidebar, popover, or dialog affected.
## Size And Privacy Impact
Does this require new assets, dependencies, network behavior, bundled PDFs, or release-size increases?

View File

@@ -2,6 +2,10 @@
Describe what changed and why.
## Vibe Coding Disclosure
Was this pull request vibe coded or assisted by generated code? If yes, describe what you manually reviewed before opening it.
## Screenshots Or Recordings
Required for UI changes. Include before and after screenshots for changed UI, or at least one screenshot/recording for new UI.
@@ -14,6 +18,7 @@ List the checks you ran:
- [ ] `swift test`
- [ ] `swift build -c release`
- [ ] `swift scripts/verify-pdf-annotations.swift`
- [ ] `scripts/verify-release-artifacts.sh`
- [ ] Manual UI check
- [ ] Not run, because:
@@ -23,6 +28,7 @@ List the checks you ran:
- [ ] I read `CONTRIBUTING.md`.
- [ ] This pull request is focused on one behavior, fix, or documentation change.
- [ ] I updated `CHANGELOG.md` for user-visible changes, or this change is not user-visible.
- [ ] I documented the before/after behavior and QA performed.
- [ ] UI changes include screenshots or recordings, or this pull request does not change UI.
- [ ] Every screenshot, recording, and committed media file in this pull request is under 1 MB.
- [ ] I explained any app-size impact from new assets, dependencies, or release packaging changes.

2
.gitignore vendored
View File

@@ -5,12 +5,14 @@ DerivedData/
dist/
release/
plans/
$tmpdir/
*.dmg
*.pkg
*.tar.gz
*.tar.xz
*.zip
*.xcuserstate
*.swp
*.xcworkspace/xcuserdata/
*.xcodeproj/xcuserdata/
*.mobileprovision

View File

@@ -7,6 +7,14 @@
- Added README badges for release, license, platform, Swift, contributions, and media-size policy.
- Added contribution, support, security, issue, and pull request policies for open-source contributors.
- Added a pull request media-size policy requiring UI screenshots or recordings and limiting each screenshot, recording, or committed media file to less than 1 MB.
- Renamed source, test, and signing directories to lowercase paths.
- Removed duplicate release/planning docs, stale screenshot targets, and the unused duplicate icon image.
- Removed redundant support/App Store copy docs and trimmed repository screenshots to one representative image.
- Consolidated release helper scripts by folding size checks into tiny archive creation and sample PDF generation into PDF annotation verification.
- Consolidated workflow audit guidance into the manual QA document.
- Consolidated engineering guidance into contributing docs and App Store packaging into release docs.
- Documented that vibe coded pull requests are welcome when they include clear change documentation, strict QA, and UI screenshots or recordings when relevant.
- Documented `https://www.akkolli.net/ihatepdfs` as the project website and `akshaykolli@hotmail.com` as the support contact.
## Version 0.4.0 (build 6) - 2026-06-25

View File

@@ -4,12 +4,13 @@ I Hate PDFs is a native macOS app for local PDF reading and annotation. Contribu
By contributing, you agree that your contribution is licensed under GNU General Public License version 2 only.
Vibe coded pull requests are welcome. This is a vibe coded repo, but vibe coded does not mean unreviewed: every change must be understandable, documented, tested at the right level, and held to the same strict QA bar as hand-written code.
## Before You Start
- Check existing issues and pull requests before starting duplicate work.
- Open an issue first for large UI changes, new dependencies, release-process changes, or features that affect PDF saving/export behavior.
- Read `docs/ENGINEERING.md` before adding dependencies, bundled assets, PDF engines, runtimes, or broad architectural changes.
- Read `docs/WORKFLOW_AUDIT.md` before changing a user workflow.
- Read `docs/QA.md` before changing a user workflow.
- Read `docs/RELEASE.md` before preparing a release.
## Development
@@ -33,15 +34,29 @@ scripts/verify-release-artifacts.sh
Run the checks that match your change and list them in the pull request. If you skip a relevant check, say why.
## Engineering Policy
- Build with Swift, SwiftUI, AppKit, PDFKit, and system frameworks that ship with macOS.
- Do not replace the app with Electron, Chromium, a web runtime, a bundled JavaScript shell, or a cross-platform UI toolkit.
- Do not bundle a PDF renderer, OCR engine, database, scripting runtime, or large framework when a macOS system API can satisfy the requirement.
- Keep third-party dependencies at or near zero. Any new package must justify shipped size, runtime cost, maintenance cost, and why system APIs are insufficient.
- Keep assets minimal. Avoid large raster images, fonts, sample PDFs, videos, model files, or generated resources in the app bundle.
- Keep expensive work page-scoped or lazy when possible.
- Treat release-size growth as a product regression. Each direct-download per-architecture installer must stay under 400,000 bytes.
- Run `scripts/make-tiny-archives.sh` before release-impacting changes; it builds and checks the per-architecture archives.
## Pull Request Policy
- Keep each pull request focused on one behavior, bug fix, or documentation change.
- Explain the user-visible behavior change, not only the implementation detail.
- For vibe coded changes, say so in the pull request and describe what you reviewed manually before opening it.
- Link the issue when one exists.
- Update `CHANGELOG.md` for user-visible changes.
- Avoid unrelated formatting churn.
- Do not add a new dependency, bundled asset, PDF engine, runtime, or release artifact without explaining the size and maintenance impact.
- Preserve the app's local-first privacy model.
- Document the before/after behavior clearly enough that a maintainer can review the intent without reverse-engineering the diff.
- Include the QA commands and manual checks that prove the change works.
## UI Screenshot Policy

View File

@@ -11,18 +11,24 @@ let package = Package(
.library(name: "IHatePDFsCore", targets: ["IHatePDFsCore"])
],
targets: [
.target(name: "IHatePDFsCore"),
.target(
name: "IHatePDFsCore",
path: "sources/core"
),
.executableTarget(
name: "IHatePDFs",
dependencies: ["IHatePDFsCore"]
dependencies: ["IHatePDFsCore"],
path: "sources/app"
),
.testTarget(
name: "IHatePDFsCoreTests",
dependencies: ["IHatePDFsCore"]
dependencies: ["IHatePDFsCore"],
path: "tests/core"
),
.testTarget(
name: "IHatePDFsTests",
dependencies: ["IHatePDFs", "IHatePDFsCore"]
dependencies: ["IHatePDFs", "IHatePDFsCore"],
path: "tests/app"
)
]
)

View File

@@ -1,5 +1,5 @@
<p align="center">
<img src="ihatepdf-profile-transparent.png" alt="I Hate PDFs app icon" width="128">
<img src="assets/app-icon.png" alt="I Hate PDFs app icon" width="128">
</p>
<h1 align="center">I Hate PDFs</h1>
@@ -15,15 +15,18 @@
<a href="https://github.com/akkolli/ihatepdfs/releases/latest"><img alt="Version" src="https://img.shields.io/github/v/release/akkolli/ihatepdfs?label=version&color=0A7AFF"></a>
<a href="#development"><img alt="Language: Swift" src="https://img.shields.io/badge/language-Swift-F05138?logo=swift&logoColor=white"></a>
<a href="CONTRIBUTING.md"><img alt="Status: active development" src="https://img.shields.io/badge/status-active%20development-16A34A"></a>
<img alt="Vibe coded" src="https://img.shields.io/badge/vibe-coded-FF69B4">
</p>
<p align="center">
<a href="https://github.com/akkolli/ihatepdfs/releases/latest">Download</a>
·
<a href="https://github.com/akkolli/ihatepdfs/issues/new?template=bug_report.md">Report a bug</a>
<a href="https://github.com/akkolli/ihatepdfs/issues/new">Report a bug</a>
·
<a href="CONTRIBUTING.md">Contribute</a>
·
<a href="mailto:akshaykolli@hotmail.com">Support</a>
·
<a href="https://www.akkolli.net/ihatepdfs/privacy">Privacy</a>
</p>
@@ -45,7 +48,7 @@ Download the v0.4 macOS DMG from the GitHub release page:
Use `IHatePDFs-v0.4-macos.dmg` for direct installation. Open the DMG, then move `I Hate PDFs.app` into `/Applications`.
The direct-download DMG is separate from the Mac App Store build. The App Store package uses bundle ID `net.akkolli.ihatepdfs` and is built with the sandbox entitlements documented in `docs/APP_STORE.md`.
The direct-download DMG is separate from the Mac App Store build. The App Store package uses bundle ID `net.akkolli.ihatepdfs` and is built with sandbox entitlements documented in `docs/RELEASE.md`.
## Features
@@ -70,9 +73,9 @@ The direct-download DMG is separate from the Mac App Store build. The App Store
## Privacy And Support
- Product and support page: <https://www.akkolli.net/ihatepdfs>
- Project website: <https://www.akkolli.net/ihatepdfs>
- Support email: <akshaykolli@hotmail.com>
- Privacy policy: <https://www.akkolli.net/ihatepdfs/privacy>
- Support policy: [SUPPORT.md](SUPPORT.md)
- Security policy: [SECURITY.md](SECURITY.md)
I Hate PDFs works with user-selected local files. It does not require an account, collect analytics, or upload PDFs.
@@ -134,7 +137,7 @@ PROVISIONING_PROFILE="$HOME/Downloads/IHatePDFs_AppStore.provisionprofile" \
scripts/make-app-store-pkg.sh
```
The App Store package is written to `dist/IHatePDFs-v0.4-macos-appstore.pkg`. More details are in `docs/APP_STORE.md`.
The App Store package is written to `dist/IHatePDFs-v0.4-macos-appstore.pkg`. More details are in `docs/RELEASE.md`.
## Installation
@@ -149,13 +152,13 @@ The project is a Swift Package with two targets:
- `IHatePDFsCore`: PDF annotation models, annotation export helpers, hit testing, color preference logic, file selection, and keyboard policies.
- `IHatePDFs`: SwiftUI macOS app, PDFKit bridge, toolbar, menus, sidebars, anchored comment popovers, opening, saving, sharing, and search.
Engineering rule: keep this a native macOS app with the smallest final bundle that still delivers the required fluidity and functionality. See `docs/ENGINEERING.md` before adding dependencies, bundled assets, PDF engines, runtimes, or broad architectural changes.
Use `docs/WORKFLOW_AUDIT.md` when checking whether a feature matches the intended user workflow before changing or releasing it.
Engineering rule: keep this a native macOS app with the smallest final bundle that still delivers the required fluidity and functionality. See `CONTRIBUTING.md` before adding dependencies, bundled assets, PDF engines, runtimes, or broad architectural changes.
Use `docs/RELEASE.md` when preparing a new version; it is the checklist for version bumps, release validation, size gates, and upload packaging.
Open source contribution policy:
- Contributions are accepted under GPL-2.0-only. See [LICENSE](LICENSE).
- Vibe coded pull requests are welcome, but they must include clear change documentation, strict QA notes, and screenshots or recordings for UI changes.
- Start with [CONTRIBUTING.md](CONTRIBUTING.md) before opening a pull request.
- UI pull requests must include before/after screenshots or a short screen recording.
- Screenshots, recordings, and committed media files included with a pull request must each be under 1 MB.
@@ -165,36 +168,21 @@ Useful checks:
```sh
swift test
swift build -c release
swift scripts/verify-sample-pdf.swift
swift scripts/verify-pdf-annotations.swift
scripts/verify-release-artifacts.sh
```
The PDF verification scripts generate and inspect standard highlight, underline, selected-text comment, reply, free-text, contents, and annotation relationship dictionaries.
The PDF verification script generates and inspects standard highlight, underline, selected-text comment, reply, free-text, contents, and annotation relationship dictionaries.
The release artifact verifier checks the current direct-download app and DMG by default. Run `REQUIRE_APP_STORE_PKG=1 scripts/verify-release-artifacts.sh` after creating an App Store package.
Manual release QA for Preview, Acrobat Reader, and browser PDF viewers is documented in `docs/QA.md`. App Store packaging is documented in `docs/APP_STORE.md`, and paste-ready App Store metadata is in `docs/APP_STORE_COPY.md`.
Manual release QA and workflow guardrails for Preview, Acrobat Reader, and browser PDF viewers are documented in `docs/QA.md`. Release and App Store packaging are documented in `docs/RELEASE.md`.
## Screenshots
## Screenshot
Screenshots live in `docs/screenshots`.
Current repository screenshots that are useful for local review:
- `docs/screenshots/default-reading.png`
- `docs/screenshots/main-window.png`
- `docs/screenshots/comments-sidebar.png`
- `docs/screenshots/dark-mode-reading.png`
- `docs/screenshots/preview-interoperability.png`
`docs/screenshots/no-document.png` and `docs/screenshots/highlight-comment-popover.png` are capture targets that need to be retaken before public release docs use them.
![Default reading mode](docs/screenshots/default-reading.png)
![Comments sidebar](docs/screenshots/comments-sidebar.png)
![Dark mode reading](docs/screenshots/dark-mode-reading.png)
## License
GNU General Public License version 2 only. See [LICENSE](LICENSE).

View File

@@ -1,33 +0,0 @@
# Release Notes
## I Hate PDFs v0.4.0
Version 0.4 is focused on keeping the app small, fast, and reader-first. The experimental Fill & Sign, form-field navigation, and PDF signing work has been removed from this release line and will be revisited later from scratch.
### What's New
- Focused document opening: PDFs open in the reader with sidebars hidden and the page fit to available width.
- Smaller app surface area after removing the experimental Fill & Sign/signing implementation.
- Recent PDFs in the empty window and File > Open Recent.
- Settings for highlight and comment colors, including opacity.
- Standalone highlights that do not open a comment editor.
- Return saves a comment or reply; Shift-Return inserts a new line.
- Mac App Store packaging for `net.akkolli.ihatepdfs`.
### Reliability Fixes
- Comment text survives when saved PDFs are opened in macOS Preview and Adobe Acrobat.
- The comment editor focuses correctly when a new selected-text comment is created.
- Comment popovers open from the actual annotated text instead of nearby whitespace.
- The app warns before unsaved annotations or reply drafts are lost.
- Search highlights clear correctly when search is closed or edited.
- The comments sidebar keeps threads, filters, replies, and selected highlights in sync.
- PDFKit page/selection observers are cleaned up when the reader view is reattached.
- Sidebar toggles now close the active sidebar mode directly instead of switching modes first.
### Version
- App version: `0.4.0`
- Build number: `6`
- Direct-download DMG name: `IHatePDFs-v0.4-macos.dmg`
- Mac App Store package name: `IHatePDFs-v0.4-macos-appstore.pkg`

View File

@@ -1,37 +1,6 @@
# Roadmap
## Version 0.1
- Native macOS SwiftUI/PDFKit app.
- Local PDF opening.
- Reading controls: scrolling, zoom, fit width, fit page, page navigation, search.
- Focused default reading mode with optional page thumbnail sidebar.
- Highlight, underline, selection-bound comment, and free-text annotations.
- Anchored comment popovers from newly created selected-text comments, underlines, free text, and clicked comment-capable annotations; plain highlights remain standalone.
- Annotation list sidebar.
- Optional comments review sidebar with grouping, collapsed filtering, replies, and navigation.
- Save, Save As, and native macOS sharing with standard PDF annotation writing.
- `.app` and `.dmg` build scripts.
- Visual QA screenshots for empty, reading, popover, comments, and dark-mode states.
## Shipped In Version 0.3
- Settings for highlight and comment colors.
- Higher-contrast default highlights and comments.
- Standalone highlights that do not open a comment editor.
- Drag-and-drop PDF opening from the empty app window.
- Return-to-save and Shift-Return-for-newline comment behavior.
- Preview-compatible exported comments for selected-text markup.
- Safer close/open/quit prompts for unsaved annotations and reply drafts.
- Mac App Store packaging path for `net.akkolli.ihatepdfs`.
## Preparing Version 0.4
- Keep the reader focused on open: sidebars are hidden and the PDF is fit to the available width.
- Preserve the lightweight annotation workflow: highlight, underline, selected-text comments, free text, replies, search, bookmarks, and native sharing.
- Remove the experimental Fill & Sign, form-field navigation, and PDF signing implementation from v0.4.
- Keep the direct-download DMG and per-architecture archives under the release size budget.
- Release metadata, docs, and packaging names prepared for `0.4.0` build `6`.
I Hate PDFs is a small native macOS reader and annotation app. The roadmap should stay focused on local PDF review, standards-compatible annotations, and a small bundle.
## Next
@@ -51,3 +20,9 @@
- Optional AI summaries or question prompts.
- iPad companion app.
- LMS integrations.
## Not Planned
- Accounts, sync, analytics, or cloud PDF upload.
- Bundled PDF engines or large runtimes when system frameworks are enough.
- Broad document-management features that pull focus away from reading and annotation.

View File

@@ -10,9 +10,7 @@ Security fixes target the latest public release and the `main` branch.
Do not report security vulnerabilities in public issues.
Use GitHub private vulnerability reporting if it is enabled for this repository. If it is not enabled, contact the maintainer through the product support page:
<https://www.akkolli.net/ihatepdfs>
Use GitHub private vulnerability reporting if it is enabled for this repository. If it is not enabled, contact the maintainer at <akshaykolli@hotmail.com>.
Include:

View File

@@ -1,19 +0,0 @@
# Support
Use the public issue tracker for reproducible bugs, feature requests, and documentation problems:
<https://github.com/akkolli/ihatepdfs/issues>
Use the product and support page for user support:
<https://www.akkolli.net/ihatepdfs>
Before opening an issue, include:
- I Hate PDFs version and build number
- macOS version
- Whether the app came from GitHub releases or the Mac App Store
- A short description of the PDF workflow involved
- Screenshots or recordings for UI problems, each under 1 MB
Do not open public issues for security vulnerabilities. Follow `SECURITY.md`.

View File

Before

Width:  |  Height:  |  Size: 7.1 KiB

After

Width:  |  Height:  |  Size: 7.1 KiB

View File

@@ -1,60 +0,0 @@
# Mac App Store Release
Bundle ID: `net.akkolli.ihatepdfs`
Current App Store build values:
- `CFBundleShortVersionString`: `0.4.0`
- `CFBundleVersion`: `6`
- Privacy policy URL: `https://www.akkolli.net/ihatepdfs/privacy`
- Marketing/support URL: `https://www.akkolli.net/ihatepdfs`
Paste-ready metadata, review notes, privacy answers, and screenshot guidance live in `docs/APP_STORE_COPY.md`.
The general release checklist is in `docs/RELEASE.md`.
## Required Apple Developer Items
- An explicit macOS App ID for `net.akkolli.ihatepdfs`.
- An App Store provisioning profile for that App ID.
- An application signing certificate installed in Keychain, usually named `Apple Distribution: ...` or `3rd Party Mac Developer Application: ...`.
- An installer signing certificate installed in Keychain, usually named `3rd Party Mac Developer Installer: ...`. Apple may label this certificate type as Mac Installer Distribution in the developer portal or Xcode.
The app only needs these sandbox entitlements right now:
- `com.apple.security.app-sandbox`
- `com.apple.security.files.user-selected.read-write`
Do not add network, Apple Events, Downloads-folder, or bookmark entitlements unless the app gains a feature that requires them.
## Build The Upload Package
Download the App Store provisioning profile from Apple Developer, then run:
```sh
APP_SIGNING_IDENTITY="3rd Party Mac Developer Application: Your Name (TEAMID)" \
INSTALLER_SIGNING_IDENTITY="3rd Party Mac Developer Installer: Your Name (TEAMID)" \
PROVISIONING_PROFILE="$HOME/Downloads/IHatePDFs_AppStore.provisionprofile" \
scripts/make-app-store-pkg.sh
```
The package is written to `dist/IHatePDFs-v0.4-macos-appstore.pkg`.
The script derives the App Store application identifier and team identifier from the provisioning profile before signing. It builds the App Store app in a temporary staging directory, so the direct-download `dist/I Hate PDFs.app` remains a clean app bundle without an embedded provisioning profile. It also clears download quarantine metadata from the staged bundle before packaging, because App Store Connect rejects packages that contain quarantine extended attributes.
If macOS opens a Keychain private-key access prompt during `codesign`, approve it, preferably with Always Allow for the selected signing certificate, and rerun the command. The build cannot finish unattended until the private key for the selected application signing certificate is allowed.
Before uploading, verify that the package matches the current build number:
```sh
REQUIRE_APP_STORE_PKG=1 scripts/verify-release-artifacts.sh
```
This catches stale package files, bundle-ID mismatches, missing embedded provisioning profiles, missing sandbox/user-selected-file entitlements, and app/package version mismatches.
Use `pkgutil --check-signature` and App Store Connect or Transporter validation for this App Store package. A local `spctl -t install` assessment is a Developer ID distribution check and may reject a package signed with the Mac App Store `3rd Party Mac Developer Installer` identity even when the package signature is valid for App Store upload.
## Upload
Upload the `.pkg` with Transporter. You can also set `VALIDATE_WITH_ALTOOL=1` when running `scripts/make-app-store-pkg.sh` if you want the script to perform an `altool` validation after packaging. After App Store Connect processes the build, select it in the app version, finish metadata, answer App Privacy, fill review notes, and submit for review.
Keep `CFBundleShortVersionString` as `0.4.0` and `CFBundleVersion` as `6` for the next upload. Increment `BUILD_NUMBER` in `scripts/release-version.sh` before uploading another build for the same version.

View File

@@ -1,97 +0,0 @@
# App Store Copy
Use this as the source of truth when filling out App Store Connect for `net.akkolli.ihatepdfs`.
## URLs
- Marketing URL: `https://www.akkolli.net/ihatepdfs`
- Support URL: `https://www.akkolli.net/ihatepdfs`
- Privacy Policy URL: `https://www.akkolli.net/ihatepdfs/privacy`
## Copyright
`2026 Akshay Kolli`
## App Information
- Name: `I Hate PDFs`
- Subtitle: `A small PDF review app`
- Category: `Productivity`
- Secondary category: `Education`
## Promotional Text
Read, highlight, comment, and review local PDFs without accounts, cloud upload, or heavyweight document management.
## Description
I Hate PDFs is a small native macOS app for reading and reviewing local PDF files.
Open a PDF, highlight important text, add comments, write free-text notes, and review everything from a compact comments sidebar. The app writes standard PDF annotations so your saved files can be opened in common PDF readers like Preview and Adobe Acrobat.
The app is intentionally lightweight. It uses native macOS document behavior, keeps your PDFs on your Mac, and does not require an account.
Features:
- Open local PDF files from disk, Finder, drag and drop, or recent documents.
- Read with native PDFKit scrolling, zoom, fit-to-width, fit-to-page, page navigation, and search.
- Highlight selected text without opening an unnecessary comment box.
- Add selected-text comments, underline comments, and free-text notes.
- Press Return to save comments and Shift-Return to insert a new line.
- Review annotations in a comments sidebar with search, filters, replies, edit, delete, and click-to-navigate.
- Customize highlight and comment colors from Settings.
- Save annotations directly into a PDF after an overwrite warning, or use Save As for a separate annotated copy.
- Share the saved PDF through the native macOS share sheet.
Privacy:
I Hate PDFs does not collect analytics, does not require sign-in, and does not upload your documents. Your PDFs stay on your Mac unless you choose to share them.
## Keywords
pdf,reader,annotate,highlight,comments,review,professor,academic,documents,notes
## What's New
Keeps the app focused on fast local PDF reading and annotation, with recent PDFs, focused open behavior, sidebar polish, bookmarks, highlight sorting, and Settings for annotation colors.
## App Review Notes
I Hate PDFs is a local macOS PDF reader and annotation utility.
No account is required. The reviewer can test with any local `.pdf` file. The app asks for user-selected file access only when opening, saving, or sharing a PDF.
Suggested review path:
1. Launch the app.
2. Open or drag in any PDF.
3. Select text and add a highlight or comment.
4. Open the comments sidebar to review annotations.
5. Save As to create an annotated copy.
6. Open Settings with Command-, or File > Settings... to change annotation colors.
7. Open Bookmarks and Highlights from the sidebar controls to verify the review views.
## App Privacy Answers
Recommended App Privacy summary:
- Data collection: no data collected.
- Tracking: no tracking.
- Third-party advertising: no.
- User account required: no.
The app works with user-selected local PDF files. It does not transmit documents to a server and does not include analytics.
## Screenshot Checklist
Use real bitmap screenshots, not drawn SVG mockups.
Required minimum set:
- Empty window with the Open PDF action and compact Recent PDFs area.
- Main reading view with a PDF page dominant.
- Highlight or comment editor popover on selected text.
- Comments sidebar with several annotations and at least one reply.
- Settings window showing color controls.
Capture light-mode screenshots first. Add dark-mode screenshots if they make the product quality clearer.

View File

@@ -1,39 +0,0 @@
# macOS Design Review
This review checks the current app against Apple's Human Interface Guidelines before a public release.
References:
- Apple HIG overview: https://developer.apple.com/design/human-interface-guidelines
- Designing for macOS: https://developer.apple.com/design/human-interface-guidelines/designing-for-macos
- Toolbars: https://developer.apple.com/design/human-interface-guidelines/toolbars
- Sidebars: https://developer.apple.com/design/human-interface-guidelines/sidebars
- Menus and the menu bar: https://developer.apple.com/design/human-interface-guidelines/menus and https://developer.apple.com/design/human-interface-guidelines/the-menu-bar
- Color: https://developer.apple.com/design/human-interface-guidelines/color
- Typography: https://developer.apple.com/design/human-interface-guidelines/typography
## Result
Status: Pass for the current version 1 implementation direction, with manual visual QA still required on physical Intel and Apple Silicon Macs before a tagged release.
## Checks
- Platform fit: The app is macOS-only, targets macOS 13 or newer, uses SwiftUI/AppKit/PDFKit, and ships as a normal `.app` bundle inside a `.dmg`.
- Window and toolbar: Primary document controls live in the titlebar toolbar, grouped by opening/sharing, navigation, zoom, annotation, search, and saving.
- Menus and shortcuts: File, View, and Annotate commands are available through native command menus with standard keyboard shortcuts where appropriate.
- Sidebars: Page thumbnails, annotation list, and comments review are optional sidebars. The default open-PDF state is single-pane reading, and sidebars open only when requested.
- Responsive layout: Compact windows use a compact toolbar/status treatment and keep sidebars mutually exclusive; regular and wide windows can show both sidebars while preserving a usable PDF reading width.
- Comments review: The comments sidebar uses a compact review-stream layout with a visible total count, add-comment affordance, collapsible page groups, hidden search/filter controls, and connected reply threads.
- Color and appearance: The UI uses system colors and materials, so light mode, dark mode, and automatic appearance inherit from macOS.
- Typography: Text uses system fonts and native SwiftUI controls; no custom brand typography is used in the reading interface.
- Reading focus: The PDF view remains the central, quiet surface; controls are compact and document-oriented.
- Accessibility basics: Native controls supply focus states and keyboard access; colors use system palettes with restrained highlight/note colors.
- Academic workflow: The open, select, highlight, comment, continue reading, save, and share path is available without accounts, sync, projects, or conversion.
## Release QA Still Required
- Run the app on both Apple Silicon and Intel hardware.
- Verify contrast and focus states in light and dark mode.
- Verify toolbar and sidebar behavior at compact, regular, and wide window sizes.
- Verify keyboard-only operation for opening, searching, navigating, annotating, saving, and reviewing comments.
- Verify VoiceOver labels for toolbar buttons and sidebar controls.

View File

@@ -1,72 +0,0 @@
# Engineering Principles
I Hate PDFs is intentionally a small native macOS app. Future work should preserve that constraint unless there is a documented, user-visible reason to do otherwise.
## Native First
- Build features with Swift, SwiftUI, AppKit, PDFKit, and other system frameworks that ship with macOS.
- Do not replace the app with Electron, Chromium, a web runtime, a bundled JavaScript app shell, or a cross-platform UI toolkit.
- Do not bundle a PDF renderer, OCR engine, database, scripting runtime, or large framework when a macOS system API can satisfy the requirement.
- Prefer native macOS controls and document behaviors over custom reimplementations when they meet the product need.
## Small By Default
Every change should aim for the smallest final app that still delivers the required fluidity, reliability, and functionality.
- Keep third-party dependencies at or near zero. Any new package must justify its shipped size, runtime cost, maintenance cost, and why system APIs are insufficient.
- Keep assets minimal. Avoid large raster images, fonts, sample PDFs, videos, model files, or generated resources in the app bundle.
- Keep build outputs out of source and releases unless they are intentional release artifacts.
- Prefer dynamic links to Apple system frameworks over vendored libraries.
- Avoid storing duplicate PDF data, rendered page caches, or annotation indexes unless profiling shows they are required for fluid interaction.
- Favor targeted updates over whole-document rescans for common interactions such as editing, replying, filtering, hovering, and sidebar refreshes.
## Size Budget
The release DMG should stay as small as practical. Treat size growth as a product regression, not just a packaging detail.
Hard release-size budget: each direct-download per-architecture installer must be under 400 KB, measured as fewer than 400,000 bytes. Universal builds may be larger, but they do not satisfy the small-download budget. Run `scripts/make-tiny-archives.sh` before release; it builds and checks `IHatePDFs-v<version>-macos-arm64.tar.xz` and `IHatePDFs-v<version>-macos-x86_64.tar.xz` by default.
Before merging release-impacting work, compare:
```sh
scripts/build-app.sh
scripts/make-dmg.sh
du -sh "dist/I Hate PDFs.app" \
"dist/I Hate PDFs.app/Contents/MacOS/IHatePDFs" \
"dist/I Hate PDFs.app/Contents/Resources/AppIcon.icns" \
dist/IHatePDFs-v*-macos.dmg
```
If a change materially increases the app bundle or DMG size, document why in the PR or commit notes. A useful rule of thumb: any dependency addition, bundled asset addition, or release-size increase above roughly 10% needs explicit justification.
## Performance Budget
Small size should not come at the expense of reader fluidity.
- Opening, scrolling, zooming, searching, annotating, saving, and sidebar navigation should remain responsive on long PDFs.
- Optimize around measured user workflows instead of speculative micro-optimizations.
- Keep expensive work page-scoped or lazy when possible.
- Use `swift test` plus the PDF verification scripts after behavior changes:
```sh
swift test
swift scripts/verify-sample-pdf.swift
swift scripts/verify-pdf-annotations.swift
```
## Release Packaging
Release builds should use the existing lightweight packaging path:
```sh
scripts/build-app.sh
scripts/make-dmg.sh
```
`scripts/build-app.sh` strips release binaries by default to reduce shipped size. Use `STRIP_RELEASE=0 scripts/build-app.sh` only when a symbol-rich release build is needed for debugging.
Universal `arm64` + `x86_64` builds are the default for public releases. Single-architecture builds are acceptable for local testing:
```sh
ARCHS="" scripts/build-app.sh
```

View File

@@ -1,35 +0,0 @@
# Functionality Audit
Date: 2026-06-29
## Current Build Scope
I Hate PDFs v0.4 is a small native macOS PDF reader and annotation app. The shipping scope is:
- Local PDF opening, recent documents, drag/drop, close-current-PDF, and focused reader startup.
- PDFKit reading, page navigation, zoom, fit controls, search, and responsive sidebars.
- Highlights, underline comments, selected-text comments, free text, replies, review state, filters, grouped comments, bookmarks, and highlight sorting.
- Settings for highlight and comment colors.
- Save, Save As, native Share, overwrite warnings, unsent reply-draft warnings, and empty temporary annotation cleanup.
- Lightweight release packaging with `.app`, `.dmg`, tiny per-architecture archives, and App Store package scripts.
## Removed Scope
The experimental Fill & Sign and PDF signing work has been removed from source, tests, scripts, settings, menus, and release docs for v0.4. This includes custom flat fill marks, form-field scanning/navigation, form choice editing, Keychain-backed PDF signing, signature validation/inspection, and signed-document save branching.
## Verification
Run before release:
```sh
swift build
swift test
swift scripts/verify-sample-pdf.swift
swift scripts/verify-pdf-annotations.swift
scripts/build-app.sh
scripts/make-dmg.sh
scripts/make-tiny-archives.sh
scripts/verify-release-artifacts.sh
```
Manual QA remains documented in `docs/QA.md`.

View File

@@ -2,6 +2,18 @@
Run this checklist before tagging a public release.
## Workflow Contract
Use this section as the source of truth when checking whether a feature matches the product workflow before changing or releasing it.
- Opened PDFs intentionally reset to focused single-pane reading with sidebars hidden.
- Plain Highlight is standalone.
- Selected-text Comment and Underline open the anchored editor.
- The visible Save toolbar icon is intentionally absent. Save remains available from the File menu and keyboard shortcut; Share remains visible.
- Compact windows intentionally make left and right sidebars mutually exclusive to preserve usable PDF width.
- Sidebar resize handles should be easy to grab through hover/cursor affordance without becoming large visual dividers.
- Experimental Fill & Sign, custom form-field navigation, PDF signing, signature inspection, and related QA are outside v0.4 scope.
## Latest v0.4 Automated QA Run
Completed on 2026-06-29:
@@ -12,7 +24,6 @@ Completed on 2026-06-29:
Before release, also run:
```sh
swift scripts/verify-sample-pdf.swift
swift scripts/verify-pdf-annotations.swift
swift build -c release --product IHatePDFs
scripts/build-app.sh
@@ -64,15 +75,24 @@ Use at least:
29. Start typing a sidebar reply without sending it, then close, replace, save, or share the PDF and verify the app warns before omitting or discarding the draft.
30. Create a new selected-text comment or free text, leave its popover empty, choose Save or Share, and verify the temporary empty annotation is discarded.
## Regression Coverage
- Focused reading layout after opening a PDF.
- Returning to the empty-window workflow after closing a PDF.
- Compact one-sidebar-at-a-time behavior.
- Page sidebar toggle closing the active Marks sidebar instead of switching to Pages first.
- Right-sidebar toolbar toggle closing and reopening the current right mode without switching tabs.
- Regular-width ability to show navigation and review sidebars together.
- Save availability for clean, dirty, and reply-draft-only states.
## External Readers
Before manual reader checks, run the automated PDF structure checks:
```sh
swift scripts/verify-sample-pdf.swift
swift scripts/verify-pdf-annotations.swift
```
These checks generate an annotated PDF, reopen it with PDFKit, and inspect raw PDF annotation dictionaries for standard `/Highlight`, `/Underline`, `/Text`, `/FreeText`, `/Contents`, `/QuadPoints`, `/IRT`, `/RT`, and `/Parent` entries.
This check generates an annotated PDF, reopens it with PDFKit, and inspects raw PDF annotation dictionaries for standard `/Highlight`, `/Underline`, `/Text`, `/FreeText`, `/Contents`, `/QuadPoints`, `/IRT`, `/RT`, and `/Parent` entries.
For Preview, Acrobat Reader, and browser PDF viewers, verify exported markup comments keep their comment text on the parent annotation's standard `/Contents` key and do not depend on PDFKit-generated `/Popup` links for highlights or underlines.

View File

@@ -23,7 +23,6 @@ Run these before tagging or uploading:
```sh
swift test
swift scripts/verify-sample-pdf.swift
swift scripts/verify-pdf-annotations.swift
scripts/build-app.sh
BUILD_APP=0 scripts/make-dmg.sh
@@ -34,13 +33,6 @@ scripts/verify-release-artifacts.sh
`scripts/make-tiny-archives.sh` builds per-architecture direct-download archives and
fails if either archive is `>= 400,000` bytes.
For signature changes, also run:
```sh
USE_TEMP_SIGNING_KEYCHAIN=1 scripts/verify-pdf-signatures.sh
scripts/prepare-acrobat-qa.sh
```
Finish the manual reader checks in `docs/QA.md` before public distribution.
## Direct Download
@@ -57,12 +49,39 @@ pollute source control.
## App Store
Use `docs/APP_STORE.md` for signing identities, provisioning profile setup, and the
upload package command. After building the App Store package, run:
Bundle ID: `net.akkolli.ihatepdfs`
Project website: `https://www.akkolli.net/ihatepdfs`
Support email: `akshaykolli@hotmail.com`
Required Apple Developer items:
- Explicit macOS App ID for `net.akkolli.ihatepdfs`.
- App Store provisioning profile for that App ID.
- Application signing certificate installed in Keychain.
- Installer signing certificate installed in Keychain.
The app only needs these sandbox entitlements right now:
- `com.apple.security.app-sandbox`
- `com.apple.security.files.user-selected.read-write`
Do not add network, Apple Events, Downloads-folder, or bookmark entitlements unless a shipped feature requires them.
Build the upload package:
```sh
APP_SIGNING_IDENTITY="3rd Party Mac Developer Application: Your Name (TEAMID)" \
INSTALLER_SIGNING_IDENTITY="3rd Party Mac Developer Installer: Your Name (TEAMID)" \
PROVISIONING_PROFILE="$HOME/Downloads/IHatePDFs_AppStore.provisionprofile" \
scripts/make-app-store-pkg.sh
```
After building the App Store package, run:
```sh
REQUIRE_APP_STORE_PKG=1 scripts/verify-release-artifacts.sh
```
Do not change App Store entitlements unless a shipped feature requires the new
capability.
Upload the `.pkg` with Transporter or App Store Connect tooling. Keep `APP_VERSION` and `BUILD_NUMBER` in `scripts/release-version.sh` aligned with App Store Connect before submitting.

View File

@@ -1,53 +0,0 @@
# Workflow Audit
Date: 2026-06-29
This file records the intended v0.4 user flow. It is the source of truth when checking whether a feature matches the product workflow before changing or releasing it.
## Current Capabilities
1. Open local PDFs from an open panel, drag/drop, recent documents, and file URLs.
2. Start each opened PDF in focused reading: sidebars hidden, PDF fit to available width, previous document sidebar state ignored.
3. Read with PDFKit scrolling, page navigation, zoom, fit width, fit page, two-page continuous view, and search.
4. Use a responsive layout across compact, regular, and wide Mac windows.
5. Use the left sidebar for page thumbnails and annotation marks.
6. Use the right sidebar for Comments, Highlights, and Bookmarks; the right-sidebar toolbar button is a visibility toggle for the active right mode.
7. Highlight selected text, use highlighter mode, and choose highlight colors.
8. Add selected-text comments, underline comments, and free-text annotations.
9. Use PDF-view shortcuts `H`, `U`, and `C` without conflicting with Command-C.
10. Edit/delete annotations and comment threads through anchored popovers and sidebar controls.
11. Review comments with search, filters, page grouping, collapsed groups, review state, replies, hover highlighting, edit/delete, and navigation.
12. Review highlights sorted by color or page.
13. Add, remove, and navigate per-document bookmarks.
14. Configure highlight and comment colors in Settings.
15. Save, Save As, Share, warn for unsent reply drafts, discard empty temporary editors, and warn before overwriting originals.
16. Package a small native app through the release scripts, DMG script, tiny archive script, and App Store package script.
## Removed From v0.4
The experimental Fill & Sign, custom form-field navigation, form choice popover, PDF signing, signature inspection, signed-document save safeguards, signature QA scripts, and related tests were removed from v0.4. Revisit that work later as a smaller design from scratch.
## Workflow Decisions
- Opened PDFs intentionally reset to focused single-pane reading with sidebars hidden. Opening a PDF should maximize the reading area and leave comments, highlights, bookmarks, page thumbnails, and annotation marks closed until the user asks for them.
- Plain Highlight is standalone. Selected-text Comment and Underline open the anchored editor.
- The visible Save toolbar icon is intentionally absent. Save remains available from the File menu and keyboard shortcut; Share remains visible.
- Compact windows intentionally make left and right sidebars mutually exclusive to preserve usable PDF width.
- Sidebar resize handles should be easy to grab through hover/cursor affordance without becoming large visual dividers.
## Regression Coverage
- Focused reading layout after opening a PDF.
- Returning to the empty-window workflow after closing a PDF.
- Compact one-sidebar-at-a-time behavior.
- Page sidebar toggle closing the active Marks sidebar instead of switching to Pages first.
- Right-sidebar toolbar toggle closing and reopening the current right mode without switching tabs.
- Regular-width ability to show navigation and review sidebars together.
- Save availability for clean, dirty, and reply-draft-only states.
## Manual QA Gaps
- Real Finder drag/drop, menu disabled states, visual Settings interaction, alert button flows, native share picker, popover text focus, and sidebar resize affordances need UI automation or manual QA.
- Preview, Acrobat Reader, and browser PDF-viewer checks remain external interoperability gates even though raw PDF structure checks exist.
- Cross-reader reply-thread display is not fully proven because PDFKit public APIs do not provide reliable object-valued `/IRT` writing. Primary annotation comments remain standard `/Contents`.
- Screenshot docs still need recapture before public marketing or release use.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 519 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 797 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 498 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 496 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 756 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 522 KiB

446
goal.md
View File

@@ -1,446 +0,0 @@
# Project Goal
Build an open-source macOS desktop application for professors to read, annotate, and share academic PDFs using standard PDF annotations that remain visible and interactive when opened by other people in common PDF readers.
The application must let a professor open a local PDF, add comments attached to selected text, highlights, and underlines, save those annotations directly into the PDF file, and share the resulting PDF with students, colleagues, or publishers. The recipient must be able to see the annotations and open the associated comment popups without needing this application.
## Core Requirement
Annotations must be written as standards-compliant PDF annotations, not stored in a separate database, sidecar file, hidden metadata format, or app-specific layer.
A PDF annotated in this app must preserve its comments when opened in:
- macOS Preview
- Adobe Acrobat Reader
- Common browser PDF viewers where supported
## Primary Users
The primary user is a professor reading academic PDFs such as journal articles, book chapters, working papers, syllabi, dissertations, and scanned course readings.
The professor needs to:
- Read PDFs comfortably on macOS
- Highlight passages
- Attach explanatory comments to highlighted text
- Attach comments to selected text from the toolbar, comments sidebar, or right-click menu
- Save and share the annotated PDF
- Trust that another person can open the file and see the comments
## Platform
The first version must run on macOS only.
Minimum supported version should be explicitly chosen before development. Recommended target:
- macOS 13 Ventura or newer
- Apple Silicon and Intel Macs
- Distributed as a downloadable `.dmg`
- Open-source repository with build instructions
## License
The project is licensed under the GNU General Public License version 2.
## Required Features
## Design and macOS User Experience
The app must feel like a polished, modern macOS application, not a generic cross-platform document viewer.
The user interface must follow Apple's Human Interface Guidelines for macOS where practical, including native-feeling window behavior, toolbar placement, sidebar behavior, menus, keyboard shortcuts, typography, spacing, focus states, and system color usage.
The app must be aesthetically restrained, calm, and pleasant for long academic reading sessions. The visual design should prioritize readability, focus, and low cognitive load over decorative styling.
The app must support:
- Light mode
- Dark mode
- Automatic appearance matching the user's macOS system setting
- Native macOS toolbar behavior
- Native macOS menu bar commands
- Native macOS keyboard shortcuts where applicable
- Smooth scrolling and zooming
- Crisp rendering on Retina displays
- Clear hover, selected, active, disabled, and focus states
- Accessible contrast for text, icons, controls, highlights, and annotation markers
- Subdued annotation colors that remain visible without looking like high-contrast demo markup
The app should use system typography, system colors, native controls, and familiar macOS interaction patterns unless there is a specific reason not to.
The PDF reading area must be visually quiet. Controls should not distract from the document. Toolbars, sidebars, popovers, and annotation panels must be useful, compact, and consistent with macOS conventions.
Annotation markers, comment icons, reply icons, and sidebar selection states must match the restrained macOS visual theme in both light mode and dark mode. They should be legible, but not harsh, neon, or visually louder than the PDF text.
The app must avoid:
- Cluttered toolbars
- Unnecessary branding inside the reading interface
- Bright or harsh color palettes
- Oversaturated comment/reply colors that clash with native macOS light or dark appearance
- Web-app-style controls that feel out of place on macOS
- Decorative gradients, oversized cards, or marketing-style layouts
- Custom UI patterns that conflict with standard macOS behavior
- Animations that slow down reading, annotation, or navigation
The app should feel appropriate for professors, researchers, graduate students, and academic professionals who spend long periods reading dense documents.
## Current UI Audit and Revised Design Direction
The current implementation is functionally promising but visually and interaction-wise off target. The screenshot evidence shows the app behaving more like a debug/admin interface around a PDF than a polished academic reading tool. The core PDF interoperability work remains valuable, but the user experience must be redesigned before the app can be considered complete.
### Current UI Problems
The current UI is wrong in these specific ways:
- Too much chrome is open by default. The page thumbnail sidebar and comments sidebar both appear immediately, leaving the PDF squeezed between panels.
- The default view does not prioritize reading. The PDF should dominate the window; sidebars should support the reading task, not frame the entire experience at all times.
- The comments sidebar is too dashboard-like. Search fields, segmented filters, type pickers, author pickers, status pickers, refresh buttons, and grouped metadata are all visible before the user asks for a review workflow.
- The app exposes a refresh button in the comments panel, which makes comments feel stale or manually synchronized. Comments must update live when annotations are created, edited, deleted, or selected.
- The comment creation flow feels like filling out a form. Adding a comment to selected text or a highlight should feel anchored, immediate, lightweight, and dismissible.
- It is unclear where to type a comment after selecting text. The app needs an obvious anchored comment popover, not a separate form-like editor whose relationship to the highlighted text is ambiguous.
- The comment workflow is too indirect. A professor should be able to select text, click Comment, immediately type in context, press Return or click away, and continue reading. Plain Highlight should stay a fast standalone marking action.
- The comments sidebar does not visibly update as part of the annotation action. A newly created or edited comment should appear immediately without requiring refresh.
- The PDF margins and surrounding gray space feel accidental and oversized. Page spacing should be tuned for reading: enough separation to orient the user, but not large dead zones.
- Toolbar controls are too dense and visually equal. Primary actions, reading controls, annotation tools, search, and save are competing for attention.
- Sidebars feel heavy. They use too much width and visual weight for routine reading.
- The right comments sidebar is open even when there is only one comment or no active review task.
- The left sidebar shows large empty vertical regions and thumbnail spacing that make the app feel unfinished.
- The annotation list and comments review views duplicate concepts without clear mode distinction.
- The selected text context menu can obscure the annotation workflow; the app should make its own annotation affordance more obvious than the system text context menu.
- The visible layout does not feel calm enough for long academic reading sessions.
### Revised Product Design Principle
The app is primarily a reading surface, not an annotation database.
The default experience after opening a PDF must be:
- A large, centered, comfortable PDF reading area.
- No right comments sidebar by default.
- No left thumbnail sidebar by default unless the user explicitly opens it or the window is wide enough and the user has previously enabled it.
- A compact native toolbar with only the most important controls visible.
- Annotation tools that are obvious but not visually dominant.
- Icon-only controls should explain their action on hover with native macOS help/tooltips.
- Toolbar icons should be visually distinct without stacked custom arrows or ambiguous overlays.
- A comments review panel that appears only when requested, when clicking a comments button, or when entering review mode.
- Automatic live updates everywhere; no manual refresh button for comments.
### Target First-Run and Open-PDF Layout
When no PDF is open:
- Show a quiet native empty state with one primary "Open PDF" action.
- Do not show disabled annotation controls as a dominant visual element.
- Keep the window simple and restrained.
When a PDF is open:
- The PDF occupies the center and most of the window.
- The default layout is single-pane reading mode.
- The toolbar contains compact groups:
- Open/save/share
- page navigation
- zoom/fit
- annotation tools
- search
- sidebar toggles
- Two-page continuous view should stay available from the View menu and keyboard shortcut, not as a persistent toolbar icon.
- The thumbnail sidebar is hidden by default and opens with a sidebar button or keyboard shortcut.
- The comments review sidebar is hidden by default and opens with a comments button or keyboard shortcut.
- The app remembers whether the user last had sidebars open for that document/window size.
- On narrow windows, opening one sidebar should not automatically force both sidebars into view.
### Target Commenting Interaction
Commenting must be immediate, anchored, and selection-based.
There must not be two primary kinds of comment posts. A user-facing comment is a message attached to selected PDF text or to an existing text markup annotation. The app must not present "Add a comment" as a loose page-marker creation flow that waits for the user to click somewhere on the page.
Required highlight comment flow:
1. The user selects text.
2. The user clicks Highlight or presses the highlight shortcut.
3. The text is highlighted immediately.
4. A small native popover appears near the highlight or in the nearest margin.
5. The popover contains a focused comment text area with placeholder text such as "Add comment".
6. The user types a comment.
7. The comment is saved automatically when the popover closes, when focus leaves the popover, or when the user presses Command-Return.
8. The comments sidebar, if open, updates immediately.
9. The user continues reading without navigating away from the PDF.
The user must not have to understand a separate form, save button, manual refresh button, or detached editor window just to attach a comment to highlighted text.
Required selected-text comment flow:
1. The user selects text with the mouse.
2. The user clicks Comment, uses the comments sidebar add-comment affordance, or right-clicks and chooses Comment.
3. The selected text receives a restrained standard PDF text markup annotation.
4. A small anchored popover opens immediately for the comment text.
5. The comment appears in the comments sidebar as one normal comment row, not as a separate post type.
6. Hovering the sidebar comment temporarily highlights the referenced text in the PDF without navigating away from the current reading position.
7. The comment saves automatically and remains standard PDF annotation contents.
Standalone page-placement comments are not a version 1 commenting flow. The app may preserve existing annotations from other readers and may use standard PDF text annotations for replies where PDFKit requires them, but the ordinary "add comment" path must be selection-bound.
Required edit flow:
- Clicking an existing highlight, underline, selection-bound comment, reply, or free-text annotation opens the anchored comment popover.
- Editing is inline and live-updating.
- Deleting an annotation is available from the popover through a clear but secondary destructive action.
- The comments sidebar can also edit comments, but it is not the primary creation experience.
### Target Comments Sidebar
The comments sidebar is a review mode, not the default annotation input surface.
The sidebar should read like a clean document-review conversation stream, closer to a native comments/chat inspector than a database table. Comments should be easy to scan in sequence, with a clear comment icon or author marker, author/time metadata, the full comment text, compact reply actions, and visible thread structure for replies.
It should:
- Be hidden by default.
- Open from a comments toolbar button, View menu command, or keyboard shortcut.
- Show a compact header with total comment count.
- Update automatically as annotations change.
- Never require or expose a manual refresh button in ordinary use.
- Group comments by page.
- Show author, date, review state, and full comment text.
- Use the row's circular marker icon to differentiate comments, highlights, underlines, and replies; do not duplicate that icon in the metadata row.
- Let users change review state directly from a compact Reviewed/Not reviewed chip in each comment row.
- Include search and filters, but hide advanced filters behind a compact filter menu or disclosure control.
- Keep replies visually subordinate to their parent comment.
- Replies should appear in the comments sidebar thread and should not create additional visible page icons on the PDF.
- Draw subtle vertical connector lines that make reply threads visually clear, like a clean comments/chat section.
- Navigate to and select the associated PDF annotation when a parent comment or reply is clicked.
- Temporarily highlight the referenced PDF text when a comment row or reply row is hovered.
- Feel like a native macOS inspector/sidebar, not a web dashboard.
The comments sidebar may have a refresh command only as a hidden/debug or menu-level recovery action if PDFKit state becomes inconsistent. It must not be a primary visible control.
### Target Annotation Sidebar and Thumbnail Sidebar
The left sidebar must have clear purpose:
- Thumbnail mode is for navigation.
- Annotation list mode is for scanning annotations.
- The app should not show both the left navigation sidebar and right comments review sidebar by default.
- If both are open, widths must be compact and the PDF must remain the dominant visual element.
- Thumbnail spacing should be dense enough to feel native and useful.
- Empty sidebar regions should be avoided.
### Visual Density, Margins, and Reading Comfort
The PDF view should feel like a high-quality macOS document reader.
Requirements:
- Page margins and inter-page spacing must be deliberately tuned.
- Fit-to-width should use available space efficiently without huge dead zones.
- Actual-size mode should not be the default if it creates awkward margins for common slides or articles.
- The background around pages should be a subtle system color, not a visually heavy gray field.
- Sidebars should use compact row spacing and native materials.
- Buttons should use icon-only labels where the meaning is standard, with accessibility labels and tooltips.
- Search should not consume excessive toolbar width.
- Annotation colors should be readable and restrained.
- There should be no visible decorative branding inside the reading interface.
### Path to Fixing the Current UI
The UI must be fixed in this order:
1. Change the default open-PDF layout to single-pane reading mode with both sidebars hidden.
2. Add a compact comments button that toggles the comments review sidebar.
3. Remove the visible comments refresh button and make annotation changes update the sidebar automatically.
4. Replace the form-like comment editor with an anchored popover tied to the selected highlight, selection-bound comment, underline, or free-text annotation.
5. Keep highlight creation as a standalone action that does not open an editor.
6. Make selected-text comment creation available from the toolbar, comments sidebar, and right-click menu, then open the anchored comment popover immediately.
7. Tune PDF page margins, page-break spacing, and fit behavior so documents use available space well.
8. Simplify and regroup the toolbar around reading and annotation tasks.
9. Make advanced comment filters collapsible or menu-based.
10. Redesign comment rows to feel like a clean threaded review/chat stream, including visible connector lines for replies and click-to-navigate behavior on comments and replies.
11. Reduce sidebar widths and row spacing.
12. Add visual QA screenshots for:
- no document open
- PDF open in default reading mode
- highlight comment popover
- selected-text comment popover
- comments review sidebar open
- dark mode reading
13. Run a design review using real academic PDFs, lecture slides, scanned readings, and long journal articles.
### Revised UI Acceptance Standard
The app is not visually acceptable until a user can open a PDF and immediately understand:
- where the document is,
- how to highlight selected text,
- where to type the comment,
- how to close the comment and keep reading,
- how to reopen the comment,
- how to show or hide all comments,
- and how to save/share the annotated PDF.
- how comment replies belong to a parent comment in the review sidebar.
No default screen should make the user feel like they are managing a database of comments before they have started reading.
### 1. PDF Opening
The app must allow the user to open a local `.pdf` file from disk.
The app must support:
- Text-based PDFs
- Scanned/image-based PDFs
- Multi-page PDFs
- Large academic PDFs of at least 500 pages
### 2. Reading Interface
The app must provide:
- Page scrolling
- Zoom in and zoom out
- Fit to width
- Fit to page
- Page number navigation
- Search within selectable text PDFs
- Sidebar with page thumbnails
- Sidebar toggle
### 3. Annotation Types
The app must support at minimum:
- Highlight annotation with optional comment
- Selection-bound comment created from selected text, including right-click Comment
- Underline annotation with optional comment
- Free-text annotation placed directly on the page
The first version does not need standalone page-placement comments, drawing, shapes, stamps, audio annotations, collaboration, OCR, or AI features.
### 4. Comment Popups
For every annotation that has a comment, the user must be able to open a popup displaying the comment text.
The popup must support:
- Viewing the full comment
- Editing the comment
- Closing the popup
- Reopening the popup by clicking the annotation
When the PDF is saved and opened in another PDF reader, the comment must still be associated with the annotation and must be openable there.
### 5. Saving
The app must support:
- Save annotations into the original PDF
- Save As a new annotated copy
- Warn before overwriting the original file
- Preserve existing PDF content
- Preserve existing annotations from other PDF readers whenever possible
### 6. Interoperability
The app must not rely on proprietary annotation storage.
A successful export means:
- Highlighted text remains highlighted
- Selection-bound comments remain visible and readable as standard PDF annotations
- Comments remain readable as popup annotation contents
- The PDF can be emailed or uploaded to an LMS without losing comments
### 7. Annotation Sidebar
The app must include an annotation list showing:
- Page number
- Annotation type
- Author name
- Date created
- First line of comment text
Clicking an item in the list must navigate to that annotation.
### 8. Comments Review Sidebar
The app must include an Adobe Acrobat-style comments review sidebar for quickly reviewing and responding to annotations across the whole PDF.
This is separate from one-off annotation popups. It should provide a persistent document-level comments panel that can be opened beside the PDF reading area.
The comments sidebar must support:
- Total comment count for the current PDF
- Comments grouped by page
- Collapsible page groups
- Search within comments
- Filtering comments by annotation type, author, and status where practical
- Author name for each comment
- Date and time created or modified
- Full comment text, not only a truncated preview
- Reply threads attached to an existing annotation comment
- Adding a reply from the sidebar
- Editing the user's own comments from the sidebar
- Deleting the user's own comments from the sidebar
- Clicking a comment to navigate to the corresponding page and annotation
- Selecting the associated annotation in the PDF view when a comment is selected
Threaded replies should be saved using standards-compliant PDF annotation reply relationships where supported by PDFKit or the chosen PDF-writing layer. If full reply interoperability is limited by common PDF readers, the app must still preserve the primary annotation comment as standard PDF annotation contents and document the known limitations.
The comments sidebar should feel native to macOS: compact, quiet, keyboard-navigable, accessible, and suitable for long review sessions.
The app must not require AI features for comment review. Any future summary feature must be optional and out of scope for version 1 unless explicitly added later.
### 9. Professor-Focused Workflow
The app should make academic annotation fast.
Required workflow:
- Open PDF
- Select text
- Click highlight
- Type optional comment
- Continue reading
- Save annotated PDF
- Share file
The app should not require accounts, cloud sync, project setup, import libraries, or document conversion.
## Out of Scope for Version 1
The following are explicitly not required:
- Real-time collaboration
- Cloud storage
- User accounts
- LMS integration
- Citation management
- OCR
- AI summarization
- Handwriting recognition
- iPad support
- Windows/Linux support
- Browser extension
- Mobile app
- Custom proprietary comment system
## Acceptance Criteria
The project is complete when:
1. A professor can open a PDF on macOS, highlight text, add a comment, save the file, and reopen it with the annotation still present.
2. A second person can open that saved PDF in macOS Preview or Adobe Acrobat Reader and see the highlight and any selected-text comment text.
3. Selection-bound comments created in the app remain visible as standard PDF annotations in other readers.
4. Existing PDF text, images, layout, bookmarks, and prior annotations are not destroyed during save.
5. The app can be built from source using documented commands.
6. The GitHub repository includes installation instructions, development setup instructions, license, screenshots, and a basic roadmap.
7. The app visually fits on macOS, supports light and dark mode, uses native-feeling controls and keyboard shortcuts, and remains pleasant to use during long PDF reading sessions.
8. The app includes a persistent comments review sidebar that shows the document comment count, groups comments by page, supports search/filtering, supports replies where interoperable, highlights referenced text on hover, and navigates from a sidebar comment to the matching PDF annotation.
9. The app passes a design review against Apple's macOS Human Interface Guidelines before the first public release.
## One-Sentence Version
Build an open-source, polished, native-feeling macOS PDF reader and annotation app for professors that saves highlights, underlines, and selection-bound comments as standard embedded PDF annotations so annotated PDFs can be shared and viewed with pop-up comments in common PDF readers without requiring the app.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.7 KiB

View File

@@ -26,10 +26,10 @@ APP_DIR="${APP_DIR:-$DIST_DIR/$APP_NAME.app}"
CONTENTS_DIR="$APP_DIR/Contents"
MACOS_DIR="$CONTENTS_DIR/MacOS"
RESOURCES_DIR="$CONTENTS_DIR/Resources"
ICON_SOURCE="${ICON_SOURCE:-$ROOT_DIR/ihatepdf-profile-transparent.png}"
ICON_SOURCE="${ICON_SOURCE:-$ROOT_DIR/assets/app-icon.png}"
if [[ ! -f "$ICON_SOURCE" ]]; then
echo "Missing app icon source: $ICON_SOURCE" >&2
echo "Set ICON_SOURCE to the path of a transparent PNG icon (for example: $ROOT_DIR/ihatepdf-profile-transparent.png)." >&2
echo "Set ICON_SOURCE to the path of a transparent PNG icon (for example: $ROOT_DIR/assets/app-icon.png)." >&2
exit 1
fi

View File

@@ -9,7 +9,7 @@ BUNDLE_ID="${BUNDLE_ID:-net.akkolli.ihatepdfs}"
APP_SIGNING_IDENTITY="${APP_SIGNING_IDENTITY:-}"
INSTALLER_SIGNING_IDENTITY="${INSTALLER_SIGNING_IDENTITY:-}"
PROVISIONING_PROFILE="${PROVISIONING_PROFILE:-}"
ENTITLEMENTS_PATH="${ENTITLEMENTS_PATH:-$ROOT_DIR/Signing/IHatePDFs-AppStore.entitlements}"
ENTITLEMENTS_PATH="${ENTITLEMENTS_PATH:-$ROOT_DIR/signing/IHatePDFs-AppStore.entitlements}"
DIST_DIR="$ROOT_DIR/dist"
PKG_PATH="${PKG_PATH:-$DIST_DIR/IHatePDFs-v$RELEASE_VERSION-macos-appstore.pkg}"
VALIDATE_WITH_ALTOOL="${VALIDATE_WITH_ALTOOL:-0}"

View File

@@ -8,6 +8,7 @@ APP_NAME="I Hate PDFs"
DIST_DIR="$ROOT_DIR/dist"
STAGING_DIR="$DIST_DIR/tiny"
ARCHS_TO_BUILD="${ARCHS_TO_BUILD:-arm64 x86_64}"
PER_ARCH_INSTALLER_MAX_BYTES="${PER_ARCH_INSTALLER_MAX_BYTES:-400000}"
if ! command -v xz >/dev/null 2>&1; then
echo "xz is required to build size-gated tiny archives with architecture filters." >&2
@@ -39,6 +40,27 @@ compression_args_for_arch() {
esac
}
file_size() {
stat -f '%z' "$1"
}
verify_under_budget() {
local path="$1"
[[ -f "$path" ]] || {
echo "missing $path" >&2
exit 1
}
local bytes
bytes="$(file_size "$path")"
if (( bytes >= PER_ARCH_INSTALLER_MAX_BYTES )); then
echo "$path is $bytes bytes; per-architecture installer budget is < $PER_ARCH_INSTALLER_MAX_BYTES bytes" >&2
exit 1
fi
echo "OK: $path is $bytes bytes (< $PER_ARCH_INSTALLER_MAX_BYTES)."
}
for ARCH in $ARCHS_TO_BUILD; do
APP_DIR="$STAGING_DIR/$ARCH/$APP_NAME.app"
ARCHIVE_PATH="$DIST_DIR/IHatePDFs-v$RELEASE_VERSION-macos-$ARCH.tar.xz"
@@ -59,6 +81,5 @@ for ARCH in $ARCHS_TO_BUILD; do
| env XZ_OPT= xz "${XZ_ARGS[@]}" -c > "$ARCHIVE_PATH"
echo "Created $ARCHIVE_PATH"
verify_under_budget "$ARCHIVE_PATH"
done
"$ROOT_DIR/scripts/verify-release-size.sh"

View File

@@ -1,5 +1,16 @@
import AppKit
import CoreGraphics
import Foundation
import PDFKit
let defaultInputPath = "dist/annotation-verification.pdf"
let arguments = Array(CommandLine.arguments.dropFirst())
let inputURL = URL(fileURLWithPath: arguments.first ?? defaultInputPath)
let verificationDate = Date(timeIntervalSince1970: 1_797_000_000)
if arguments.isEmpty {
try generateVerificationPDF(at: inputURL)
}
struct AnnotationSummary {
var highlights = 0
@@ -49,9 +60,6 @@ enum VerificationError: Error, CustomStringConvertible {
}
}
let inputPath = CommandLine.arguments.dropFirst().first ?? "dist/annotation-verification.pdf"
let inputURL = URL(fileURLWithPath: inputPath)
guard let document = CGPDFDocument(inputURL as CFURL) else {
throw VerificationError.unreadablePDF(inputURL.path)
}
@@ -289,3 +297,164 @@ func requireTextKeys(
try requireString(in: dictionary, key: "T", page: page, index: index)
try requireString(in: dictionary, key: "M", page: page, index: index)
}
func generateVerificationPDF(at outputURL: URL) throws {
try FileManager.default.createDirectory(
at: outputURL.deletingLastPathComponent(),
withIntermediateDirectories: true
)
let page = PDFPage()
let document = PDFDocument()
document.insert(page, at: 0)
let highlight = PDFAnnotation(
bounds: CGRect(x: 72, y: 620, width: 260, height: 24),
forType: .highlight,
withProperties: nil
)
highlight.markupType = .highlight
highlight.color = NSColor(calibratedRed: 0.88, green: 0.72, blue: 0.46, alpha: 0.24)
highlight.quadrilateralPoints = quadPoints(width: 260, height: 24)
standardize(
highlight,
name: "verify-highlight",
contents: "This is a standards-compliant PDF highlight comment.",
author: "Professor"
)
page.addAnnotation(highlight)
let selectedTextComment = PDFAnnotation(
bounds: CGRect(x: 72, y: 594, width: 260, height: 22),
forType: .highlight,
withProperties: nil
)
selectedTextComment.markupType = .highlight
selectedTextComment.color = NSColor(calibratedRed: 0.88, green: 0.72, blue: 0.46, alpha: 0.10)
selectedTextComment.quadrilateralPoints = quadPoints(width: 260, height: 22)
standardize(
selectedTextComment,
name: "verify-selected-text-comment",
contents: "This selected-text comment is saved as standard parent annotation contents.",
author: "Professor"
)
_ = selectedTextComment.setValue("Comment", forAnnotationKey: PDFAnnotationKey(rawValue: "IHatePDFsKind"))
page.addAnnotation(selectedTextComment)
let underline = PDFAnnotation(
bounds: CGRect(x: 72, y: 570, width: 260, height: 24),
forType: .underline,
withProperties: nil
)
underline.markupType = .underline
underline.color = NSColor(calibratedRed: 0.48, green: 0.53, blue: 0.62, alpha: 0.56)
underline.quadrilateralPoints = quadPoints(width: 260, height: 24)
standardize(
underline,
name: "verify-underline",
contents: "This underline comment should remain openable.",
author: "Professor"
)
page.addAnnotation(underline)
let textNote = PDFAnnotation(
bounds: CGRect(x: 360, y: 620, width: 28, height: 28),
forType: .text,
withProperties: nil
)
textNote.iconType = .note
textNote.color = NSColor(calibratedRed: 0.64, green: 0.59, blue: 0.49, alpha: 0.90)
standardize(
textNote,
name: "verify-text-note",
contents: "This standard PDF text annotation remains visible in common PDF readers.",
author: "Professor"
)
page.addAnnotation(textNote)
let reply = PDFAnnotation(
bounds: CGRect(x: 402, y: 586, width: 24, height: 24),
forType: .text,
withProperties: nil
)
reply.iconType = .comment
reply.color = NSColor(calibratedRed: 0.52, green: 0.58, blue: 0.60, alpha: 0.88)
standardize(
reply,
name: "verify-reply",
contents: "This reply is saved as PDF reply data without drawing an extra page icon.",
author: "Reader"
)
_ = reply.setValue("verify-text-note", forAnnotationKey: PDFAnnotationKey(rawValue: "IRT"))
_ = reply.setValue("R", forAnnotationKey: PDFAnnotationKey(rawValue: "RT"))
reply.shouldDisplay = false
reply.shouldPrint = false
page.addAnnotation(reply)
let freeText = PDFAnnotation(
bounds: CGRect(x: 72, y: 500, width: 260, height: 50),
forType: .freeText,
withProperties: nil
)
freeText.font = NSFont.systemFont(ofSize: 13)
freeText.fontColor = NSColor(calibratedWhite: 0.22, alpha: 1)
freeText.color = NSColor(calibratedRed: 0.91, green: 0.86, blue: 0.75, alpha: 0.32)
freeText.alignment = .left
let border = PDFBorder()
border.lineWidth = 0.75
freeText.border = border
standardize(
freeText,
name: "verify-free-text",
contents: "Free text remains visible on the PDF page.",
author: "Professor"
)
page.addAnnotation(freeText)
guard document.write(to: outputURL) else {
fatalError("Unable to write \(outputURL.path)")
}
let reopened = PDFDocument(url: outputURL)!
let annotations = reopened.page(at: 0)!.annotations
precondition(annotations.contains { matches($0, .highlight) && $0.contents?.contains("highlight") == true })
precondition(annotations.contains { matches($0, .highlight) && $0.contents?.contains("selected-text comment") == true })
precondition(annotations.contains { matches($0, .underline) && $0.contents?.contains("underline") == true })
precondition(annotations.contains { matches($0, .text) && $0.contents?.contains("text annotation") == true })
precondition(annotations.contains { matches($0, .text) && $0.contents?.contains("reply") == true && !$0.shouldDisplay && !$0.shouldPrint })
precondition(annotations.contains { matches($0, .freeText) && $0.contents?.contains("Free text") == true })
}
func standardize(
_ annotation: PDFAnnotation,
name: String,
contents: String,
author: String
) {
annotation.contents = contents
annotation.userName = author
annotation.modificationDate = verificationDate
annotation.shouldDisplay = true
annotation.shouldPrint = true
_ = annotation.setValue(name, forAnnotationKey: .name)
_ = annotation.setValue(author, forAnnotationKey: .textLabel)
_ = annotation.setValue(verificationDate, forAnnotationKey: .date)
_ = annotation.setValue("D:20261215132000Z00'00'", forAnnotationKey: PDFAnnotationKey(rawValue: "CreationDate"))
_ = annotation.setValue("Unmarked", forAnnotationKey: PDFAnnotationKey(rawValue: "State"))
}
func quadPoints(width: CGFloat, height: CGFloat) -> [NSValue] {
[
NSValue(point: CGPoint(x: 0, y: height)),
NSValue(point: CGPoint(x: width, y: height)),
NSValue(point: CGPoint(x: 0, y: 0)),
NSValue(point: CGPoint(x: width, y: 0))
]
}
func matches(_ annotation: PDFAnnotation, _ subtype: PDFAnnotationSubtype) -> Bool {
guard let type = annotation.type else { return false }
let raw = subtype.rawValue
let normalized = raw.hasPrefix("/") ? String(raw.dropFirst()) : raw
return type == raw || type == normalized
}

View File

@@ -1,40 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
source "$ROOT_DIR/scripts/release-version.sh"
DIST_DIR="${DIST_DIR:-$ROOT_DIR/dist}"
PER_ARCH_INSTALLER_MAX_BYTES="${PER_ARCH_INSTALLER_MAX_BYTES:-400000}"
PER_ARCH_INSTALLER_EXTENSION="${PER_ARCH_INSTALLER_EXTENSION:-tar.xz}"
fail() {
echo "release size verification failed: $*" >&2
exit 1
}
file_size() {
stat -f '%z' "$1"
}
verify_under_budget() {
local path="$1"
[[ -f "$path" ]] || fail "missing $path"
local bytes
bytes="$(file_size "$path")"
if (( bytes >= PER_ARCH_INSTALLER_MAX_BYTES )); then
fail "$path is $bytes bytes; per-architecture installer budget is < $PER_ARCH_INSTALLER_MAX_BYTES bytes"
fi
echo "OK: $path is $bytes bytes (< $PER_ARCH_INSTALLER_MAX_BYTES)."
}
if (( $# > 0 )); then
for artifact in "$@"; do
verify_under_budget "$artifact"
done
else
verify_under_budget "$DIST_DIR/IHatePDFs-v$RELEASE_VERSION-macos-arm64.$PER_ARCH_INSTALLER_EXTENSION"
verify_under_budget "$DIST_DIR/IHatePDFs-v$RELEASE_VERSION-macos-x86_64.$PER_ARCH_INSTALLER_EXTENSION"
fi

View File

@@ -1,167 +0,0 @@
import AppKit
import Foundation
import PDFKit
let outputURL = URL(fileURLWithPath: CommandLine.arguments.dropFirst().first ?? "dist/annotation-verification.pdf")
try FileManager.default.createDirectory(
at: outputURL.deletingLastPathComponent(),
withIntermediateDirectories: true
)
let page = PDFPage()
let document = PDFDocument()
document.insert(page, at: 0)
let verificationDate = Date(timeIntervalSince1970: 1_797_000_000)
let highlight = PDFAnnotation(
bounds: CGRect(x: 72, y: 620, width: 260, height: 24),
forType: .highlight,
withProperties: nil
)
highlight.markupType = .highlight
highlight.color = NSColor(calibratedRed: 0.88, green: 0.72, blue: 0.46, alpha: 0.24)
highlight.quadrilateralPoints = quadPoints(width: 260, height: 24)
standardize(
highlight,
name: "verify-highlight",
contents: "This is a standards-compliant PDF highlight comment.",
author: "Professor"
)
page.addAnnotation(highlight)
let selectedTextComment = PDFAnnotation(
bounds: CGRect(x: 72, y: 594, width: 260, height: 22),
forType: .highlight,
withProperties: nil
)
selectedTextComment.markupType = .highlight
selectedTextComment.color = NSColor(calibratedRed: 0.88, green: 0.72, blue: 0.46, alpha: 0.10)
selectedTextComment.quadrilateralPoints = quadPoints(width: 260, height: 22)
standardize(
selectedTextComment,
name: "verify-selected-text-comment",
contents: "This selected-text comment is saved as standard parent annotation contents.",
author: "Professor"
)
_ = selectedTextComment.setValue("Comment", forAnnotationKey: PDFAnnotationKey(rawValue: "IHatePDFsKind"))
page.addAnnotation(selectedTextComment)
let underline = PDFAnnotation(
bounds: CGRect(x: 72, y: 570, width: 260, height: 24),
forType: .underline,
withProperties: nil
)
underline.markupType = .underline
underline.color = NSColor(calibratedRed: 0.48, green: 0.53, blue: 0.62, alpha: 0.56)
underline.quadrilateralPoints = quadPoints(width: 260, height: 24)
standardize(
underline,
name: "verify-underline",
contents: "This underline comment should remain openable.",
author: "Professor"
)
page.addAnnotation(underline)
let textNote = PDFAnnotation(
bounds: CGRect(x: 360, y: 620, width: 28, height: 28),
forType: .text,
withProperties: nil
)
textNote.iconType = .note
textNote.color = NSColor(calibratedRed: 0.64, green: 0.59, blue: 0.49, alpha: 0.90)
standardize(
textNote,
name: "verify-text-note",
contents: "This standard PDF text annotation remains visible in common PDF readers.",
author: "Professor"
)
page.addAnnotation(textNote)
let reply = PDFAnnotation(
bounds: CGRect(x: 402, y: 586, width: 24, height: 24),
forType: .text,
withProperties: nil
)
reply.iconType = .comment
reply.color = NSColor(calibratedRed: 0.52, green: 0.58, blue: 0.60, alpha: 0.88)
standardize(
reply,
name: "verify-reply",
contents: "This reply is saved as PDF reply data without drawing an extra page icon.",
author: "Reader"
)
_ = reply.setValue("verify-text-note", forAnnotationKey: PDFAnnotationKey(rawValue: "IRT"))
_ = reply.setValue("R", forAnnotationKey: PDFAnnotationKey(rawValue: "RT"))
reply.shouldDisplay = false
reply.shouldPrint = false
page.addAnnotation(reply)
let freeText = PDFAnnotation(
bounds: CGRect(x: 72, y: 500, width: 260, height: 50),
forType: .freeText,
withProperties: nil
)
freeText.font = NSFont.systemFont(ofSize: 13)
freeText.fontColor = NSColor(calibratedWhite: 0.22, alpha: 1)
freeText.color = NSColor(calibratedRed: 0.91, green: 0.86, blue: 0.75, alpha: 0.32)
freeText.alignment = .left
let border = PDFBorder()
border.lineWidth = 0.75
freeText.border = border
standardize(
freeText,
name: "verify-free-text",
contents: "Free text remains visible on the PDF page.",
author: "Professor"
)
page.addAnnotation(freeText)
guard document.write(to: outputURL) else {
fatalError("Unable to write \(outputURL.path)")
}
let reopened = PDFDocument(url: outputURL)!
let annotations = reopened.page(at: 0)!.annotations
precondition(annotations.contains { matches($0, .highlight) && $0.contents?.contains("highlight") == true })
precondition(annotations.contains { matches($0, .highlight) && $0.contents?.contains("selected-text comment") == true })
precondition(annotations.contains { matches($0, .underline) && $0.contents?.contains("underline") == true })
precondition(annotations.contains { matches($0, .text) && $0.contents?.contains("text annotation") == true })
precondition(annotations.contains { matches($0, .text) && $0.contents?.contains("reply") == true && !$0.shouldDisplay && !$0.shouldPrint })
precondition(annotations.contains { matches($0, .freeText) && $0.contents?.contains("Free text") == true })
print("Verified standard PDF annotations in \(outputURL.path)")
func standardize(
_ annotation: PDFAnnotation,
name: String,
contents: String,
author: String
) {
annotation.contents = contents
annotation.userName = author
annotation.modificationDate = verificationDate
annotation.shouldDisplay = true
annotation.shouldPrint = true
_ = annotation.setValue(name, forAnnotationKey: .name)
_ = annotation.setValue(author, forAnnotationKey: .textLabel)
_ = annotation.setValue(verificationDate, forAnnotationKey: .date)
_ = annotation.setValue("D:20261215132000Z00'00'", forAnnotationKey: PDFAnnotationKey(rawValue: "CreationDate"))
_ = annotation.setValue("Unmarked", forAnnotationKey: PDFAnnotationKey(rawValue: "State"))
}
func quadPoints(width: CGFloat, height: CGFloat) -> [NSValue] {
[
NSValue(point: CGPoint(x: 0, y: height)),
NSValue(point: CGPoint(x: width, y: height)),
NSValue(point: CGPoint(x: 0, y: 0)),
NSValue(point: CGPoint(x: width, y: 0))
]
}
func matches(_ annotation: PDFAnnotation, _ subtype: PDFAnnotationSubtype) -> Bool {
guard let type = annotation.type else { return false }
let raw = subtype.rawValue
let normalized = raw.hasPrefix("/") ? String(raw.dropFirst()) : raw
return type == raw || type == normalized
}