Security & Privacy

What Your Photos Tell Your Visitors: EXIF, GPS and WordPress

WordPress keeps the file you uploaded exactly as it arrived, sitting next to the resized copies it actually serves. If that file came off a phone, it very likely still carries the coordinates of the room it was taken in.

What Your Photos Tell Your Visitors: EXIF, GPS and WordPress

A candlemaker photographs a new product on her kitchen table, drops the file into the media library, and the shop page looks fine. WordPress has quietly turned that one upload into eight or nine files. One of them, the one nobody looks at, is the JPEG her phone wrote, byte for byte, still sitting in wp-content/uploads/2026/08/. Inside it is a latitude and a longitude accurate to a few metres, and they point at her flat.

This is not an obscure edge case. WordPress ships a warning about it in the privacy policy text core suggests for you, in wp-admin/includes/class-wp-privacy-policy-content.php: “If you upload images to the website, you should avoid uploading images with embedded location data (EXIF GPS) included. Visitors to the website can download and extract any location data from images on the website.”

The advice you find elsewhere is usually some version of “don’t worry, WordPress strips it when it resizes”. That is half true on some servers, flatly wrong on others, and never true of the file you actually uploaded. Here is what core really does, checked against the source, and what to do about it.

Table of EXIF fields a phone photo carries, including GPS coordinates accurate to a few metres, timestamp, camera model and the orientation flag WordPress needs

What is actually inside the file

EXIF is a block of tags the camera writes into the image file itself, in a segment called APP1 that sits right after the two bytes marking the start of a JPEG. It is not a sidecar, not a database record, not something WordPress adds. It travels with the file wherever the file goes.

A typical block holds camera make and model, lens, focal length, aperture, shutter speed, ISO, an orientation flag, the date and time the shutter fired, and the name and version of the last piece of software to save the file. Some cameras add a body serial number. Phones add a GPS sub-block: GPSLatitude, GPSLongitude and their hemisphere references, usually altitude and often a compass bearing. The coordinates are stored as three rational numbers each (degrees, minutes, seconds), and on a modern handset they are commonly good to within a few metres.

IPTC is a separate, older block from the news wire world, stored in a different segment (APP13). It carries the fields an editor cares about: creator, credit line, copyright notice, caption, headline and keywords. XMP is a third block again, an XML document usually written by Adobe software, increasingly used for rights statements and accessibility text.

Three independent blocks, three different jobs. Removing one does not touch the other two, which is why “I ran an optimiser over it” is not an answer to “does it still have coordinates”.

What WordPress reads, and what it keeps

The relevant function is wp_read_image_metadata(), in wp-admin/includes/image.php. It runs twice during a normal upload: once inside media_handle_upload(), to work out a title, caption and alt text for the attachment, and again inside wp_create_image_subsizes(), to build the stored metadata array.

It does not copy the metadata wholesale. It fills a fixed array with exactly these keys: aperture, credit, camera, caption, created_timestamp, copyright, focal_length, iso, shutter_speed, title, orientation, keywords and alt. That array is saved under the image_meta key of the attachment’s _wp_attachment_metadata post meta.

Look at what is missing from that list: latitude and longitude. Core calls PHP’s exif_read_data(), which does return GPSLatitude and GPSLongitude along with everything else, and it hands the whole raw array to the wp_read_image_metadata filter as the fifth argument. But it never stores the coordinates. If you have read that WordPress saves your GPS position to the database, core does not.

What it does store is public. The REST API’s media_details field is wp_get_attachment_metadata() handed back verbatim, and an attachment with the inherit status is readable without logging in. A request to /wp-json/wp/v2/media/123 returns the camera model, the credit line, the copyright notice, the keywords and the capture timestamp of any image on the site, to anyone.

The original file is the one that matters

WordPress never rewrites the file you uploaded. Every derivative is a new file written alongside it. If your upload is wider or taller than 2560 pixels (the default value of the big_image_size_threshold filter), core makes a downscaled copy, appends -scaled to the name, points _wp_attached_file at that copy, and records the untouched original’s filename in $image_meta['original_image']. The mechanics of that split, and of the stack of files a single upload produces, are covered in how WordPress turns one upload into a stack of files.

