diff --git a/src/main/java/bdv/ij/ExportImagePlusAsN5PlugIn.java b/src/main/java/bdv/ij/ExportImagePlusAsN5PlugIn.java
new file mode 100644
index 0000000000000000000000000000000000000000..911ec379e76044aa67a3486561af2357ee086b3e
--- /dev/null
+++ b/src/main/java/bdv/ij/ExportImagePlusAsN5PlugIn.java
@@ -0,0 +1,403 @@
+package bdv.ij;
+
+import bdv.export.ExportMipmapInfo;
+import bdv.export.ExportScalePyramid.AfterEachPlane;
+import bdv.export.ExportScalePyramid.LoopbackHeuristic;
+import bdv.export.ProgressWriter;
+import bdv.export.ProposeMipmaps;
+import bdv.export.SubTaskProgressWriter;
+import bdv.export.n5.WriteSequenceToN5;
+import bdv.ij.util.PluginHelper;
+import bdv.ij.util.ProgressWriterIJ;
+import bdv.img.imagestack.ImageStackImageLoader;
+import bdv.img.n5.N5ImageLoader;
+import bdv.img.virtualstack.VirtualStackImageLoader;
+import bdv.spimdata.SequenceDescriptionMinimal;
+import bdv.spimdata.SpimDataMinimal;
+import bdv.spimdata.XmlIoSpimDataMinimal;
+import fiji.util.gui.GenericDialogPlus;
+import ij.IJ;
+import ij.ImageJ;
+import ij.ImagePlus;
+import ij.WindowManager;
+import java.awt.Checkbox;
+import java.awt.TextField;
+import java.awt.event.ItemEvent;
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Map;
+import mpicbg.spim.data.SpimDataException;
+import mpicbg.spim.data.generic.sequence.BasicViewSetup;
+import mpicbg.spim.data.generic.sequence.TypedBasicImgLoader;
+import mpicbg.spim.data.registration.ViewRegistration;
+import mpicbg.spim.data.registration.ViewRegistrations;
+import mpicbg.spim.data.sequence.Channel;
+import mpicbg.spim.data.sequence.FinalVoxelDimensions;
+import mpicbg.spim.data.sequence.TimePoint;
+import mpicbg.spim.data.sequence.TimePoints;
+import net.imglib2.FinalDimensions;
+import net.imglib2.RandomAccessibleInterval;
+import net.imglib2.realtransform.AffineTransform3D;
+import net.imglib2.util.Intervals;
+import org.janelia.saalfeldlab.n5.Compression;
+import org.janelia.saalfeldlab.n5.Lz4Compression;
+import org.janelia.saalfeldlab.n5.RawCompression;
+import org.scijava.command.Command;
+import org.scijava.plugin.Plugin;
+
+/**
+ * ImageJ plugin to export the current image to xml/n5.
+ *
+ * @author Tobias Pietzsch
+ */
+@Plugin(type = Command.class,
+	menuPath = "Plugins>BigDataViewer>Export Current Image as XML/N5")
+public class ExportImagePlusAsN5PlugIn implements Command
+{
+	public static void main( final String[] args )
+	{
+		new ImageJ();
+		final ImagePlus imp = IJ.openImage( "/Users/pietzsch/workspace/data/confocal-series.tif" );
+		imp.show();
+		new ExportImagePlusAsN5PlugIn().run();
+	}
+
+	@Override
+	public void run()
+	{
+		if ( ij.Prefs.setIJMenuBar )
+			System.setProperty( "apple.laf.useScreenMenuBar", "true" );
+
+		// get the current image
+		final ImagePlus imp = WindowManager.getCurrentImage();
+
+		// make sure there is one
+		if ( imp == null )
+		{
+			IJ.showMessage( "Please open an image first." );
+			return;
+		}
+
+		// check the image type
+		switch ( imp.getType() )
+		{
+		case ImagePlus.GRAY8:
+		case ImagePlus.GRAY16:
+		case ImagePlus.GRAY32:
+			break;
+		default:
+			IJ.showMessage( "Only 8, 16, 32-bit images are supported currently!" );
+			return;
+		}
+
+		// check the image dimensionality
+		if ( imp.getNDimensions() < 2 )
+		{
+			IJ.showMessage( "Image must be at least 2-dimensional!" );
+			return;
+		}
+
+		// get calibration and image size
+		final double pw = imp.getCalibration().pixelWidth;
+		final double ph = imp.getCalibration().pixelHeight;
+		final double pd = imp.getCalibration().pixelDepth;
+		String punit = imp.getCalibration().getUnit();
+		if ( punit == null || punit.isEmpty() )
+			punit = "px";
+		final FinalVoxelDimensions voxelSize = new FinalVoxelDimensions( punit, pw, ph, pd );
+		final int w = imp.getWidth();
+		final int h = imp.getHeight();
+		final int d = imp.getNSlices();
+		final FinalDimensions size = new FinalDimensions( w, h, d );
+
+		// propose reasonable mipmap settings
+		final ExportMipmapInfo autoMipmapSettings = ProposeMipmaps.proposeMipmaps( new BasicViewSetup( 0, "", size, voxelSize ) );
+
+		// show dialog to get output paths, resolutions, subdivisions, min-max option
+		final Parameters params = getParameters( autoMipmapSettings );
+		if ( params == null )
+			return;
+
+		final ProgressWriter progressWriter = new ProgressWriterIJ();
+		progressWriter.out().println( "starting export..." );
+
+		// create ImgLoader wrapping the image
+		final TypedBasicImgLoader< ? > imgLoader;
+		final Runnable clearCache;
+		final boolean isVirtual = imp.getStack() != null && imp.getStack().isVirtual();
+		if ( isVirtual )
+		{
+			final VirtualStackImageLoader< ?, ?, ? > il;
+			switch ( imp.getType() )
+			{
+			case ImagePlus.GRAY8:
+				il = VirtualStackImageLoader.createUnsignedByteInstance( imp );
+				break;
+			case ImagePlus.GRAY16:
+				il = VirtualStackImageLoader.createUnsignedShortInstance( imp );
+				break;
+			case ImagePlus.GRAY32:
+			default:
+				il = VirtualStackImageLoader.createFloatInstance( imp );
+				break;
+			}
+			imgLoader = il;
+			clearCache = il.getCacheControl()::clearCache;
+		}
+		else
+		{
+			switch ( imp.getType() )
+			{
+			case ImagePlus.GRAY8:
+				imgLoader = ImageStackImageLoader.createUnsignedByteInstance( imp );
+				break;
+			case ImagePlus.GRAY16:
+				imgLoader = ImageStackImageLoader.createUnsignedShortInstance( imp );
+				break;
+			case ImagePlus.GRAY32:
+			default:
+				imgLoader = ImageStackImageLoader.createFloatInstance( imp );
+				break;
+			}
+			clearCache = () -> {};
+		}
+
+		final int numTimepoints = imp.getNFrames();
+		final int numSetups = imp.getNChannels();
+
+		// create SourceTransform from the images calibration
+		final AffineTransform3D sourceTransform = new AffineTransform3D();
+		sourceTransform.set( pw, 0, 0, 0, 0, ph, 0, 0, 0, 0, pd, 0 );
+
+		// write n5
+		final HashMap< Integer, BasicViewSetup > setups = new HashMap<>( numSetups );
+		for ( int s = 0; s < numSetups; ++s )
+		{
+			final BasicViewSetup setup = new BasicViewSetup( s, String.format( "channel %d", s + 1 ), size, voxelSize );
+			setup.setAttribute( new Channel( s + 1 ) );
+			setups.put( s, setup );
+		}
+		final ArrayList< TimePoint > timepoints = new ArrayList<>( numTimepoints );
+		for ( int t = 0; t < numTimepoints; ++t )
+			timepoints.add( new TimePoint( t ) );
+		final SequenceDescriptionMinimal seq = new SequenceDescriptionMinimal( new TimePoints( timepoints ), setups, imgLoader, null );
+
+		Map< Integer, ExportMipmapInfo > perSetupExportMipmapInfo;
+		perSetupExportMipmapInfo = new HashMap<>();
+		final ExportMipmapInfo mipmapInfo = new ExportMipmapInfo( params.resolutions, params.subdivisions );
+		for ( final BasicViewSetup setup : seq.getViewSetupsOrdered() )
+			perSetupExportMipmapInfo.put( setup.getId(), mipmapInfo );
+
+		// LoopBackHeuristic:
+		// - If saving more than 8x on pixel reads use the loopback image over
+		//   original image
+		// - For virtual stacks also consider the cache size that would be
+		//   required for all original planes contributing to a "plane of
+		//   blocks" at the current level. If this is more than 1/4 of
+		//   available memory, use the loopback image.
+		final long planeSizeInBytes = imp.getWidth() * imp.getHeight() * imp.getBytesPerPixel();
+		final long ijMaxMemory = IJ.maxMemory();
+		final int numCellCreatorThreads = Math.max( 1, PluginHelper.numThreads() - 1 );
+		final LoopbackHeuristic loopbackHeuristic = new LoopbackHeuristic()
+		{
+			@Override
+			public boolean decide( final RandomAccessibleInterval< ? > originalImg, final int[] factorsToOriginalImg, final int previousLevel, final int[] factorsToPreviousLevel, final int[] chunkSize )
+			{
+				if ( previousLevel < 0 )
+					return false;
+
+				if ( Intervals.numElements( factorsToOriginalImg ) / Intervals.numElements( factorsToPreviousLevel ) >= 8 )
+					return true;
+
+				if ( isVirtual )
+				{
+					final long requiredCacheSize = planeSizeInBytes * factorsToOriginalImg[ 2 ] * chunkSize[ 2 ];
+					if ( requiredCacheSize > ijMaxMemory / 4 )
+						return true;
+				}
+
+				return false;
+			}
+		};
+
+		final AfterEachPlane afterEachPlane = new AfterEachPlane()
+		{
+			@Override
+			public void afterEachPlane( final boolean usedLoopBack )
+			{
+				if ( !usedLoopBack && isVirtual )
+				{
+					final long free = Runtime.getRuntime().freeMemory();
+					final long total = Runtime.getRuntime().totalMemory();
+					final long max = Runtime.getRuntime().maxMemory();
+					final long actuallyFree = max - total + free;
+
+					if ( actuallyFree < max / 2 )
+						clearCache.run();
+				}
+			}
+
+		};
+
+		try
+		{
+			Compression compression = params.deflate ? new Lz4Compression() : new RawCompression();
+			WriteSequenceToN5.writeN5File( seq, perSetupExportMipmapInfo, compression, params.n5File, loopbackHeuristic, afterEachPlane, numCellCreatorThreads, new SubTaskProgressWriter( progressWriter, 0, 0.95 ) );
+
+			// write xml sequence description
+			final N5ImageLoader n5Loader = new N5ImageLoader( params.n5File, null );
+			final SequenceDescriptionMinimal seqh5 = new SequenceDescriptionMinimal( seq, n5Loader );
+
+			final ArrayList< ViewRegistration > registrations = new ArrayList<>();
+			for ( int t = 0; t < numTimepoints; ++t )
+				for ( int s = 0; s < numSetups; ++s )
+					registrations.add( new ViewRegistration( t, s, sourceTransform ) );
+
+			final File basePath = params.seqFile.getParentFile();
+			final SpimDataMinimal spimData = new SpimDataMinimal( basePath, seqh5, new ViewRegistrations( registrations ) );
+
+			new XmlIoSpimDataMinimal().save( spimData, params.seqFile.getAbsolutePath() );
+			progressWriter.setProgress( 1.0 );
+		}
+		catch ( final SpimDataException | IOException e )
+		{
+			throw new RuntimeException( e );
+		}
+		progressWriter.out().println( "done" );
+	}
+
+	protected static class Parameters
+	{
+		final boolean setMipmapManual;
+
+		final int[][] resolutions;
+
+		final int[][] subdivisions;
+
+		final File seqFile;
+
+		final File n5File;
+
+		final boolean deflate;
+
+		public Parameters(
+				final boolean setMipmapManual, final int[][] resolutions, final int[][] subdivisions,
+				final File seqFile, final File n5File,
+				final boolean deflate )
+		{
+			this.setMipmapManual = setMipmapManual;
+			this.resolutions = resolutions;
+			this.subdivisions = subdivisions;
+			this.seqFile = seqFile;
+			this.n5File = n5File;
+			this.deflate = deflate;
+		}
+	}
+
+	static boolean lastSetMipmapManual = false;
+
+	static String lastSubsampling = "{1,1,1}, {2,2,1}, {4,4,2}";
+
+	static String lastChunkSizes = "{32,32,4}, {16,16,8}, {8,8,8}";
+
+	static boolean lastDeflate = true;
+
+	static String lastExportPath = "./export.xml";
+
+	protected Parameters getParameters( final ExportMipmapInfo autoMipmapSettings  )
+	{
+		while ( true )
+		{
+			final GenericDialogPlus gd = new GenericDialogPlus( "Export for BigDataViewer as XML/N5" );
+
+			gd.addCheckbox( "manual_mipmap_setup", lastSetMipmapManual );
+			final Checkbox cManualMipmap = ( Checkbox ) gd.getCheckboxes().lastElement();
+			gd.addStringField( "Subsampling_factors", lastSubsampling, 25 );
+			final TextField tfSubsampling = ( TextField ) gd.getStringFields().lastElement();
+			gd.addStringField( "N5_chunk_sizes", lastChunkSizes, 25 );
+			final TextField tfChunkSizes = ( TextField ) gd.getStringFields().lastElement();
+
+			gd.addMessage( "" );
+			gd.addCheckbox( "use_deflate_compression", lastDeflate );
+
+			gd.addMessage( "" );
+			PluginHelper.addSaveAsFileField( gd, "Export_path", lastExportPath, 25 );
+
+			final String autoSubsampling = ProposeMipmaps.getArrayString( autoMipmapSettings.getExportResolutions() );
+			final String autoChunkSizes = ProposeMipmaps.getArrayString( autoMipmapSettings.getSubdivisions() );
+			gd.addDialogListener( ( dialog, e ) -> {
+				gd.getNextBoolean();
+				gd.getNextString();
+				gd.getNextString();
+				gd.getNextBoolean();
+				gd.getNextString();
+				if ( e instanceof ItemEvent && e.getID() == ItemEvent.ITEM_STATE_CHANGED && e.getSource() == cManualMipmap )
+				{
+					final boolean useManual = cManualMipmap.getState();
+					tfSubsampling.setEnabled( useManual );
+					tfChunkSizes.setEnabled( useManual );
+					if ( !useManual )
+					{
+						tfSubsampling.setText( autoSubsampling );
+						tfChunkSizes.setText( autoChunkSizes );
+					}
+				}
+				return true;
+			} );
+
+			tfSubsampling.setEnabled( lastSetMipmapManual );
+			tfChunkSizes.setEnabled( lastSetMipmapManual );
+			if ( !lastSetMipmapManual )
+			{
+				tfSubsampling.setText( autoSubsampling );
+				tfChunkSizes.setText( autoChunkSizes );
+			}
+
+			gd.showDialog();
+			if ( gd.wasCanceled() )
+				return null;
+
+			lastSetMipmapManual = gd.getNextBoolean();
+			lastSubsampling = gd.getNextString();
+			lastChunkSizes = gd.getNextString();
+			lastDeflate = gd.getNextBoolean();
+			lastExportPath = gd.getNextString();
+
+			// parse mipmap resolutions and cell sizes
+			final int[][] resolutions = PluginHelper.parseResolutionsString( lastSubsampling );
+			final int[][] subdivisions = PluginHelper.parseResolutionsString( lastChunkSizes );
+			if ( resolutions.length == 0 )
+			{
+				IJ.showMessage( "Cannot parse subsampling factors " + lastSubsampling );
+				continue;
+			}
+			if ( subdivisions.length == 0 )
+			{
+				IJ.showMessage( "Cannot parse n5 chunk sizes " + lastChunkSizes );
+				continue;
+			}
+			else if ( resolutions.length != subdivisions.length )
+			{
+				IJ.showMessage( "subsampling factors and n5 chunk sizes must have the same number of elements" );
+				continue;
+			}
+
+			String seqFilename = lastExportPath;
+			if ( !seqFilename.endsWith( ".xml" ) )
+				seqFilename += ".xml";
+			final File seqFile = new File( seqFilename );
+			final File parent = seqFile.getParentFile();
+			if ( parent == null || !parent.exists() || !parent.isDirectory() )
+			{
+				IJ.showMessage( "Invalid export filename " + seqFilename );
+				continue;
+			}
+			final String n5Filename = seqFilename.substring( 0, seqFilename.length() - 4 ) + ".n5";
+			final File n5File = new File( n5Filename );
+
+			return new Parameters( lastSetMipmapManual, resolutions, subdivisions, seqFile, n5File, lastDeflate );
+		}
+	}
+}