diff --git a/README.md b/README.md index d8ac7ed..a94b5cc 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@

Free, private tools for PDF, images, audio, Word, Excel and PowerPoint that never see your files.

- 69 tools — compress, convert, edit, redact, remove backgrounds, edit photos, merge, split, cut, OCR, transcribe — running 100% in your browser.
+ 70 tools — compress, convert, edit, redact, remove backgrounds, edit photos, make collages, merge, split, cut, OCR, transcribe — running 100% in your browser.
No uploads. No sign-up. No watermark. No limits. Free forever. 16 languages.

@@ -16,7 +16,7 @@

Open Fizzdoc →  ·  - All tools  ·  + All tools  ·  How it works  ·  Contribute

@@ -27,7 +27,7 @@ Apache-2.0 license 0 bytes uploaded Free forever - 69 tools + 70 tools 16 languages PRs welcome

@@ -53,7 +53,7 @@ Most free online file tools ask you to upload your contract, payslip, bank state | What you get | | |---|---| | 🔒 **Your file stays on your device** | No upload endpoint exists, and the CSP blocks connections to any other site | -| 🧰 **69 tools in one place** | PDF, Word, Excel, PowerPoint, images, audio, JSON and Mermaid | +| 🧰 **70 tools in one place** | PDF, Word, Excel, PowerPoint, images, audio, JSON and Mermaid | | ✏️ **Edit PDF text in its own font** | Changed lines are rewritten inside the PDF with its embedded font, size and colour | | 🎙️ **Speech to text on the device** | Whisper runs in the browser: TXT, SRT or VTT, offline after a one-time download | | 🆓 **Free forever** | No account, no daily limits, no watermark, no premium tier | @@ -111,6 +111,7 @@ Most free online file tools ask you to upload your contract, payslip, bank state | **Remove the password** from a bank statement, or add one | [Unlock PDF](https://fizzdoc.com/unlock-pdf/) · [Protect PDF](https://fizzdoc.com/protect-pdf/) | | **Remove the background** from a photo, or put white, a colour, a blur or another photo behind, at full resolution | [Remove Background](https://fizzdoc.com/remove-background/) | | **Edit a photo**: crop it square or to story size, fix the light and colour, add a look, text or emoji | [Photo Editor](https://fizzdoc.com/photo-editor/) | +| **Make a collage** of 2 to 9 photos for a post, a story or an A4 print | [Collage Maker](https://fizzdoc.com/collage-maker/) | | **Copy text** from a scanned PDF, a photo or a screenshot | [OCR PDF](https://fizzdoc.com/ocr-pdf/) · [Image to Text](https://fizzdoc.com/image-to-text/) | | Combine **photos or screenshots into one PDF** | [JPG to PDF](https://fizzdoc.com/jpg-to-pdf/) · [PNG to PDF](https://fizzdoc.com/png-to-pdf/) | | Convert **everyday formats**: PNG ↔ JPG, WebP → JPG, Excel ↔ CSV | [Convert Image](https://fizzdoc.com/convert-image/) · [Excel to CSV](https://fizzdoc.com/excel-to-csv/) · [CSV to Excel](https://fizzdoc.com/csv-to-excel/) | @@ -118,7 +119,7 @@ Most free online file tools ask you to upload your contract, payslip, bank state | Export a **Mermaid diagram as PNG or SVG** for slides, docs or a README | [Mermaid to PNG](https://fizzdoc.com/mermaid-to-png/) · [Mermaid to SVG](https://fizzdoc.com/mermaid-to-svg/) | | **Remove hidden author and company names** before sharing an Office file or PDF | [Word](https://fizzdoc.com/remove-word-metadata/) · [Excel](https://fizzdoc.com/remove-excel-metadata/) · [PowerPoint](https://fizzdoc.com/remove-powerpoint-metadata/) · [PDF](https://fizzdoc.com/remove-pdf-metadata/) | -## 🧰 All 69 tools +## 🧰 All 70 tools | Format | Tools | |---|---| @@ -127,7 +128,7 @@ Most free online file tools ask you to upload your contract, payslip, bank state | **Word** | [Word → PDF](https://fizzdoc.com/word-to-pdf/) · [Compress](https://fizzdoc.com/compress-word/) · [Remove metadata](https://fizzdoc.com/remove-word-metadata/) · [Extract images](https://fizzdoc.com/extract-images-from-word/) | | **Excel** | [Excel → CSV](https://fizzdoc.com/excel-to-csv/) · [CSV → Excel](https://fizzdoc.com/csv-to-excel/) · [Excel → JSON](https://fizzdoc.com/excel-to-json/) · [JSON → Excel](https://fizzdoc.com/json-to-excel/) · [Compress](https://fizzdoc.com/compress-excel/) · [Remove metadata](https://fizzdoc.com/remove-excel-metadata/) · [Extract images](https://fizzdoc.com/extract-images-from-excel/) | | **PowerPoint** | [Compress](https://fizzdoc.com/compress-powerpoint/) · [Remove metadata](https://fizzdoc.com/remove-powerpoint-metadata/) · [Extract images](https://fizzdoc.com/extract-images-from-powerpoint/) | -| **Images** | [Remove background](https://fizzdoc.com/remove-background/) (AI on your device, full resolution) · [Remove GIF background](https://fizzdoc.com/remove-gif-background/) · [Remove video background](https://fizzdoc.com/remove-video-background/) (no green screen, sound kept) · [WhatsApp sticker maker](https://fizzdoc.com/whatsapp-sticker-maker/) (text in fun fonts and emoji you drag into place) · [Photo editor](https://fizzdoc.com/photo-editor/) (crop to square or story size, turn, light and colour, looks, text and emoji) · [Compress](https://fizzdoc.com/compress-image/) (to [20 KB](https://fizzdoc.com/compress-image-to-20kb/), [50 KB](https://fizzdoc.com/compress-image-to-50kb/), [100 KB](https://fizzdoc.com/compress-image-to-100kb/) for forms) · [Resize](https://fizzdoc.com/resize-image/) (larger or smaller) · [Convert](https://fizzdoc.com/convert-image/) · [PNG → JPG](https://fizzdoc.com/png-to-jpg/) · [JPG → PNG](https://fizzdoc.com/jpg-to-png/) · [WebP → JPG](https://fizzdoc.com/webp-to-jpg/) · [JPG → WebP](https://fizzdoc.com/jpg-to-webp/) · [Image → Text](https://fizzdoc.com/image-to-text/) (select text right on the photo, like Live Text) · [Mermaid → PNG](https://fizzdoc.com/mermaid-to-png/) · [Mermaid → SVG](https://fizzdoc.com/mermaid-to-svg/) (diagrams from code, with a preview and ready-to-use examples) | +| **Images** | [Remove background](https://fizzdoc.com/remove-background/) (AI on your device, full resolution) · [Remove GIF background](https://fizzdoc.com/remove-gif-background/) · [Remove video background](https://fizzdoc.com/remove-video-background/) (no green screen, sound kept) · [WhatsApp sticker maker](https://fizzdoc.com/whatsapp-sticker-maker/) (text in fun fonts and emoji you drag into place) · [Photo editor](https://fizzdoc.com/photo-editor/) (crop to square or story size, turn, light and colour, looks, text and emoji) · [Collage maker](https://fizzdoc.com/collage-maker/) (2 to 9 photos, square, story or A4) · [Compress](https://fizzdoc.com/compress-image/) (to [20 KB](https://fizzdoc.com/compress-image-to-20kb/), [50 KB](https://fizzdoc.com/compress-image-to-50kb/), [100 KB](https://fizzdoc.com/compress-image-to-100kb/) for forms) · [Resize](https://fizzdoc.com/resize-image/) (larger or smaller) · [Convert](https://fizzdoc.com/convert-image/) · [PNG → JPG](https://fizzdoc.com/png-to-jpg/) · [JPG → PNG](https://fizzdoc.com/jpg-to-png/) · [WebP → JPG](https://fizzdoc.com/webp-to-jpg/) · [JPG → WebP](https://fizzdoc.com/jpg-to-webp/) · [Image → Text](https://fizzdoc.com/image-to-text/) (select text right on the photo, like Live Text) · [Mermaid → PNG](https://fizzdoc.com/mermaid-to-png/) · [Mermaid → SVG](https://fizzdoc.com/mermaid-to-svg/) (diagrams from code, with a preview and ready-to-use examples) | | **Audio** | [Cut & Split Audio](https://fizzdoc.com/split-audio/) (trim or split an MP3/M4A on a waveform, preview every part) · [Merge Audio](https://fizzdoc.com/merge-audio/) (listen before you download) — no re-encoding, so no quality loss · [Audio to Text](https://fizzdoc.com/audio-to-text/) · [MP3 to Text](https://fizzdoc.com/mp3-to-text/) · [Video to Text](https://fizzdoc.com/video-to-text/) · [Subtitle Generator](https://fizzdoc.com/subtitle-generator/) (SRT/VTT) — speech to text with OpenAI's Whisper running on your device; the model downloads once, then works offline | **Google Docs, Sheets and Slides** work too: download as .docx / .xlsx / .pptx and use the matching tool. Nothing is sent to Google. diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 6b94195..b76c240 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -86,6 +86,7 @@ sequenceDiagram | `src/tools/edit-pdf.ts` | In-page PDF editor: change text, add text, white-out | pdf.js, pdf-lib, `pdf-text.ts` | | `src/tools/ocr-viewer.ts` | Image with selectable recognized text (Live Text style) | – | | `src/tools/photo-edit.ts`, `src/engine/photo.ts` | Photo Editor: crop shapes with drag-to-frame, turn and flip, light and colour as plain pixel maths, one-tap looks, text and emoji layers (shared with the Sticker Maker), saved at full size | Canvas | +| `src/tools/collage.ts` | Collage Maker: 1 to 9 photos in equal-cell layouts (`cells`/`place` in `photo.ts`), each moved and zoomed in its frame; previews use small copies, saving decodes one photo at a time at full size. Shares its tabs, rulers and icons with the Photo Editor (`editor-ui.ts`) | Canvas | ## Design decisions diff --git a/src/app.ts b/src/app.ts index a77aa0e..787a3a7 100644 --- a/src/app.ts +++ b/src/app.ts @@ -93,12 +93,14 @@ function setUp(op: ToolOp) { const busy = () => !!worker || localBusy; let printFrame: HTMLIFrameElement | undefined; // The on-page editors (Edit PDF, Redact PDF) follow the chosen file. - type Editor = { save(options: Options, onProgress: (fraction: number) => void): Promise; destroy(): void }; + // update() is for editors that use the whole list (the Collage Maker): it follows adds, removals and reordering. + type Editor = { save(options: Options, onProgress: (fraction: number) => void): Promise; destroy(): void; update?(files: File[]): void }; const EDITORS: Partial Promise>> = { 'edit-pdf': async (file, view) => (await import('./tools/edit-pdf')).openEditor(file, view), 'audio-split': async (file, view) => (await import('./tools/audio-split')).openSplitter(file, view), 'remove-bg': async (file, view) => (await import('./tools/remove-bg')).openBackgroundEditor(file, view, options()), 'photo-edit': async (file, view) => (await import('./tools/photo-edit')).openPhotoEditor(file, view), + collage: async (_, view) => (await import('./tools/collage')).openCollage(files, view), 'redact-pdf': async (file, view) => { const redactor = await (await import('./tools/redact-pdf')).openRedactor(file, view); return { save: (o, onProgress) => redactor.save(rasterOptions(o), onProgress), destroy: redactor.destroy }; @@ -193,6 +195,7 @@ function setUp(op: ToolOp) { 'redact-pdf': async (_, o) => (await editor!.ready).save(o, progressLabel), 'remove-bg': async (_, o) => (await editor!.ready).save(o, progressLabel), 'photo-edit': async (_, o) => (await editor!.ready).save(o, progressLabel), + collage: async (_, o) => (await editor!.ready).save(o, progressLabel), 'audio-split': async () => (await editor!.ready).save({}, progressLabel), 'audio-merge': async (f) => { const { parseAudio, write, formatTime, extension } = await import('./engine/audio'); @@ -210,7 +213,10 @@ function setUp(op: ToolOp) { /** The editor follows the chosen file: opening a new one replaces it, removing it closes it. */ function syncEditor() { const open = EDITORS[op]; - if (!open || editor?.file === files[0]) return; + if (!open) return; + // The collage follows the whole list in place: adding, removing or reordering photos. + if (op === 'collage' && editor && files.length) return void editor.ready.then((e) => e.update?.(files), () => {}); + if (editor?.file === files[0]) return; editor?.ready.then((e) => e.destroy(), () => {}); viewer!.replaceChildren(); editor = undefined; diff --git a/src/engine/photo.ts b/src/engine/photo.ts index 867075c..7a1764b 100644 --- a/src/engine/photo.ts +++ b/src/engine/photo.ts @@ -88,3 +88,33 @@ export function frame(w: number, h: number, ratio: number, zoom: number, cx: num const y = Math.min(h - fh, Math.max(0, Math.round(cy * h - fh / 2))); return [x, y, fw, fh]; } + +/** A collage cell as fractions of the collage: [x, y, width, height]. */ +export type Cell = [number, number, number, number]; + +/** Equal cells: `counts` photos per row, or per column when `columns` is set. */ +export const cells = (counts: number[], columns = false): Cell[] => + counts.flatMap((n, r) => + Array.from({ length: n }, (_, i): Cell => (columns ? [r / counts.length, i / n, 1 / counts.length, 1 / n] : [i / n, r / counts.length, 1 / n, 1 / counts.length])), + ); + +/** A cell in pixels on a W×H collage, with the same gap between cells and around the edge. */ +export const place = ([x, y, w, h]: Cell, W: number, H: number, gap: number): Cell => [ + gap + x * (W - gap), + gap + y * (H - gap), + w * (W - gap) - gap, + h * (H - gap) - gap, +]; + +/** The layouts offered for each number of photos, the first being the default: [photos per row (or column), columns?]. */ +export const LAYOUTS: Record = { + 1: [[[1]]], + 2: [[[2]], [[1, 1]]], + 3: [[[3]], [[1, 1, 1]], [[1, 2]], [[1, 2], true]], + 4: [[[2, 2]], [[4]], [[1, 3]], [[1, 3], true]], + 5: [[[2, 3]], [[3, 2]], [[2, 1, 2]]], + 6: [[[3, 3]], [[2, 2, 2]]], + 7: [[[3, 4]], [[2, 3, 2]]], + 8: [[[4, 4]], [[2, 2, 2, 2]]], + 9: [[[3, 3, 3]], [[4, 1, 4]]], +}; diff --git a/src/i18n.ts b/src/i18n.ts index 55804d4..67d9f01 100644 --- a/src/i18n.ts +++ b/src/i18n.ts @@ -385,6 +385,16 @@ export const UI = { 'pe.fade': 'Fade', 'pe.sepia': 'Sepia', 'pe.noir': 'Noir', + 'co.preview': 'Preview of the collage', + 'co.layout': 'Layout', + 'co.photo': 'Photo', + 'co.space': 'Space', + 'co.gap': 'Gap', + 'co.corners': 'Round corners', + 'co.a4': 'A4 print', + 'co.max': 'Only the first 9 photos are used.', + 'co.order': 'To swap photos, change their order in the list above.', + 'co.tapHint': 'Tap a photo to pick it, then drag to move it in its frame.', 'st.text': 'Text', 'st.hint': 'Drag the text on the picture. Pinch with two fingers, or use the sliders, to resize and turn it.', 'st.add': 'Add text', diff --git a/src/i18n/bn.json b/src/i18n/bn.json index 1b23da2..c9b8d43 100644 --- a/src/i18n/bn.json +++ b/src/i18n/bn.json @@ -301,6 +301,16 @@ "pe.fade": "ফিকে", "pe.sepia": "সেপিয়া", "pe.noir": "নোয়ার", + "co.preview": "কোলাজের প্রিভিউ", + "co.layout": "লেআউট", + "co.photo": "ছবি", + "co.space": "স্পেস", + "co.gap": "ফাঁক", + "co.corners": "গোল কোণ", + "co.a4": "A4 প্রিন্ট", + "co.max": "শুধু প্রথম 9টি ছবি ব্যবহার করা হয়।", + "co.order": "ছবির জায়গা বদলাতে, ওপরের তালিকায় সেগুলোর ক্রম বদলান।", + "co.tapHint": "একটি ছবি বেছে নিতে তাতে ট্যাপ করুন, তারপর ফ্রেমের ভেতরে সরাতে টানুন।", "st.text": "লেখা", "st.hint": "লেখাটি ছবির ওপর টেনে সরান। বড়-ছোট করতে ও ঘোরাতে দুই আঙুলে পিঞ্চ করুন, বা স্লাইডার ব্যবহার করুন।", "st.add": "লেখা যোগ করুন", @@ -3071,6 +3081,49 @@ "whatsapp sticker maker" ] }, + "collage-maker": { + "name": "কোলাজ মেকার", + "summary": "2 থেকে 9টি ছবি একটাই ছবিতে সাজান, ফাঁক, গোল কোণ আর লেখাসহ।", + "action": "কোলাজ ডাউনলোড করুন", + "title": "ফ্রি ফটো কোলাজ মেকার — আপলোড নেই, ওয়াটারমার্ক নেই | Fizzdoc", + "description": "ব্রাউজারে বিনামূল্যে ফটো কোলাজ বানান: 2 থেকে 9টি ছবির জন্য লেআউট বাছুন, ফাঁক, কোণ ও সাইজ ঠিক করুন, লেখা যোগ করুন। আপলোড নেই, সাইন-আপ নেই, ওয়াটারমার্ক নেই।", + "h1": "ছবি আপলোড না করেই ফটো কোলাজ বানান", + "lede": "2 থেকে 9টি ছবি বেছে নিন, একটা লেআউট আর আকৃতি (বর্গাকার, স্টোরি, A4 ইত্যাদি) বেছে নিন, প্রতিটি ছবি তার ফ্রেমে সরান ও জুম করুন, আর ফাঁক, গোল কোণ, ব্যাকগ্রাউন্ডের রং ও লেখা যোগ করুন। সবকিছু আপনার নিজের ডিভাইসেই হয়।", + "steps": [ + "2 থেকে 9টি ছবি বেছে নিন। তালিকার ক্রমই কোলাজের ক্রম।", + "লেআউট ও আকৃতি বেছে নিন, প্রতিটি ছবি সরান ও জুম করুন, আর ফাঁক, কোণ, ব্যাকগ্রাউন্ড ও লেখা ঠিক করুন।", + "সাইজ ও ফরম্যাট বেছে নিন, তারপর “কোলাজ ডাউনলোড করুন”-এ ক্লিক করুন।" + ], + "faq": [ + [ + "আমার ফাইল কি কোথাও আপলোড হয়?", + "না। Fizzdoc পুরো কাজটা আপনার ব্রাউজার ট্যাবের ভেতরেই করে, WebAssembly ও JavaScript-এ কম্পাইল করা ওপেন-সোর্স ইঞ্জিন ব্যবহার করে। আপনার ফাইল আপনার ডিভাইস থেকে পড়া হয়, লোকালি প্রসেস হয়, আর ফলাফল সরাসরি আপনার ডিভাইসেই সেভ হয়। আপনি নিজেই যাচাই করতে পারেন: ব্রাউজারের ডেভেলপার টুলস খুলুন, Network ট্যাবে যান আর একটা কাজ চালান — কোনো রিকোয়েস্টে আপনার ফাইল যায় না।" + ], + [ + "একটা কোলাজে কতগুলো ছবি রাখা যায়?", + "1 থেকে 9টি। ছবির সংখ্যা অনুযায়ী আলাদা আলাদা লেআউট আছে, যেমন পাশাপাশি, ওপর-নিচে, একটা বড় ছবির সঙ্গে ছোট ছোট ছবি, বা গ্রিড। 9টির বেশি যোগ করলে প্রথম 9টি ব্যবহার হয়।" + ], + [ + "প্রতিটি ছবি কি সরানো বা জুম করা যায়?", + "হ্যাঁ। “ছবি” খুলুন, একটা ছবি বেছে নিতে তাতে ট্যাপ করুন, তারপর টেনে ঠিক করুন তার ফ্রেমে কী দেখা যাবে, আর কাছ থেকে দেখাতে “জুম” ব্যবহার করুন। ছবির জায়গা বদলাতে ফাইলের তালিকায় সেগুলোর ক্রম বদলান।" + ], + [ + "কোন কোন সাইজে সেভ করা যায়?", + "সোশ্যাল মিডিয়ার জন্য “বর্গাকার”, “পোর্ট্রেট 4:5” ও “স্টোরি 9:16”, এছাড়া “চওড়া 16:9”, আর প্রিন্টের জন্য “A4 প্রিন্ট”: ছোট দিকে 1080 বা 2160 px, বা A4-এর জন্য 300 dpi-তে 2480 px। JPG, PNG বা WebP হিসেবে, কোনো ওয়াটারমার্ক ছাড়া।" + ], + [ + "Fizzdoc কি ফ্রি?", + "হ্যাঁ, আর এটা সবসময় ফ্রি থাকবে: কোনো সাইন-আপ নেই, ওয়াটারমার্ক নেই, দৈনিক সীমা নেই, আর কোনো প্রিমিয়াম প্ল্যানও নেই। Fizzdoc Apache-2.0 লাইসেন্সের অধীনে ওপেন সোর্স, তাই যে কেউ নিজের কপি চালাতে পারে।" + ] + ], + "keywords": [ + "ফটো কোলাজ মেকার", + "ছবি দিয়ে কোলাজ বানানো", + "কোলাজ মেকার ফ্রি", + "দুটি ছবি একসাথে জোড়া", + "ওয়াটারমার্ক ছাড়া কোলাজ" + ] + }, "photo-editor": { "name": "ফটো এডিটর", "summary": "ছবি ক্রপ করুন, ঘোরান, উজ্জ্বল করুন আর তাতে লেখা বা ইমোজি যোগ করুন, সরাসরি আপনার ব্রাউজারে।", diff --git a/src/i18n/de.json b/src/i18n/de.json index a9d4854..9c9b576 100644 --- a/src/i18n/de.json +++ b/src/i18n/de.json @@ -301,6 +301,16 @@ "pe.fade": "Verblasst", "pe.sepia": "Sepia", "pe.noir": "Noir", + "co.preview": "Vorschau der Collage", + "co.layout": "Layout", + "co.photo": "Foto", + "co.space": "Abstände", + "co.gap": "Abstand", + "co.corners": "Runde Ecken", + "co.a4": "A4-Druck", + "co.max": "Nur die ersten 9 Fotos werden verwendet.", + "co.order": "Um Fotos zu tauschen, ändern Sie ihre Reihenfolge in der Liste oben.", + "co.tapHint": "Auf ein Foto tippen, um es auszuwählen, dann ziehen, um es im Rahmen zu verschieben.", "st.text": "Text", "st.hint": "Zieh den Text auf dem Bild an seinen Platz. Mit zwei Fingern zusammenziehen oder die Regler nutzen, um ihn zu vergrößern und zu drehen.", "st.add": "Text hinzufügen", @@ -3071,6 +3081,49 @@ "transparenter Sticker webp" ] }, + "collage-maker": { + "name": "Collage erstellen", + "summary": "2 bis 9 Fotos in einem Bild vereinen, mit Abstand, runden Ecken und Text.", + "action": "Collage herunterladen", + "title": "Fotocollage Kostenlos — Ohne Upload, Ohne Wasserzeichen | Fizzdoc", + "description": "Fotocollage kostenlos im Browser erstellen: Layout für 2 bis 9 Fotos, Abstand, Ecken, Größe und Text. Ohne Upload, Anmeldung oder Wasserzeichen.", + "h1": "Fotocollage erstellen, ohne Ihre Fotos hochzuladen", + "lede": "Wählen Sie 2 bis 9 Fotos, ein Layout und ein Format (quadratisch, Story, A4 und mehr), verschieben und vergrößern Sie jedes Foto in seinem Rahmen und fügen Sie Abstand, runde Ecken, eine Hintergrundfarbe und Text hinzu. Alles geschieht auf Ihrem Gerät.", + "steps": [ + "Wählen Sie 2 bis 9 Fotos. Die Reihenfolge der Liste ist die Reihenfolge in der Collage.", + "Wählen Sie ein Layout und ein Format, verschieben und vergrößern Sie jedes Foto und stellen Sie Abstand, Ecken, Hintergrund und Text ein.", + "Wählen Sie Größe und Format und klicken Sie auf „Collage herunterladen“." + ], + "faq": [ + [ + "Werden meine Dateien irgendwohin hochgeladen?", + "Nein. Fizzdoc erledigt die Arbeit direkt in Ihrem Browser-Tab, mit Open-Source-Engines, die zu WebAssembly und JavaScript kompiliert sind. Ihre Datei wird von Ihrem Gerät gelesen, lokal verarbeitet, und das Ergebnis wird direkt wieder auf Ihrem Gerät gespeichert. Sie können das selbst überprüfen: Öffnen Sie die Entwicklertools Ihres Browsers, wechseln Sie zum Netzwerk-Tab und führen Sie einen Vorgang aus — keine Anfrage enthält Ihre Datei." + ], + [ + "Wie viele Fotos kann eine Collage haben?", + "Von 1 bis 9. Für jede Anzahl von Fotos gibt es eigene Layouts, etwa nebeneinander, übereinander, ein großes Foto mit kleineren oder ein Raster. Wenn Sie mehr als 9 hinzufügen, werden die ersten 9 verwendet." + ], + [ + "Kann ich jedes Foto verschieben oder vergrößern?", + "Ja. Öffnen Sie „Foto“, tippen Sie auf ein Foto, um es auszuwählen, und ziehen Sie es dann, um festzulegen, was in seinem Rahmen zu sehen ist. Mit „Zoom“ gehen Sie näher heran. Um Fotos zu tauschen, ändern Sie ihre Reihenfolge in der Dateiliste." + ], + [ + "Welche Größen kann ich speichern?", + "„Quadratisch“, „Hochformat 4:5“ und „Story 9:16“ für soziale Medien, „Querformat 16:9“ und „A4-Druck“: 1080 oder 2160 px an der kurzen Seite oder 2480 px für A4 bei 300 dpi. Als JPG, PNG oder WebP, ohne Wasserzeichen." + ], + [ + "Ist Fizzdoc kostenlos?", + "Ja, und das bleibt so: keine Anmeldung, kein Wasserzeichen, kein Tageslimit und keine Premium-Stufe. Fizzdoc ist Open Source unter der Apache-2.0-Lizenz, sodass jeder seine eigene Kopie betreiben kann." + ] + ], + "keywords": [ + "Fotocollage erstellen", + "Collage erstellen kostenlos", + "Bilder zusammenfügen", + "mehrere Fotos in einem Bild", + "Fotocollage ohne Wasserzeichen" + ] + }, "photo-editor": { "name": "Foto bearbeiten", "summary": "Ein Foto zuschneiden, drehen, aufhellen und mit Text oder Emojis versehen, direkt im Browser.", diff --git a/src/i18n/es.json b/src/i18n/es.json index 65df411..388d577 100644 --- a/src/i18n/es.json +++ b/src/i18n/es.json @@ -301,6 +301,16 @@ "pe.fade": "Desvanecido", "pe.sepia": "Sepia", "pe.noir": "Noir", + "co.preview": "Vista previa del collage", + "co.layout": "Diseño", + "co.photo": "Foto", + "co.space": "Espacio", + "co.gap": "Separación", + "co.corners": "Esquinas redondeadas", + "co.a4": "A4 para imprimir", + "co.max": "Solo se usan las 9 primeras fotos.", + "co.order": "Para intercambiar fotos, cambia su orden en la lista de arriba.", + "co.tapHint": "Toca una foto para elegirla y luego arrástrala para moverla dentro de su marco.", "st.text": "Texto", "st.hint": "Arrastra el texto sobre la imagen. Pellizca con dos dedos, o usa los controles, para cambiar su tamaño y girarlo.", "st.add": "Añadir texto", @@ -3071,6 +3081,49 @@ "sticker webp transparente" ] }, + "collage-maker": { + "name": "Creador de collages", + "summary": "Junta de 2 a 9 fotos en una sola imagen, con separación, esquinas redondeadas y texto.", + "action": "Descargar collage", + "title": "Collage de Fotos Gratis — Sin Subir Nada ni Marca de Agua | Fizzdoc", + "description": "Haz un collage de fotos gratis en tu navegador: diseño para 2 a 9 fotos, separación, esquinas, tamaño y texto. Sin subir nada, sin registro ni marca de agua.", + "h1": "Haz un collage de fotos sin subir tus fotos", + "lede": "Elige de 2 a 9 fotos, escoge un diseño y un formato (cuadrado, historia, A4 y más), mueve y amplía cada foto dentro de su marco, y añade separación, esquinas redondeadas, un color de fondo y texto. Todo ocurre en tu dispositivo.", + "steps": [ + "Elige de 2 a 9 fotos. El orden de la lista es el orden en el collage.", + "Escoge un diseño y un formato, mueve y amplía cada foto, y ajusta la separación, las esquinas, el fondo y el texto.", + "Elige el tamaño y el formato, y haz clic en «Descargar collage»." + ], + "faq": [ + [ + "¿Se suben mis archivos a algún sitio?", + "No. Fizzdoc hace todo el trabajo dentro de la pestaña de tu navegador, con motores de código abierto compilados a WebAssembly y JavaScript. Tu archivo se lee desde tu dispositivo, se procesa localmente y el resultado se guarda directamente en tu dispositivo. Puedes comprobarlo tú mismo: abre las herramientas de desarrollador de tu navegador, ve a la pestaña Red y ejecuta una tarea — ninguna solicitud lleva tu archivo." + ], + [ + "¿Cuántas fotos puede tener un collage?", + "De 1 a 9. Cada número de fotos tiene sus propios diseños, como una al lado de otra, una encima de otra, una foto grande con otras más pequeñas o una cuadrícula. Si añades más de 9, se usan las 9 primeras." + ], + [ + "¿Puedo mover o ampliar cada foto?", + "Sí. Abre «Foto», toca una foto para elegirla, arrástrala para decidir qué se ve en su marco y usa «Zoom» para acercarte. Para intercambiar fotos, cambia su orden en la lista de archivos." + ], + [ + "¿Qué tamaños puedo guardar?", + "«Cuadrado», «Vertical 4:5» e «Historia 9:16» para redes sociales, «Horizontal 16:9» y «A4 para imprimir»: 1080 o 2160 px en el lado corto, o 2480 px para A4 a 300 dpi. En JPG, PNG o WebP, sin marca de agua." + ], + [ + "¿Es gratis Fizzdoc?", + "Sí, y seguirá siendo gratis: sin registro, sin marca de agua, sin límite diario y sin plan premium. Fizzdoc es de código abierto bajo la licencia Apache-2.0, así que cualquiera puede ejecutar su propia copia." + ] + ], + "keywords": [ + "hacer collage de fotos", + "collage de fotos online gratis", + "juntar varias fotos en una", + "montaje de fotos online", + "unir fotos en una sola imagen" + ] + }, "photo-editor": { "name": "Editor de Fotos", "summary": "Recorta, gira, aclara y añade texto o emojis a una foto, directamente en tu navegador.", diff --git a/src/i18n/fr.json b/src/i18n/fr.json index 12b251f..0f3b2a2 100644 --- a/src/i18n/fr.json +++ b/src/i18n/fr.json @@ -301,6 +301,16 @@ "pe.fade": "Délavé", "pe.sepia": "Sépia", "pe.noir": "Noir", + "co.preview": "Aperçu du collage", + "co.layout": "Disposition", + "co.photo": "Photo", + "co.space": "Espace", + "co.gap": "Espacement", + "co.corners": "Coins arrondis", + "co.a4": "Impression A4", + "co.max": "Seules les 9 premières photos sont utilisées.", + "co.order": "Pour échanger des photos, changez leur ordre dans la liste ci-dessus.", + "co.tapHint": "Touchez une photo pour la choisir, puis faites-la glisser pour la déplacer dans son cadre.", "st.text": "Texte", "st.hint": "Faites glisser le texte sur l’image. Pincez avec deux doigts, ou utilisez les curseurs, pour le redimensionner et le faire pivoter.", "st.add": "Ajouter du texte", @@ -3071,6 +3081,49 @@ "sticker webp transparent" ] }, + "collage-maker": { + "name": "Créateur de collage", + "summary": "Réunissez 2 à 9 photos en une seule image, avec un espacement, des coins arrondis et du texte.", + "action": "Télécharger le collage", + "title": "Créer un Collage Photo Gratuit — Sans Envoi ni Filigrane | Fizzdoc", + "description": "Créez un collage photo gratuit dans le navigateur : disposition pour 2 à 9 photos, espacement, coins, taille et texte. Sans envoi, inscription ni filigrane.", + "h1": "Créez un collage photo sans envoyer vos photos", + "lede": "Choisissez 2 à 9 photos, une disposition et un format (carré, story, A4 et plus), déplacez et agrandissez chaque photo dans son cadre, puis ajoutez un espacement, des coins arrondis, une couleur de fond et du texte. Tout se passe sur votre appareil.", + "steps": [ + "Choisissez 2 à 9 photos. L’ordre de la liste est l’ordre dans le collage.", + "Choisissez une disposition et un format, déplacez et agrandissez chaque photo, et réglez l’espacement, les coins, le fond et le texte.", + "Choisissez la taille et le format, puis cliquez sur « Télécharger le collage »." + ], + "faq": [ + [ + "Mes fichiers sont-ils envoyés quelque part ?", + "Non. Fizzdoc effectue tout le travail dans votre onglet de navigateur, grâce à des moteurs open source compilés en WebAssembly et en JavaScript. Votre fichier est lu depuis votre appareil, traité localement, puis le résultat est enregistré directement sur votre appareil. Vous pouvez le vérifier vous-même : ouvrez les outils de développement de votre navigateur, allez dans l'onglet Réseau et lancez une opération — aucune requête ne transporte votre fichier." + ], + [ + "Combien de photos peut contenir un collage ?", + "De 1 à 9. Chaque nombre de photos a ses propres dispositions, par exemple côte à côte, l’une au-dessus de l’autre, une grande photo avec des plus petites, ou une grille. Si vous en ajoutez plus de 9, les 9 premières sont utilisées." + ], + [ + "Puis-je déplacer ou agrandir chaque photo ?", + "Oui. Ouvrez « Photo », touchez une photo pour la choisir, puis faites-la glisser pour régler ce qui apparaît dans son cadre, et utilisez « Zoom » pour vous rapprocher. Pour échanger des photos, changez leur ordre dans la liste des fichiers." + ], + [ + "Quelles tailles puis-je enregistrer ?", + "« Carré », « Portrait 4:5 » et « Story 9:16 » pour les réseaux sociaux, « Paysage 16:9 » et « Impression A4 » : 1080 ou 2160 px sur le petit côté, ou 2480 px pour l’A4 à 300 dpi. En JPG, PNG ou WebP, sans filigrane." + ], + [ + "Est-ce que Fizzdoc est gratuit ?", + "Oui, et ça restera gratuit : pas d'inscription, pas de filigrane, pas de limite quotidienne et pas d'offre premium. Fizzdoc est open source sous licence Apache-2.0, donc n'importe qui peut faire tourner sa propre copie." + ] + ], + "keywords": [ + "faire un collage photo", + "collage photo en ligne gratuit", + "montage photo plusieurs photos", + "assembler des photos", + "pêle-mêle photo" + ] + }, "photo-editor": { "name": "Éditeur photo", "summary": "Recadrez, faites pivoter, éclaircissez et ajoutez du texte ou des emojis à une photo, directement dans votre navigateur.", diff --git a/src/i18n/hi.json b/src/i18n/hi.json index a25f571..e62324b 100644 --- a/src/i18n/hi.json +++ b/src/i18n/hi.json @@ -301,6 +301,16 @@ "pe.fade": "फ़ीका", "pe.sepia": "सेपिया", "pe.noir": "नोयर", + "co.preview": "कोलाज का प्रीव्यू", + "co.layout": "लेआउट", + "co.photo": "फ़ोटो", + "co.space": "स्पेस", + "co.gap": "गैप", + "co.corners": "गोल कोने", + "co.a4": "A4 प्रिंट", + "co.max": "सिर्फ़ पहली 9 फ़ोटो इस्तेमाल होती हैं।", + "co.order": "फ़ोटो की जगह बदलने के लिए, ऊपर की लिस्ट में उनका क्रम बदलें।", + "co.tapHint": "किसी फ़ोटो को चुनने के लिए उस पर टैप करें, फिर उसे उसके फ़्रेम में खिसकाने के लिए खींचें।", "st.text": "टेक्स्ट", "st.hint": "टेक्स्ट को तस्वीर पर खींचें। छोटा-बड़ा करने और घुमाने के लिए दो उंगलियों से पिंच करें, या स्लाइडर इस्तेमाल करें।", "st.add": "टेक्स्ट जोड़ें", @@ -3071,6 +3081,49 @@ "whatsapp sticker maker" ] }, + "collage-maker": { + "name": "कोलाज मेकर", + "summary": "2 से 9 फ़ोटो को एक ही तस्वीर में लगाएँ, गैप, गोल कोनों और टेक्स्ट के साथ।", + "action": "कोलाज डाउनलोड करें", + "title": "फ्री फ़ोटो कोलाज मेकर — न अपलोड, न वॉटरमार्क | Fizzdoc", + "description": "ब्राउज़र में मुफ़्त फ़ोटो कोलाज बनाएँ: 2 से 9 फ़ोटो के लिए लेआउट चुनें, गैप, कोने और साइज़ सेट करें, टेक्स्ट जोड़ें। न अपलोड, न साइन-अप, न वॉटरमार्क।", + "h1": "अपनी फ़ोटो अपलोड किए बिना फ़ोटो कोलाज बनाएँ", + "lede": "2 से 9 फ़ोटो चुनें, एक लेआउट और शेप (चौकोर, स्टोरी, A4 वगैरह) चुनें, हर फ़ोटो को उसके फ़्रेम में खिसकाएँ और ज़ूम करें, और गैप, गोल कोने, बैकग्राउंड का रंग और टेक्स्ट जोड़ें। यह सब आपके अपने डिवाइस पर होता है।", + "steps": [ + "2 से 9 फ़ोटो चुनें। लिस्ट का क्रम ही कोलाज में फ़ोटो का क्रम है।", + "लेआउट और शेप चुनें, हर फ़ोटो को खिसकाएँ और ज़ूम करें, और गैप, कोने, बैकग्राउंड और टेक्स्ट सेट करें।", + "साइज़ और फ़ॉर्मेट चुनें, फिर “कोलाज डाउनलोड करें” पर क्लिक करें।" + ], + "faq": [ + [ + "क्या मेरी फ़ाइलें कहीं अपलोड होती हैं?", + "नहीं। Fizzdoc यह काम आपके ब्राउज़र टैब के अंदर ही करता है, ओपन-सोर्स इंजन की मदद से जो WebAssembly और JavaScript में कंपाइल किए गए हैं। आपकी फ़ाइल आपके डिवाइस से पढ़ी जाती है, लोकली प्रोसेस होती है, और नतीजा सीधे आपके डिवाइस पर सेव हो जाता है। आप खुद चेक कर सकते हैं: अपने ब्राउज़र के डेवलपर टूल्स खोलें, Network टैब पर जाएँ और कोई काम चलाएँ — कोई भी रिक्वेस्ट आपकी फ़ाइल नहीं ले जाती।" + ], + [ + "एक कोलाज में कितनी फ़ोटो हो सकती हैं?", + "1 से 9 तक। फ़ोटो की गिनती के हिसाब से अलग-अलग लेआउट हैं, जैसे अगल-बगल, ऊपर-नीचे, एक बड़ी फ़ोटो के साथ कुछ छोटी फ़ोटो, या ग्रिड। अगर आप 9 से ज़्यादा फ़ोटो जोड़ते हैं, तो पहली 9 इस्तेमाल होती हैं।" + ], + [ + "क्या मैं हर फ़ोटो को खिसका या ज़ूम कर सकता हूँ?", + "हाँ। “फ़ोटो” खोलें, किसी फ़ोटो को चुनने के लिए उस पर टैप करें, फिर उसे खींचकर तय करें कि उसके फ़्रेम में क्या दिखे, और पास से दिखाने के लिए “ज़ूम” इस्तेमाल करें। फ़ोटो की जगह बदलने के लिए, फ़ाइल लिस्ट में उनका क्रम बदलें।" + ], + [ + "मैं किन साइज़ में सेव कर सकता हूँ?", + "सोशल मीडिया के लिए “चौकोर”, “पोर्ट्रेट 4:5” और “स्टोरी 9:16”, साथ ही “चौड़ा 16:9”, और प्रिंट के लिए “A4 प्रिंट”: छोटी साइड पर 1080 या 2160 px, या A4 के लिए 300 dpi पर 2480 px। JPG, PNG या WebP में, बिना वॉटरमार्क के।" + ], + [ + "क्या Fizzdoc मुफ़्त है?", + "हाँ, और यह हमेशा फ्री रहेगा: कोई साइन-अप नहीं, कोई वॉटरमार्क नहीं, कोई डेली लिमिट नहीं और कोई प्रीमियम प्लान नहीं। Fizzdoc, Apache-2.0 लाइसेंस के तहत ओपन सोर्स है, इसलिए कोई भी अपनी खुद की कॉपी चला सकता है।" + ] + ], + "keywords": [ + "फोटो कोलाज मेकर", + "फोटो कोलाज बनाएं", + "कोलाज मेकर फ्री", + "दो फोटो एक साथ जोड़ें", + "बिना वॉटरमार्क फोटो कोलाज" + ] + }, "photo-editor": { "name": "फ़ोटो एडिटर", "summary": "फ़ोटो को काटें, घुमाएँ, रोशनी बढ़ाएँ और उस पर शब्द या इमोजी जोड़ें, सीधे अपने ब्राउज़र में।", diff --git a/src/i18n/id.json b/src/i18n/id.json index 8e23cbf..9f89500 100644 --- a/src/i18n/id.json +++ b/src/i18n/id.json @@ -301,6 +301,16 @@ "pe.fade": "Pudar", "pe.sepia": "Sepia", "pe.noir": "Noir", + "co.preview": "Pratinjau kolase", + "co.layout": "Tata letak", + "co.photo": "Foto", + "co.space": "Spasi", + "co.gap": "Jarak", + "co.corners": "Sudut bulat", + "co.a4": "Cetak A4", + "co.max": "Hanya 9 foto pertama yang dipakai.", + "co.order": "Untuk menukar foto, ubah urutannya di daftar di atas.", + "co.tapHint": "Ketuk foto untuk memilihnya, lalu seret untuk menggesernya di dalam bingkainya.", "st.text": "Teks", "st.hint": "Seret teks di atas gambar. Cubit dengan dua jari, atau pakai penggeser, untuk mengubah ukuran dan memutarnya.", "st.add": "Tambah teks", @@ -3071,6 +3081,49 @@ "stiker webp transparan" ] }, + "collage-maker": { + "name": "Pembuat Kolase", + "summary": "Gabungkan 2 sampai 9 foto jadi satu gambar, dengan jarak, sudut bulat, dan teks.", + "action": "Unduh kolase", + "title": "Buat Kolase Foto Gratis — Tanpa Unggah, Tanpa Watermark | Fizzdoc", + "description": "Buat kolase foto gratis di browser: pilih tata letak untuk 2 sampai 9 foto, atur jarak, sudut, dan ukuran, tambah teks. Tanpa unggah, daftar, atau watermark.", + "h1": "Buat kolase foto tanpa mengunggah foto Anda", + "lede": "Pilih 2 sampai 9 foto, pilih tata letak dan bentuk (persegi, story, A4, dan lainnya), geser dan zoom setiap foto di dalam bingkainya, lalu tambahkan jarak, sudut bulat, warna latar, dan teks. Semuanya dikerjakan di perangkat Anda sendiri.", + "steps": [ + "Pilih 2 sampai 9 foto. Urutan di daftar adalah urutan di kolase.", + "Pilih tata letak dan bentuk, geser dan zoom setiap foto, lalu atur jarak, sudut, latar, dan teks.", + "Pilih ukuran dan format, lalu klik “Unduh kolase”." + ], + "faq": [ + [ + "Apakah file saya diunggah ke suatu tempat?", + "Tidak. Fizzdoc melakukan semua proses di dalam tab browser Anda, menggunakan mesin open source yang dikompilasi ke WebAssembly dan JavaScript. File Anda dibaca dari perangkat Anda, diproses secara lokal, dan hasilnya langsung disimpan kembali ke perangkat Anda. Anda bisa membuktikannya sendiri: buka developer tools browser Anda, lihat tab Network, lalu jalankan sebuah proses — tidak ada request yang membawa file Anda." + ], + [ + "Berapa banyak foto dalam satu kolase?", + "Dari 1 sampai 9. Setiap jumlah foto punya tata letaknya sendiri, seperti berdampingan, atas-bawah, satu foto besar dengan beberapa foto kecil, atau grid. Jika Anda menambahkan lebih dari 9, yang dipakai adalah 9 foto pertama." + ], + [ + "Bisakah saya menggeser atau memperbesar setiap foto?", + "Bisa. Buka Foto, ketuk sebuah foto untuk memilihnya, lalu seret untuk memilih bagian yang terlihat di bingkainya, dan pakai Zoom untuk memperbesar. Untuk menukar foto, ubah urutannya di daftar file." + ], + [ + "Ukuran apa saja yang bisa saya simpan?", + "Persegi, Potret 4:5, dan Story 9:16 untuk media sosial, Lebar 16:9, serta Cetak A4 untuk dicetak: 1080 atau 2160 px di sisi pendek, atau 2480 px untuk A4 pada 300 dpi. Dalam format JPG, PNG, atau WebP, tanpa watermark." + ], + [ + "Apakah Fizzdoc gratis?", + "Ya, dan akan tetap gratis: tanpa daftar, tanpa watermark, tanpa batas harian, dan tanpa paket berbayar. Fizzdoc adalah open source di bawah lisensi Apache-2.0, jadi siapa pun bisa menjalankan salinannya sendiri." + ] + ], + "keywords": [ + "buat kolase foto", + "kolase foto online gratis", + "gabungkan foto jadi satu", + "bikin kolase foto", + "kolase foto tanpa watermark" + ] + }, "photo-editor": { "name": "Editor Foto", "summary": "Potong, putar, cerahkan, dan tambahkan tulisan atau emoji ke foto, langsung di browser Anda.", diff --git a/src/i18n/it.json b/src/i18n/it.json index c4a9f1b..a939cbf 100644 --- a/src/i18n/it.json +++ b/src/i18n/it.json @@ -301,6 +301,16 @@ "pe.fade": "Sbiadito", "pe.sepia": "Seppia", "pe.noir": "Noir", + "co.preview": "Anteprima del collage", + "co.layout": "Layout", + "co.photo": "Foto", + "co.space": "Spazio", + "co.gap": "Spaziatura", + "co.corners": "Angoli arrotondati", + "co.a4": "Stampa A4", + "co.max": "Vengono usate solo le prime 9 foto.", + "co.order": "Per scambiare le foto, cambia il loro ordine nell’elenco qui sopra.", + "co.tapHint": "Tocca una foto per sceglierla, poi trascinala per spostarla nella sua cornice.", "st.text": "Testo", "st.hint": "Trascina il testo sull’immagine. Pizzica con due dita, o usa i cursori, per ridimensionarlo e ruotarlo.", "st.add": "Aggiungi testo", @@ -3071,6 +3081,49 @@ "sticker webp trasparente" ] }, + "collage-maker": { + "name": "Crea Collage", + "summary": "Unisci da 2 a 9 foto in un’unica immagine, con spaziatura, angoli arrotondati e testo.", + "action": "Scarica collage", + "title": "Crea Collage di Foto Gratis — Senza Upload né Filigrana | Fizzdoc", + "description": "Crea un collage di foto gratis nel browser: layout da 2 a 9 foto, spaziatura, angoli, dimensione e testo. Senza upload, registrazione né filigrana.", + "h1": "Crea un collage di foto senza caricare le tue foto", + "lede": "Scegli da 2 a 9 foto, un layout e un formato (quadrato, storia, A4 e altri), sposta e ingrandisci ogni foto nella sua cornice e aggiungi spaziatura, angoli arrotondati, un colore di sfondo e testo. Tutto avviene sul tuo dispositivo.", + "steps": [ + "Scegli da 2 a 9 foto. L’ordine dell’elenco è l’ordine nel collage.", + "Scegli un layout e un formato, sposta e ingrandisci ogni foto e regola spaziatura, angoli, sfondo e testo.", + "Scegli la dimensione e il formato, poi clicca su “Scarica collage”." + ], + "faq": [ + [ + "I miei file vengono caricati da qualche parte?", + "No. Fizzdoc esegue il lavoro direttamente nella scheda del browser, usando motori open source compilati in WebAssembly e JavaScript. Il file viene letto dal tuo dispositivo, elaborato in locale, e il risultato viene salvato di nuovo sul tuo dispositivo. Puoi verificarlo tu stesso: apri gli strumenti per sviluppatori del browser, vai alla scheda Rete ed esegui un'operazione — nessuna richiesta trasporta il tuo file." + ], + [ + "Quante foto può avere un collage?", + "Da 1 a 9. Ogni numero di foto ha i suoi layout, come affiancate, una sopra l’altra, una foto grande con altre più piccole o una griglia. Se ne aggiungi più di 9, vengono usate le prime 9." + ], + [ + "Posso spostare o ingrandire ogni foto?", + "Sì. Apri “Foto”, tocca una foto per sceglierla, poi trascinala per decidere cosa si vede nella cornice e usa “Zoom” per avvicinarti. Per scambiare le foto, cambia il loro ordine nell’elenco dei file." + ], + [ + "Quali dimensioni posso salvare?", + "“Quadrato”, “Verticale 4:5” e “Storia 9:16” per i social, “Orizzontale 16:9” e “Stampa A4”: 1080 o 2160 px sul lato corto, oppure 2480 px per l’A4 a 300 dpi. In JPG, PNG o WebP, senza filigrana." + ], + [ + "Fizzdoc è gratuito?", + "Sì, e resterà gratuito: niente registrazione, niente filigrana, nessun limite giornaliero e nessun piano premium. Fizzdoc è open source con licenza Apache-2.0, quindi chiunque può eseguire la propria copia." + ] + ], + "keywords": [ + "creare collage foto", + "collage foto online gratis", + "unire foto in una", + "collage foto senza filigrana", + "mettere più foto in una" + ] + }, "photo-editor": { "name": "Editor Foto", "summary": "Ritaglia, ruota, schiarisci e aggiungi testo o emoji a una foto, direttamente nel browser.", diff --git a/src/i18n/mr.json b/src/i18n/mr.json index 31c7cad..6e6ef98 100644 --- a/src/i18n/mr.json +++ b/src/i18n/mr.json @@ -301,6 +301,16 @@ "pe.fade": "फिका", "pe.sepia": "सेपिया", "pe.noir": "नोयर", + "co.preview": "कोलाजचा प्रीव्ह्यू", + "co.layout": "लेआउट", + "co.photo": "फोटो", + "co.space": "स्पेस", + "co.gap": "अंतर", + "co.corners": "गोल कोपरे", + "co.a4": "A4 प्रिंट", + "co.max": "फक्त पहिले 9 फोटो वापरले जातात.", + "co.order": "फोटोंची जागा बदलण्यासाठी, वरच्या यादीत त्यांचा क्रम बदला.", + "co.tapHint": "फोटो निवडण्यासाठी त्यावर टॅप करा, मग त्याच्या फ्रेममध्ये हलवण्यासाठी ओढा.", "st.text": "मजकूर", "st.hint": "मजकूर चित्रावर ओढा. लहान-मोठा करण्यासाठी आणि फिरवण्यासाठी दोन बोटांनी पिंच करा, किंवा स्लायडर वापरा.", "st.add": "मजकूर जोडा", @@ -3071,6 +3081,49 @@ "whatsapp sticker maker" ] }, + "collage-maker": { + "name": "कोलाज मेकर", + "summary": "2 ते 9 फोटो एकाच चित्रात लावा, अंतर, गोल कोपरे आणि मजकुरासह.", + "action": "कोलाज डाउनलोड करा", + "title": "मोफत फोटो कोलाज मेकर — अपलोड नाही, वॉटरमार्क नाही | Fizzdoc", + "description": "ब्राउझरमध्ये मोफत फोटो कोलाज बनवा: 2 ते 9 फोटोंसाठी लेआउट निवडा, अंतर, कोपरे आणि आकार ठरवा, मजकूर जोडा. ना अपलोड, ना साइन-अप, ना वॉटरमार्क.", + "h1": "फोटो अपलोड न करता फोटो कोलाज बनवा", + "lede": "2 ते 9 फोटो निवडा, लेआउट आणि शेप (चौरस, स्टोरी, A4 वगैरे) निवडा, प्रत्येक फोटो त्याच्या फ्रेममध्ये हलवा आणि झूम करा, आणि अंतर, गोल कोपरे, बॅकग्राउंडचा रंग आणि मजकूर जोडा. हे सर्व तुमच्याच डिव्हाइसवर होते.", + "steps": [ + "2 ते 9 फोटो निवडा. यादीतला क्रम हाच कोलाजमधला क्रम असतो.", + "लेआउट आणि शेप निवडा, प्रत्येक फोटो हलवा आणि झूम करा, आणि अंतर, कोपरे, बॅकग्राउंड आणि मजकूर ठरवा.", + "आकार आणि फॉरमॅट निवडा, मग “कोलाज डाउनलोड करा” वर क्लिक करा." + ], + "faq": [ + [ + "माझ्या फाइल्स कुठेही अपलोड होतात का?", + "नाही. Fizzdoc हे काम तुमच्या ब्राउझर टॅबमध्येच करतो, WebAssembly आणि JavaScript मध्ये कंपाइल केलेल्या ओपन-सोर्स इंजिनांचा वापर करून. तुमची फाइल तुमच्या डिव्हाइसवरून वाचली जाते, स्थानिक पातळीवर प्रोसेस होते आणि निकाल थेट तुमच्या डिव्हाइसवर सेव्ह होतो. हे तुम्ही स्वतः पडताळून पाहू शकता: ब्राउझरचे डेव्हलपर टूल्स उघडा, Network टॅबवर जा आणि एखादे काम चालवा — कोणतीही रिक्वेस्ट तुमची फाइल घेऊन जात नाही." + ], + [ + "एका कोलाजमध्ये किती फोटो असू शकतात?", + "1 ते 9. फोटोंच्या संख्येनुसार वेगवेगळे लेआउट आहेत, जसे शेजारी-शेजारी, एकाखाली एक, एक मोठा फोटो आणि सोबत लहान फोटो, किंवा ग्रिड. 9 पेक्षा जास्त फोटो जोडल्यास पहिले 9 वापरले जातात." + ], + [ + "प्रत्येक फोटो हलवता किंवा झूम करता येतो का?", + "होय. “फोटो” उघडा, फोटो निवडण्यासाठी त्यावर टॅप करा, मग त्याच्या फ्रेममध्ये काय दिसेल ते निवडण्यासाठी तो ओढा, आणि जवळून पाहण्यासाठी “झूम” वापरा. फोटोंची जागा बदलण्यासाठी, फाइल यादीत त्यांचा क्रम बदला." + ], + [ + "कोणत्या आकारात सेव्ह करता येते?", + "सोशल मीडियासाठी “चौरस”, “पोर्ट्रेट 4:5” आणि “स्टोरी 9:16”, शिवाय “रुंद 16:9”, आणि प्रिंटसाठी “A4 प्रिंट”: लहान बाजूला 1080 किंवा 2160 px, किंवा A4 साठी 300 dpi वर 2480 px. JPG, PNG किंवा WebP मध्ये, वॉटरमार्कशिवाय." + ], + [ + "Fizzdoc मोफत आहे का?", + "होय, आणि हा कायम मोफत राहील: साइन-अप नाही, वॉटरमार्क नाही, रोजची मर्यादा नाही आणि प्रीमियम प्लॅनही नाही. Fizzdoc हा Apache-2.0 लायसन्स अंतर्गत ओपन सोर्स आहे, त्यामुळे कोणीही स्वतःची कॉपी चालवू शकतो." + ] + ], + "keywords": [ + "फोटो कोलाज मेकर", + "फोटो कोलाज बनवा", + "कोलाज मेकर मोफत", + "दोन फोटो एकत्र जोडा", + "वॉटरमार्कशिवाय फोटो कोलाज" + ] + }, "photo-editor": { "name": "फोटो एडिटर", "summary": "फोटो क्रॉप करा, फिरवा, उजळ करा आणि त्यावर शब्द किंवा इमोजी जोडा, थेट तुमच्या ब्राउझरमध्ये.", diff --git a/src/i18n/nl.json b/src/i18n/nl.json index ef32ac8..1f0f8d1 100644 --- a/src/i18n/nl.json +++ b/src/i18n/nl.json @@ -301,6 +301,16 @@ "pe.fade": "Vervaagd", "pe.sepia": "Sepia", "pe.noir": "Noir", + "co.preview": "Voorbeeld van de collage", + "co.layout": "Indeling", + "co.photo": "Foto", + "co.space": "Ruimte", + "co.gap": "Tussenruimte", + "co.corners": "Ronde hoeken", + "co.a4": "A4-afdruk", + "co.max": "Alleen de eerste 9 foto's worden gebruikt.", + "co.order": "Wil je foto's verwisselen? Verander dan hun volgorde in de lijst hierboven.", + "co.tapHint": "Tik op een foto om hem te kiezen en sleep dan om hem in zijn vak te verplaatsen.", "st.text": "Tekst", "st.hint": "Sleep de tekst over de afbeelding. Knijp met twee vingers, of gebruik de schuifjes, om hem groter te maken en te draaien.", "st.add": "Tekst toevoegen", @@ -3071,6 +3081,49 @@ "transparante sticker webp" ] }, + "collage-maker": { + "name": "Collagemaker", + "summary": "Zet 2 tot 9 foto's in één afbeelding, met tussenruimte, ronde hoeken en tekst.", + "action": "Collage downloaden", + "title": "Gratis Fotocollage Maken — Zonder Upload, Zonder Watermerk | Fizzdoc", + "description": "Maak gratis een fotocollage in je browser: indeling voor 2 tot 9 foto's, tussenruimte, hoeken, formaat en tekst naar keuze. Geen upload, account of watermerk.", + "h1": "Maak een fotocollage zonder je foto's te uploaden", + "lede": "Kies 2 tot 9 foto's, kies een indeling en een vorm (vierkant, story, A4 en meer), verplaats en zoom elke foto in zijn vak, en voeg tussenruimte, ronde hoeken, een achtergrondkleur en tekst toe. Alles gebeurt op je eigen apparaat.", + "steps": [ + "Kies 2 tot 9 foto's. De volgorde in de lijst is de volgorde in de collage.", + "Kies een indeling en een vorm, verplaats en zoom elke foto, en stel tussenruimte, hoeken, achtergrond en tekst in.", + "Kies het formaat en het bestandstype en klik dan op “Collage downloaden”." + ], + "faq": [ + [ + "Worden mijn bestanden ergens geüpload?", + "Nee. Fizzdoc doet het werk in je browsertab, met opensource-engines die zijn gecompileerd naar WebAssembly en JavaScript. Je bestand wordt van je apparaat gelezen, lokaal verwerkt, en het resultaat wordt direct weer op je apparaat opgeslagen. Je kunt dit zelf controleren: open de ontwikkelaarstools van je browser, ga naar het tabblad Netwerk en voer een taak uit — er wordt geen enkel verzoek met je bestand verstuurd." + ], + [ + "Hoeveel foto's kan een collage hebben?", + "Van 1 tot 9. Elk aantal foto's heeft eigen indelingen, zoals naast elkaar, onder elkaar, één grote foto met kleinere foto's, of een raster. Voeg je er meer dan 9 toe, dan worden de eerste 9 gebruikt." + ], + [ + "Kan ik elke foto verplaatsen of erop inzoomen?", + "Ja. Open Foto, tik op een foto om hem te kiezen, sleep hem om te kiezen wat er in zijn vak te zien is en gebruik Zoom om dichterbij te komen. Wil je foto's verwisselen? Verander dan hun volgorde in de bestandenlijst." + ], + [ + "Welke formaten kan ik opslaan?", + "Vierkant, Staand 4:5 en Story 9:16 voor sociale media, Breed 16:9, en A4-afdruk om te printen: 1080 of 2160 px aan de korte zijde, of 2480 px voor A4 bij 300 dpi. Als JPG, PNG of WebP, zonder watermerk." + ], + [ + "Is Fizzdoc gratis?", + "Ja, en het blijft gratis: geen account, geen watermerk, geen dagelijkse limiet en geen premium-abonnement. Fizzdoc is open source onder de Apache-2.0-licentie, dus iedereen kan zijn eigen versie draaien." + ] + ], + "keywords": [ + "fotocollage maken", + "collage maken gratis", + "foto's samenvoegen", + "collage maker online", + "fotocollage zonder watermerk" + ] + }, "photo-editor": { "name": "Foto-editor", "summary": "Snijd een foto bij, draai hem, maak hem lichter en zet er tekst of emoji op, direct in je browser.", diff --git a/src/i18n/pl.json b/src/i18n/pl.json index ac28824..4248811 100644 --- a/src/i18n/pl.json +++ b/src/i18n/pl.json @@ -301,6 +301,16 @@ "pe.fade": "Wyblakły", "pe.sepia": "Sepia", "pe.noir": "Noir", + "co.preview": "Podgląd kolażu", + "co.layout": "Układ", + "co.photo": "Zdjęcie", + "co.space": "Odstępy", + "co.gap": "Odstęp", + "co.corners": "Zaokrąglone rogi", + "co.a4": "Wydruk A4", + "co.max": "Używanych jest tylko pierwszych 9 zdjęć.", + "co.order": "Aby zamienić zdjęcia miejscami, zmień ich kolejność na liście powyżej.", + "co.tapHint": "Dotknij zdjęcia, aby je wybrać, a potem przeciągnij, aby przesunąć je w ramce.", "st.text": "Tekst", "st.hint": "Przeciągnij tekst na obrazku. Zsuń dwa palce albo użyj suwaków, żeby zmienić rozmiar i obrócić tekst.", "st.add": "Dodaj tekst", @@ -3071,6 +3081,49 @@ "przezroczysta naklejka webp" ] }, + "collage-maker": { + "name": "Kreator kolaży", + "summary": "Połącz od 2 do 9 zdjęć w jeden obraz, z odstępem, zaokrąglonymi rogami i tekstem.", + "action": "Pobierz kolaż", + "title": "Kolaż ze Zdjęć za Darmo — Bez Wysyłania i Znaku Wodnego | Fizzdoc", + "description": "Zrób za darmo kolaż w przeglądarce: wybierz układ dla 2 do 9 zdjęć, ustaw odstęp, rogi i rozmiar, dodaj tekst. Bez wysyłania, rejestracji i znaku wodnego.", + "h1": "Zrób kolaż ze zdjęć bez ich wysyłania", + "lede": "Wybierz od 2 do 9 zdjęć, wybierz układ i kształt (kwadrat, relacja, A4 i inne), przesuń i powiększ każde zdjęcie w jego ramce oraz dodaj odstęp, zaokrąglone rogi, kolor tła i tekst. Wszystko dzieje się na Twoim urządzeniu.", + "steps": [ + "Wybierz od 2 do 9 zdjęć. Kolejność na liście to kolejność w kolażu.", + "Wybierz układ i kształt, przesuń i powiększ każde zdjęcie oraz ustaw odstęp, rogi, tło i tekst.", + "Wybierz rozmiar i format, a potem kliknij „Pobierz kolaż”." + ], + "faq": [ + [ + "Czy moje pliki są gdzieś przesyłane?", + "Nie. Fizzdoc wykonuje całą pracę w karcie przeglądarki, korzystając z otwartoźródłowych silników skompilowanych do WebAssembly i JavaScript. Twój plik jest odczytywany z urządzenia, przetwarzany lokalnie, a wynik trafia od razu z powrotem na Twoje urządzenie. Możesz to sam sprawdzić: otwórz narzędzia deweloperskie przeglądarki, przejdź do zakładki Sieć i uruchom zadanie — żadne żądanie nie przenosi Twojego pliku." + ], + [ + "Ile zdjęć może mieć kolaż?", + "Od 1 do 9. Każda liczba zdjęć ma własne układy, na przykład obok siebie, jedno pod drugim, jedno duże zdjęcie z mniejszymi albo siatka. Jeśli dodasz więcej niż 9, użytych zostanie pierwszych 9." + ], + [ + "Czy mogę przesunąć lub powiększyć każde zdjęcie?", + "Tak. Otwórz „Zdjęcie”, dotknij zdjęcia, aby je wybrać, a potem przeciągnij je, aby wybrać, co widać w ramce, i użyj suwaka „Powiększenie”, aby je przybliżyć. Aby zamienić zdjęcia miejscami, zmień ich kolejność na liście plików." + ], + [ + "Jakie rozmiary mogę zapisać?", + "„Kwadrat”, „Pionowy 4:5” i „Relacja 9:16” do mediów społecznościowych, „Szeroki 16:9” oraz „Wydruk A4” do druku: 1080 lub 2160 px na krótszym boku albo 2480 px dla A4 przy 300 dpi. Jako JPG, PNG lub WebP, bez znaku wodnego." + ], + [ + "Czy Fizzdoc jest darmowy?", + "Tak, i zawsze będzie za darmo: bez rejestracji, bez znaku wodnego, bez dziennego limitu i bez płatnej wersji. Fizzdoc jest open source na licencji Apache-2.0, więc każdy może uruchomić własną kopię." + ] + ], + "keywords": [ + "kolaż ze zdjęć", + "kolaż zdjęć online za darmo", + "połącz zdjęcia w jedno", + "fotokolaż online", + "kolaż bez znaku wodnego" + ] + }, "photo-editor": { "name": "Edytor zdjęć", "summary": "Przytnij, obróć i rozjaśnij zdjęcie oraz dodaj do niego napis lub emoji, prosto w przeglądarce.", diff --git a/src/i18n/pt.json b/src/i18n/pt.json index 960a196..c3c3dd5 100644 --- a/src/i18n/pt.json +++ b/src/i18n/pt.json @@ -301,6 +301,16 @@ "pe.fade": "Desbotado", "pe.sepia": "Sépia", "pe.noir": "Noir", + "co.preview": "Prévia da colagem", + "co.layout": "Layout", + "co.photo": "Foto", + "co.space": "Espaço", + "co.gap": "Espaçamento", + "co.corners": "Cantos arredondados", + "co.a4": "A4 para impressão", + "co.max": "Só as 9 primeiras fotos são usadas.", + "co.order": "Para trocar as fotos de lugar, mude a ordem delas na lista acima.", + "co.tapHint": "Toque em uma foto para escolhê-la e depois arraste para movê-la dentro do quadro.", "st.text": "Texto", "st.hint": "Arraste o texto sobre a imagem. Faça o gesto de pinça com dois dedos, ou use os controles, para mudar o tamanho e girar.", "st.add": "Adicionar texto", @@ -3071,6 +3081,49 @@ "figurinha webp transparente" ] }, + "collage-maker": { + "name": "Criador de Colagens", + "summary": "Junte de 2 a 9 fotos em uma só imagem, com espaçamento, cantos arredondados e texto.", + "action": "Baixar colagem", + "title": "Colagem de Fotos Grátis — Sem Upload, Sem Marca d'Água | Fizzdoc", + "description": "Faça uma colagem de fotos grátis no navegador: layout para 2 a 9 fotos, espaçamento, cantos, tamanho e texto. Sem upload, cadastro ou marca d'água.", + "h1": "Faça uma colagem de fotos sem enviar suas fotos", + "lede": "Escolha de 2 a 9 fotos, um layout e um formato (quadrado, story, A4 e mais), mova e dê zoom em cada foto dentro do quadro e adicione espaçamento, cantos arredondados, uma cor de fundo e texto. Tudo acontece no seu aparelho.", + "steps": [ + "Escolha de 2 a 9 fotos. A ordem da lista é a ordem na colagem.", + "Escolha um layout e um formato, mova e dê zoom em cada foto e ajuste o espaçamento, os cantos, o fundo e o texto.", + "Escolha o tamanho e o formato e clique em “Baixar colagem”." + ], + "faq": [ + [ + "Meus arquivos são enviados para algum lugar?", + "Não. O Fizzdoc faz todo o trabalho dentro da aba do seu navegador, usando mecanismos de código aberto compilados para WebAssembly e JavaScript. Seu arquivo é lido do seu dispositivo, processado localmente, e o resultado é salvo direto de volta nele. Você pode conferir por conta própria: abra as ferramentas de desenvolvedor do navegador, vá até a aba Rede e rode uma tarefa — nenhuma requisição leva seu arquivo." + ], + [ + "Quantas fotos uma colagem pode ter?", + "De 1 a 9. Cada quantidade de fotos tem seus próprios layouts, como lado a lado, uma em cima da outra, uma foto grande com outras menores ou uma grade. Se você adicionar mais de 9, as 9 primeiras são usadas." + ], + [ + "Posso mover ou dar zoom em cada foto?", + "Sim. Abra “Foto”, toque em uma foto para escolhê-la, arraste para definir o que aparece no quadro e use “Zoom” para aproximar. Para trocar as fotos de lugar, mude a ordem delas na lista de arquivos." + ], + [ + "Quais tamanhos posso salvar?", + "“Quadrado”, “Retrato 4:5” e “Story 9:16” para redes sociais, “Paisagem 16:9” e “A4 para impressão”: 1080 ou 2160 px no lado menor, ou 2480 px para A4 a 300 dpi. Em JPG, PNG ou WebP, sem marca d'água." + ], + [ + "O Fizzdoc é gratuito?", + "Sim, e vai continuar sendo grátis: sem cadastro, sem marca d'água, sem limite diário e sem plano premium. O Fizzdoc é open source sob a licença Apache-2.0, então qualquer um pode rodar sua própria cópia." + ] + ], + "keywords": [ + "fazer colagem de fotos", + "colagem de fotos online grátis", + "montagem de fotos online", + "juntar fotos em uma só", + "mosaico de fotos" + ] + }, "photo-editor": { "name": "Editor de Fotos", "summary": "Corte, gire, clareie e adicione texto ou emoji a uma foto, direto no navegador.", diff --git a/src/i18n/ta.json b/src/i18n/ta.json index b40bb6d..68a9377 100644 --- a/src/i18n/ta.json +++ b/src/i18n/ta.json @@ -301,6 +301,16 @@ "pe.fade": "வெளிர்", "pe.sepia": "செபியா", "pe.noir": "நுவார்", + "co.preview": "கொலாஜின் முன்னோட்டம்", + "co.layout": "தளவமைப்பு", + "co.photo": "படம்", + "co.space": "இடம்", + "co.gap": "இடைவெளி", + "co.corners": "வட்ட மூலைகள்", + "co.a4": "A4 அச்சு", + "co.max": "முதல் 9 படங்கள் மட்டுமே பயன்படுத்தப்படும்.", + "co.order": "படங்களை இடம் மாற்ற, மேலே உள்ள பட்டியலில் அவற்றின் வரிசையை மாற்றவும்.", + "co.tapHint": "ஒரு படத்தைத் தேர்ந்தெடுக்க அதைத் தட்டவும், பிறகு அதன் பிரேமுக்குள் நகர்த்த இழுக்கவும்.", "st.text": "உரை", "st.hint": "உரையைப் படத்தின் மேல் இழுக்கவும். அளவை மாற்றவும் சுழற்றவும் இரண்டு விரல்களால் பிஞ்ச் செய்யவும், அல்லது ஸ்லைடர்களைப் பயன்படுத்தவும்.", "st.add": "உரை சேர்", @@ -3071,6 +3081,49 @@ "whatsapp sticker maker" ] }, + "collage-maker": { + "name": "கொலாஜ் மேக்கர்", + "summary": "2 முதல் 9 புகைப்படங்களை இடைவெளி, வட்ட மூலைகள், உரையுடன் ஒரே படமாக இணைக்கவும்.", + "action": "கொலாஜைப் பதிவிறக்கு", + "title": "கொலாஜ் மேக்கர் இலவசம் — பதிவேற்றம், வாட்டர்மார்க் இல்லை | Fizzdoc", + "description": "பிரௌசரில் இலவசமாக புகைப்பட கொலாஜ் உருவாக்குங்கள்: 2 முதல் 9 படங்களுக்கு தளவமைப்பு, இடைவெளி, மூலைகள், அளவு, உரை. பதிவேற்றம், சைன் அப், வாட்டர்மார்க் இல்லை.", + "h1": "புகைப்படங்களைப் பதிவேற்றாமல் கொலாஜ் உருவாக்குங்கள்", + "lede": "2 முதல் 9 படங்களைத் தேர்ந்தெடுங்கள், ஒரு தளவமைப்பையும் வடிவத்தையும் (சதுரம், ஸ்டோரி, A4 மற்றும் பல) தேர்ந்தெடுங்கள், ஒவ்வொரு படத்தையும் அதன் பிரேமுக்குள் நகர்த்தி ஜூம் செய்யுங்கள், இடைவெளி, வட்ட மூலைகள், பின்னணி நிறம், உரை சேருங்கள். எல்லாம் உங்கள் சொந்தச் சாதனத்திலேயே நடக்கும்.", + "steps": [ + "2 முதல் 9 படங்களைத் தேர்ந்தெடுக்கவும். பட்டியலின் வரிசையே கொலாஜின் வரிசை.", + "தளவமைப்பையும் வடிவத்தையும் தேர்ந்தெடுக்கவும், ஒவ்வொரு படத்தையும் நகர்த்தி ஜூம் செய்யவும், இடைவெளி, மூலைகள், பின்னணி, உரையை அமைக்கவும்.", + "அளவையும் ஃபார்மேட்டையும் தேர்ந்தெடுத்து, “கொலாஜைப் பதிவிறக்கு” என்பதைக் கிளிக் செய்யவும்." + ], + "faq": [ + [ + "எனது கோப்புகள் எங்காவது பதிவேற்றப்படுமா?", + "இல்லை. Fizzdoc உங்கள் பிரௌசர் டேபிற்குள்ளேயே வேலை செய்கிறது, WebAssembly மற்றும் JavaScript-ல் தொகுக்கப்பட்ட ஓப்பன் சோர்ஸ் இன்ஜின்களைப் பயன்படுத்தி. உங்கள் கோப்பு உங்கள் டிவைஸிலிருந்து படிக்கப்பட்டு, அதிலேயே செயலாக்கப்பட்டு, முடிவு நேரடியாக உங்கள் டிவைஸில் சேமிக்கப்படுகிறது. இதை நீங்களே சரிபார்க்கலாம்: உங்கள் பிரௌசரின் டெவலப்பர் டூல்ஸை திறந்து, Network டேபிற்குச் சென்று, ஒரு பணியை இயக்குங்கள் — எந்த ரிக்வெஸ்ட்டும் உங்கள் கோப்பை எடுத்துச் செல்லாது." + ], + [ + "ஒரு கொலாஜில் எத்தனை படங்கள் இருக்கலாம்?", + "1 முதல் 9 வரை. படங்களின் எண்ணிக்கைக்கு ஏற்ப தனித்தனி தளவமைப்புகள் உண்டு, எடுத்துக்காட்டாக அருகருகே, ஒன்றன் கீழ் ஒன்றாக, ஒரு பெரிய படமும் சிறிய படங்களும், அல்லது கட்டங்களாக. 9-க்கு மேல் சேர்த்தால், முதல் 9 பயன்படுத்தப்படும்." + ], + [ + "ஒவ்வொரு படத்தையும் நகர்த்தவோ ஜூம் செய்யவோ முடியுமா?", + "ஆம். “படம்” என்பதைத் திறந்து, ஒரு படத்தைத் தேர்ந்தெடுக்க அதைத் தட்டுங்கள், பிறகு அதன் பிரேமில் எது தெரிய வேண்டும் என்பதைத் தேர்ந்தெடுக்க அதை இழுங்கள், அருகில் காட்ட “ஜூம்” பயன்படுத்துங்கள். படங்களை இடம் மாற்ற, கோப்புப் பட்டியலில் அவற்றின் வரிசையை மாற்றுங்கள்." + ], + [ + "எந்த அளவுகளில் சேமிக்கலாம்?", + "சமூக ஊடகங்களுக்கு “சதுரம்”, “போர்ட்ரெய்ட் 4:5”, “ஸ்டோரி 9:16”, அத்துடன் “அகன்ற 16:9”, அச்சிட “A4 அச்சு”: குறுகிய பக்கத்தில் 1080 அல்லது 2160 px, அல்லது A4-க்கு 300 dpi-இல் 2480 px. JPG, PNG அல்லது WebP ஆக, வாட்டர்மார்க் இல்லாமல்." + ], + [ + "Fizzdoc இலவசமா?", + "ஆம், இது எப்போதும் இலவசமாகவே இருக்கும்: சைன் அப் இல்லை, வாட்டர்மார்க் இல்லை, தினசரி வரம்பு இல்லை, பிரீமியம் திட்டமும் இல்லை. Fizzdoc Apache-2.0 லைசென்ஸின் கீழ் ஓப்பன் சோர்ஸ், எனவே யார் வேண்டுமானாலும் தங்கள் சொந்தக் காப்பியை இயக்கலாம்." + ] + ], + "keywords": [ + "போட்டோ கொலாஜ் மேக்கர்", + "புகைப்பட கொலாஜ் செய்ய", + "கொலாஜ் மேக்கர் இலவசம்", + "இரண்டு போட்டோவை ஒன்றாக சேர்க்க", + "வாட்டர்மார்க் இல்லாத கொலாஜ்" + ] + }, "photo-editor": { "name": "புகைப்படத்தைத் திருத்து", "summary": "உங்கள் பிரௌசரிலேயே புகைப்படத்தைச் செதுக்கவும், சுழற்றவும், பிரகாசமாக்கவும், சொற்கள் அல்லது எமோஜி சேர்க்கவும்.", diff --git a/src/i18n/te.json b/src/i18n/te.json index 39523c6..6545cd2 100644 --- a/src/i18n/te.json +++ b/src/i18n/te.json @@ -301,6 +301,16 @@ "pe.fade": "వెలిసిన", "pe.sepia": "సెపియా", "pe.noir": "న్వార్", + "co.preview": "కొలాజ్ ప్రివ్యూ", + "co.layout": "లేఅవుట్", + "co.photo": "ఫోటో", + "co.space": "స్పేస్", + "co.gap": "గ్యాప్", + "co.corners": "గుండ్రని మూలలు", + "co.a4": "A4 ప్రింట్", + "co.max": "మొదటి 9 ఫోటోలు మాత్రమే వాడబడతాయి.", + "co.order": "ఫోటోల స్థానాలు మార్చడానికి, పైన ఉన్న జాబితాలో వాటి క్రమాన్ని మార్చండి.", + "co.tapHint": "ఒక ఫోటోను ఎంచుకోవడానికి దానిపై నొక్కండి, ఆపై దాని ఫ్రేమ్‌లో జరపడానికి లాగండి.", "st.text": "టెక్స్ట్", "st.hint": "టెక్స్ట్‌ను చిత్రంపై లాగండి. పరిమాణం మార్చడానికి, తిప్పడానికి రెండు వేళ్లతో పించ్ చేయండి, లేదా స్లైడర్లు వాడండి.", "st.add": "టెక్స్ట్ జోడించండి", @@ -3071,6 +3081,49 @@ "whatsapp sticker maker" ] }, + "collage-maker": { + "name": "కొలాజ్ మేకర్", + "summary": "2 నుండి 9 ఫోటోలను గ్యాప్, గుండ్రని మూలలు, టెక్స్ట్‌తో ఒకే చిత్రంలో పెట్టండి.", + "action": "కొలాజ్ డౌన్‌లోడ్ చేయండి", + "title": "ఉచిత ఫోటో కొలాజ్ మేకర్ — అప్‌లోడ్ లేదు, వాటర్‌మార్క్ లేదు | Fizzdoc", + "description": "బ్రౌజర్‌లో ఉచితంగా ఫోటో కొలాజ్: 2 నుండి 9 ఫోటోలకు లేఅవుట్ ఎంచుకోండి, గ్యాప్, మూలలు, సైజ్ సెట్ చేయండి, టెక్స్ట్ జోడించండి. అప్‌లోడ్, సైన్-అప్, వాటర్‌మార్క్ లేదు.", + "h1": "మీ ఫోటోలను అప్‌లోడ్ చేయకుండానే ఫోటో కొలాజ్ తయారు చేయండి", + "lede": "2 నుండి 9 ఫోటోలు ఎంచుకోండి, ఒక లేఅవుట్, ఆకారం (చతురస్రం, స్టోరీ, A4 మొదలైనవి) ఎంచుకోండి, ప్రతి ఫోటోను దాని ఫ్రేమ్‌లో జరిపి జూమ్ చేయండి, గ్యాప్, గుండ్రని మూలలు, బ్యాక్‌గ్రౌండ్ రంగు, టెక్స్ట్ జోడించండి. అంతా మీ సొంత పరికరంలోనే జరుగుతుంది.", + "steps": [ + "2 నుండి 9 ఫోటోలు ఎంచుకోండి. జాబితాలోని క్రమమే కొలాజ్‌లోని క్రమం.", + "లేఅవుట్, ఆకారం ఎంచుకోండి, ప్రతి ఫోటోను జరిపి జూమ్ చేయండి, గ్యాప్, మూలలు, బ్యాక్‌గ్రౌండ్, టెక్స్ట్ సెట్ చేయండి.", + "సైజ్, ఫార్మాట్ ఎంచుకుని, “కొలాజ్ డౌన్‌లోడ్ చేయండి” క్లిక్ చేయండి." + ], + "faq": [ + [ + "మీ ఫైల్స్ ఎక్కడైనా అప్‌లోడ్ అవుతాయా?", + "లేదు. Fizzdoc ఈ పనిని మీ బ్రౌజర్ ట్యాబ్‌లోనే చేస్తుంది, WebAssembly మరియు JavaScriptకి కంపైల్ చేసిన ఓపెన్ సోర్స్ ఇంజన్‌లతో. మీ ఫైల్ మీ డివైజ్ నుండి చదవబడి, స్థానికంగా ప్రాసెస్ చేయబడి, ఫలితం నేరుగా మీ డివైజ్‌కే తిరిగి సేవ్ చేయబడుతుంది. మీరే దీన్ని చెక్ చేసుకోవచ్చు: బ్రౌజర్ డెవలపర్ టూల్స్ తెరిచి, Network ట్యాబ్‌కి వెళ్లి ఒక జాబ్ రన్ చేయండి — ఏ రిక్వెస్ట్ కూడా మీ ఫైల్‌ను తీసుకెళ్లదు." + ], + [ + "ఒక కొలాజ్‌లో ఎన్ని ఫోటోలు ఉండవచ్చు?", + "1 నుండి 9 వరకు. ఫోటోల సంఖ్యను బట్టి వేర్వేరు లేఅవుట్‌లు ఉన్నాయి, ఉదాహరణకు పక్కపక్కన, ఒకదాని కింద ఒకటి, ఒక పెద్ద ఫోటోతో పాటు చిన్న ఫోటోలు, లేదా గ్రిడ్. 9 కంటే ఎక్కువ జోడిస్తే, మొదటి 9 వాడబడతాయి." + ], + [ + "ప్రతి ఫోటోను జరపవచ్చా లేదా జూమ్ చేయవచ్చా?", + "అవును. “ఫోటో” తెరిచి, ఒక ఫోటోను ఎంచుకోవడానికి దానిపై నొక్కండి, ఆపై దాని ఫ్రేమ్‌లో ఏం కనిపించాలో ఎంచుకోవడానికి దాన్ని లాగండి, దగ్గరగా చూడటానికి “జూమ్” వాడండి. ఫోటోల స్థానాలు మార్చడానికి, ఫైల్ జాబితాలో వాటి క్రమాన్ని మార్చండి." + ], + [ + "ఏ సైజ్‌లలో సేవ్ చేయవచ్చు?", + "సోషల్ మీడియా కోసం “చతురస్రం”, “పోర్ట్రెయిట్ 4:5”, “స్టోరీ 9:16”, అలాగే “వెడల్పాటి 16:9”, ప్రింట్ కోసం “A4 ప్రింట్”: చిన్న వైపున 1080 లేదా 2160 px, లేదా A4 కోసం 300 dpi వద్ద 2480 px. JPG, PNG లేదా WebPగా, వాటర్‌మార్క్ లేకుండా." + ], + [ + "Fizzdoc ఉచితమా?", + "అవును, ఇది ఎప్పటికీ ఉచితంగానే ఉంటుంది: సైన్-అప్ లేదు, వాటర్‌మార్క్ లేదు, రోజువారీ పరిమితి లేదు, ప్రీమియం ప్లాన్ కూడా లేదు. Fizzdoc Apache-2.0 లైసెన్స్ కింద ఓపెన్ సోర్స్, కాబట్టి ఎవరైనా తమ సొంత కాపీని రన్ చేసుకోవచ్చు." + ] + ], + "keywords": [ + "ఫోటో కొలాజ్ మేకర్", + "ఫోటో కొలాజ్ తయారు చేయడం", + "కొలాజ్ మేకర్ ఉచితం", + "రెండు ఫోటోలు కలపడం", + "వాటర్‌మార్క్ లేని కొలాజ్" + ] + }, "photo-editor": { "name": "ఫోటో ఎడిటర్", "summary": "మీ బ్రౌజర్‌లోనే ఫోటోను క్రాప్ చేయండి, తిప్పండి, వెలుతురు పెంచండి, మాటలు లేదా ఎమోజీ జోడించండి.", diff --git a/src/i18n/tr.json b/src/i18n/tr.json index e8b265c..9f6500c 100644 --- a/src/i18n/tr.json +++ b/src/i18n/tr.json @@ -301,6 +301,16 @@ "pe.fade": "Soluk", "pe.sepia": "Sepya", "pe.noir": "Noir", + "co.preview": "Kolajın önizlemesi", + "co.layout": "Düzen", + "co.photo": "Fotoğraf", + "co.space": "Boşluk", + "co.gap": "Aralık", + "co.corners": "Yuvarlak köşeler", + "co.a4": "A4 baskı", + "co.max": "Yalnızca ilk 9 fotoğraf kullanılır.", + "co.order": "Fotoğrafların yerini değiştirmek için yukarıdaki listede sıralarını değiştirin.", + "co.tapHint": "Seçmek için bir fotoğrafa dokunun, sonra çerçevesinde kaydırmak için sürükleyin.", "st.text": "Metin", "st.hint": "Metni resmin üzerinde sürükleyin. Boyutunu değiştirmek ve döndürmek için iki parmakla sıkıştırın ya da kaydırıcıları kullanın.", "st.add": "Metin ekle", @@ -3071,6 +3081,49 @@ "şeffaf çıkartma webp" ] }, + "collage-maker": { + "name": "Kolaj Yapıcı", + "summary": "2 ila 9 fotoğrafı aralık, yuvarlak köşeler ve yazıyla tek bir resimde birleştirin.", + "action": "Kolajı indir", + "title": "Ücretsiz Fotoğraf Kolajı Yapma — Yükleme Yok, Filigran Yok | Fizzdoc", + "description": "Tarayıcıda ücretsiz fotoğraf kolajı yapın: 2 ila 9 fotoğraf için düzen seçin, aralığı, köşeleri ve boyutu ayarlayın, yazı ekleyin. Yükleme, kayıt, filigran yok.", + "h1": "Fotoğraflarınızı yüklemeden kolaj yapın", + "lede": "2 ila 9 fotoğraf seçin, bir düzen ve bir şekil (kare, hikaye, A4 ve daha fazlası) seçin, her fotoğrafı kendi çerçevesinde kaydırıp yakınlaştırın; aralık, yuvarlak köşeler, arka plan rengi ve yazı ekleyin. Her şey kendi cihazınızda yapılır.", + "steps": [ + "2 ila 9 fotoğraf seçin. Listedeki sıra, kolajdaki sıradır.", + "Bir düzen ve şekil seçin, her fotoğrafı kaydırıp yakınlaştırın; aralığı, köşeleri, arka planı ve yazıyı ayarlayın.", + "Boyutu ve formatı seçin, sonra “Kolajı indir”e tıklayın." + ], + "faq": [ + [ + "Dosyalarım herhangi bir yere yükleniyor mu?", + "Hayır. Fizzdoc, WebAssembly ve JavaScript'e derlenmiş açık kaynaklı motorları kullanarak işlemi tarayıcı sekmenizin içinde yapar. Dosyanız cihazınızdan okunur, yerel olarak işlenir ve sonuç doğrudan cihazınıza kaydedilir. Bunu kendiniz de kontrol edebilirsiniz: tarayıcınızın geliştirici araçlarını açın, Ağ (Network) sekmesine gidin ve bir işlem çalıştırın — hiçbir istek dosyanızı taşımaz." + ], + [ + "Bir kolajda kaç fotoğraf olabilir?", + "1 ile 9 arası. Her fotoğraf sayısının kendi düzenleri vardır: yan yana, alt alta, büyük bir fotoğrafla birkaç küçük fotoğraf ya da ızgara gibi. 9'dan fazla eklerseniz ilk 9 fotoğraf kullanılır." + ], + [ + "Her fotoğrafı kaydırabilir veya yakınlaştırabilir miyim?", + "Evet. Fotoğraf sekmesini açın, seçmek için bir fotoğrafa dokunun, sonra çerçevede neyin görüneceğini seçmek için sürükleyin ve yaklaşmak için Yakınlaştırma kaydırıcısını kullanın. Fotoğrafların yerini değiştirmek için dosya listesindeki sıralarını değiştirin." + ], + [ + "Hangi boyutlarda kaydedebilirim?", + "Sosyal medya için Kare, Dikey 4:5 ve Hikaye 9:16, ayrıca Geniş 16:9 ve yazdırmak için A4 baskı: kısa kenarda 1080 veya 2160 px, A4 için ise 300 dpi'da 2480 px. JPG, PNG veya WebP olarak, filigransız." + ], + [ + "Fizzdoc ücretsiz mi?", + "Evet, ve ücretsiz kalacak: kayıt yok, filigran yok, günlük limit yok, ücretli katman yok. Fizzdoc, Apache-2.0 lisansıyla açık kaynaktır, bu yüzden herkes kendi kopyasını çalıştırabilir." + ] + ], + "keywords": [ + "fotoğraf kolajı yapma", + "ücretsiz kolaj yapma", + "fotoğraf birleştirme", + "online kolaj yapma", + "filigransız kolaj yapma" + ] + }, "photo-editor": { "name": "Fotoğraf Düzenleyici", "summary": "Fotoğrafı kırpın, döndürün, aydınlatın ve üzerine yazı ya da emoji ekleyin, doğrudan tarayıcınızda.", diff --git a/src/i18n/vi.json b/src/i18n/vi.json index ee7e09b..b832c79 100644 --- a/src/i18n/vi.json +++ b/src/i18n/vi.json @@ -301,6 +301,16 @@ "pe.fade": "Phai màu", "pe.sepia": "Sepia", "pe.noir": "Noir", + "co.preview": "Xem trước ảnh ghép", + "co.layout": "Bố cục", + "co.photo": "Ảnh", + "co.space": "Giãn cách", + "co.gap": "Khoảng cách", + "co.corners": "Bo góc", + "co.a4": "In A4", + "co.max": "Chỉ dùng 9 ảnh đầu tiên.", + "co.order": "Để đổi chỗ ảnh, hãy đổi thứ tự của chúng trong danh sách ở trên.", + "co.tapHint": "Chạm vào một ảnh để chọn, rồi kéo để di chuyển ảnh trong khung.", "st.text": "Chữ", "st.hint": "Kéo chữ trên ảnh. Chụm hai ngón tay, hoặc dùng thanh trượt, để đổi cỡ và xoay chữ.", "st.add": "Thêm chữ", @@ -3071,6 +3081,49 @@ "sticker webp nền trong suốt" ] }, + "collage-maker": { + "name": "Ghép ảnh", + "summary": "Ghép 2 đến 9 ảnh thành một bức ảnh, có khoảng cách, bo góc và chữ.", + "action": "Tải ảnh ghép xuống", + "title": "Ghép Ảnh Online Miễn Phí — Không Tải Lên, Không Watermark | Fizzdoc", + "description": "Ghép ảnh miễn phí trong trình duyệt: chọn bố cục cho 2 đến 9 ảnh, chỉnh khoảng cách, góc và kích thước, thêm chữ. Không tải lên, không đăng ký, không watermark.", + "h1": "Ghép ảnh mà không cần tải ảnh lên", + "lede": "Chọn 2 đến 9 ảnh, chọn bố cục và khung hình (vuông, story, A4 và nhiều khung khác), di chuyển và thu phóng từng ảnh trong khung của nó, rồi thêm khoảng cách, bo góc, màu nền và chữ. Mọi thứ diễn ra ngay trên thiết bị của bạn.", + "steps": [ + "Chọn 2 đến 9 ảnh. Thứ tự trong danh sách là thứ tự trong ảnh ghép.", + "Chọn bố cục và khung hình, di chuyển và thu phóng từng ảnh, rồi chỉnh khoảng cách, góc, nền và chữ.", + "Chọn kích thước và định dạng, rồi nhấp “Tải ảnh ghép xuống”." + ], + "faq": [ + [ + "Tệp của tôi có được tải lên đâu không?", + "Không. Fizzdoc xử lý mọi thứ ngay trong tab trình duyệt của bạn, sử dụng các engine mã nguồn mở được biên dịch sang WebAssembly và JavaScript. Tệp của bạn được đọc từ thiết bị, xử lý cục bộ, rồi kết quả được lưu thẳng trở lại thiết bị. Bạn có thể tự kiểm chứng: mở công cụ dành cho nhà phát triển của trình duyệt, vào tab Network và thực hiện một thao tác — sẽ không có yêu cầu nào mang theo tệp của bạn." + ], + [ + "Một ảnh ghép có thể có bao nhiêu ảnh?", + "Từ 1 đến 9. Mỗi số lượng ảnh có các bố cục riêng, như đặt cạnh nhau, xếp trên dưới, một ảnh lớn kèm các ảnh nhỏ hơn, hoặc dạng lưới. Nếu bạn thêm hơn 9 ảnh, 9 ảnh đầu tiên sẽ được dùng." + ], + [ + "Tôi có thể di chuyển hoặc phóng to từng ảnh không?", + "Có. Mở “Ảnh”, chạm vào một ảnh để chọn, rồi kéo để chọn phần hiển thị trong khung, và dùng “Thu phóng” để nhìn gần hơn. Để đổi chỗ ảnh, hãy đổi thứ tự của chúng trong danh sách tệp." + ], + [ + "Tôi có thể lưu những kích thước nào?", + "“Vuông”, “Dọc 4:5” và “Story 9:16” cho mạng xã hội, “Ngang 16:9”, và “In A4” để in: 1080 hoặc 2160 px ở cạnh ngắn, hoặc 2480 px cho A4 ở 300 dpi. Dưới dạng JPG, PNG hoặc WebP, không có watermark." + ], + [ + "Fizzdoc có miễn phí không?", + "Có, và sẽ luôn miễn phí: không cần đăng ký, không watermark, không giới hạn hằng ngày và không có gói trả phí. Fizzdoc là mã nguồn mở theo giấy phép Apache-2.0, nên ai cũng có thể tự chạy một bản sao của riêng mình." + ] + ], + "keywords": [ + "ghép ảnh online", + "ghép ảnh miễn phí", + "ghép nhiều ảnh thành một", + "làm ảnh collage", + "ghép ảnh không watermark" + ] + }, "photo-editor": { "name": "Chỉnh sửa ảnh", "summary": "Cắt, xoay, làm sáng và thêm chữ hoặc biểu tượng cảm xúc vào ảnh, ngay trong trình duyệt.", diff --git a/src/seo.ts b/src/seo.ts index b9fec72..17d59b8 100644 --- a/src/seo.ts +++ b/src/seo.ts @@ -108,6 +108,7 @@ const ICON_PATHS: Record = { 'edit-pdf': '', 'remove-bg': '', 'photo-edit': '', + collage: '', 'redact-pdf': '', 'scan-pdf': '', 'ocr-pdf': SCAN, @@ -207,6 +208,7 @@ const OP_ENGINES: Partial> = { 'mermaid-image': ['Mermaid'], 'image-convert': ['Canvas', 'fflate'], 'photo-edit': ['Canvas'], + collage: ['Canvas'], 'audio-split': ['Web Audio', 'fflate'], 'audio-merge': ['Web Audio', 'fflate'], // Word, Excel and PowerPoint files are ZIP archives of XML. @@ -314,7 +316,7 @@ function optionsHtml(c: Copy, tool: Tool) { if (tool.op === 'remove-bg' && tool.preset?.format === 'sticker') return ''; if (tool.op === 'remove-bg') return ``; - if (tool.op === 'photo-edit') + if (tool.op === 'photo-edit' || tool.op === 'collage') return ``; if (tool.op === 'mermaid-image') return ` @@ -376,7 +378,7 @@ function workspaceHtml(c: Copy, tool: Tool) { : null; const { accept, choose, multiple } = input(c, tool); return ` -
+
    - ${['edit-pdf', 'redact-pdf', 'image-ocr', 'audio-split', 'remove-bg', 'photo-edit'].includes(tool.op) ? '
    ' : ''} + ${['edit-pdf', 'redact-pdf', 'image-ocr', 'audio-split', 'remove-bg', 'photo-edit', 'collage'].includes(tool.op) ? '
    ' : ''} ${multiple ? `` : ''}
    ${ @@ -557,7 +559,7 @@ const VERB = /^(Add|Compress|Convert|Cut|Delete|Edit|Extract|Merge|Protect|Redac /** Names that keep their capital in a sentence ("How to remove Word metadata"). */ const PROPER = new Set(['Word', 'Excel', 'PowerPoint', 'Markdown', 'Mermaid', 'WhatsApp', 'Aadhaar']); /** The photo tools, linked to each other first under "More tools". */ -const PHOTO_TOOLS = ['photo-editor', 'remove-background', 'whatsapp-sticker-maker', 'compress-image', 'resize-image', 'convert-image', 'remove-gif-background', 'remove-video-background']; +const PHOTO_TOOLS = ['photo-editor', 'collage-maker', 'remove-background', 'whatsapp-sticker-maker', 'compress-image', 'resize-image', 'convert-image', 'remove-gif-background', 'remove-video-background']; const howTo = (c: Copy, tool: Tool) => { if (c.lang !== 'en') return c.t('tool.how', { tool: tool.name }); if (VERB.test(tool.name)) return `How to ${tool.name.replace(/\b[A-Z][a-z]+\b/g, (word) => (PROPER.has(word) ? word : word.toLowerCase()))}`; @@ -906,6 +908,7 @@ ${HOME.what} - **How do I make a photo's background white?** Same page: after the background is removed, pick White under Background and save as JPG or PNG. - **Can I remove the background from a GIF or a video?** Yes: ${SITE.url}/remove-gif-background/ keeps every frame and its timing; ${SITE.url}/remove-video-background/ keeps the sound. - **Is there a free photo editor that doesn't upload my photo?** ${SITE.url}/photo-editor/ crops to square, 4:5, 16:9 or story size, turns and flips, sets brightness, contrast, saturation and warmth, adds one-tap looks, text and emoji, and saves at full size, all on the device, with no sign-up. +- **How do I make a photo collage without uploading the photos?** ${SITE.url}/collage-maker/ puts 1 to 9 photos in a grid (square, 4:5, story, 16:9 or A4 for printing), lets each photo be moved and zoomed in its frame, sets the gap, round corners, background and text, and saves JPG, PNG or WebP with no watermark. - **How do I make a WhatsApp sticker from a photo?** ${SITE.url}/whatsapp-sticker-maker/ removes the background, adds a white outline, text and emoji, and saves a 512×512 WebP under 100 KB; WhatsApp's own "Create sticker" then adds it to the sticker tray. ## Why Fizzdoc instead of other online file tools diff --git a/src/site.ts b/src/site.ts index 966750c..c8a4203 100644 --- a/src/site.ts +++ b/src/site.ts @@ -15,6 +15,7 @@ export type ToolOp = | 'redact-pdf' | 'remove-bg' | 'photo-edit' + | 'collage' | 'scan-pdf' | 'ocr-pdf' | 'pdf-to-word' @@ -1320,6 +1321,41 @@ TOOLS.push( preset: { format: 'sticker' }, input: { accept: 'image/*' }, }, + { + op: 'collage', + format: 'image', + slug: 'collage-maker', + name: 'Collage Maker', + summary: 'Put 2 to 9 photos in one picture, with a gap, round corners and text.', + action: 'Download collage', + title: 'Free Photo Collage Maker — No Upload, No Watermark | Fizzdoc', + description: + 'Make a photo collage free in your browser: pick a layout for 2 to 9 photos, set the gap, corners and size, add text. No upload, no sign-up, no watermark.', + h1: 'Make a photo collage without uploading your photos', + lede: 'Choose 2 to 9 photos, pick a layout and a shape (square, story, A4 and more), move and zoom each photo in its frame, and add a gap, round corners, a background colour and text. It all happens on your device.', + steps: [ + 'Choose 2 to 9 photos. The order of the list is the order in the collage.', + 'Pick a layout and a shape, move and zoom each photo, and set the gap, corners, background and text.', + 'Pick the size and format, then click “Download collage”.', + ], + faq: [ + PRIVACY_FAQ[0], + [ + 'How many photos can a collage have?', + 'From 1 to 9. Each number of photos has its own layouts, such as side by side, stacked, one big photo with smaller ones, or a grid. If you add more than 9, the first 9 are used.', + ], + [ + 'Can I move or zoom each photo?', + 'Yes. Open Photo, tap a photo to pick it, then drag it to choose what shows in its frame, and use Zoom to get closer. To swap photos, change their order in the file list.', + ], + [ + 'What sizes can I save?', + 'Square, Portrait 4:5 and Story 9:16 for social media, Wide 16:9, and A4 for printing: 1080 or 2160 px on the short side, or 2480 px for A4 at 300 dpi. As JPG, PNG or WebP, with no watermark.', + ], + PRIVACY_FAQ[1], + ], + input: { accept: 'image/*', multiple: true }, + }, { op: 'photo-edit', format: 'image', @@ -1833,6 +1869,13 @@ const KEYWORDS: Record = { 'remove video background without green screen', 'transparent background video', ], + 'collage-maker': [ + 'photo collage maker', + 'collage maker online free', + 'combine photos into one', + 'picture grid maker', + 'photo collage without watermark', + ], 'photo-editor': [ 'free online photo editor', 'crop photo to square', diff --git a/src/tools/collage.ts b/src/tools/collage.ts new file mode 100644 index 0000000..af328ea --- /dev/null +++ b/src/tools/collage.ts @@ -0,0 +1,376 @@ +// Collage Maker: 1 to 9 photos in a grid, each one moved and zoomed inside its own frame, with a gap, round +// corners, a background colour and words or emoji on top. The preview works from small copies of the photos; +// saving decodes each one at full size, one at a time, so phones don't run out of memory. +import './remove-bg.css'; +import './photo-edit.css'; +import { LAYOUTS, cells, fit, frame, place, type Cell } from '../engine/photo'; +import type { Output } from '../engine/local'; +import { stickerText } from './sticker-text'; +import { bringIntoView, button, decode, element, encode, row, saveButton, slider, tabs, throttle, type Icon } from './editor-ui'; +import { t, type UiKey } from '../i18n'; + +export interface Collage { + save(options: { format?: string }): Promise; + update(files: File[]): void; + destroy(): void; +} + +/** Longest side of the on-screen preview, and of the small copy kept of each photo for it. */ +const PREVIEW = 1200; +/** At most this many photos; the rest of the list is left out. */ +const MAX = 9; +/** Collage shapes as width ÷ height; A4 is for printing. */ +const SHAPES = { square: 1, portrait: 4 / 5, story: 9 / 16, wide: 16 / 9, a4: 210 / 297 } as const; +type Shape = keyof typeof SHAPES; +/** Saved size by the short side, in pixels: social posts, sharp, and A4 at 300 dpi. */ +const SIZES = [1080, 2160, 2480]; +const TABS = ['layout', 'photo', 'space', 'text'] as const; +type Tab = (typeof TABS)[number]; +const LABELS: Record = { layout: ['layout', 'co.layout'], photo: ['photo', 'co.photo'], space: ['space', 'co.space'], text: ['text', 'st.text'] }; +const BACKGROUNDS = { white: '#ffffff', black: '#111111' } as const; + +/** Where a photo sits inside its frame; it follows the photo when the list is reordered. */ +interface Crop { + zoom: number; + cx: number; + cy: number; +} +const CENTRE: Crop = { zoom: 1, cx: 0.5, cy: 0.5 }; + +export async function openCollage(files: File[], viewer: HTMLElement): Promise { + const state = { layout: 0, shape: 'square' as Shape, gap: 1.5, corners: 0, background: '#ffffff' as string }; + const crops = new Map(); + const thumbs = new Map(); + let photos: File[] = []; + let selected = 0; + let destroyed = false; + const lang = document.documentElement.lang || undefined; + const percent = new Intl.NumberFormat(lang, { maximumFractionDigits: 1 }); + const decimal = new Intl.NumberFormat(lang, { minimumFractionDigits: 1, maximumFractionDigits: 1 }); + + /** A small copy of a photo for the preview (nothing bigger is kept in memory). */ + const thumb = async (file: File) => { + const full = await decode(file); + const [w, h] = fit(full.width, full.height, PREVIEW); + const small = await createImageBitmap(full, { resizeWidth: w, resizeHeight: h, resizeQuality: 'high' }); + full.close(); + return small; + }; + + const stage = element('div', 'bg-stage'); + const canvas = element('canvas', 'bg-canvas'); + canvas.setAttribute('role', 'img'); + canvas.setAttribute('aria-label', t('co.preview')); + stage.append(canvas); + + const layoutCells = (): Cell[] => { + const [counts, columns] = LAYOUTS[photos.length][state.layout]; + return cells(counts, columns); + }; + /** Collage size from its longest side, or from its short side (the saved sizes). */ + const sized = (longest: number): [number, number] => { + const ratio = SHAPES[state.shape]; + return ratio >= 1 ? [longest, Math.round(longest / ratio)] : [Math.round(longest * ratio), longest]; + }; + const bySide = (short: number): [number, number] => { + const ratio = SHAPES[state.shape]; + return ratio >= 1 ? [Math.round(short * ratio), short] : [short, Math.round(short / ratio)]; + }; + /** Photo `i`'s cell in pixels on a W×H collage. */ + const box = (i: number, W: number, H: number) => place(layoutCells()[i], W, H, (state.gap / 100) * Math.min(W, H)); + /** Draws a photo (any size of it: the frame works in fractions) into cell `i`. */ + const drawCell = (context: CanvasRenderingContext2D, image: ImageBitmap, i: number, W: number, H: number) => { + const [dx, dy, dw, dh] = box(i, W, H); + const crop = crops.get(photos[i]) ?? CENTRE; + const [sx, sy, sw, sh] = frame(image.width, image.height, dw / dh, crop.zoom, crop.cx, crop.cy); + context.save(); + context.beginPath(); + context.roundRect(dx, dy, dw, dh, (state.corners / 100) * Math.min(W, H)); + context.clip(); + context.drawImage(image, sx, sy, sw, sh, dx, dy, dw, dh); + context.restore(); + }; + let text: ReturnType | undefined; + /** The preview, from the small copies. */ + const draw = throttle(() => { + [canvas.width, canvas.height] = sized(PREVIEW); + const context = canvas.getContext('2d')!; + context.fillStyle = state.background; + context.fillRect(0, 0, canvas.width, canvas.height); + photos.forEach((file, i) => { + const image = thumbs.get(file); + if (image) drawCell(context, image, i, canvas.width, canvas.height); + }); + // The picked photo is outlined while the Photo tab is open. + if (ui.current() === 'photo' && photos.length) { + const [dx, dy, dw, dh] = box(selected, canvas.width, canvas.height); + context.lineWidth = Math.max(3, canvas.width * 0.004); + context.strokeStyle = '#ffb547'; + context.strokeRect(dx, dy, dw, dh); + } + text?.draw(context, canvas.width, canvas.height, ui.current() === 'text'); + }); + + // ---- Top bar: a note (too many photos, or one that can't be read), and save ---- + const top = element('div', 'pe-top'); + const note = element('p', 'bg-hint co-note'); + note.setAttribute('role', 'status'); + top.append(note, saveButton(t('ws.download'))); + + const ui = tabs(viewer, TABS, (key) => [LABELS[key][0], t(LABELS[key][1])], () => draw()); + const panels = ui.panels; + + // ---- Layout: the grid, the shape and the size ---- + const layoutRow = row(t('co.layout'), 'pe-looks co-layouts'); + let builtLayouts = ''; + const shapeRow = row(t('pe.shape'), 'pe-chips'); + const shapeButtons = (Object.keys(SHAPES) as Shape[]).map((shape) => { + const node = button(t(shape === 'a4' ? 'co.a4' : (`pe.${shape}` as UiKey))); + node.onclick = () => { + state.shape = shape; + sync(); + }; + return node; + }); + shapeRow.append(...shapeButtons); + const sizeSelect = element('select'); + const sizeLabel = element('label', 'pe-size', t('bg.size')); + sizeLabel.append(sizeSelect); + panels.layout.append(layoutRow, shapeRow, sizeLabel); + + // ---- Photo: pick one (on the picture, or with the numbered buttons), then move and zoom it in its frame ---- + const pickRow = row(t('co.photo'), 'pe-chips'); + const zoom = slider(t('pe.zoom'), 1, 3, 0.05, (v) => `${decimal.format(v)}×`, (value) => { + current().zoom = value; + draw(); + }); + const resetPhoto = button(t('pe.reset')); + resetPhoto.onclick = () => { + crops.delete(photos[selected]); + sync(); + }; + panels.photo.append(element('p', 'bg-hint', t('co.tapHint')), pickRow, zoom.wrap, resetPhoto, element('p', 'bg-hint', t('co.order'))); + const current = () => { + const file = photos[selected]; + if (!crops.has(file)) crops.set(file, { ...CENTRE }); + return crops.get(file)!; + }; + + // ---- Space: gap, corners and background ---- + const gap = slider(t('co.gap'), 0, 5, 0.25, (v) => `${percent.format(v)}%`, (value) => { + state.gap = value; + draw(); + }); + const corners = slider(t('co.corners'), 0, 10, 0.5, (v) => `${percent.format(v)}%`, (value) => { + state.corners = value; + draw(); + }); + const backgroundRow = row(t('bg.background'), 'pe-chips'); + const backgroundButtons = (Object.keys(BACKGROUNDS) as (keyof typeof BACKGROUNDS)[]).map((key) => { + const node = button(t(`bg.${key}` as UiKey)); + node.onclick = () => { + state.background = BACKGROUNDS[key]; + sync(); + }; + return node; + }); + // Any colour: a real button that opens the browser's own colour picker (the picker itself stays hidden). + const picker = element('input'); + picker.type = 'color'; + picker.value = '#ff8a00'; + picker.className = 'bg-hidden'; + picker.setAttribute('aria-label', t('bg.custom')); + picker.oninput = () => { + state.background = picker.value; + sync(); + }; + const custom = button(t('bg.custom')); + custom.onclick = () => { + state.background = picker.value; + sync(); + picker.click(); + }; + backgroundRow.append(...backgroundButtons, custom, picker); + panels.space.append(gap.wrap, corners.wrap, backgroundRow); + + /** Reflects the state in the controls and redraws. */ + function sync() { + // The layout buttons are rebuilt only when the photo count or the shape changes, so focus stays put. + const key = `${photos.length}|${state.shape}`; + if (key !== builtLayouts) { + builtLayouts = key; + layoutRow.replaceChildren( + ...(LAYOUTS[photos.length] ?? []).map(([counts, columns], index) => { + const node = button('', 'pe-look'); + const mini = element('canvas'); + [mini.width, mini.height] = sized(96); + const context = mini.getContext('2d')!; + context.fillStyle = '#3f3f46'; + for (const cell of cells(counts, columns)) { + const [x, y, w, h] = place(cell, mini.width, mini.height, 4); + context.fillRect(x, y, w, h); + } + node.append(mini); + node.setAttribute('aria-label', `${t('co.layout')} ${index + 1}`); + node.onclick = () => { + state.layout = index; + sync(); + }; + return node; + }), + ); + } + [...layoutRow.children].forEach((node, i) => node.setAttribute('aria-pressed', String(i === state.layout))); + [...pickRow.children].forEach((node, i) => node.setAttribute('aria-pressed', String(i === selected))); + shapeButtons.forEach((node, i) => node.setAttribute('aria-pressed', String(Object.keys(SHAPES)[i] === state.shape))); + const preset = (Object.values(BACKGROUNDS) as string[]).includes(state.background); + backgroundButtons.forEach((node, i) => node.setAttribute('aria-pressed', String(Object.values(BACKGROUNDS)[i] === state.background))); + custom.setAttribute('aria-pressed', String(!preset)); + gap.set(state.gap); + corners.set(state.corners); + if (photos.length) zoom.set(current().zoom); + const picked = sizeSelect.value || String(SIZES[0]); + sizeSelect.replaceChildren(...SIZES.map((short) => new Option(t('bg.widthOption', { w: bySide(short)[0], h: bySide(short)[1] }), String(short)))); + sizeSelect.value = picked; + canvas.tabIndex = ui.current() === 'photo' ? 0 : -1; // arrow keys move the picked photo + draw(); + } + + // ---- On the picture: tap a photo to pick it, drag to move it in its frame; arrow keys do the same ---- + /** Moves the picked photo by a fraction of its frame, stopping at the photo's edges. */ + const nudge = (crop: Crop, from: { cx: number; cy: number }, dx: number, dy: number) => { + const image = thumbs.get(photos[selected]); + if (!image) return; + const [, , dw, dh] = box(selected, canvas.width, canvas.height); + const [, , sw, sh] = frame(image.width, image.height, dw / dh, crop.zoom, crop.cx, crop.cy); + const clamp = (v: number, half: number) => Math.min(1 - half, Math.max(half, v)); + // Dragging the photo right shows more of its left side. + crop.cx = clamp(from.cx - dx * (sw / image.width), sw / image.width / 2); + crop.cy = clamp(from.cy - dy * (sh / image.height), sh / image.height / 2); + draw(); + }; + let drag: { id: number; x: number; y: number; cx: number; cy: number } | undefined; + canvas.addEventListener('pointerdown', (event) => { + if (drag || event.defaultPrevented || event.button !== 0 || ui.current() !== 'photo') return; + const rect = canvas.getBoundingClientRect(); + const x = ((event.clientX - rect.left) / rect.width) * canvas.width; + const y = ((event.clientY - rect.top) / rect.height) * canvas.height; + const index = photos.findIndex((_, i) => { + const [dx, dy, dw, dh] = box(i, canvas.width, canvas.height); + return x >= dx && x <= dx + dw && y >= dy && y <= dy + dh; + }); + if (index < 0) return; + event.preventDefault(); + canvas.setPointerCapture(event.pointerId); + selected = index; + const crop = current(); + drag = { id: event.pointerId, x: event.clientX, y: event.clientY, cx: crop.cx, cy: crop.cy }; + sync(); + }); + canvas.addEventListener('pointermove', (event) => { + if (event.pointerId !== drag?.id) return; + const rect = canvas.getBoundingClientRect(); + const [, , dw, dh] = box(selected, canvas.width, canvas.height); + const scale = canvas.width / rect.width; + nudge(current(), drag, ((event.clientX - drag.x) * scale) / dw, ((event.clientY - drag.y) * scale) / dh); + }); + for (const type of ['pointerup', 'pointercancel'] as const) + canvas.addEventListener(type, (event) => { + if (event.pointerId === drag?.id) drag = undefined; + }); + canvas.addEventListener('keydown', (event) => { + const step = { ArrowLeft: [-1, 0], ArrowRight: [1, 0], ArrowUp: [0, -1], ArrowDown: [0, 1] }[event.key]; // moves the photo, like a drag + if (!step || ui.current() !== 'photo' || !photos.length) return; + event.preventDefault(); + const by = event.shiftKey ? 0.2 : 0.05; + const crop = current(); + nudge(crop, crop, step[0] * by, step[1] * by); + }); + + /** + * Follows the file list: new photos get a small copy, removed ones are let go, the order is the list's. + * A photo the browser can't read is left out and named in the note. + */ + async function update(list: File[]) { + const usable: File[] = []; + const unreadable: string[] = []; + for (const file of list.slice(0, MAX)) { + if (!thumbs.has(file)) + try { + thumbs.set(file, await thumb(file)); + } catch { + unreadable.push(file.name); + continue; + } + if (destroyed) return; + usable.push(file); + } + for (const [file, image] of thumbs) + if (!usable.includes(file)) { + image.close(); + thumbs.delete(file); + crops.delete(file); + } + if (!usable.length) { + const { LocalError } = await import('../engine/local'); + throw new LocalError('BAD_IMAGE'); + } + if (usable.length !== photos.length) state.layout = 0; + photos = usable; + selected = Math.min(selected, photos.length - 1); + note.textContent = [list.length > MAX ? t('co.max') : '', unreadable.length ? `${t('error.BAD_IMAGE')} (${unreadable.join(', ')})` : ''].filter(Boolean).join(' '); + pickRow.replaceChildren( + ...photos.map((file, i) => { + const node = button(String(i + 1)); + node.setAttribute('aria-label', `${i + 1}. ${file.name}`); + node.onclick = () => { + selected = i; + sync(); + }; + return node; + }), + ); + sync(); + } + // Updates run one after another, so a quick add-then-remove can't finish in the wrong order. + let pending: Promise = Promise.resolve(); + const queue = (list: File[]) => (pending = pending.catch(() => {}).then(() => update(list))); + + text = stickerText(canvas, draw, () => ui.current() !== 'text'); + panels.text.append(text.panel); + await queue(files); + viewer.classList.add('bg-editor', 'pe-editor', 'co-editor'); + viewer.replaceChildren(top, stage, ui.tabList, ...TABS.map((key) => panels[key])); + ui.show('layout'); + bringIntoView(viewer); + + return { + update: (list) => void queue(list), + async save(options) { + await pending; // the collage shows every photo in the list before it is saved + await document.fonts.ready; + const format = options.format === 'png' ? 'png' : options.format === 'webp' ? 'webp' : 'jpg'; + const [W, H] = bySide(Number(sizeSelect.value)); + const out = new OffscreenCanvas(W, H); + const context = out.getContext('2d') as unknown as CanvasRenderingContext2D; + context.fillStyle = state.background; + context.fillRect(0, 0, W, H); + // One photo at a time at full size, let go as soon as it is drawn. + for (const [i, file] of photos.entries()) { + const image = await decode(file); + drawCell(context, image, i, W, H); + image.close(); + } + text?.draw(context, W, H); + const { blob, ext } = await encode(out, format); + return { blob, name: `collage.${ext}`, summary: `${W} × ${H} px` }; + }, + destroy() { + destroyed = true; + for (const image of thumbs.values()) image.close(); + viewer.classList.remove('bg-editor', 'pe-editor', 'co-editor'); + delete viewer.dataset.tab; + viewer.replaceChildren(); + }, + }; +} diff --git a/src/tools/editor-ui.ts b/src/tools/editor-ui.ts new file mode 100644 index 0000000..8ad126a --- /dev/null +++ b/src/tools/editor-ui.ts @@ -0,0 +1,158 @@ +// Building blocks shared by the Photo Editor and the Collage Maker: buttons, our own line icons, rulers and +// the tab bar. The look lives in photo-edit.css (.pe-*). + +export const element = (tag: K, className = '', text = '') => { + const node = document.createElement(tag); + if (className) node.className = className; + if (text) node.textContent = text; + return node; +}; + +export const button = (label: string, className = 'bg-btn') => { + const node = element('button', className, label); + node.type = 'button'; + return node; +}; + +// Our own line icons, drawn like the site's tool icons. +const ICONS = { + looks: '', + crop: '', + light: '', + text: '', + compare: '', + save: '', + layout: '', + photo: '', + space: '', +}; +export type Icon = keyof typeof ICONS; + +/** A button with one of the icons above and a visible label. */ +export const iconButton = (icon: Icon, label: string, className: string) => { + const node = button('', className); + node.innerHTML = ``; + node.append(element('span', '', label)); + return node; +}; + +/** A labelled group of buttons. */ +export const row = (label: string, className: string) => { + const node = element('div', className); + node.setAttribute('role', 'group'); + node.setAttribute('aria-label', label); + return node; +}; + +/** A ruler: the browser's own range input (so keys and screen readers work), with its name and a big value. */ +export function slider(label: string, min: number, max: number, step: number, show: (value: number) => string, onInput: (value: number) => void) { + const range = element('input'); + range.type = 'range'; + range.min = String(min); + range.max = String(max); + range.step = String(step); + range.setAttribute('aria-label', label); + const value = element('span', 'pe-value'); + value.setAttribute('aria-hidden', 'true'); // the range announces itself through aria-valuetext + const head = element('span', 'pe-slider-head'); + head.append(element('span', 'pe-name', label), value); + const wrap = element('label', min < 0 ? 'pe-slider' : 'pe-slider from-start'); + wrap.append(head, range); + const set = (v: number) => { + range.value = String(v); + value.textContent = show(v); + range.setAttribute('aria-valuetext', value.textContent); + // -1…1 for the fill from the middle, or 0…1 from the start when the ruler starts at its lowest value. + range.style.setProperty('--v', String(min < 0 ? v / max : (v - min) / (max - min))); + }; + range.oninput = () => { + set(Number(range.value)); + onInput(Number(range.value)); + }; + return { range, wrap, set }; +} + +/** + * The tab bar and one panel per tab. `show(key)` switches; arrow keys, Home and End move between tabs. + * `viewer.dataset.tab` names the open one, for CSS. + */ +export function tabs(viewer: HTMLElement, keys: readonly K[], label: (key: K) => [Icon, string], onShow: () => void) { + let current = keys[0]; + const tabList = element('div', 'pe-tabs'); + tabList.setAttribute('role', 'tablist'); + const panels = {} as Record; + const buttons = keys.map((key) => { + const node = iconButton(...label(key), 'pe-tab'); + node.id = `pe-tab-${key}`; + node.setAttribute('role', 'tab'); + node.setAttribute('aria-controls', `pe-${key}`); + node.onclick = () => show(key); + const panel = element('div', 'pe-panel'); + panel.id = `pe-${key}`; + panel.setAttribute('role', 'tabpanel'); + panel.setAttribute('aria-labelledby', node.id); + panels[key] = panel; + return node; + }); + tabList.append(...buttons); + tabList.addEventListener('keydown', (event) => { + const at = keys.indexOf(current); + const next = { ArrowRight: at + 1, ArrowLeft: at - 1, Home: 0, End: keys.length - 1 }[event.key]; + if (next === undefined) return; + event.preventDefault(); + show(keys[(next + keys.length) % keys.length]); + buttons[keys.indexOf(current)].focus(); + }); + function show(key: K) { + current = key; + viewer.dataset.tab = key; + keys.forEach((k, i) => { + buttons[i].setAttribute('aria-selected', String(k === key)); + buttons[i].tabIndex = k === key ? 0 : -1; + panels[k].hidden = k !== key; + }); + onShow(); + } + return { tabList, panels, show, current: () => current }; +} + +/** Opens a photo (turned the right way up from its EXIF); a file the browser can't read becomes BAD_IMAGE. */ +export async function decode(file: File) { + try { + return await createImageBitmap(file); + } catch { + const { LocalError } = await import('../engine/local'); + throw new LocalError('BAD_IMAGE'); + } +} + +/** Calls `paint` at most once a frame, however fast sliders and drags ask. */ +export function throttle(paint: () => void) { + let queued = false; + return () => { + if (queued) return; + queued = true; + requestAnimationFrame(() => { + queued = false; + paint(); + }); + }; +} + +/** Brings the editor into view once the page around it has settled (the file list and options appear too). */ +export const bringIntoView = (viewer: HTMLElement) => + requestAnimationFrame(() => requestAnimationFrame(() => viewer.scrollIntoView({ block: 'start', behavior: 'instant' }))); + +/** The top bar's Download button: it runs the page's own save, with its progress and result. */ +export function saveButton(label: string) { + const node = iconButton('save', label, 'pe-save'); + node.onclick = () => document.getElementById('run')?.click(); + return node; +} + +/** Encodes a finished canvas; the extension follows the type the browser really wrote (Safari may not write WebP). */ +export async function encode(canvas: OffscreenCanvas, format: string) { + const blob = await canvas.convertToBlob({ type: format === 'jpg' ? 'image/jpeg' : `image/${format}`, quality: 0.92 }); + const ext = ({ 'image/jpeg': 'jpg', 'image/png': 'png', 'image/webp': 'webp' } as Record)[blob.type] ?? format; + return { blob, ext }; +} diff --git a/src/tools/photo-edit.css b/src/tools/photo-edit.css index c0eeb7a..93babef 100644 --- a/src/tools/photo-edit.css +++ b/src/tools/photo-edit.css @@ -123,6 +123,11 @@ touch-action: pan-y; /* a sideways drag changes the value; up and down still scroll the page */ cursor: ew-resize; } +/* Collage: in the Photo tab a finger moves the picked photo in its frame. */ +.pe-editor.co-editor[data-tab='photo'] .bg-canvas { + touch-action: none; + cursor: grab; +} /* ---- Tabs ---- */ .pe-tabs { @@ -424,3 +429,27 @@ clip-path: inset(50%); } } + +/* Collage Maker: a note in the top bar (too many photos, or one that can't be read) and the layout thumbnails. */ +.co-note { + margin: 0; + padding-inline-start: 8px; +} +.co-layouts .pe-look { + justify-content: center; + width: 64px; + min-height: 44px; +} +.co-layouts .pe-look:first-child { + margin-inline-end: 0; + padding-inline-end: 4px; + border-inline-end: 0; +} +.co-layouts .pe-look canvas { + aspect-ratio: auto; + background: #0a0a0b; +} +.co-layouts .pe-look[aria-pressed='true']::after { + right: 0; + bottom: 0; +} diff --git a/src/tools/photo-edit.ts b/src/tools/photo-edit.ts index 146dcc4..460160b 100644 --- a/src/tools/photo-edit.ts +++ b/src/tools/photo-edit.ts @@ -7,6 +7,7 @@ import './photo-edit.css'; import { FILTERS, NONE, adjustPixels, combine, fit, frame, turned, type Adjust, type Filter } from '../engine/photo'; import type { Output } from '../engine/local'; import { stickerText } from './sticker-text'; +import { bringIntoView, button, decode, element, encode, iconButton, row, saveButton, slider, tabs, throttle } from './editor-ui'; import { t, type UiKey } from '../i18n'; export interface PhotoEditor { @@ -25,48 +26,11 @@ const SIZES = [100, 75, 50, 25]; const TABS = ['looks', 'crop', 'light', 'text'] as const; type Tab = (typeof TABS)[number]; -// Our own line icons, drawn like the site's tool icons. -const svg = (paths: string) => - ``; -const ICONS = { - looks: '', - crop: '', - light: '', - text: '', - compare: '', - save: '', -}; const LABELS: Record = { looks: 'pe.looks', crop: 'pe.crop', light: 'pe.light', text: 'st.text' }; -const element = (tag: K, className = '', text = '') => { - const node = document.createElement(tag); - if (className) node.className = className; - if (text) node.textContent = text; - return node; -}; -const button = (label: string, className = 'bg-btn') => { - const node = element('button', className, label); - node.type = 'button'; - return node; -}; -/** A button with one of the icons above and a visible label. */ -const iconButton = (icon: keyof typeof ICONS, label: string, className: string) => { - const node = button('', className); - node.innerHTML = svg(ICONS[icon]); - node.append(element('span', '', label)); - return node; -}; - export async function openPhotoEditor(file: File, viewer: HTMLElement): Promise { - let photo: ImageBitmap; - try { - photo = await createImageBitmap(file); // also turns phone photos the right way up (EXIF) - } catch { - const { LocalError } = await import('../engine/local'); - throw new LocalError('BAD_IMAGE'); - } + const photo = await decode(file); const state = { shape: 'original' as Shape, zoom: 1, cx: 0.5, cy: 0.5, turns: 0, flipX: false, flipY: false, look: 'original' as Filter, own: { ...NONE } as Adjust }; - let tab: Tab = 'looks'; let chosen: Slider = 'brightness'; // the slider shown on phones, and the one a sideways swipe changes /** The kept part of the turned photo: [x, y, width, height] in photo pixels. */ const kept = () => { @@ -117,97 +81,19 @@ export async function openPhotoEditor(file: File, viewer: HTMLElement): Promise< context.globalCompositeOperation = 'source-over'; } // The text's selection box only shows while the Text tab is open. - if (!plain) text?.draw(context, target.width, target.height, target === canvas && tab === 'text'); - }; - // Sliders fire fast: draw at most once a frame. - let queued = false; - const draw = () => { - if (queued) return; - queued = true; - requestAnimationFrame(() => { - queued = false; - paint(canvas, PREVIEW, original); - }); - }; - - const row = (label: string, className: string) => { - const node = element('div', className); - node.setAttribute('role', 'group'); - node.setAttribute('aria-label', label); - return node; - }; - /** A ruler: the browser's own range input (so keys and screen readers work), with its name and a big signed value. */ - const slider = (label: string, min: number, max: number, step: number, show: (value: number) => string, onInput: (value: number) => void) => { - const range = element('input'); - range.type = 'range'; - range.min = String(min); - range.max = String(max); - range.step = String(step); - range.setAttribute('aria-label', label); - const value = element('span', 'pe-value'); - value.setAttribute('aria-hidden', 'true'); // the range announces itself through aria-valuetext - const head = element('span', 'pe-slider-head'); - head.append(element('span', 'pe-name', label), value); - const wrap = element('label', 'pe-slider'); - wrap.append(head, range); - const set = (v: number) => { - range.value = String(v); - value.textContent = show(v); - range.setAttribute('aria-valuetext', value.textContent); - // -1…1 for the fill from the middle (or 0…1 from the start for Zoom). - range.style.setProperty('--v', String(min < 0 ? v / max : (v - min) / (max - min))); - }; - range.oninput = () => { - set(Number(range.value)); - onInput(Number(range.value)); - }; - return { range, wrap, set }; + if (!plain) text?.draw(context, target.width, target.height, target === canvas && tab() === 'text'); }; + const draw = throttle(() => paint(canvas, PREVIEW, original)); // ---- Top bar: hold to compare, and save ---- const top = element('div', 'pe-top'); const compare = iconButton('compare', t('bg.compare'), 'bg-btn bg-compare pe-compare'); - const save = iconButton('save', t('ws.download'), 'pe-save'); - save.onclick = () => document.getElementById('run')?.click(); // the page's own save, with its progress and result - top.append(compare, save); + top.append(compare, saveButton(t('ws.download'))); // ---- Tabs ---- - const tabList = element('div', 'pe-tabs'); - tabList.setAttribute('role', 'tablist'); - const panels = {} as Record; - const tabButtons = TABS.map((key) => { - const node = iconButton(key, t(LABELS[key]), 'pe-tab'); - node.id = `pe-tab-${key}`; - node.setAttribute('role', 'tab'); - node.setAttribute('aria-controls', `pe-${key}`); - node.onclick = () => show(key); - const panel = element('div', 'pe-panel'); - panel.id = `pe-${key}`; - panel.setAttribute('role', 'tabpanel'); - panel.setAttribute('aria-labelledby', node.id); - panels[key] = panel; - return node; - }); - tabList.append(...tabButtons); - // Arrow keys move between tabs. - tabList.addEventListener('keydown', (event) => { - const at = TABS.indexOf(tab); - const next = { ArrowRight: at + 1, ArrowLeft: at - 1, Home: 0, End: TABS.length - 1 }[event.key]; - if (next === undefined) return; - event.preventDefault(); - show(TABS[(next + TABS.length) % TABS.length]); - tabButtons[TABS.indexOf(tab)].focus(); - }); - function show(key: Tab) { - tab = key; - viewer.dataset.tab = key; - TABS.forEach((k, i) => { - tabButtons[i].setAttribute('aria-selected', String(k === key)); - tabButtons[i].tabIndex = k === key ? 0 : -1; - panels[k].hidden = k !== key; - }); - draw(); - } + const ui = tabs(viewer, TABS, (key) => [key, t(LABELS[key])], () => draw()); + const panels = ui.panels; + const tab = () => ui.current(); // ---- Looks: each one shows the photo with that look ---- const lookRow = row(t('pe.looks'), 'pe-looks'); @@ -245,7 +131,6 @@ export async function openPhotoEditor(file: File, viewer: HTMLElement): Promise< state.zoom = value; sync(); }); - zoom.wrap.classList.add('from-start'); const dragHint = element('p', 'bg-hint', t('pe.dragHint')); const turnRow = row(t('pe.turn'), 'pe-chips'); const turnButton = (label: UiKey, act: () => void) => { @@ -338,7 +223,7 @@ export async function openPhotoEditor(file: File, viewer: HTMLElement): Promise< const clamp = (value: number, half: number) => Math.min(1 - half, Math.max(half, value)); const startDrag = (event: PointerEvent) => { if (drag || event.defaultPrevented || event.button !== 0) return; // one finger at a time; a text layer took it - if (tab === 'crop' ? !canvas.classList.contains('pe-movable') : tab !== 'light') return; + if (tab() === 'crop' ? !canvas.classList.contains('pe-movable') : tab() !== 'light') return; event.preventDefault(); canvas.setPointerCapture(event.pointerId); drag = { id: event.pointerId, x: event.clientX, y: event.clientY, cx: state.cx, cy: state.cy, value: state.own[chosen] }; @@ -346,7 +231,7 @@ export async function openPhotoEditor(file: File, viewer: HTMLElement): Promise< canvas.addEventListener('pointermove', (event) => { if (event.pointerId !== drag?.id) return; const rect = canvas.getBoundingClientRect(); - if (tab === 'light') { + if (tab() === 'light') { // The photo's full width is the whole range, -100 to +100. state.own[chosen] = Math.round(Math.min(100, Math.max(-100, drag.value + ((event.clientX - drag.x) / rect.width) * 200))); sliders[SLIDERS.indexOf(chosen)].set(state.own[chosen]); @@ -388,14 +273,13 @@ export async function openPhotoEditor(file: File, viewer: HTMLElement): Promise< compare.addEventListener('keyup', () => showOriginal(false)); // The text layers come from the Sticker Maker; registered first, so a touch on a layer moves the layer. - text = stickerText(canvas, draw, () => tab !== 'text'); + text = stickerText(canvas, draw, () => tab() !== 'text'); panels.text.append(text.panel); canvas.addEventListener('pointerdown', startDrag); - viewer.replaceChildren(top, stage, tabList, ...TABS.map((key) => panels[key])); - show('looks'); + viewer.replaceChildren(top, stage, ui.tabList, ...TABS.map((key) => panels[key])); + ui.show('looks'); sync(); - // Bring the editor into view once the page around it has settled (the file list and options appear too). - requestAnimationFrame(() => requestAnimationFrame(() => viewer.scrollIntoView({ block: 'start', behavior: 'instant' }))); + bringIntoView(viewer); return { async save(options) { @@ -403,16 +287,14 @@ export async function openPhotoEditor(file: File, viewer: HTMLElement): Promise< const [, , w, h] = kept().box; const format = options.format === 'png' ? 'png' : options.format === 'webp' ? 'webp' : 'jpg'; const longest = Math.round((Math.max(w, h) * Number(sizeSelect.value)) / 100); - const type = format === 'jpg' ? 'image/jpeg' : `image/${format}`; // JPG has no see-through pixels: a clear part of the photo becomes white instead of black. const render = async (maxArea?: number) => { const out = new OffscreenCanvas(1, 1); paint(out, longest, false, format === 'jpg' ? '#ffffff' : undefined, maxArea); - return { out, blob: await out.convertToBlob({ type, quality: 0.92 }) }; + return { out, ...(await encode(out, format)) }; }; // Full size first; a phone that can't make a canvas that big (about 16.7 million pixels on iPhone) gets the largest it can. - const { out, blob } = await render(Infinity).catch(() => render()); - const ext = ({ 'image/jpeg': 'jpg', 'image/png': 'png', 'image/webp': 'webp' } as Record)[blob.type] ?? format; + const { out, blob, ext } = await render(Infinity).catch(() => render()); return { blob, name: `${file.name.replace(/\.[^.]+$/, '')}-edited.${ext}`, summary: `${out.width} × ${out.height} px` }; }, destroy() { diff --git a/tests/app.e2e.ts b/tests/app.e2e.ts index 69fb160..9853ed1 100644 --- a/tests/app.e2e.ts +++ b/tests/app.e2e.ts @@ -1671,6 +1671,52 @@ test('photo editor: crops to a square, turns, applies a look and adds text, then expect(seen.violations).toEqual([]); }); +test('collage maker: four photos in a 2×2 grid, in list order, then reordered, saved at 1080 px', async ({ page }) => { + const seen = watch(page); + await page.goto('/collage-maker/'); + const solid = async (color: string) => { + const bytes = await page.evaluate(async (fill) => { + const canvas = new OffscreenCanvas(400, 300); + const c = canvas.getContext('2d')!; + c.fillStyle = fill; + c.fillRect(0, 0, 400, 300); + return [...new Uint8Array(await (await canvas.convertToBlob({ type: 'image/png' })).arrayBuffer())]; + }, color); + return { name: `${color.slice(1)}.png`, mimeType: 'image/png', buffer: Buffer.from(bytes) }; + }; + await page.locator('#file-input').setInputFiles([await solid('#ff0000'), await solid('#00ff00'), await solid('#0000ff'), await solid('#ffff00')]); + await expect(page.getByRole('tab', { name: 'Layout' })).toHaveAttribute('aria-selected', 'true'); + // Photo: a finger moves the picked photo (no page scroll), and each photo can be picked with a button too. + await page.getByRole('tab', { name: 'Photo' }).click(); + await expect(page.locator('.bg-canvas')).toHaveCSS('touch-action', 'none'); + await page.getByRole('group', { name: 'Photo' }).getByRole('button', { name: '2. 00ff00.png' }).click(); + await expect(page.getByRole('button', { name: '2. 00ff00.png' })).toHaveAttribute('aria-pressed', 'true'); + await page.getByRole('tab', { name: 'Space' }).click(); + await page.getByRole('slider', { name: 'Gap' }).fill('0'); + await page.getByRole('combobox', { name: 'Save as' }).selectOption('png'); + const quadrants = async () => { + await page.getByRole('button', { name: 'Download collage' }).click(); + await expect(page.locator('#status')).toContainText('1080 × 1080 px'); + const saved = await downloadBytes(page); + expect(saved.name).toBe('collage.png'); + return page.evaluate(async (data) => { + const bitmap = await createImageBitmap(new Blob([new Uint8Array(data)], { type: 'image/png' })); + const c = new OffscreenCanvas(bitmap.width, bitmap.height).getContext('2d')!; + c.drawImage(bitmap, 0, 0); + const at = (x: number, y: number) => [...c.getImageData(x, y, 1, 1).data.slice(0, 3)].map((v) => (v > 127 ? 1 : 0)).join(''); + return { size: [bitmap.width, bitmap.height], cells: [at(270, 270), at(810, 270), at(270, 810), at(810, 810)] }; + }, [...saved.bytes]); + }; + const first = await quadrants(); + expect(first.size).toEqual([1080, 1080]); + // Red, green, blue, yellow: in the order of the file list. + expect(first.cells).toEqual(['100', '010', '001', '110']); + // Moving the first photo down swaps the first two cells. + await page.locator('#file-list li').first().getByRole('button', { name: 'Move down' }).click(); + await expect.poll(async () => (await quadrants()).cells).toEqual(['010', '100', '001', '110']); + expect(seen.violations).toEqual([]); +}); + test('phones use the small background model, so the big one never loads and the tab stays alive', async ({ browser, baseURL }) => { test.setTimeout(240_000); for (const phone of [true, false]) { diff --git a/tests/photo.test.ts b/tests/photo.test.ts index 0f90c25..2ebc61e 100644 --- a/tests/photo.test.ts +++ b/tests/photo.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { FILTERS, NONE, adjustPixels, combine, fit, frame, turned } from '../src/engine/photo'; +import { FILTERS, LAYOUTS, NONE, adjustPixels, cells, combine, fit, frame, place, turned } from '../src/engine/photo'; const pixel = (r: number, g: number, b: number) => new Uint8ClampedArray([r, g, b, 255]); @@ -52,3 +52,26 @@ describe('photo edits', () => { } }); }); + +describe('collage layouts', () => { + it('has one cell per photo, filling the collage with no overlaps', () => { + for (const [count, layouts] of Object.entries(LAYOUTS)) + for (const [counts, columns] of layouts) { + const all = cells(counts, columns); + expect(all.length, `${counts} ${columns ?? ''}`).toBe(Number(count)); + expect(all.reduce((sum, [, , w, h]) => sum + w * h, 0)).toBeCloseTo(1); + for (const [i, a] of all.entries()) + for (const b of all.slice(i + 1)) { + const apart = a[0] + a[2] <= b[0] + 1e-9 || b[0] + b[2] <= a[0] + 1e-9 || a[1] + a[3] <= b[1] + 1e-9 || b[1] + b[3] <= a[1] + 1e-9; + expect(apart).toBe(true); + } + } + }); + + it('leaves the same gap around the edge and between cells', () => { + const [left, right] = cells([2]).map((cell) => place(cell, 1000, 500, 20)); + expect(left).toEqual([20, 20, 470, 460]); + expect(right[0] - (left[0] + left[2])).toBe(20); + expect(1000 - (right[0] + right[2])).toBe(20); + }); +});