Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions .github/ISSUE_TEMPLATE/user_feedback.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
name: User feedback
description: Tell us how ImageTool worked for you
title: "[Feedback]: "
body:
- type: markdown
attributes:
value: Thanks for using ImageTool. Real usage feedback helps decide what to build next.
- type: dropdown
id: task
attributes:
label: What did you use ImageTool for?
options:
- Convert image formats
- Compress images
- Resize images
- Remove image metadata
- Offline use
- Something else
validations:
required: true
- type: textarea
id: outcome
attributes:
label: What worked or did not work?
description: Do not attach private or sensitive images.
validations:
required: true
- type: textarea
id: improvement
attributes:
label: What would make ImageTool more useful?
- type: input
id: browser
attributes:
label: Browser and device
placeholder: Edge on Windows 11, Safari on iPhone, etc.
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,16 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),

## [Unreleased]

## [0.3.0] - 2026-08-15

### Added

- Installable Progressive Web App manifest and icons
- Offline app shell powered by a privacy-preserving service worker
- Install prompt and online/offline status indicator
- Privacy-friendly system share control and structured user feedback form
- Automated checks for manifest and offline-cache completeness

## [0.2.0] - 2026-08-15

### Added
Expand Down
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ ImageTool converts PNG, JPG, and WebP images locally. Your files are never uploa
- Responsive interface for desktop and mobile
- Zero runtime dependencies
- Remove source metadata by re-encoding image pixels in the browser
- Install the app and use its core interface offline

## Try it locally

Expand All @@ -35,6 +36,12 @@ Then open <http://127.0.0.1:5173>.

No `npm install` step is required.

## Install for offline use

