Google Merchant Center 2027 Image Requirements: Audit Your Feed

Google Merchant Center 2027 Image Requirements: Audit Your Feed

Rewarx practical lab · November 3, 2026 · Synthetic fixtures

Google Merchant Center 2027 Image Requirements: Audit Your Feed

Google announces a minimum of 500×500 pixels for all products from January 31, 2027, and recommends 1500×1500 or larger. The documented ceilings are 64 megapixels and 16 MB per file. These are different checks: a 500×500 asset can meet the announced minimum without reaching the recommendation. Check the current official image_link specification.

Use the read-only program below to turn a CSV image list into a dimensions, format, byte-size and readiness report. It was executed against synthetic files on November 3, 2026. It does not certify Merchant Center approval, inspect product accuracy, or change your feed.

Actual synthetic fixture results

FixturePixelsBytes500px ready1500px recommendedClassification
synthetic-399x600.png399 × 6002043FalseFalseBELOW_500
synthetic-499x499.png499 × 4991839FalseFalseBELOW_500
synthetic-500x500.png500 × 5001842TrueFalseNo technical issue found
synthetic-1500x1500.png1500 × 150010027TrueTrueNo technical issue found
synthetic-corrupt.png × 25FalseFalseREAD_OR_DECODE_ERROR
synthetic HTTPS 404 transport fixture × FalseFalseHTTP_404

Four files were generated with Pillow, one deliberately damaged file was read, and the HTTP 404 path was tested with a controlled transport fixture. This is six scenarios, not six merchant images or a production sample. The 404 response was simulated locally; no public hosting outage is inferred.

Run the preflight

Use Python 3.9+ and Pillow. Save the complete program as merchant_image_preflight.py. A manifest has an image_link header and one path or public HTTPS URL per row. Local paths are resolved relative to the CSV. Choose a separate output file.

python -m pip install Pillow
python merchant_image_preflight.py images.csv --output image-report.csv
Complete image preflight program
"""Read-only image manifest audit. Python 3.9+; pip install Pillow.
CSV requires image_link (public HTTPS URL or local path relative to CSV).
Run: python merchant_image_preflight.py manifest.csv --output image-report.csv
Does not test policy compliance or upload anything.
"""
import argparse,csv,http.client,io,ipaddress,socket,ssl,time
from pathlib import Path
from urllib.parse import urlsplit
from PIL import Image,UnidentifiedImageError

MAX_BYTES=16_000_000
MAX_PIXELS=64_000_000
ALLOWED={'JPEG','PNG','WEBP','GIF','BMP','TIFF'}

class AuditError(Exception):pass

def public_target(url):
    u=urlsplit(url)
    if u.scheme!='https' or not u.hostname or u.username or u.password or u.port not in (None,443):
        raise AuditError('URL_NOT_PUBLIC_HTTPS')
    try:ips=list(dict.fromkeys(x[4][0] for x in socket.getaddrinfo(u.hostname,443,type=socket.SOCK_STREAM)))
    except socket.gaierror:raise AuditError('DNS_ERROR')
    if not ips or any(not ipaddress.ip_address(x).is_global for x in ips):raise AuditError('NONPUBLIC_ADDRESS')
    return u,ips[0]

def fetch_public(url):
    u,ip=public_target(url)
    # Pin the validated address while retaining TLS hostname verification.
    conn=http.client.HTTPSConnection(u.hostname,443,timeout=8)
    started=time.monotonic()
    try:
        raw=socket.create_connection((ip,443),timeout=8)
        try:conn.sock=ssl.create_default_context().wrap_socket(raw,server_hostname=u.hostname)
        except Exception:raw.close();raise
        path=u.path or '/'
        if u.query:path+='?'+u.query
        conn.request('GET',path,headers={'User-Agent':'ReadOnlyImagePreflight/1.0','Accept':'image/*'})
        resp=conn.getresponse()
        if resp.status!=200:raise AuditError('HTTP_'+str(resp.status))
        # Redirects are deliberately not followed.
        if int(resp.getheader('Content-Length') or 0)>MAX_BYTES:raise AuditError('FILE_TOO_LARGE')
        data=bytearray()
        while True:
            if time.monotonic()-started>25:raise AuditError('TIME_BUDGET')
            chunk=resp.read(min(65536,MAX_BYTES+1-len(data)))
            if not chunk:break
            data.extend(chunk)
            if len(data)>MAX_BYTES:raise AuditError('FILE_TOO_LARGE')
        return bytes(data)
    finally:conn.close()