So the file your visitors load is IMG_4471-scaled.jpg, and IMG_4471.jpg is sitting in the same folder, unmodified and still web accessible. Nobody has to break anything to reach it. The URL is the served one with -scaled removed. It is also handed out directly: wp_prepare_attachment_for_js() puts originalImageURL and originalImageName into the media modal for logged-in editors, and media_details.original_image in the public REST response gives the exact filename to anyone who asks.

The same applies when core converts a format rather than resizing. Since WordPress 6.7, wp_get_image_editor_output_format() maps HEIC and HEIF uploads to JPEG by default, so an iPhone photo shared in its native format arrives, gets converted, and the original HEIC stays on disk as original_image with its GPS sub-block intact.

From there, reading the metadata is not a technique. It is a right-click. Preview on macOS shows it in the inspector, Windows shows a subset in file properties, every photo app has a details pane, and browser extensions read it straight off an image on a page. If the coordinates are in the file, they are available to anyone who wants them.

GD strips it, ImageMagick keeps it

This is the part most write-ups get backwards, and it depends entirely on which image library your host has installed.

With GD, WP_Image_Editor_GD decodes the JPEG into a raw pixel buffer, resamples it, and writes a new file with imagejpeg(). There is no mechanism for carrying metadata across that boundary, so the derivative comes out with no EXIF and no IPTC at all. Nothing is being stripped deliberately. The data simply never makes the trip.

With ImageMagick it is the opposite, and it is deliberate. WP_Image_Editor_Imagick::thumbnail_image() calls strip_meta() on every resize, gated behind the image_strip_meta filter, which defaults to true. But strip_meta() walks the image’s profiles and skips a protected list, and the source comments give the reason for each one:

  • icc and icm, colour profile information, so colours do not shift
  • iptc, described in core as copyright data
  • exif, described as orientation data
  • xmp, described as rights usage data

The EXIF profile is protected as a whole. There is no way through that code path to keep the orientation flag and drop the GPS sub-block, so on an ImageMagick host the coordinates travel into the -scaled file, into medium, into thumbnail, into everything. WP_Image_Editor_Imagick sits first in the default list of implementations that _wp_image_editor_choose() tries, so it wins wherever it is installed, which on managed WordPress hosting is most of the time. Site Health, under Info and then Media Handling, names the winner in a field called Active editor.

The image_strip_meta filter is no help either way. Return false and strip_meta() never runs, so nothing is removed. Return true and it runs, but the protected list spares the EXIF profile. The filter only decides the fate of the profiles nobody was worried about.

Table showing that the original upload always keeps its metadata while resized versions lose it under GD but keep it under ImageMagick, which protects the exif and iptc profiles

Who this actually bites

The pattern is always the same: someone photographs something at home and publishes the picture, not the address.

  • A photographer building a portfolio. Studio shots, test frames and behind-the-scenes images all pin the same set of coordinates, and one of them is where the equipment is kept.
  • A small shop owner photographing stock on a kitchen table or a spare room floor. Every product listing carries the same location, so the pattern is obvious even from one photo.
  • Estate and letting sites, where interior shots of an empty property go out with the exact position of a building nobody is living in.
  • Nurseries, schools and clubs publishing photos taken by parents. The coordinates in those files belong to whoever took the photo, not to the organisation posting it.

The timestamp deserves a mention on its own. created_timestamp is the moment the shutter fired, not the moment you published, and WordPress puts it in the public REST response. Location plus time, repeated across a few dozen images, is a routine.

None of which makes “strip everything” the correct default. A landscape photographer may want coordinates on the file. A studio absolutely wants its copyright notice travelling with the image. The right question is which block, not whether.

Checking what is already in your library

Before changing anything, find out what you are dealing with. For a single attachment, three lines will do. Run this through WP-CLI with wp eval-file, or from a temporary snippet, rather than leaving it in a theme.

$meta = wp_get_attachment_metadata( 42 );

// Everything WordPress kept from the camera and IPTC blocks.
print_r( $meta['image_meta'] );

// The file exactly as it was uploaded, still on disk.
echo wp_get_original_image_path( 42 );

That tells you what is in the database. It does not tell you what is in the file, because core never stored the coordinates. For that you have to read the original directly. The function below is read only: it opens files and reports, and changes nothing.

/**
 * Lists attachments whose original file still carries GPS coordinates.
 * Read only. Nothing is written or modified.
 */