Open the [live ImageTool site](https://fish34851-hash.github.io/ImageTool/) in Chrome or Edge. Use the **Install app** control in the browser or the in-page install button when it appears.

After the first successful visit, the application shell can open without reaching the server. Image files and converted results remain temporary local browser data and are not added to the offline cache.

## Check the code

```bash
Expand All @@ -48,6 +55,8 @@ ImageTool uses the browser's Canvas API. Selected images stay on your device and

Re-encoding through Canvas produces a new image from pixel data, so source EXIF and other file metadata are not copied to the output.

The offline service worker caches only same-origin ImageTool application files. It does not intercept, upload, or store selected images.

## Roadmap

- Offline installation as a Progressive Web App
Expand All @@ -59,6 +68,8 @@ Re-encoding through Canvas produces a new image from pixel data, so source EXIF

Contributions are welcome. Read [CONTRIBUTING.md](CONTRIBUTING.md) before opening an issue or pull request.

Used ImageTool for a real task? [Share feedback](https://github.com/fish34851-hash/ImageTool/issues/new/choose) without uploading any private images. Reports from real users guide the roadmap.

Project documentation:

- [Roadmap](ROADMAP.md)
Expand Down
6 changes: 3 additions & 3 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,14 @@ This roadmap describes intended directions, not guaranteed deadlines. Priorities
- Quality controls and image preview
- Privacy-first, zero-dependency browser architecture

## In progress

### v0.2 — Batch workflow and maintainability

- Batch conversion for up to 20 images
- Proportional resizing
- Automated tests and pull-request checks
- Security, contribution, and community documentation

## Planned
## In progress

### v0.3 — Offline installation

Expand All @@ -28,6 +26,8 @@ This roadmap describes intended directions, not guaranteed deadlines. Priorities
- Clear update behavior and cache controls
- Browser-level offline tests

## Planned

### v0.4 — Editing and access

- Rotation and cropping
Expand Down
Binary file added assets/icon-192.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/icon-512.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/icon-maskable-512.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions assets/icon-maskable.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions assets/icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ ImageTool is a zero-dependency static web application. It can run from a local f
- `src/app.js` owns browser interaction, image decoding, Canvas rendering, and downloads.
- `src/core.js` contains deterministic formatting, filename, and resize calculations shared by the app and tests.
- `src/styles.css` provides the responsive visual system.
- `manifest.webmanifest` describes the installable application experience.
- `service-worker.js` caches only the same-origin application shell for offline use.
- `assets/` contains source SVG and rasterized PWA icons.
- `server.js` is an optional local development server built only with Node.js standard modules.

## Image data flow
Expand All @@ -23,6 +26,7 @@ ImageTool is a zero-dependency static web application. It can run from a local f

- Selected images must never be uploaded or included in a network request.
- The application must not add analytics, advertising, remote scripts, or remote fonts.
- Offline caching must remain limited to same-origin application assets and must not cache selected images or converted results.
- Output files are created from decoded pixels; source EXIF and other file metadata are not copied.
- Features that would weaken these guarantees require prior public discussion and documentation.

Expand Down
24 changes: 17 additions & 7 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="ImageTool 是一个完全在浏览器本地运行的批量图片转换与尺寸调整工具。" />
<meta name="theme-color" content="#f5f1e8" />
<link rel="manifest" href="./manifest.webmanifest" />
<link rel="icon" href="./assets/icon.svg" type="image/svg+xml" />
<link rel="stylesheet" href="./src/styles.css" />
<title>ImageTool — 私密的本地图片工具</title>
</head>
Expand All @@ -15,12 +17,15 @@
<span class="brand-mark">I</span>
<span>ImageTool</span>
</a>
<div class="privacy-pill">
<svg viewBox="0 0 24 24" aria-hidden="true">
<rect x="5" y="10" width="14" height="10" rx="2"></rect>
<path d="M8.5 10V7.5a3.5 3.5 0 0 1 7 0V10"></path>
</svg>
<span>100% 本地处理</span>
<div class="header-actions">
<button class="install-button hidden" id="install-button" type="button">安装应用</button>
<div class="privacy-pill">
<svg viewBox="0 0 24 24" aria-hidden="true">
<rect x="5" y="10" width="14" height="10" rx="2"></rect>
<path d="M8.5 10V7.5a3.5 3.5 0 0 1 7 0V10"></path>
</svg>
<span>100% 本地处理</span>
</div>
</div>
</header>

Expand Down Expand Up @@ -125,7 +130,12 @@ <h2>把图片拖到这里</h2>

<footer>
<span>ImageTool · 你的图片只属于你</span>
<a href="https://github.com/fish34851-hash/ImageTool">GitHub</a>
<div class="footer-links">
<span class="network-status" id="network-status">在线</span>
<button class="footer-button" id="share-button" type="button">分享</button>
<a href="https://github.com/fish34851-hash/ImageTool/issues/new/choose">反馈</a>
<a href="https://github.com/fish34851-hash/ImageTool">GitHub</a>
</div>
</footer>
</div>

Expand Down
45 changes: 45 additions & 0 deletions manifest.webmanifest
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
{
"name": "ImageTool — Private Image Converter",
"short_name": "ImageTool",
"description": "Batch-convert and resize images privately in your browser.",
"lang": "zh-CN",
"id": "./",
"start_url": "./",
"scope": "./",
"display": "standalone",
"prefer_related_applications": false,
"background_color": "#f5f1e8",
"theme_color": "#2e4a37",
"icons": [
{
"src": "./assets/icon-192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any"
},
{
"src": "./assets/icon-512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any"
},
{
"src": "./assets/icon-maskable-512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "./assets/icon.svg",
"sizes": "any",
"type": "image/svg+xml",
"purpose": "any"
},
{
"src": "./assets/icon-maskable.svg",
"sizes": "any",
"type": "image/svg+xml",
"purpose": "maskable"
}
]
}
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
{
"name": "imagetool",
"private": true,
"version": "0.2.0",
"version": "0.3.0",
"description": "A private, browser-based image converter with zero dependencies.",
"scripts": {
"dev": "node server.js",
"check": "node --check src/core.js && node --check src/app.js && node --check server.js",
"check": "node --check src/core.js && node --check src/app.js && node --check service-worker.js && node --check server.js",
"test": "node --test tests/*.test.js"
},
"keywords": ["image", "converter", "privacy", "png", "jpeg", "webp"],
Expand Down
2 changes: 2 additions & 0 deletions server.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ const contentTypes = {
'.html': 'text/html; charset=utf-8',
'.js': 'text/javascript; charset=utf-8',
'.json': 'application/json; charset=utf-8',
'.webmanifest': 'application/manifest+json; charset=utf-8',
'.svg': 'image/svg+xml',
'.png': 'image/png',
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
Expand Down
63 changes: 63 additions & 0 deletions service-worker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
const CACHE_NAME = 'imagetool-v0.3.0'
const APP_SHELL = [
'./',
'./index.html',
'./manifest.webmanifest',
'./assets/icon-192.png',
'./assets/icon-512.png',
'./assets/icon-maskable-512.png',
'./assets/icon.svg',
'./assets/icon-maskable.svg',
'./src/styles.css',
'./src/core.js',
'./src/app.js',
]

self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME)
.then((cache) => cache.addAll(APP_SHELL))
.then(() => self.skipWaiting()),
)
})

self.addEventListener('activate', (event) => {
event.waitUntil(
Promise.all([
caches.keys().then((keys) => Promise.all(
keys
.filter((key) => key.startsWith('imagetool-') && key !== CACHE_NAME)
.map((key) => caches.delete(key)),
)),
self.clients.claim(),
]),
)
})

self.addEventListener('fetch', (event) => {
const request = event.request
const url = new URL(request.url)
if (request.method !== 'GET' || url.origin !== self.location.origin) return

if (request.mode === 'navigate') {
event.respondWith(
fetch(request)
.then((response) => {
const copy = response.clone()
caches.open(CACHE_NAME).then((cache) => cache.put('./index.html', copy))
return response
})
.catch(() => caches.match('./index.html')),
)
return
}

event.respondWith(
caches.match(request).then((cached) => cached || fetch(request).then((response) => {
if (!response.ok) return response
const copy = response.clone()
caches.open(CACHE_NAME).then((cache) => cache.put(request, copy))
return response
})),
)
})
Loading