def inspect_bytes(data):
    if len(data)>MAX_BYTES:raise AuditError('FILE_TOO_LARGE')
    with Image.open(io.BytesIO(data)) as im:
        w,h=im.size;fmt=im.format
        if w*h>MAX_PIXELS:raise AuditError('PIXELS_TOO_LARGE')
        im.verify()
    # Verify pixel decoding too, not only the container header.
    with Image.open(io.BytesIO(data)) as im:im.load()
    return {'width':w,'height':h,'format':fmt,'bytes':len(data),
            'ready_500':w>=500 and h>=500 and fmt in ALLOWED,
            'recommended_1500':w>=1500 and h>=1500,
            'error':('UNSUPPORTED_FORMAT' if fmt not in ALLOWED else ('BELOW_500' if w<500 or h<500 else ''))}

def audit(source,base):
    out={'source':source,'width':'','height':'','format':'','bytes':'','ready_500':False,'recommended_1500':False,'error':''}
    try:
        if source.startswith(('http:','https:')):data=fetch_public(source)
        else:
            if '://' in source:raise AuditError('INVALID_SOURCE_SCHEME')
            path=(Path(base)/source).resolve()
            if path.stat().st_size>MAX_BYTES:raise AuditError('FILE_TOO_LARGE')
            data=path.read_bytes()
        out['bytes']=len(data)
        out.update(inspect_bytes(data))
    except AuditError as e:out['error']=str(e)
    except FileNotFoundError:out['error']='FILE_NOT_FOUND'
    except (UnidentifiedImageError,OSError,ValueError,SyntaxError) as e:out['error']='READ_OR_DECODE_ERROR'
    except Exception as e:out['error']='FETCH_ERROR_'+type(e).__name__
    return out

def main():
    p=argparse.ArgumentParser();p.add_argument('manifest');p.add_argument('--output',required=True);a=p.parse_args()
    src=Path(a.manifest);dest=Path(a.output)
    if dest.resolve()==src.resolve():raise SystemExit('Output must not replace input')
    with src.open(newline='',encoding='utf-8-sig') as f:
        reader=csv.DictReader(f)
        if 'image_link' not in (reader.fieldnames or []):raise SystemExit('image_link header required')
        rows=[audit(r['image_link'].strip(),src.parent) for r in reader]
    with dest.open('w',newline='',encoding='utf-8') as f:
        writer=csv.DictWriter(f,fieldnames=['source','width','height','format','bytes','ready_500','recommended_1500','error']);writer.writeheader();writer.writerows(rows)
    print(f'{len(rows)} rows audited; {sum(r["ready_500"] for r in rows)} meet this technical 500px check')

if __name__=='__main__':main()

Copyable synthetic input generator

import csv
from pathlib import Path
from PIL import Image
root = Path("synthetic-image-fixtures")
root.mkdir(exist_ok=True)
names = []
for w, h in [(399,600), (499,499), (500,500), (1500,1500)]:
    name = f"synthetic-{w}x{h}.png"
    Image.new("RGB", (w,h), (30,110,85)).save(root/name)
    names.append(name)
(root/"synthetic-corrupt.png").write_bytes(b"synthetic corrupt fixture")
names.append("synthetic-corrupt.png")
with (root/"images.csv").open("w", newline="") as f:
    writer = csv.writer(f)
    writer.writerow(["image_link"])
    writer.writerows([[name] for name in names])
python merchant_image_preflight.py synthetic-image-fixtures/images.csv --output image-report.csv
Actual local-file CLI output
source,width,height,format,bytes,ready_500,recommended_1500,error
synthetic-399x600.png,399,600,PNG,2043,False,False,BELOW_500
synthetic-499x499.png,499,499,PNG,1839,False,False,BELOW_500
synthetic-500x500.png,500,500,PNG,1842,True,False,
synthetic-1500x1500.png,1500,1500,PNG,10027,True,True,
synthetic-corrupt.png,,,,25,False,False,READ_OR_DECODE_ERROR

How remote scanning is bounded

The program accepts public HTTPS on port 443, resolves and rejects non-public addresses, and connects to the validated address with TLS hostname verification. It does not follow redirects. Downloads have a 16,000,000-byte cap, socket timeouts and a read-time budget. DNS resolution still depends on the operating system resolver. Slow or redirected images are review findings, not proof that Google cannot fetch them.

Only use source URLs you are authorized to fetch. The program performs GET requests and never uploads files, signs in or edits a merchant account. It checks one image at a time. Animated files are evaluated on the initial image; color, overlays, alpha correctness and every animation frame are outside scope.