function wpie_find_located_images( $limit = 500 ) {
	$found = array();

	if ( ! function_exists( 'exif_read_data' ) ) {
		return $found;
	}

	$ids = get_posts(
		array(
			'post_type'      => 'attachment',
			'post_mime_type' => array( 'image/jpeg', 'image/tiff' ),
			'numberposts'    => $limit,
			'fields'         => 'ids',
		)
	);

	foreach ( $ids as $id ) {
		$path = wp_get_original_image_path( $id );

		if ( ! $path || ! file_exists( $path ) ) {
			continue;
		}

		$exif = @exif_read_data( $path );

		if ( is_array( $exif ) && ! empty( $exif['GPSLatitude'] ) ) {
			$found[ $id ] = $path;
		}
	}

	return $found;
}

On a library of any size this is usually the moment you discover that the answer is “most of the phone photos, none of the stock images”. If you would rather not run code across a few thousand attachments, WunderPaint’s media library manager reads the embedded blocks per image and can remove either just the location tags or every block, without re-encoding the file. Either way it pairs naturally with a general media library cleanup, since the originals nobody is serving are often the same files nobody needed to keep.

You do not have to take anyone’s word for what sits in your own files. Drop a photo below and the viewer reads the EXIF block straight out of the bytes: camera, lens, exposure, timestamps, and the coordinates if they are in there. It runs in this browser tab, the file is never uploaded anywhere.

EXIF viewer

Open a photo and read what it carries: camera, lens, settings, dates, and the spot on the map where the shutter fired. Drop a whole folder of them in and strip the lot in one go, losslessly, by rewriting the container instead of encoding the picture again. Every file is read here in the browser tab and none of them is ever uploaded.

Drop photos here
or press Enter to choose them. One to read in detail, or many to clean at once. JPEG, PNG, WebP and TIFF.

Nothing loaded yet.

Preview

The picture is drawn from your own disk. Nothing about it is sent anywhere.

Drop a photo above to see its tags.

What gets read

Camera and lens, exposure settings, the date and time the shutter fired, the editing software, the author and copyright line, and the GPS position if the camera stored one. Every value is shown next to its raw EXIF tag id, so you can look any of them up.

No file yet

Cleaning is lossless on JPEG, PNG and WebP: the file is walked segment by segment and only the metadata segments are dropped, so every byte of compressed image data survives and the picture is pixel for pixel the one you started with. A TIFF cannot be treated that way, because in a TIFF the metadata structure is the file, and a GIF has no EXIF to begin with. For those two the older route is still there: the picture is drawn onto a canvas and encoded again, which does cost a little quality.

WunderPaint
The Dynamic Design and Automation Studio
WunderPaint is a layered image editor for your WordPress media library. WunderPaint Studio is the same thing in any browser, free and without an account.

How to remove EXIF data in WordPress on upload

The most reliable place to remove EXIF data is before the file ever reaches WordPress, and I will come back to that. But if uploads arrive from clients or contributors you do not control, you need something that runs on the server.

Hook choice matters more than it looks. The wp_handle_upload filter fires as soon as the file lands in the uploads directory, before WordPress has read anything from it. Strip there and you also destroy the orientation flag, so maybe_exif_rotate() finds nothing to act on and portrait phone photos stay sideways in every generated size. The wp_generate_attachment_metadata filter fires after core has read the metadata, rotated the image and written every derivative, which is exactly what you want.

The helper below removes the EXIF segment from a JPEG by rewriting the file’s segment list. It does not decode or re-encode the pixels, so there is no quality loss, and it leaves the IPTC and XMP blocks in place. It does rewrite the file: once it has run, the EXIF data is not recoverable from that file. Test it on a copy first.

/**
 * Removes the Exif block from a JPEG without re-encoding the pixels.
 * IPTC (credit, copyright) and XMP are left in place.
 *
 * This rewrites the file. The Exif data is gone for good afterwards.
 *
 * @param string $file Absolute path to a JPEG file.
 * @return bool True if the file was rewritten, false if it was left alone.
 */
