@swiftlyme/image-uploader
v1.1.4
Published
Feature-rich Angular image/PDF uploader: multi-file upload, drag-and-drop reordering, crop/rotate/flip editing, image replacement, format conversion (JPEG/PNG/WebP) and compression, and configurable layouts.
Maintainers
Readme
ImageUploaderLib
A powerful, feature-rich Angular image uploader component with drag-and-drop, cropping, editing, PDF support, and replacement capabilities.
👉 Live Demo · 📖 Full Documentation
🚀 Features
- ✅ Multi-File Upload - Select several files at once; they fill the clicked slot plus the next available empty slots
- ✅ Drag & Drop - Reorder images with smooth animations using Angular CDK
- ✅ Advanced Crop Editor - Crop, rotate, and flip images with live canvas preview, resizable from any corner
- ✅ Image Replacement - Replace existing images without losing position
- ✅ PDF Support - Upload, preview, and view PDF documents
- ✅ Multiple Aspect Ratios - 1:1, 16:9, 4:3, 9:16, or custom ratios
- ✅ Free Crop Mode - Crop images to any size without constraints
- ✅ Multiple Layouts -
grid,slider,hero,showcase,banner,mosaic,compact,split - ✅ Output Format Conversion - Force uploads/edits into JPEG, PNG, or WebP regardless of the source format
- ✅ Image Compression - Reduce file sizes with adjustable compression quality
- ✅ Custom Icons - Override the edit/delete/replace/upload icon classes
- ✅ Loading Indicator - Per-image spinner while a thumbnail is decoding
- ✅ Download Support - Download edited/cropped images, always named with the correct extension
- ✅ Granular Permissions - Toggle edit, replace, delete, download, and drag features
- ✅ Event Emissions - Track upload, replace, delete, and change events
- ✅ Pre-load URLs - Initialize with existing image URLs
- ✅ Container Queries - Smart responsive behavior based on parent container size
- ✅ Mobile Responsive - Optimized for all screen sizes, including micro views
- ✅ Accessibility - ARIA labels and keyboard support
📦 Installation
Requires Angular 19.2+ (
@angular/core,@angular/common, and@angular/cdkare all peer dependencies pinned to^19.2.0). Installing into an older Angular app will hit an npm peer-dependency conflict.
npm install @swiftlyme/image-uploaderDependencies
pdfjs-dist (used for PDF thumbnail generation) is installed automatically as a regular dependency — nothing to do there.
@angular/cdk is a peer dependency (needs to match your installed Angular major version) — install it explicitly if you don't already have it:
npm install @angular/cdkBootstrap 5 and Bootstrap Icons are required for styling but are not npm dependencies of this package (the component only references their CSS classes, it doesn't import any Bootstrap JS) — install and load them yourself:
npm install bootstrap@5
npm install bootstrap-iconsStyle Requirements
Add Bootstrap 5 and Bootstrap Icons to your angular.json:
{
"styles": ["node_modules/bootstrap/dist/css/bootstrap.min.css", "node_modules/bootstrap-icons/font/bootstrap-icons.css", "src/styles.css"]
}🎯 Quick Start
1. Import the Module
ImageUploaderLibComponent is standalone, so it can go directly into another standalone component's imports:
import { Component } from "@angular/core";
import { ImageUploaderLibComponent } from "@swiftlyme/image-uploader";
@Component({
selector: "app-your-component",
standalone: true,
imports: [ImageUploaderLibComponent],
templateUrl: "./your-component.html",
})
export class YourComponent {}...or into an NgModule-based app the same way (standalone components can be listed in @NgModule.imports):
import { ImageUploaderLibComponent } from "@swiftlyme/image-uploader";
@NgModule({
imports: [
ImageUploaderLibComponent,
// ... other imports
],
})
export class AppModule {}2. Add to Your Template
Every input has a sensible default, so the bare tag already works:
<lib-image-uploader-lib></lib-image-uploader-lib>A more typical real-world setup, configured and listening for changes:
<lib-image-uploader-lib [maxImages]="6" [aspectRatio]="1" [layoutMode]="'grid'" (imagesChange)="handleImagesChange($event)" (imageUpload)="onImageUpload($event)" (imageReplace)="onImageReplace($event)" (imageDelete)="onImageDelete($event)" (imageEdit)="onImageEdit($event)"> </lib-image-uploader-lib>3. Handle Events in Component
export class YourComponent {
handleImagesChange(images: any[]) {
console.log("Images updated:", images);
}
onImageUpload(image: any) {
console.log("New image uploaded:", image);
}
onImageReplace(event: any) {
console.log("Image replaced:", event);
// event contains: { old, new, index }
}
onImageDelete(image: any) {
console.log("Image deleted:", image);
}
onImageEdit(image: any) {
console.log("Image edited:", image);
}
}📚 API Reference
Input Properties
| Property | Type | Default | Description |
| ----------------- | --------------------------------------------- | ----------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| maxImages | number | 1 | Maximum number of images/files allowed |
| aspectRatio | number | 1 | Aspect ratio for cropping (e.g., 1, 16/9, 4/3) |
| enableFreeCrop | boolean | false | Allow free-form cropping without ratio constraints |
| layoutMode | string | 'grid' | Layout style: 'grid', 'slider', 'hero', 'showcase', 'banner', 'mosaic', 'compact', or 'split' |
| allowDrag | boolean | true | Enable drag-and-drop reordering |
| allowEdit | boolean | true | Show edit button for cropping/rotating images (not available for PDFs) |
| allowReplace | boolean | true | Show replace button to swap images or PDFs |
| allowDelete | boolean | true | Show delete button |
| allowDownload | boolean | true | Show download button |
| compressionRate | number | 0 | Compression percentage (0-95, 0 = no compression). When > 0 and outputFormat is 'original', output is forced to JPEG |
| outputFormat | 'original' \| 'jpeg' \| 'png' \| 'webp' | 'original' | Force every stored image (uploads and crop/edit output) into a specific format, regardless of the source format |
| outputQuality | number | 0.92 | Quality (0-1) used for JPEG/WebP conversion when compressionRate is 0 |
| imageUrls | string[] | [] | Pre-load images from URLs |
| editIcon | string | 'bi bi-pencil' | Icon class(es) for the Edit button |
| deleteIcon | string | 'bi bi-trash' | Icon class(es) for the Delete button |
| replaceIcon | string | 'bi bi-arrow-repeat' | Icon class(es) for the Replace button |
| placeholderIcon | string | 'bi bi-cloud-arrow-up' | Icon class(es) for the empty-slot upload placeholder |
| pdfWorkerSrc | string | '' | Override the PDF.js worker script URL (e.g. a self-hosted copy). Empty uses the unpkg CDN default matching your installed pdfjs-dist version |
| previewFit | 'cover' \| 'contain' \| 'fill' \| 'scale-down' \| 'none' | 'cover' | CSS object-fit applied to each gallery thumbnail |
Output Events
| Event | Payload | Description |
| -------------- | --------------------- | --------------------------------------------------------- |
| imagesChange | any[] | Emitted when images array changes (any operation) |
| imageUpload | any | Emitted when a new image/PDF is uploaded to an empty slot |
| imageReplace | { old, new, index } | Emitted when an existing image/PDF is replaced |
| imageDelete | any | Emitted when an image/PDF is deleted |
| imageEdit | any | Emitted when an image/PDF is edited (any crop) |
🎨 Layout Modes
Grid (Default)
Responsive grid that automatically adapts to container size using CSS Container Queries.
<lib-image-uploader-lib [layoutMode]="'grid'" [maxImages]="6"></lib-image-uploader-lib>Smart Responsive Features:
- Wide containers (>350px): Shows multiple columns based on screen size
- Narrow containers (<350px): Automatically stacks images vertically
- Micro containers (<150px): Hides text, shows icon-only controls
Slider
Horizontal scrollable carousel with navigation buttons and snap-to-center behavior.
<lib-image-uploader-lib [layoutMode]="'slider'" [maxImages]="10"></lib-image-uploader-lib>Features:
- Smooth scroll-snap navigation
- Previous/Next buttons with auto-disable at boundaries
- Swipe support on mobile devices
- Hides scrollbar for clean appearance
🎨 Column Sizing
The grid layout dynamically adjusts based on image index and screen size:
| Image Position | Desktop (lg+) | Tablet (md) | Mobile (sm) | | -------------- | ------------- | -------------- | -------------- | | First Image | 6 cols (50%) | 12 cols (100%) | 12 cols (100%) | | Other Images | 3 cols (25%) | 6 cols (50%) | 12 cols (100%) |
For slider mode, all images maintain consistent sizing.
📄 PDF Support
The component fully supports PDF uploads with special handling:
Upload PDF
PDF support is built in and always on — every upload slot already accepts image/*,application/pdf (this isn't a configurable input; there's no accept property on the component).
<lib-image-uploader-lib [maxImages]="5"> </lib-image-uploader-lib>PDF Features
- ✅ PDF icon preview with filename
- ✅ View button opens PDF in new tab
- ✅ Replace functionality
- ✅ Delete functionality
- ✅ Drag and reorder alongside images
- ❌ Edit/crop not available (images only)
Detect PDF in Events
onImageUpload(file: any) {
// Use `fileType` (set for every fresh upload) rather than `file?.type` --
// `file` is null for anything loaded from `imageUrls` instead of picked
// by the user.
if (file.fileType === 'application/pdf') {
console.log('PDF uploaded:', file.file.name);
} else {
console.log('Image uploaded');
}
}🔧 Advanced Usage
Multiple File Upload
Clicking an empty slot opens a file picker that accepts multiple files at once. Each selected file fills the slot you clicked plus, in order, the next available empty slots — an already-filled slot is only ever overwritten if it's the exact one you clicked.
<lib-image-uploader-lib [maxImages]="10"> </lib-image-uploader-lib>If you select more files than there are empty slots left, the extras are skipped (a warning is logged to the console) rather than overwriting images you already have.
The dedicated "Replace" button on a filled slot stays single-file by design — replacing is a 1-for-1 swap.
Output Format Conversion
Force every stored image — both fresh uploads and crop/edit output — into a specific format, regardless of what was originally selected. Useful for normalizing uploads (e.g. converting .webp/.png to .jpg) or standardizing on a smaller format like WebP.
<lib-image-uploader-lib [outputFormat]="'webp'" [outputQuality]="0.85" [maxImages]="5"> </lib-image-uploader-lib>outputFormat="'original'"(default) — no conversion, stored exactly as selected/edited.outputFormat="'jpeg'"/"'png'"/"'webp'"— every upload and crop save is re-encoded via canvas into that format. The resultingFile's name is also renamed to match (e.g.photo.webp→photo.jpg).outputQuality(0-1, default0.92) controls JPEG/WebP quality whencompressionRateis0; ifcompressionRateis set, it drives quality instead (1 - compressionRate / 100), same as the legacy compression-only behavior.- PDFs are never converted.
- Pre-loaded
imageUrlsentries are not converted either —outputFormatonly applies to files the user actually uploads or crops/edits through this component. A remote seed URL is downloaded and displayed as-is. - Not every browser can encode every format via canvas (older Safari in particular can't produce WebP) — if that happens, a warning is logged and the browser's actual fallback format is used instead of silently mislabeling the file.
Crop Position/Size Is Remembered Per Image
Reopening the edit modal on an image you've already cropped restores the exact crop box (position and size) you last saved for it, instead of resetting to the default centered box. No configuration needed — this is automatic.
- Scoped to that specific image's slot — cropping one image a certain way has no effect on any other image's crop box.
- Kept in memory only, on the slot object itself: it survives as long as the component instance does, and is gone on a page refresh/navigation away, or the moment that slot is replaced with a different file (a Replace creates a brand-new slot, so it can't inherit crop geometry that belonged to a differently-shaped image).
- If you change
aspectRatio(andenableFreeCropis off) between edits such that the saved box's ratio no longer matches, it's discarded and the default box is used instead rather than showing a distorted crop. - Rotating within an edit session still resets the crop box as before (rotation changes the canvas dimensions, so the old box wouldn't make sense anyway) — this only affects what you see when reopening Edit.
Tap to View Full Size
Clicking/tapping a thumbnail (or a PDF's placeholder tile) opens a lightweight preview — the same modal chrome as the edit modal, but with no crop overlay, resize handles, or rotate/flip/save controls, just the image at full size. Click the backdrop or the close button to dismiss.
<lib-image-uploader-lib [maxImages]="6"></lib-image-uploader-lib>
<!-- No configuration needed -- tapping any filled slot opens the preview automatically -->For images this opens a new modal (isViewMode); for PDFs it reuses the existing viewPdf() behavior (opens the PDF in a new tab), same as the dedicated "View PDF" button. Unlike the edit modal, the preview renders a plain <img> rather than drawing to a canvas, so it also works for remote images that would otherwise fail canvas operations due to missing CORS headers.
Preview Fit (contain/cover/fill)
Control how each thumbnail image fills its slot via previewFit, mapped directly to CSS object-fit.
<lib-image-uploader-lib [previewFit]="'contain'" [maxImages]="6"></lib-image-uploader-lib>'cover'(default) — fills the slot, cropping overflow. Matches every prior version's behavior exactly.'contain'— shows the whole image letterboxed, nothing cropped.'fill','scale-down','none'— the remaining standardobject-fitvalues, for the less common cases.
Custom Icons
Override any control's icon with your own icon font classes (Bootstrap Icons by default, but any class-based icon system works — Font Awesome, Material Icons via mat-icon won't work directly since it needs an element, but any <i class="...">-style font icon does).
<lib-image-uploader-lib editIcon="'fa fa-pen'" deleteIcon="'fa fa-trash-can'" replaceIcon="'fa fa-arrows-rotate'" placeholderIcon="'fa fa-upload'"> </lib-image-uploader-lib>Custom Aspect Ratio
<lib-image-uploader-lib [aspectRatio]="21/9" [maxImages]="4"> </lib-image-uploader-lib>Free Crop Mode
<lib-image-uploader-lib [enableFreeCrop]="true" [maxImages]="3"> </lib-image-uploader-lib>Image Compression
<lib-image-uploader-lib [compressionRate]="50" [maxImages]="5"> </lib-image-uploader-lib>Compression ranges from 0 (no compression) to 95 (maximum compression). With outputFormat left at 'original', a non-zero compressionRate still re-encodes the image as JPEG (matching earlier versions' behavior); set outputFormat explicitly to control the format independently of compression.
Pre-loaded Images
export class YourComponent {
existingImages = ["https://example.com/image1.jpg", "https://example.com/image2.jpg", "https://example.com/image3.jpg"];
}<lib-image-uploader-lib [imageUrls]="existingImages" [maxImages]="5"> </lib-image-uploader-lib>Granular Permissions
<lib-image-uploader-lib [allowDrag]="true" [allowEdit]="true" [allowReplace]="true" [allowDelete]="true" [allowDownload]="false"> </lib-image-uploader-lib>Replace Event Handling
export class YourComponent {
onImageReplace(event: any) {
const { old, new: newImage, index } = event;
// Delete old file from server
this.deleteFromServer(old.originalUrl);
// Upload new file to server
this.uploadToServer(newImage.file, index);
// Update database
this.updateDatabase(index, newImage);
// Show notification
this.showNotification(`File ${index + 1} replaced successfully`);
}
}🎯 Real-World Examples
E-commerce Product Gallery
<lib-image-uploader-lib [maxImages]="8" [aspectRatio]="1" [layoutMode]="'slider'" [allowDrag]="true" [allowReplace]="true" [compressionRate]="30" (imagesChange)="updateProductImages($event)" (imageReplace)="handleProductImageReplace($event)"> </lib-image-uploader-lib>User Profile Picture
<lib-image-uploader-lib [maxImages]="1" [aspectRatio]="1" [layoutMode]="'grid'" [allowDelete]="false" [allowReplace]="true" (imageUpload)="uploadAvatar($event)" (imageReplace)="updateAvatar($event)"> </lib-image-uploader-lib>Photo Album
<lib-image-uploader-lib [maxImages]="20" [enableFreeCrop]="true" [layoutMode]="'grid'" [allowDrag]="true" [allowDownload]="true" (imagesChange)="saveAlbum($event)"> </lib-image-uploader-lib>Document Management (Images + PDFs)
<lib-image-uploader-lib [maxImages]="15" [aspectRatio]="210/297" [layoutMode]="'grid'" [allowReplace]="true" [compressionRate]="60" (imagesChange)="processDocuments($event)"> </lib-image-uploader-lib>Sidebar Upload (Narrow Container)
<div class="col-lg-2">
<lib-image-uploader-lib [maxImages]="5" [layoutMode]="'grid'" [aspectRatio]="1"> </lib-image-uploader-lib>
</div>Automatically stacks vertically and adjusts controls for narrow spaces
🎨 Edit Modal Features
The fullscreen edit modal provides comprehensive image editing:
Controls Available
- Crop - Drag overlay to reposition, drag corners to resize
- Rotate Left - Rotate 90° counter-clockwise
- Rotate Right - Rotate 90° clockwise
- Flip Horizontal - Mirror image horizontally
- Flip Vertical - Mirror image vertically
Modal Features
- ✅ Fullscreen overlay (95vw × 95vh)
- ✅ Canvas-based rendering
- ✅ Live preview with grid overlay
- ✅ Resize handles on all corners
- ✅ Dark background for better visibility
- ✅ Keyboard support (ESC to close)
Usage
<!-- Edit button appears automatically when allowEdit is true -->
<lib-image-uploader-lib [allowEdit]="true"></lib-image-uploader-lib>🖱️ Drag & Drop Features
Reordering
- Drag images to reorder them
- Visual drag preview with shadow
- Smooth animations during drop
- Works in both grid and slider modes
Drag Handles
- Full card is draggable when
allowDragistrue - Controls remain clickable during drag operations
- Automatic z-index management
Disable Dragging
<lib-image-uploader-lib [allowDrag]="false"></lib-image-uploader-lib>📐 Container Query Responsiveness
The component uses CSS Container Queries for intelligent responsive behavior:
Breakpoints
- >350px: Normal multi-column grid
- <350px: Single column vertical stack
- <150px: Micro view (icon-only controls)
Micro View Optimizations
- Upload text hidden
- Icon scaled down to 1.5rem
- Edit/Delete buttons reduced to 24×24px
- 4px button spacing
Example: Responsive Sidebar
<div class="sidebar" style="width: 200px;">
<lib-image-uploader-lib [maxImages]="3"></lib-image-uploader-lib>
<!-- Automatically uses single-column layout -->
</div>🎨 Styling & Customization
The component uses Bootstrap 5 for styling. Customize appearance by:
1. Override Bootstrap Variables
// styles.scss
$primary: #6366f1;
$warning: #f59e0b;
$danger: #ef4444;
$info: #0dcaf0;
@import "bootstrap/scss/bootstrap";2. Custom CSS Classes
/* Customize upload slot */
.upload-slot {
border-color: #8b5cf6 !important;
}
.upload-slot:hover {
background-color: #f3e8ff !important;
}
/* Customize control buttons */
.controls-overlay button {
opacity: 0.95 !important;
}
/* Customize edit modal */
.modal-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}3. Card Customization
/* Adjust card styling */
.card {
border-radius: 12px !important;
overflow: hidden;
}
.card-body {
padding: 0.5rem !important;
}📊 Image Object Structure
When images/PDFs are emitted through events, they have this structure:
{
id: string; // Unique identifier
originalUrl: string; // Data URL (post-conversion, if outputFormat/compressionRate applied) or external URL
croppedUrl: string | null; // Data URL of cropped/edited image (null until edited; not used for PDFs)
croppedFile?: File; // File object for the cropped/edited output, once edited
previewUrl?: string; // Thumbnail data URL (PDFs use this for the generated first-page preview)
file: File | null; // Uploaded File object, post-conversion if outputFormat/compressionRate applied
// (null for URL-based/remote items)
fileType?: string; // The uploaded file's MIME type, e.g. 'image/jpeg' or 'application/pdf'
isRemote?: boolean; // Flag indicating if loaded from a pre-set imageUrls entry
loading?: boolean; // True while the slot's <img> is still decoding
}Check File Type
// Mirrors the component's own internal check: `fileType` covers fresh
// uploads, and the `originalUrl` suffix check covers PDFs that arrived via
// a pre-loaded `imageUrls` entry (those have `file: null` and no `fileType`).
function isPdf(slot: any): boolean {
return slot?.fileType === "application/pdf" || !!slot?.originalUrl?.endsWith(".pdf");
}🚦 Event Flow
User Action → Event Emitted → Payload
─────────────────────────────────────────────────────────────────
Upload to empty slot → imageUpload → image/pdf
Replace existing → imageReplace → { old, new, index }
Delete image/PDF → imageDelete → image/pdf
Drag to reorder → imagesChange → images[]
Crop/edit save → imagesChange → images[]🛠 Troubleshooting
Component looks broken/unstyled (no rounded buttons, no card layout, icons showing as boxes)?
- This is almost always a missed step from Style Requirements — Bootstrap 5 and Bootstrap Icons CSS are required and are not bundled into this package, you have to add them to
angular.json(or import them instyles.css/styles.scss) yourself - Confirm both stylesheets actually appear in your built page's
<head>(view source / devtools)
Images not uploading?
- Check that
maxImagesis not exceeded - Verify file type is an image or PDF
- Check browser console for errors
Replace button not showing?
- Ensure
allowReplaceistrue - Verify the slot has an image/PDF (not empty)
- Check that the file has finished loading
Wrong image replaced when multiple uploaders are on the page?
- Fixed in
v1.1.4. In earlier versions, every<lib-image-uploader-lib>on a page rendered its hidden replace input with the sameid(file-input-0for any single-slot uploader), andreplaceImage()resolved it withdocument.getElementById()— which always returns the first match in the document. Clicking "replace" on any uploader therefore drove the first uploader on the page, which then emittedimageReplace/imageUploadfor its own field. Make sure you're onv1.1.4+ - This was purely internal — no template or API change is needed in your app
Crop editor not working?
- Make sure
allowEditistrue - Crop is only available for images (not PDFs)
- For external URLs, ensure CORS is enabled on the image server
- Check that the image has loaded successfully
- If
saveCrop()throws aSecurityError(tainted canvas) on an external image with correct CORS headers, make sure you're onv1.1.2+ — earlier versions could reuse a non-CORS cache entry for the same URL (e.g. from the plain<img>thumbnail) and silently taint the canvas
Download not working?
- External images may have CORS restrictions
- Check browser console for fetch errors
- For remote images, the browser may open them instead of downloading
Downloaded file has the wrong extension/format?
- As of
v1.1.0, the downloaded filename's extension is always derived from the actual downloaded content, not a guess — if you're still seeing a mismatch, make sure you're onv1.1.0+ - If
outputFormatis left at'original'andcompressionRateis0, no conversion happens at all — the file is whatever format it originally was (including a pre-loadedimageUrlsentry, which is never converted)
Images resetting or disappearing after upload/replace?
- Fixed in
v1.1.0— previously, bindingimageUrlsto an inline array literal (e.g.[imageUrls]="['a.jpg']") could cause uploaded/replaced images to be wiped by unrelated change-detection cycles. Make sure you're onv1.1.0+ - If it still happens, check whether you're re-assigning
imageUrlsto a genuinely new array of URLs elsewhere in your app — that's expected to re-seed the slots
Slider buttons not working?
- Ensure
layoutModeis set to'slider' - Check that there are enough images to scroll
- Buttons auto-disable at scroll boundaries
PDF not viewing?
- Check that popup blockers are not preventing the new tab
- Verify the PDF file is valid
- Check browser console for errors
Controls not visible in narrow containers?
- Container queries require modern browser support
- Check that CSS is properly loaded
- Verify Bootstrap Icons are included
📋 Browser Support
- ✅ Chrome (latest) - Full support
- ✅ Firefox (latest) - Full support
- ✅ Safari (latest) - Full support
- ✅ Edge (latest) - Full support
- ✅ Mobile browsers (iOS Safari, Chrome Mobile) - Full support
- ⚠️ Container Queries require modern browsers (Chrome 105+, Safari 16+, Firefox 110+)
⚠️ Server-Side Rendering (Angular Universal)
This component is browser-only. It uses document, window, Image, FileReader, and <canvas> directly and unconditionally (including at module load time, to configure the PDF.js worker). Rendering it during SSR will throw. Wrap it in a client-only guard (isPlatformBrowser, a *ngIf gated on an afterNextRender/isBrowser flag, or @defer (on viewport) with SSR disabled for that block) if your app uses Angular Universal.
🎯 Performance Tips
Image Compression
- Use
compressionRateto reduce file sizes - Recommended: 30-50 for web, 60-70 for thumbnails
Lazy Loading
For large galleries, consider loading images on demand:
// Load first batch
initialImages = imageUrls.slice(0, 6);
// Load more on scroll
loadMore() {
this.images.push(...imageUrls.slice(6, 12));
}Optimize Canvas Rendering
- Crop operations use canvas for better performance
- Automatically handles high-DPI displays
🤝 Contributing
The rest of this section is for people working on this package's own source, not people consuming it in their app — skip it if you're just installing and using the component.
Contributions are welcome! Please follow these steps:
- Fork the repository
- Create a feature branch:
git checkout -b feature/my-feature - Commit your changes:
git commit -am 'Add new feature' - Push to the branch:
git push origin feature/my-feature - Submit a pull request
Building the Library
ng build image-uploader-lib --configuration productionBuild artifacts are placed in dist/image-uploader-lib.
Running Tests
ng test image-uploader-libPublishing to NPM
- Build:
ng build image-uploader-lib --configuration production cd dist/image-uploader-lib- Bump
versioninpackage.jsonif needed npm publish
📝 Changelog
v1.1.4 (Latest)
- 🐛 Fixed the wrong uploader receiving the file when several
<lib-image-uploader-lib>instances are on one page. The hidden "replace"<input>was givenid="file-input-{{slotIndex}}", but every instance numbers its own slots from0— so N single-slot uploaders on a page all renderedid="file-input-0".replaceImage()then calleddocument.getElementById('file-input-' + index), which returns the first match in the whole document, so clicking "replace" on any card clicked the first uploader's input and the picked file was emitted (imageUpload/imageReplace) from that first component instead of the one you clicked. The lookup is now per-instance via@ViewChildren, keyed by adata-slot-indexattribute; the non-uniqueidhas been removed from both the replace input and the empty-slot input. No API change — see Wrong image replaced when multiple uploaders are on the page
v1.1.3
- 📚 Docs-only release. No code changes from
v1.1.2— republished solely so the README on npm matches what actually shipped in1.1.2: the crop-box alignment fix (last bullet underv1.1.2below) was already in1.1.2's code but was missing from that version's published README
v1.1.2
- 🔒 Fixed a canvas-tainting bug in the edit modal that could leave
saveCrop()throwingSecurityErrorwith no visible cause.openEditModal()previously handed the remote URL straight toimg.crossOrigin = 'anonymous'; img.src = url— the browser could satisfy that request from an HTTP cache entry populated by an earlier non-CORS request for the same URL (e.g. the plain<img>gallery thumbnail, which has nocrossoriginattribute), silently tainting the canvas regardless of what the server's CORS headers actually allow. The editor nowfetch()es the image explicitly withcache: 'no-store'and loads it into the<img>as a same-origin blob URL, so the canvas can never be tainted by cache reuse —crossOriginis no longer needed and has been removed. Does not append a cache-busting query param (which would break S3 SigV4-presigned URLs by invalidating their signature) — the fix never touches the URL itself - ✨ Crop position/size is now remembered per image. Reopening the edit modal on an already-cropped image restores its exact last-saved crop box instead of resetting to the default centered box. In-memory only, scoped per slot -- see Crop Position/Size Is Remembered Per Image
- ✨ New "tap to view full size" preview. Clicking a thumbnail (or a PDF tile) opens a lightweight modal with just the image, no crop/rotate UI -- see Tap to View Full Size
- ✨ New
previewFitinput ('cover'default,'contain','fill','scale-down','none') to control each thumbnail'sobject-fit-- see Preview Fit - 🐛 Fixed crop box misalignment for extreme aspect ratios.
resetCrop(),saveCrop(), and the drag/resize bounds inonDrag()all assumed the canvas's top-left corner sat at(0, 0)within.image-wrapper. For aspect ratios extreme enough that the canvas's ownmax-width/max-heightrender it smaller than the wrapper (centering it with an offset), this made the crop overlay drift out of alignment with the actual image, and the cropped output could be sourced from the wrong region of the canvas. All three now account for the canvas's realoffsetLeft/offsetTop
v1.1.1
- 🐛 Fixed
pdfjs-distbeing loaded eagerly, bloating every consumer's initial bundle.1.1.0imported it with a top-levelimport * as pdfjsLib from 'pdfjs-dist', which forces the entire library (~400 kB raw) into the initial bundle for every app using this component, even ones that never upload a PDF. It's now dynamicallyimport()'d only insidegeneratePdfThumbnail(), so bundlers split it into its own chunk fetched on demand. Measured effect on this repo's own demo build: initial bundle840.44 kB → 437.64 kBraw (249.38 kB → 152.22 kBtransfer), withpdfjs-distnow a 402.28 kB lazy chunk instead - ✨ New
pdfWorkerSrcinput — override the PDF.js worker script URL (e.g. a self-hosted copy) instead of the hardcoded unpkg CDN default; leave unset to keep the previous CDN behavior
Note: the entries below
v1.1.0don't line up with what's actually on npm (the registry's newest published version before this release was1.0.1— seenpm view @swiftlyme/image-uploader versions). Thev2.x/v2.1.0entries appear to predate an actual publish under those numbers. Left as historical record;v1.1.0is the next real release off of1.0.1.
v1.1.0
New features:
- ✨ Multi-file selection on upload — pick several files at once; they fill the clicked slot plus subsequent empty slots
- ✨
outputFormatinput — force uploads and crop/edit output into'jpeg','png', or'webp'regardless of the source format - ✨
outputQualityinput — control JPEG/WebP re-encode quality independently ofcompressionRate - ✨
compressionRateis now actually applied to uploads and crop/edit output (previously accepted but unused) - ✨
editIcon/deleteIcon/replaceIcon/placeholderIconinputs for custom control icons - ✨ Per-slot loading spinner while a thumbnail is decoding
Bug fixes:
- 🐛 Fixed uploaded/replaced images silently reverting to the initial
imageUrlsseed:ngOnChangeswas rebuilding all slots on anyimageUrls/maxImagesreference change (e.g. an inline array literal in a template re-evaluates to a new reference on every change-detection cycle), which could wipe local upload state. Slots are now only rebuilt when the content actually changes, and growing/shrinkingmaxImagesno longer discards existing images - 🐛 Fixed control buttons (Edit, Replace, Delete, Download, slider nav arrows) missing
type="button"— inside a<form>, clicking them could trigger the form's submit action - 🐛 Fixed the crop editor's resize handles: dragging from the top/left corners could silently freeze near the container edges due to an all-or-nothing bounds check; all four corners now resize correctly and keep the locked aspect ratio
- 🐛 Fixed
compressImage()hanging indefinitely if the source image failed to load (missingonerror) - 🐛 Fixed
downloadImage()naming downloaded files.png(or keeping a stale extension) regardless of the actual format — the filename now always matches the real downloaded content - 🐛 Fixed the crop/edit save step not honoring
compressionRate/outputFormatthe same way uploads do - 🐛 Fixed the package manifest:
pdfjs-dist(used unconditionally for PDF support) was missing fromdependenciesentirely, and@angular/cdk(used for drag-and-drop) was missing frompeerDependencies— a freshnpm install @swiftlyme/image-uploaderwould fail to resolvepdfjs-distat build time
v2.1.0
- ✨ Added PDF upload and preview support
- ✨ Added slider layout mode with navigation buttons
- ✨ Implemented CSS Container Queries for smart responsiveness
- ✨ Added micro-view optimizations for narrow containers
- ✨ Enhanced scroll-snap behavior in slider mode
- 🎨 Improved upload placeholder with responsive text
- 🎨 Added gradient background to modal header
- 🐛 Fixed fullscreen modal positioning
- 🐛 Fixed resize handles visibility
- 🐛 Fixed control buttons in micro views
v2.0.0
- ✨ Added image replacement feature
- ✨ New
imageReplaceevent emission - ✨ New
allowReplaceinput property - 🎨 Yellow warning button for replace action
- 🐛 Fixed file input handling for replacements
- 📚 Updated documentation
v1.0.0
- 🎉 Initial release
- ✨ Drag and drop support
- ✨ Crop/edit functionality
- ✨ Grid layout mode
- ✨ Image compression
📄 License
MIT License - feel free to use this library in your projects.
🙏 Acknowledgments
- Built with Angular
- Drag & Drop powered by Angular CDK
- Styled with Bootstrap 5
- Icons from Bootstrap Icons
- Canvas-based image editing
📞 Support
For issues, questions, or feature requests, email [email protected].
🔗 Links
Made with ❤️ using Angular
