{
  "id": "jnJH1poep1kXrG2U",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "My workflow",
  "tags": [],
  "nodes": [
    {
      "id": "39d8d355-9c4a-4202-9ca9-0c3219d4e207",
      "name": "When clicking \u2018Execute workflow\u2019",
      "type": "n8n-nodes-base.manualTrigger",
      "position": [
        -1168,
        1376
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "1933da34-7848-4659-b0c9-0ea81029e472",
      "name": "Get many messages",
      "type": "n8n-nodes-base.gmail",
      "position": [
        -912,
        1376
      ],
      "parameters": {
        "simple": false,
        "filters": {
          "sender": "user@example.com",
          "readStatus": "unread",
          "includeSpamTrash": true
        },
        "options": {},
        "operation": "getAll"
      },
      "credentials": {
        "gmailOAuth2": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "694fb2d1-c31a-4b07-b320-434f7dfe8c3a",
      "name": "Extract Hero Image SRC From HTML",
      "type": "n8n-nodes-base.code",
      "position": [
        -544,
        1376
      ],
      "parameters": {
        "jsCode": "/**\n * n8n Function node\n * Extract hero <img> src, alt, and title from Gmail HTML.\n *\n * Input: items[0].json.html (string)\n * Output: { hero: { src, alt, title }, validations: {...}, raw: { imgTag, container } }\n */\n\nfunction extractAttr(tag, attrName) {\n  // Extracts attribute values from a tag string safely\n  const re = new RegExp(`${attrName}\\\\s*=\\\\s*(\"([^\"]*)\"|'([^']*)'|([^\\\\s>]+))`, 'i');\n  const m = tag.match(re);\n  return m ? (m[2] || m[3] || m[4] || '') : '';\n}\n\nfunction firstImgTag(html) {\n  // Find the hero container and the first <img> inside it\n\n  // 1) Prefer a container with id=\"heroImage...\"\n  const heroDivMatch = html.match(\n    /<div\\s+class=[\"']mktoImg[\"']\\s+id=[\"']heroImage[^\"']*[\"'][\\s\\S]*?>[\\s\\S]*?<\\/div>/i\n  );\n  if (heroDivMatch) {\n    const heroDiv = heroDivMatch[0];\n    const imgMatch = heroDiv.match(/<img\\b[^>]*>/i);\n    return { imgTag: imgMatch ? imgMatch[0] : '', container: heroDiv };\n  }\n\n  // 2) Fallback: module row with id starting \"fullWidthImageModule\"\n  const moduleMatch = html.match(\n    /<tr\\s+class=[\"']mktoModule[\"']\\s+id=[\"']fullWidthImageModule[^\"']*[\"'][\\s\\S]*?>[\\s\\S]*?<\\/tr>/i\n  );\n  if (moduleMatch) {\n    const mod = moduleMatch[0];\n    const imgMatch = mod.match(/<img\\b[^>]*>/i);\n    return { imgTag: imgMatch ? imgMatch[0] : '', container: mod };\n  }\n\n  // 3) Last resort: first centered hero-like <img> with max-width ~ 680\n  const genericMatch = html.match(\n    /<img\\b[^>]*\\b(max-width:\\s*680px|width\\s*=\\s*[\"']680[\"'])[^>]*>/i\n  );\n  return { imgTag: genericMatch ? genericMatch[0] : '', container: '' };\n}\n\nconst itemsOut = [];\n\nfor (const item of items) {\n  const html = (item.json && item.json.html) ? item.json.html : '';\n\n  if (!html || typeof html !== 'string') {\n    itemsOut.push({\n      json: {\n        error: 'HTML content not found at items[0].json.html',\n      },\n    });\n    continue;\n  }\n\n  const { imgTag, container } = firstImgTag(html);\n\n  if (!imgTag) {\n    itemsOut.push({\n      json: {\n        error: 'Hero image tag not found',\n        hint: 'Ensure the hero block has id=\"heroImage...\" or the module id starts with \"fullWidthImageModule\".',\n      },\n    });\n    continue;\n  }\n\n  // Extract attributes\n  const src = extractAttr(imgTag, 'src');\n  const alt = extractAttr(imgTag, 'alt');\n  const title = extractAttr(imgTag, 'title');\n\n  // Minimal validations\n  const validations = {\n    hasSrc: Boolean(src),\n    srcIsHttps: /^https:\\/\\//i.test(src),\n    altPresent: alt.trim().length > 0,\n    titlePresent: title.trim().length > 0,\n    altTitleSame: alt.trim() && title.trim() ? alt.trim() === title.trim() : null,\n  };\n\n  itemsOut.push({\n    json: {\n      hero: { src, alt, title },\n      validations,\n      raw: { imgTag, container },\n    },\n  });\n}\n\nreturn itemsOut;"
      },
      "typeVersion": 2
    },
    {
      "id": "022156ac-a677-4e25-96d4-fc4a5433bb20",
      "name": "Convert Image File from HTML to Binary",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        -224,
        1504
      ],
      "parameters": {
        "url": "={\n  \"hero\": {\n    \"src\": \"https://dl.dropboxusercontent.com/scl/fi/<FILE_ID>/<generic-image.png>\",\n    \"alt\": \"Hero Image\",\n    \"title\": \"Hero Image Title\"\n  }\n}",
        "options": {
          "response": {
            "response": {
              "responseFormat": "file"
            }
          }
        }
      },
      "typeVersion": 4.3
    },
    {
      "id": "3a6f5dc9-0528-41c3-829b-9f8fb6414b96",
      "name": "Convert Image File from Dropbox to Binary",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        -240,
        1312
      ],
      "parameters": {
        "url": "=https://dl.dropboxusercontent.com/scl/fi/<FILE_ID>/<generic-image.png>",
        "options": {
          "response": {
            "response": {
              "responseFormat": "file"
            }
          }
        }
      },
      "typeVersion": 4.3
    },
    {
      "id": "7b721510-5fb1-4fc4-b9ee-16040e783f06",
      "name": "Combine Image Outputs",
      "type": "n8n-nodes-base.merge",
      "position": [
        256,
        1408
      ],
      "parameters": {},
      "typeVersion": 3.2
    },
    {
      "id": "4879f75c-c085-406b-9465-8e1dcacc408a",
      "name": "Extract Content Match Results",
      "type": "n8n-nodes-base.code",
      "position": [
        464,
        1408
      ],
      "parameters": {
        "jsCode": "// Get all input items\nconst items = $input.all();\n\n// Extract ParsedText from input1 and input2\nconst sourceAText = items[0]?.json?.ParsedResults?.[0]?.ParsedText || \"\";\nconst sourceBText = items[1]?.json?.ParsedResults?.[0]?.ParsedText || \"\";\n\n// Normalize strings\nfunction normalize(str) {\n  return str.replace(/\\s+/g, \" \").trim().toLowerCase();\n}\n\nconst normA = normalize(sourceAText);\nconst normB = normalize(sourceBText);\n\n// Comparison logic\nlet result;\nif (!normA && !normB) {\n  result = \"No content in either source\";\n} else if (!normA || !normB) {\n  result = \"Missing content in one source\";\n} else if (normA === normB) {\n  result = \"Exact Match\";\n} else if (normA.includes(normB) || normB.includes(normA)) {\n  result = \"Partial Match\";\n} else {\n  result = \"Mismatch\";\n}\n\n// Return as columns\nreturn [\n  {\n    \"Column A (Source A Content)\": sourceAText,\n    \"Column B (Source B Content)\": sourceBText,\n    \"Column C (Result)\": result\n  }\n];"
      },
      "typeVersion": 2
    },
    {
      "id": "1f58fa91-7a48-40ef-8ece-477878d496b2",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -2176,
        1120
      ],
      "parameters": {
        "width": 832,
        "height": 624,
        "content": "## Main Overview \u2013 Image Content Validation Flow\n\nThis workflow automates the image content validation process for email QA by comparing the text extracted from expected images stored in Dropbox with the text extracted from actual images found in the email HTML. Instead of manually downloading images, running OCR tools, and checking if the promotional/header image content matches design specifications, this workflow performs the entire process automatically. The flow ensures consistency between the creative assets and the final email build, detects missing or incorrect image content, and logs standardized results back into Google Sheets.\n\n**How it works**\n1. The expected image Dropbox URL is passed directly into an HTTP node, which downloads the expected image in 2. binary format.\n3. The workflow extracts the actual image src from the email HTML (e.g., hero image or any targeted image block).\n4. The actual image is also downloaded in binary format using another HTTP node.\n5. Both binary images are sent individually to the OCR.Space API to extract readable text.\n6. A JS node compares the Expected Image Text vs Actual Image Text to determine a Match or Mismatch.\n7. The workflow logs Expected Text, Actual Text, and Result back into Google Sheets.\n\n**Setup steps**\n* Create Google Sheets columns:\n* SectionId, ExpectedImageURL, ExpectedText, ActualText, Result.\n* Ensure the Dropbox link is a direct-access URL (no login required).\n* Add your OCR.Space API key to the HTTP POST node.\n* Confirm the HTML node extracts the correct image src from the email HTML.\n* Test using a sample email to verify OCR accuracy and layout."
      },
      "typeVersion": 1
    },
    {
      "id": "33031145-aab5-42e7-8b88-a845d7ca5848",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -624,
        1200
      ],
      "parameters": {
        "color": 7,
        "width": 1552,
        "height": 512,
        "content": "## Image Validation\nFetch expected image from Dropbox and actual image from HTML, extract text using OCR.Space, compare expected vs actual text, and log extracted text + results into Google Sheet."
      },
      "typeVersion": 1
    },
    {
      "id": "78dbbd3e-ab76-4832-89b5-6169ac0a6577",
      "name": "Log Image Checks to Excel",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        704,
        1408
      ],
      "parameters": {
        "columns": {
          "value": {
            "Result": "={{ $json['Column C (Result)'] }}",
            "HTML Hero Content": "={{ $json['Column B (HTML Content)'] }}",
            "Dropbox Hero Content": "={{ $json['Column A (Dropbox Content)'] }}"
          },
          "schema": [
            {
              "id": "Dropbox Hero Content",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Dropbox Hero Content",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "HTML Hero Content",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "HTML Hero Content",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Result",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Result",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [
            "Dropbox Hero Content"
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {},
        "operation": "appendOrUpdate",
        "sheetName": {
          "__rl": true,
          "mode": "id",
          "value": "=// Parameters provided by user in node config\nconst sheetId = this.getNodeParameter(\"sheetId\", 0) || \"YOUR_SPREADSHEET_ID\";\nconst sheetName = this.getNodeParameter(\"sheetName\", 0) || \"Sheet1\";\n\nconst { google } = require(\"googleapis\");\nconst sheets = google.sheets({ version: \"v4\", auth });\n\nconst response = await sheets.spreadsheets.values.get({\n  spreadsheetId: sheetId,\n  range: sheetName,\n});\n\nreturn response.data.values.map(row => ({ json: { row } }));"
        },
        "documentId": {
          "__rl": true,
          "mode": "url",
          "value": "=https://docs.google.com/spreadsheets/d/<YOUR_SHEET_ID>/edit"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.7
    },
    {
      "id": "48a348f7-2988-4dfe-84ff-638216262486",
      "name": "Extracting Expected Image Content from OCR1",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        16,
        1312
      ],
      "parameters": {
        "url": "=const response = await fetch(\"https://api.ocr.space/parse/image\", {\n  method: \"POST\",\n  headers: {\n    \"apikey\": process.env.OCR_API_KEY,   // use environment variable\n  },\n  body: new FormData({\n    file: myFile,                        // user-provided file\n    language: \"eng\",                     // configurable language\n  })\n});",
        "method": "POST",
        "options": {},
        "sendBody": true,
        "contentType": "multipart-form-data",
        "sendHeaders": true,
        "bodyParameters": {
          "parameters": [
            {
              "name": "data",
              "parameterType": "formBinaryData",
              "inputDataFieldName": "data"
            }
          ]
        },
        "headerParameters": {
          "parameters": [
            {
              "name": "=headers: {\n  \"apikey\": process.env.OCR_API_KEY\n}",
              "value": "=headers: {\n  \"apikey\": process.env.OCR_API_KEY\n}"
            }
          ]
        }
      },
      "typeVersion": 4.3
    },
    {
      "id": "69f800ff-39b6-4158-a0e7-8cfa966f208e",
      "name": "Extracting Actual Image Content from OCR",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        32,
        1488
      ],
      "parameters": {
        "url": "=const response = await fetch(\"https://api.ocr.space/parse/image\", {\n  method: \"POST\",\n  headers: {\n    \"apikey\": process.env.OCR_API_KEY,   // use environment variable\n  },\n  body: new FormData({\n    file: myFile,                        // user-provided file\n    language: \"eng\",                     // configurable language\n  })\n});",
        "method": "POST",
        "options": {},
        "sendBody": true,
        "contentType": "multipart-form-data",
        "sendHeaders": true,
        "bodyParameters": {
          "parameters": [
            {
              "name": "data",
              "parameterType": "formBinaryData",
              "inputDataFieldName": "=data"
            }
          ]
        },
        "headerParameters": {
          "parameters": [
            {
              "name": "=headers: {\n  \"apikey\": process.env.OCR_API_KEY\n}",
              "value": "=headers: {\n  \"apikey\": process.env.OCR_API_KEY\n}"
            }
          ]
        }
      },
      "typeVersion": 4.3
    }
  ],
  "active": false,
  "settings": {
    "binaryMode": "separate",
    "callerPolicy": "workflowsFromSameOwner",
    "timeSavedMode": "fixed",
    "availableInMCP": false,
    "executionOrder": "v1",
    "saveExecutionProgress": true
  },
  "versionId": "a06236d6-4ce7-4099-8233-697e253c4433",
  "connections": {
    "Get many messages": {
      "main": [
        [
          {
            "node": "Extract Hero Image SRC From HTML",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Combine Image Outputs": {
      "main": [
        [
          {
            "node": "Extract Content Match Results",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Log Image Checks to Excel": {
      "main": [
        []
      ]
    },
    "Extract Content Match Results": {
      "main": [
        [
          {
            "node": "Log Image Checks to Excel",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract Hero Image SRC From HTML": {
      "main": [
        [
          {
            "node": "Convert Image File from HTML to Binary",
            "type": "main",
            "index": 0
          },
          {
            "node": "Convert Image File from Dropbox to Binary",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "When clicking \u2018Execute workflow\u2019": {
      "main": [
        [
          {
            "node": "Get many messages",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Convert Image File from HTML to Binary": {
      "main": [
        [
          {
            "node": "Extracting Actual Image Content from OCR",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extracting Actual Image Content from OCR": {
      "main": [
        [
          {
            "node": "Combine Image Outputs",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Convert Image File from Dropbox to Binary": {
      "main": [
        [
          {
            "node": "Extracting Expected Image Content from OCR1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extracting Expected Image Content from OCR1": {
      "main": [
        [
          {
            "node": "Combine Image Outputs",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}