function wpie_strip_jpeg_exif( $file ) {
	$data = file_get_contents( $file );

	if ( false === $data || strlen( $data ) < 4 || "xFFxD8" !== substr( $data, 0, 2 ) ) {
		return false;
	}

	$out      = "xFFxD8";
	$pos      = 2;
	$len      = strlen( $data );
	$stripped = false;
	$complete = false;

	while ( $pos + 2 <= $len && "xFF" === $data[ $pos ] ) {
		$marker = ord( $data[ $pos + 1 ] );

		// Padding byte between segments.
		if ( 0xFF === $marker ) {
			++$pos;
			continue;
		}

		// Start of scan: the compressed pixels follow, copy the rest untouched.
		if ( 0xDA === $marker ) {
			$out     .= substr( $data, $pos );
			$complete = true;
			break;
		}

		// Standalone markers carry no length field.
		if ( 0x01 === $marker || ( $marker >= 0xD0 && $marker <= 0xD9 ) ) {
			$out .= substr( $data, $pos, 2 );
			$pos += 2;
			continue;
		}

		if ( $pos + 4 > $len ) {
			return false;
		}

		$size = unpack( 'n', substr( $data, $pos + 2, 2 ) )[1];

		if ( $size < 2 || $pos + 2 + $size > $len ) {
			return false;
		}

		$is_exif = 0xE1 === $marker && "Exifx00x00" === substr( $data, $pos + 4, 6 );

		if ( $is_exif ) {
			$stripped = true;
		} else {
			$out .= substr( $data, $pos, $size + 2 );
		}

		$pos += $size + 2;
	}

	if ( ! $complete || ! $stripped ) {
		return false;
	}

	return false !== file_put_contents( $file, $out );
}

Only the APP1 segment that starts with the Exif marker is dropped. XMP also lives in an APP1 segment, and it is left alone because its payload starts with an Adobe namespace URI instead. Then wire the helper to new uploads. It cleans the sub-sizes as well, because on an ImageMagick host those carry the same block.

add_filter(
	'wp_generate_attachment_metadata',
	function ( $metadata, $attachment_id, $context ) {
		if ( 'create' !== $context || 'image/jpeg' !== get_post_mime_type( $attachment_id ) ) {
			return $metadata;
		}

		$attached = get_attached_file( $attachment_id );

		if ( ! $attached ) {
			return $metadata;
		}

		// The uploaded original, plus the file WordPress now serves as "full".
		$originals = array_unique(
			array_filter( array( wp_get_original_image_path( $attachment_id ), $attached ) )
		);

		foreach ( $originals as $path ) {
			if ( file_exists( $path ) ) {
				wpie_strip_jpeg_exif( $path );
			}
		}

		// On ImageMagick hosts the sub-sizes carry the same block.
		if ( ! empty( $metadata['sizes'] ) ) {
			$dir = trailingslashit( dirname( $attached ) );

			foreach ( $metadata['sizes'] as $name => $size ) {
				if ( empty( $size['file'] ) ) {
					continue;
				}

				$path = $dir . $size['file'];

				if ( file_exists( $path ) && wpie_strip_jpeg_exif( $path ) ) {
					clearstatcache( true, $path );
					$metadata['sizes'][ $name ]['filesize'] = wp_filesize( $path );
				}
			}
		}

		if ( file_exists( $attached ) ) {
			clearstatcache( true, $attached );
			$metadata['filesize'] = wp_filesize( $attached );
		}

		return $metadata;
	},
	10,
	3
);

This affects new uploads only. Existing files stay as they are until you run the stripper over them yourself, which you should do deliberately and with a backup, not as a side effect of loading a page. Both snippets belong in a small site-specific plugin or a child theme’s functions file rather than in a parent theme that will be updated over you. Where custom code belongs and how to edit it safely covers the trade-offs.

Doing it before the upload instead

Server-side stripping is a safety net. It is not the first line, because it only ever runs on files that have already left the photographer’s machine and been sent over the wire.

On the phone itself, both platforms let you drop location when sharing. On recent versions of iOS the share sheet has an options row where location can be switched off before sending. On Android, the camera app has a location tagging switch, and turning it off means the coordinates are never written in the first place. Desktop editors handle this in the export dialog: Lightroom’s export panel has a metadata dropdown with options that remove location while keeping copyright, and Photoshop’s Export As writes a much thinner block than Save As does. The exact wording moves between versions, so look for the word location rather than a specific menu path.

For a folder of files already on disk, exiftool is the precise tool, and it can remove one block without disturbing the rest.

# Remove only the location tags, keep camera settings and credit.
exiftool -gps:all= -overwrite_original photo.jpg

# Remove every metadata block from every JPEG in a folder.
exiftool -all= -overwrite_original -ext jpg .

