npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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.

Readme

ImageUploaderLib

A powerful, feature-rich Angular image uploader component with drag-and-drop, cropping, editing, PDF support, and replacement capabilities.

Angular TypeScript License

👉 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/cdk are 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-uploader

Dependencies

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/cdk

Bootstrap 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-icons

Style 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 resulting File's name is also renamed to match (e.g. photo.webp → photo.jpg).
  • outputQuality (0-1, default 0.92) controls JPEG/WebP quality when compressionRate is 0; if compressionRate is set, it drives quality instead (1 - compressionRate / 100), same as the legacy compression-only behavior.
  • PDFs are never converted.
  • Pre-loaded imageUrls entries are not converted either — outputFormat only 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 (and enableFreeCrop is 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 standard object-fit values, 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 allowDrag is true
  • 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 in styles.css/styles.scss) yourself
  • Confirm both stylesheets actually appear in your built page's <head> (view source / devtools)

Images not uploading?

  • Check that maxImages is not exceeded
  • Verify file type is an image or PDF
  • Check browser console for errors

Replace button not showing?

  • Ensure allowReplace is true
  • 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 same id (file-input-0 for any single-slot uploader), and replaceImage() resolved it with document.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 emitted imageReplace/imageUpload for its own field. Make sure you're on v1.1.4+
  • This was purely internal — no template or API change is needed in your app

Crop editor not working?

  • Make sure allowEdit is true
  • 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 a SecurityError (tainted canvas) on an external image with correct CORS headers, make sure you're on v1.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 on v1.1.0+
  • If outputFormat is left at 'original' and compressionRate is 0, no conversion happens at all — the file is whatever format it originally was (including a pre-loaded imageUrls entry, which is never converted)

Images resetting or disappearing after upload/replace?

  • Fixed in v1.1.0 — previously, binding imageUrls to 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 on v1.1.0+
  • If it still happens, check whether you're re-assigning imageUrls to a genuinely new array of URLs elsewhere in your app — that's expected to re-seed the slots

Slider buttons not working?

  • Ensure layoutMode is 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 compressionRate to 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:

  1. Fork the repository
  2. Create a feature branch: git checkout -b feature/my-feature
  3. Commit your changes: git commit -am 'Add new feature'
  4. Push to the branch: git push origin feature/my-feature
  5. Submit a pull request

Building the Library

ng build image-uploader-lib --configuration production

Build artifacts are placed in dist/image-uploader-lib.

Running Tests

ng test image-uploader-lib

Publishing to NPM

  1. Build: ng build image-uploader-lib --configuration production
  2. cd dist/image-uploader-lib
  3. Bump version in package.json if needed
  4. 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 given id="file-input-{{slotIndex}}", but every instance numbers its own slots from 0 — so N single-slot uploaders on a page all rendered id="file-input-0". replaceImage() then called document.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 a data-slot-index attribute; the non-unique id has 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 in 1.1.2: the crop-box alignment fix (last bullet under v1.1.2 below) was already in 1.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() throwing SecurityError with no visible cause. openEditModal() previously handed the remote URL straight to img.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 no crossorigin attribute), silently tainting the canvas regardless of what the server's CORS headers actually allow. The editor now fetch()es the image explicitly with cache: 'no-store' and loads it into the <img> as a same-origin blob URL, so the canvas can never be tainted by cache reuse — crossOrigin is 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 previewFit input ('cover' default, 'contain', 'fill', 'scale-down', 'none') to control each thumbnail's object-fit -- see Preview Fit
  • 🐛 Fixed crop box misalignment for extreme aspect ratios. resetCrop(), saveCrop(), and the drag/resize bounds in onDrag() all assumed the canvas's top-left corner sat at (0, 0) within .image-wrapper. For aspect ratios extreme enough that the canvas's own max-width/max-height render 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 real offsetLeft/offsetTop

v1.1.1

  • 🐛 Fixed pdfjs-dist being loaded eagerly, bloating every consumer's initial bundle. 1.1.0 imported it with a top-level import * 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 dynamically import()'d only inside generatePdfThumbnail(), so bundlers split it into its own chunk fetched on demand. Measured effect on this repo's own demo build: initial bundle 840.44 kB → 437.64 kB raw (249.38 kB → 152.22 kB transfer), with pdfjs-dist now a 402.28 kB lazy chunk instead
  • ✨ New pdfWorkerSrc input — 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.0 don't line up with what's actually on npm (the registry's newest published version before this release was 1.0.1 — see npm view @swiftlyme/image-uploader versions). The v2.x/v2.1.0 entries appear to predate an actual publish under those numbers. Left as historical record; v1.1.0 is the next real release off of 1.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
  • ✨ outputFormat input — force uploads and crop/edit output into 'jpeg', 'png', or 'webp' regardless of the source format
  • ✨ outputQuality input — control JPEG/WebP re-encode quality independently of compressionRate
  • ✨ compressionRate is now actually applied to uploads and crop/edit output (previously accepted but unused)
  • ✨ editIcon / deleteIcon / replaceIcon / placeholderIcon inputs for custom control icons
  • ✨ Per-slot loading spinner while a thumbnail is decoding

Bug fixes:

  • 🐛 Fixed uploaded/replaced images silently reverting to the initial imageUrls seed: ngOnChanges was rebuilding all slots on any imageUrls/maxImages reference 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/shrinking maxImages no 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 (missing onerror)
  • 🐛 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/outputFormat the same way uploads do
  • 🐛 Fixed the package manifest: pdfjs-dist (used unconditionally for PDF support) was missing from dependencies entirely, and @angular/cdk (used for drag-and-drop) was missing from peerDependencies — a fresh npm install @swiftlyme/image-uploader would fail to resolve pdfjs-dist at 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 imageReplace event emission
  • ✨ New allowReplace input 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

📞 Support

For issues, questions, or feature requests, email [email protected].

🔗 Links


Made with ❤️ using Angular