Interpret the report before fixing assets

  • BELOW_500 means one dimension is below the announced minimum; a 399×600 portrait is not ready just because its longest side exceeds 500.
  • The 1500px flag is a recommendation marker, not an extra minimum requirement.
  • READ_OR_DECODE_ERROR means the file could not be parsed and decoded; retain the original before investigating.
  • HTTP_404 identifies the returned response status; other network failures have separate classifications.
  • A technically ready file can still be misleading, visually poor or disallowed. Review product truth and channel policy separately.

Reproduce the 15-test suite

Save both preflight programs above from their respective guides, plus this test file, in one directory. Run python test_practical_preflights_20261103.py. The suite uses temporary local files and a controlled HTTP response for the 404 case. It sends no real upload or paid request.

Complete unit test source
import csv,io,json,tempfile,unittest,sys
from pathlib import Path
from unittest.mock import patch
from urllib.error import HTTPError
from PIL import Image
sys.path.insert(0,str(Path('assets/repro').resolve()))
import merchant_image_preflight as imageaudit
import shopify_variant_csv_preflight as variants

def png(w,h):
    b=io.BytesIO();Image.new('RGB',(w,h),'teal').save(b,format='PNG');return b.getvalue()

class ImageTests(unittest.TestCase):
    def test_dimension_boundaries(self):
        for w,h,ready,rec in [(399,600,False,False),(499,499,False,False),(500,500,True,False),(1500,1500,True,True)]:
            with self.subTest(size=(w,h)):
                r=imageaudit.inspect_bytes(png(w,h));self.assertEqual(r['ready_500'],ready);self.assertEqual(r['recommended_1500'],rec)
    def test_corrupt(self):
        with tempfile.TemporaryDirectory() as d:
            p=Path(d)/'bad.png';p.write_bytes(b'not an image');self.assertEqual(imageaudit.audit('bad.png',d)['error'],'READ_OR_DECODE_ERROR')
    def test_404_adapter(self):
        with patch.object(imageaudit,'fetch_public',side_effect=imageaudit.AuditError('HTTP_404')):
            self.assertEqual(imageaudit.audit('https://example.com/missing.png','.')['error'],'HTTP_404')
    def test_local_network_blocked(self):
        with self.assertRaises(imageaudit.AuditError):imageaudit.public_target('https://127.0.0.1/a')
    def test_scheme_blocked(self):
        with self.assertRaises(imageaudit.AuditError):imageaudit.public_target('http://example.com/a')
    def test_large_bytes(self):
        with self.assertRaises(imageaudit.AuditError):imageaudit.inspect_bytes(b'0'*(imageaudit.MAX_BYTES+1))
    def test_http404_transport(self):
        # The fetch function itself receives a simulated HTTP response; no internet server required.
        class Sock:
            def close(self):pass
        class Resp:
            status=404
        class Conn:
            def __init__(self,*a,**k):pass
            def request(self,*a,**k):pass
            def getresponse(self):return Resp()
            def close(self):pass
        with patch.object(imageaudit,'public_target',return_value=(imageaudit.urlsplit('https://example.com/missing'), '93.184.216.34')),patch.object(imageaudit.http.client,'HTTPSConnection',Conn),patch.object(imageaudit.socket,'create_connection',return_value=Sock()),patch.object(imageaudit.ssl,'create_default_context') as ctx:
            ctx.return_value.wrap_socket.return_value=Sock()
            with self.assertRaisesRegex(imageaudit.AuditError,'HTTP_404'):imageaudit.fetch_public('https://example.com/missing')