The second form takes the colour profile with it, which can shift the appearance of wide-gamut images; adding --icc_profile:all to that command keeps the profile. ImageMagick’s own magick input.jpg -strip output.jpg does the same broad removal and carries the same caveat, plus it re-encodes the image, so you pay a quality cost that exiftool does not charge you.

One thing worth knowing about image CDNs: services that resize on delivery generally serve clean derivatives, because they re-encode. That does not clean your origin. The file on your own server keeps whatever it had, and it is usually still reachable.

Comparison of stripping metadata before upload, during upload with a filter, or across an existing library, and which situation each one fits

The metadata worth keeping

Blanket removal throws away the fields that were designed to protect you. The IPTC block is where a headline (2#105), a byline (2#080), a credit line (2#110), a copyright notice (2#116), a caption (2#120) and keywords (2#025) live, and wp_read_image_metadata() reads all of them by those exact record numbers.

It does not just read them, it uses them. During media_handle_upload(), an IPTC headline or a short caption becomes the attachment’s title, the caption becomes the attachment’s excerpt, which is the Caption field in the media library, and the copyright and credit end up in image_meta where a theme can print them under the image. Set those fields once in an export preset and every upload arrives already attributed, with no typing in the media library.

WordPress 7.0 added a genuinely useful piece to this. wp_get_image_alttext() parses the XMP block for the Iptc4xmpCore:AltTextAccessibility property and, if it finds one, uses it as the attachment’s alt text, picking the entry that best matches the site locale. Alt text written once in the photo editor now follows the file into every site it is used on, which beats remembering to fill the field in the media modal. It does not save you from thinking about the text itself, which is a separate craft covered in writing alt text people can actually use.

So the useful policy is not “strip everything”. It is: kill the GPS sub-block, keep the IPTC and XMP blocks, and decide about the camera settings based on whether anyone benefits from reading them.

Symptoms and causes

The thumbnail has no metadata but the full-size URL still shows coordinates. Your server is using GD, which rebuilds derivatives from raw pixels and cannot carry metadata across. The file you uploaded was never rewritten, so it kept everything.

Every size carries the location, thumbnails included. Your server is using ImageMagick. WP_Image_Editor_Imagick::strip_meta() protects the exif, iptc and xmp profiles by design, so they are copied into each generated file.

You ran a metadata-removal plugin and the original is still dirty. Most image optimisers work on the sizes WordPress serves. The file named in image_meta['original_image'] is not served by any template, so it is frequently skipped, and it is the file with everything in it.

Photos started uploading sideways after you added a stripping filter. The filter ran before maybe_exif_rotate() could read the orientation tag. Move it to wp_generate_attachment_metadata, which fires after rotation and after every sub-size has been written.

An attachment’s title or caption filled itself in with something you did not type. That is wp_read_image_metadata() doing its job, pulling an IPTC headline or caption out of the file during upload. Harmless, and often helpful once you know where it comes from.

Where that leaves you

The mental model that keeps this straight is that WordPress treats your upload as an archive copy. It reads a few fields out of it, generates a family of derivatives from it, and then leaves it alone forever. Everything above follows from that one design decision. The derivatives may or may not be clean depending on your host’s image library, but the archive copy is always exactly what you sent, and it is always reachable.

The practical version is short. Check what your library is holding before you change anything. Turn off location tagging at the camera, because a coordinate that was never written cannot leak. Add a server-side strip for uploads you do not control, hooked late enough that WordPress has already used the orientation flag and the IPTC fields. Then leave the credit and copyright blocks alone, because those are the ones working for you.

Most sites will find that a few dozen files need attention and the rest never had anything in them. That is a good outcome, and it is worth twenty minutes to know which group you are in rather than assuming.

What Your Photos Tell Your Visitors: EXIF, GPS and WordPress

Table of Contents

Learn it by building something

Every week one thing you can make the same afternoon, from dynamic templates to 3D type. Written down step by step.

One mail a week, and then it ends.
Unsubscribe in one click.

Print & Craft

From Screen to Paper: What Actually Changes When a Design Gets Printed

Resolution that is not a property of the file, colors that cannot be mixed in ink, and a blade that never lands exactly on the line you drew.

Security & Privacy

WordPress File Permissions: 644, 755 and When Something Else

chmod 777 makes the error disappear by handing write access to every process on the server. What the three digits mean, the standard WordPress set, and why ownership decides which digit even gets read.

Photo Editing

Never Destroy Pixels You Might Want Back: A Working Method

Every edit is either reversible or it is not, and the difference costs nothing at the moment you make it. It shows up later, always at the worst time.

Design Fundamentals

Contrast Is a Number, Not an Opinion

Contrast is the one accessibility metric that is pure arithmetic: two colours in, one number out, and the number either clears the threshold or it does not. Check a pair in the browser, then learn what the ratio measures, why relative luminance is not the average of the channels, where the large text exception really starts, and what to do when the brand colour fails.

Security & Privacy

Black out every piece of text in a screenshot before you share it

A tool that guesses which text is sensitive will miss the one that mattered. This one covers every text field it finds and lets you click back what can stay. It also refuses to default to a blur, because blurring and pixelation can be undone, and there is published work showing exactly how.

Photo Editing

Take the scratches and dust out of a scanned family photograph

A scratch is thin and disagrees with its surroundings in almost every direction at once. A real edge disagrees in one or two. That single difference finds the damage with plain arithmetic, and an inpainting model fills what you agree to. All of it in your browser.

Download the free WunderPaint Plugin for WordPress

The WunderPaint workspace with the layers panel, adjustment sliders, text style presets and the asset library along the bottom

The Image Editor & Design Studio

Everything described here can be done in the browser, on your own site. The live demo runs the full editor with nothing to install.

Free

Chaos Art

Autonomous painters make one-of-a-kind abstract art in 3D space - gestures, art movements, painterly media, and embeds that paint a new original for every visitor.

Pro

Particle Strokes

Paint with swarms of light: twenty-two movements, a stamp you draw yourself, and curves that give a stroke a shape - the swarm keeps painting for a few seconds after you let go.

Pro

City Diorama

Any place on earth as a miniature you could hold: real streets, water and building footprints raised into a 3D diorama - or wrapped around a sphere as your own tiny planet.

Free

Papercut Art

Layered paper pictures with real depth - a photo sliced along its actual depth into up to twenty layers, parametric landscapes, animals and clouds you can shape, letters with real counters, and a look that runs on three dials.

Pro

3D Earth Studio

A hyperrealistic globe - day and night with city lights, live clouds, atmosphere halo, country borders and highlights, click-to-place markers with flight-route arcs, satellite orbits, seamless rotation video and a live website embed.

Free

Mystic Studio

Turn a birth date into wall art - a real natal chart with houses and aspects, the moon of that night, zodiac and Chinese zodiac posters, numerology cards and a synastry wheel for two, in eight artful themes.

Free

Marble Bath

Marble paper on a virtual water bath - drop, rake and comb real Ebru patterns with gestures, flowers and classic recipes, razor-sharp at any size and re-editable as a layer.

Free

Day Ring

Turn a day into a beautiful circular schedule - colour-coded time blocks as arcs around a 24-hour clock, with concentric rings for overlaps, emoji, templates and a legend.

Free

Code Shot

Turn code into a gorgeous, share-ready image - syntax highlighting, editor themes, window frames and diff highlighting - then drop it into your design as a re-editable layer.

Pro

3D Solar System Studio

Build a date-accurate 3D solar system - real planet positions for any date, photoreal textures and one slider from artistic to true scale - then drop it into your design as an editable layer.

Pro

3D Molecule Studio

Build a real 3D molecule - from a curated library, the periodic table or a SMILES string - then style it, measure it and drop it into your design as an editable layer.

Pro

3D Textile Studio

Drop your design onto cloth that behaves like the material you pick: silk falls soft, felt holds its shape, flag fabric snaps in the wind. Hang it, blow it and drape it, then lay the finished drape back into your document as a picture.

Pro

3D Particle Studio

Point the engine at any layer and it becomes a cloud of particles that keeps its colours, flowing through a sphere, a galaxy or your own outline. Keep the frame you like as a still, or embed the running engine so it keeps moving on your page.

Free

Origami

Put your own picture on the paper and watch that very sheet fold itself into a crane or a box. Every step is a station you can stop at and turn around in 3D, which is exactly where printed diagrams leave you alone.

Pro

3D Flip Studio

A hardcover you can leaf through, a limp magazine, a strewn pile of sheets, a sticker peeling off its backing. The curl is real geometry, so the print never slides across the paper.