class CSVTests(unittest.TestCase):
    def scan(self,rows,headers=None):
        headers=headers or ['URL handle','Title','SKU','Option1 name','Option1 value','Product image URL','Variant image URL']
        with tempfile.TemporaryDirectory() as d:
            p=Path(d)/'products.csv'
            with p.open('w',newline='') as f:w=csv.writer(f);w.writerow(headers);w.writerows(rows)
            original=p.read_bytes();result=variants.validate(p);self.assertEqual(p.read_bytes(),original);return result
    def test_quoted_comma_and_shared_image(self):
        u='https://example.com/red.png';r=self.scan([['shirt','Shirt, cotton','S','Color','Red',u,u],['shirt','','M','Color','Blue',u,u]])
        self.assertEqual(r,[])
    def test_legacy(self):
        self.assertEqual(self.scan([['shirt','Shirt','S','Color','Red','','https://example.com/r.png']],['Handle','Title','Variant SKU','Option1 Name','Option1 Value','Image Src','Variant Image']),[])
    def test_duplicate_key(self):
        row=['shirt','Shirt','S','Color','Red','','https://example.com/r.png'];r=self.scan([row,row]);self.assertIn('DUPLICATE_VARIANT_KEY',[x['code'] for x in r])
    def test_blank_and_invalid(self):
        r=self.scan([['shirt','Shirt','S','Color','Red','',''],['shoe','Shoe','X','Color','Black','','file:///a']]);codes={x['code'] for x in r};self.assertTrue({'BLANK_IMAGE_REVIEW','INVALID_URL'}<=codes)
    def test_crossproduct(self):
        r=self.scan([['a','A','A','Color','Red','https://example.com/a.png','https://example.com/b.png'],['b','B','B','Color','Blue','https://example.com/b.png','https://example.com/b.png']]);self.assertIn('CROSS_PRODUCT_IMAGE_REVIEW',[x['code'] for x in r])
    def test_gallery_row(self):
        self.assertEqual(self.scan([['a','','','','','https://example.com/a.png','']]),[])
    def test_option_dependency(self):
        r=self.scan([['a','A','A','','','','https://example.com/a.png']]);self.assertIn('OPTION_DEPENDENCY',[x['code'] for x in r])
    def test_multiline_line_numbers(self):
        r=self.scan([['a','A\nlong','A','Color','Red','','https://example.com/a.png'],['a','','B','Color','Blue','','']]);self.assertEqual(r[0]['line'],4)

if __name__=='__main__':unittest.main(verbosity=2)
Actual test log
test_blank_and_invalid (__main__.CSVTests) ... ok
test_crossproduct (__main__.CSVTests) ... ok
test_duplicate_key (__main__.CSVTests) ... ok
test_gallery_row (__main__.CSVTests) ... ok
test_legacy (__main__.CSVTests) ... ok
test_multiline_line_numbers (__main__.CSVTests) ... ok
test_option_dependency (__main__.CSVTests) ... ok
test_quoted_comma_and_shared_image (__main__.CSVTests) ... ok
test_404_adapter (__main__.ImageTests) ... ok
test_corrupt (__main__.ImageTests) ... ok
test_dimension_boundaries (__main__.ImageTests) ... ok
test_http404_transport (__main__.ImageTests) ... ok
test_large_bytes (__main__.ImageTests) ... ok
test_local_network_blocked (__main__.ImageTests) ... ok
test_scheme_blocked (__main__.ImageTests) ... ok

----------------------------------------------------------------------
Ran 15 tests in 0.046s

OK

Related practical guides

  • Validate Shopify variant CSV mappings
  • Test XMP preservation while resizing
  • Product image disclaimer limits
  • Artifact diagnosis

Sources checked November 3, 2026

  • Google image requirements

Rewarx workflow entry

Inspect the Rewarx background-removal entry page. Its primary button leads to Studio authentication. The validators and metadata lab here run locally; this article does not claim the Studio performs these checks or preserves metadata automatically.

Check HTTP delivery before file specifications

An image URL may return HTML, a permission error or a redirect instead of the asset. Use the read-only image HTTP diagnostic and local server fixtures to record the response, then return to this guide for dimensions and file checks.

https://www.rewarx.com/blogs/merchant-center-image-requirements-feed-audit

Rewarx Studio | AI-Powered Product Photography & Image Generator

Turn snapshots into professional, high-converting product photos in batches. Cut costs by 90% and launch your collection in minutes.

Create Stunning Product Photos in Batches

Rewarx Studio is fine-tuned to understand the material physics and lighting requirements of 20+ specialized industries, including electronics, cosmetics, fashion, jewelry, home decor, and beverages.

Our virtual photography studio provides precise control over lighting, depth, and material textures. Perfect for high-end catalog shots, Etsy, Amazon, Shopify, and eBay sellers.

The Full AI Production Suite

  • AI Photography Studio: Professional virtual photography with precise control over lighting and textures.
  • AI Lookalike Creator: Match the aesthetic, lighting, and composition of any reference photo.
  • AI Model Studio: Integrate professional human models with your products naturally with realistic shadows.
  • AI Ghost Mannequin: Create a 3D "Invisible" mannequin effect showing inner linings and volume.
  • AI Mockup Generator: Apply patterns and graphics onto 3D items with absolute physical accuracy.
  • AI Group Shot Studio: Cohesively synthesize multiple products into a single scene with perfect lighting.
  • AI Product Page Builder: Generate conversion-optimized listing asset sets in a single click.
  • AI Commercial Ad Poster: Combine product focal points with premium typography for high-converting ads.

Corporate Headquarters

Rewarx Limited, Suite 400, 548 Market Street, San Francisco, CA 94104, United States. Email: studio@rewarx.com