Skip to content

Add the new QR code#1406

Merged
tarunnjoshi merged 16 commits intodevfrom
dynamic-qr-code-with-some-more-details
Sep 3, 2025
Merged

Add the new QR code#1406
tarunnjoshi merged 16 commits intodevfrom
dynamic-qr-code-with-some-more-details

Conversation

@tarunnjoshi
Copy link
Copy Markdown
Member

@tarunnjoshi tarunnjoshi commented Sep 3, 2025

Add the new QR code

Summary by CodeRabbit

  • New Features

    • Generates a second “poster” QR (branded PNG) alongside existing QR codes for events, collection camps, dropping centers and activities.
    • New public endpoint to produce/save poster-style QR images.
  • Enhancements

    • Poster images include centered logo, two-line Proxima Nova heading, bold "Venue:" with wrapped address pulled from the event/camp record.
    • Higher-quality QR sizing, automatic logo scaling, better memory cleanup, and final composite saved as the delivered asset.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Sep 3, 2025

Warning

Rate limit exceeded

@tarunnjoshi has exceeded the limit for the number of commits or files that can be reviewed per hour. Please wait 8 minutes and 14 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between eda2e97 and 046fb45.

📒 Files selected for processing (1)
  • wp-content/civi-extensions/goonjcustom/Civi/Traits/QrCodeable.php (3 hunks)

Walkthrough

Fetches venue/address via Event or EckEntity (Collection_Camp variants), renders a higher-resolution QR (scale 12), composes a branded PNG (logo, two-line heading with FontSpring proximanova Bold/Regular, centered QR, wrapped venue text) via GD/TTF, saves via self::saveQrCode, returns TRUE on success, FALSE on error.

Changes

Cohort / File(s) Summary
QR trait — address lookup & imports
wp-content/civi-extensions/goonjcustom/Civi/Traits/QrCodeable.php
Added imports (Civi\Api4\Event, Civi\Api4\Address, Civi\Api4\EckEntity); address retrieval now branches by customGroupName (Event_QR → load Event → loc_block_id.address_id; otherwise → EckEntity::get('Collection_Camp', FALSE) with multi-field selects and subtype handling). Throws on unknown subtype.
QR trait — composition & fonts
wp-content/civi-extensions/goonjcustom/Civi/Traits/QrCodeable.php
QR scale increased (10→12). Base64 QR decoded → GD image → resized (0.7×). Loads WP uploads logo, resizes (~0.5× new QR width) preserving alpha. Composes canvas with two-line heading (proximanova Bold/Regular), centered QR, bold “Venue:” and wrapped address, renders final PNG to memory, frees GD resources.
QR trait — save behavior & API
wp-content/civi-extensions/goonjcustom/Civi/Traits/QrCodeable.php
Final composed image saved via self::saveQrCode($finalImage, $saveOptions); added public static generateQrCodeForPoster($data, $entityId, $saveOptions) which composes and saves poster QR. Exceptions logged and result returns FALSE on error, TRUE on success.
Service callers — poster QR invocation
wp-content/civi-extensions/goonjcustom/Civi/*Service.php
CollectionCampService.php, DroppingCenterService.php, GoonjActivitiesService.php, GoonjInitiatedEventsService.php, InstitutionCollectionCampService.php, InstitutionDroppingCenterService.php, InstitutionGoonjActivitiesService.php
Each service now builds a $saveOptionsForPoster (customGroupName: Collection_Camp_QR_Code, customFieldName: QR_Code_For_Poster) and calls self::generateQrCodeForPoster($data, $id, $saveOptionsForPoster) before/alongside existing generateQrCode call. Original QR generation remains.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor Caller
  participant QrTrait as QrCodeable
  participant EntitySvc as Event / EckEntity
  participant GD as GD/Image
  participant Storage as saveQrCode

  Caller->>QrTrait: generateQrCode(entityId, customGroupName, ...)
  alt customGroupName == Event_QR
    QrTrait->>EntitySvc: load Event -> loc_block_id.address_id
    EntitySvc-->>QrTrait: Address
  else
    QrTrait->>EntitySvc: EckEntity::get('Collection_Camp', FALSE) + fields
    EntitySvc-->>QrTrait: Collection_Camp / subtype address fields
  end
  QrTrait->>GD: render QR (scale=12) -> base64
  QrTrait->>GD: decode → imagecreatefromstring → resize (0.7×)
  QrTrait->>GD: load uploads logo → resize (~0.5× QR) → preserve alpha
  QrTrait->>GD: compose canvas (heading, centered QR, venue text) → output PNG
  QrTrait->>Storage: saveQrCode(finalImage, {baseFileName, saveOptions})
  Storage-->>QrTrait: success/failure
  QrTrait-->>Caller: TRUE / FALSE
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Review notes / best-practice flags

  • Single-responsibility violation: large monolithic method mixes data access, business logic, image composition, text layout, and IO — extract responsibilities into helpers.
  • Duplication: multiple services call the same poster-generation pattern; consider centralizing invocation or a service-level adapter to reduce repetition.
  • Hard-coded assets/configs: font paths, logo filename, scale factors, and magic numbers are embedded; make configurable and validate existence.
  • Error handling: broad try/catch returning FALSE hides failure details; prefer narrower catches, explicit logging, and guaranteed resource cleanup.
  • Text wrapping & encoding: manual wrapping must handle UTF-8/multibyte; use font metrics consistently and centralize wrapping logic.
  • Resource safety: ensure all GD resources are freed on all code paths (including exceptions).
  • Tests: no evidence of unit/integration tests for the new poster flow — add tests for address resolution branches and image composition.

Possibly related PRs

Suggested labels

status : ready for review

Suggested reviewers

  • pokhiii

Poem

A QR, a logo, lines aligned,
Fonts that speak and pixels refined,
Address wrapped and canvas made,
Two codes born before the raid,

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch dynamic-qr-code-with-some-more-details

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore or @coderabbit ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
wp-content/civi-extensions/goonjcustom/Civi/Traits/QrCodeable.php (2)

22-27: Explicitly disable base64 output to avoid extra decode and binary corruption risk

Set QROptions::$outputBase64=false and work with raw PNG bytes directly. This removes the need to strip the data URI and base64_decode, and is faster. In v5 the default is a base64 data URI; this option flips it to raw binary. (github.com, newreleases.io)

Apply:

   $options = new QROptions([
     'version'    => 5,
     'outputType' => QRCode::OUTPUT_IMAGE_PNG,
     'eccLevel'   => QRCode::ECC_L,
     'scale'      => 10,
+    'outputBase64' => false,
   ]);

109-113: Catch Throwable, not just Exception

GD/TypeErrors are Errors, not Exceptions. Catch Throwable to avoid fatals and log more context.

- catch (\Exception $e) {
-   \CRM_Core_Error::debug_log_message('Error generating QR code: ' . $e->getMessage());
+ catch (\Throwable $e) {
+   \CRM_Core_Error::debug_log_message('Error generating QR code: ' . $e->getMessage());
🧹 Nitpick comments (4)
wp-content/civi-extensions/goonjcustom/Civi/Traits/QrCodeable.php (4)

69-76: Avoid hardcoded caption; make it configurable or omit

Use a supplied caption and default to empty to prevent unexpected user-facing text.

- $text = "Hello here is the qr code";
+ $text = (string)($saveOptions['caption'] ?? '');
  // Reserve space for text.
  $fontHeight = 30;
-
- $canvasHeight = $newLogoHeight + $qrHeight + $fontHeight + 20;
+ $extraTextHeight = $text !== '' ? $fontHeight : 0;
+ $canvasHeight = $newLogoHeight + $qrHeight + $extraTextHeight + 20;

85-92: Only render text when provided

Minor conditional to avoid empty baseline.

- $black = imagecolorallocate($canvas, 0, 0, 0);
- $x = 10;
- $y = $newLogoHeight + $qrHeight + 20;
- imagestring($canvas, 5, $x, $y, $text, $black);
+ if ($text !== '') {
+   $black = imagecolorallocate($canvas, 0, 0, 0);
+   $x = 10;
+   $y = $newLogoHeight + $qrHeight + 20;
+   imagestring($canvas, 5, $x, $y, $text, $black);
+ }

98-101: Guard resource cleanup

Only destroy if created.

- imagedestroy($qrImage);
- imagedestroy($logoImage);
- imagedestroy($resizedLogo);
- imagedestroy($canvas);
+ if ($qrImage) { imagedestroy($qrImage); }
+ if ($logoImage) { imagedestroy($logoImage); }
+ if ($resizedLogo) { imagedestroy($resizedLogo); }
+ if ($canvas) { imagedestroy($canvas); }

41-41: Prefix global WP functions for clarity/perf

\wp_upload_dir() is clearer and avoids namespace lookups.

- $upload_dir = wp_upload_dir();
+ $upload_dir = \wp_upload_dir();
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 522a825 and 7f664b4.

📒 Files selected for processing (1)
  • wp-content/civi-extensions/goonjcustom/Civi/Traits/QrCodeable.php (1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2024-10-11T09:53:14.458Z
Learnt from: belwalshubham
PR: ColoredCow/goonj#284
File: wp-content/civi-extensions/goonjcustom/Civi/CollectionCampService.php:520-520
Timestamp: 2024-10-11T09:53:14.458Z
Learning: In `wp-content/civi-extensions/goonjcustom/Civi/CollectionCampService.php`, it's acceptable that the `reGenerateCollectionCampQr` method calls `generateQrCode` with only the `$collectionCampId` parameter, as QR codes are generated when the Dropping Center is authorized. We'll implement changes if any issues arise in the future.

Applied to files:

  • wp-content/civi-extensions/goonjcustom/Civi/Traits/QrCodeable.php
🔇 Additional comments (1)
wp-content/civi-extensions/goonjcustom/Civi/Traits/QrCodeable.php (1)

52-67: LGTM on resize math and alpha handling

Comment thread wp-content/civi-extensions/goonjcustom/Civi/Traits/QrCodeable.php Outdated
Comment on lines +34 to +38
// --- Step 1: Create GD image from QR ---
$qrImage = imagecreatefromstring($qrcode);
$qrWidth = imagesx($qrImage);
$qrHeight = imagesy($qrImage);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Guard against GD image creation failures to prevent TypeError fatals

imagecreatefromstring() can return false; imagesx()/imagesy() would then throw. Add validation and a clear error.

- $qrImage = imagecreatefromstring($qrcode);
- $qrWidth = imagesx($qrImage);
- $qrHeight = imagesy($qrImage);
+ $qrImage = imagecreatefromstring($qrcode);
+ if (!$qrImage) {
+   throw new \RuntimeException('Failed to create QR image from binary');
+ }
+ $qrWidth  = imagesx($qrImage);
+ $qrHeight = imagesy($qrImage);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// --- Step 1: Create GD image from QR ---
$qrImage = imagecreatefromstring($qrcode);
$qrWidth = imagesx($qrImage);
$qrHeight = imagesy($qrImage);
// --- Step 1: Create GD image from QR ---
$qrImage = imagecreatefromstring($qrcode);
if (!$qrImage) {
throw new \RuntimeException('Failed to create QR image from binary');
}
$qrWidth = imagesx($qrImage);
$qrHeight = imagesy($qrImage);
🤖 Prompt for AI Agents
In wp-content/civi-extensions/goonjcustom/Civi/Traits/QrCodeable.php around
lines 34 to 38, imagecreatefromstring() can return false which will cause
imagesx()/imagesy() to throw TypeErrors; add a guard that checks if $qrImage ===
false (or !is_resource/!is_object depending on GD), and if so free any allocated
resources, log a clear error or throw a descriptive exception (e.g.
InvalidArgumentException with the raw QR data length or GD error), and
return/abort before calling imagesx/imagesy so subsequent code never receives a
boolean.

Comment on lines +41 to +48
$upload_dir = wp_upload_dir();
$logoUrl = $upload_dir['baseurl'] . '/2024/09/Goonj-logo-10June20-300x193-1.png';
error_log('path: ' . print_r($logoUrl, TRUE));

error_log('upload_dir: ' . print_r($upload_dir, TRUE));
$logoData = file_get_contents($logoUrl);
$logoImage = imagecreatefromstring($logoData);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Don’t hardcode logo path or fetch over HTTP; read from filesystem and handle errors

  • Hardcoded year/month will break later.
  • file_get_contents() over HTTP adds latency/fragility; use uploads basedir path first.
  • Remove noisy error_log() calls.
- $upload_dir = wp_upload_dir();
- $logoUrl = $upload_dir['baseurl'] . '/2024/09/Goonj-logo-10June20-300x193-1.png';
- error_log('path: ' . print_r($logoUrl, TRUE));
-
- error_log('upload_dir: ' . print_r($upload_dir, TRUE));
- $logoData = file_get_contents($logoUrl);
- $logoImage = imagecreatefromstring($logoData);
+ $upload_dir = \wp_upload_dir();
+ // Prefer filesystem; allow passing a custom logo path via $saveOptions['logoPath'].
+ $logoPath = $saveOptions['logoPath'] ?? ($upload_dir['basedir'] . '/brand/Goonj-logo.png');
+ if (!\file_exists($logoPath)) {
+   // Fallback to URL if present; otherwise fail fast.
+   $logoUrl = $saveOptions['logoUrl'] ?? null;
+   if (!$logoUrl) {
+     throw new \RuntimeException("Logo not found at $logoPath and no logoUrl provided");
+   }
+   $logoData = \wp_remote_retrieve_body(\wp_remote_get($logoUrl, ['timeout' => 5]));
+   if (!$logoData) {
+     throw new \RuntimeException("Failed to download logo from $logoUrl");
+   }
+   $logoImage = imagecreatefromstring($logoData);
+ } else {
+   $logoImage = imagecreatefrompng($logoPath);
+ }
+ if (!$logoImage) {
+   throw new \RuntimeException('Failed to create logo image resource');
+ }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
$upload_dir = wp_upload_dir();
$logoUrl = $upload_dir['baseurl'] . '/2024/09/Goonj-logo-10June20-300x193-1.png';
error_log('path: ' . print_r($logoUrl, TRUE));
error_log('upload_dir: ' . print_r($upload_dir, TRUE));
$logoData = file_get_contents($logoUrl);
$logoImage = imagecreatefromstring($logoData);
$upload_dir = \wp_upload_dir();
// Prefer filesystem; allow passing a custom logo path via $saveOptions['logoPath'].
$logoPath = $saveOptions['logoPath'] ?? ($upload_dir['basedir'] . '/brand/Goonj-logo.png');
if (!\file_exists($logoPath)) {
// Fallback to URL if present; otherwise fail fast.
$logoUrl = $saveOptions['logoUrl'] ?? null;
if (!$logoUrl) {
throw new \RuntimeException("Logo not found at $logoPath and no logoUrl provided");
}
$logoData = \wp_remote_retrieve_body(\wp_remote_get($logoUrl, ['timeout' => 5]));
if (!$logoData) {
throw new \RuntimeException("Failed to download logo from $logoUrl");
}
$logoImage = imagecreatefromstring($logoData);
} else {
$logoImage = imagecreatefrompng($logoPath);
}
if (!$logoImage) {
throw new \RuntimeException('Failed to create logo image resource');
}
🤖 Prompt for AI Agents
In wp-content/civi-extensions/goonjcustom/Civi/Traits/QrCodeable.php around
lines 41 to 48, avoid hardcoding the uploads URL/年月 and fetching the logo over
HTTP: build the path using wp_upload_dir()['basedir'] (not baseurl) and a
relative filename or configurable setting, check file_exists and is_readable
before reading, remove the noisy error_log() calls, use file_get_contents on the
local filesystem path (or imagecreatefrompng/gd functions) wrapped in try/if
checks and handle failures by returning/logging a clear error or falling back to
a default image.

Comment on lines +103 to +108
$baseFileName = "qr_code_{$entityId}.png";
$saveOptions['baseFileName'] = $baseFileName;
$saveOptions['entityId'] = $entityId;

self::saveQrCode($qrcode, $saveOptions);
self::saveQrCode($finalImage, $saveOptions);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Propagate save result instead of always returning TRUE

Return FALSE if attachment creation fails; optionally return the attachment on success.

- $baseFileName = "qr_code_{$entityId}.png";
+ $baseFileName = "qr_code_{$entityId}.png";
  $saveOptions['baseFileName'] = $baseFileName;
  $saveOptions['entityId'] = $entityId;
-
- self::saveQrCode($finalImage, $saveOptions);
+ $attachment = self::saveQrCode($finalImage, $saveOptions);
+ if ($attachment === false) {
+   return FALSE;
+ }
 ...
- return TRUE;
+ return TRUE; // or `return $attachment;` if callers expect the attachment data

If callers rely on a truthy attachment array, switch the final return to $attachment.

Also applies to: 115-116


🏁 Script executed:

#!/bin/bash
# Locate QrCodeable.php
QR=$(fd QrCodeable.php)
echo "QrCodeable file: $QR"
# Show definition and usage of saveQrCode
rg -n -C3 "saveQrCode" "$QR"

Length of output: 807


🏁 Script executed:

#!/bin/bash
sed -n '120,200p' wp-content/civi-extensions/goonjcustom/Civi/Traits/QrCodeable.php

Length of output: 2615


Propagate save result instead of always returning TRUE
– In generateQrCode (lines 103–108), assign

$attachment = self::saveQrCode($finalImage, $saveOptions);
if ($attachment === FALSE) {
  return FALSE;
}
return $attachment;

instead of ignoring the call and returning TRUE.
– Apply the same pattern to the save call at lines 115–116.

🤖 Prompt for AI Agents
In wp-content/civi-extensions/goonjcustom/Civi/Traits/QrCodeable.php around
lines 103–108 (and similarly at lines 115–116), the result of self::saveQrCode
is currently ignored and the method always returns TRUE; change both calls to
capture the return (e.g. $attachment = self::saveQrCode($finalImage,
$saveOptions)), check for a FALSE failure (if ($attachment === FALSE) return
FALSE;) and otherwise return the $attachment so callers receive the actual saved
attachment result.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (4)
wp-content/civi-extensions/goonjcustom/Civi/Traits/QrCodeable.php (4)

29-33: Stop double-handling PNG bytes; make output deterministic (raw OR base64, not both).

Current code strips a data URI and base64-decodes unconditionally. This only works if render() returns a base64 data URI; if it already returns raw PNG, decoding will corrupt the image. Make it explicit and remove the decode.

Option A (preferred): force raw bytes and drop decoding.

-      // Generate QR.
-      $qrcode = (new QRCode($options))->render($data);
-      $qrcode = str_replace('data:image/png;base64,', '', $qrcode);
-      $qrcode = base64_decode($qrcode);
+      // Generate QR as raw PNG (no data URI, no base64).
+      $options = new QROptions([
+        'version'       => 5,
+        'outputType'    => QRCode::OUTPUT_IMAGE_PNG,
+        'eccLevel'      => QRCode::ECC_L,
+        'scale'         => 10,
+        'outputBase64'  => false,
+      ]);
+      $qrcode = (new QRCode($options))->render($data);

Option B: keep base64 flow but set it explicitly and retain the decode.

+      $options = new QROptions([
+        // ...
+        'outputBase64' => true,
+      ]);
       $qrcode = (new QRCode($options))->render($data);
       $qrcode = str_replace('data:image/png;base64,', '', $qrcode);
       $qrcode = base64_decode($qrcode);

Pick one path; do not mix them.


33-35: Guard GD resource creation before using imagesx()/imagesy().

imagecreatefromstring() can return false; calling imagesx/imagesy on it will fatal.

-      $qrImage = imagecreatefromstring($qrcode);
-      $qrWidth = imagesx($qrImage);
-      $qrHeight = imagesy($qrImage);
+      $qrImage = imagecreatefromstring($qrcode);
+      if ($qrImage === false) {
+        throw new \RuntimeException('Failed to create QR image from binary');
+      }
+      $qrWidth  = imagesx($qrImage);
+      $qrHeight = imagesy($qrImage);

37-44: Don’t hardcode dated uploads path or fetch over HTTP; use filesystem with fallbacks.

  • The hardcoded /2024/09 path will break later.
  • Avoid HTTP fetch from baseurl; read from basedir and allow an override via $saveOptions.
  • Also, call global WP functions with a leading backslash under a namespace.
-      // Load logo (from uploads folder URL).
-      $upload_dir = wp_upload_dir();
-      $logoUrl = $upload_dir['baseurl'] . '/2024/09/Goonj-logo-10June20-300x193-1.png';
-      $logoData = file_get_contents($logoUrl);
-      $logoImage = imagecreatefromstring($logoData);
+      // Load logo from filesystem (configurable), fallback to URL if provided.
+      $upload_dir = \wp_upload_dir();
+      $logoPath = $saveOptions['logoPath'] ?? ($upload_dir['basedir'] . '/brand/Goonj-logo.png');
+      if (\file_exists($logoPath)) {
+        $logoImage = imagecreatefrompng($logoPath);
+      } elseif (!empty($saveOptions['logoUrl'])) {
+        $logoData = \wp_remote_retrieve_body(\wp_remote_get($saveOptions['logoUrl'], ['timeout' => 5]));
+        $logoImage = $logoData ? imagecreatefromstring($logoData) : false;
+      } else {
+        $logoImage = false;
+      }
+      if ($logoImage === false) {
+        throw new \RuntimeException('Failed to load logo image');
+      }
       $logoWidth = imagesx($logoImage);
       $logoHeight = imagesy($logoImage);

102-106: Propagate the save result; don’t always return TRUE.

Capture the attachment result and return it (or FALSE on failure). This prevents silent failures and gives callers the file metadata.

       $saveOptions['baseFileName'] = $baseFileName;
       $saveOptions['entityId'] = $entityId;
-      self::saveQrCode($finalImage, $saveOptions);
+      $attachment = self::saveQrCode($finalImage, $saveOptions);
+      if ($attachment === false) {
+        return false;
+      }
+      return $attachment;
@@
-    return TRUE;
+    // Unreachable if returning above; if you prefer boolean API, return ($attachment !== false).
+    return TRUE;

If callers expect a boolean today, we can keep the signature and just return ($attachment !== false);. Please confirm the call sites.

Also applies to: 113-113

🧹 Nitpick comments (2)
wp-content/civi-extensions/goonjcustom/Civi/Traits/QrCodeable.php (2)

45-57: Preserve transparency when resizing the logo; add failure checks.

Without pre-filling a fully transparent background, edges may appear with dark halos; also check allocation succeeded.

       $resizedLogo = imagecreatetruecolor($newLogoWidth, $newLogoHeight);
-      imagealphablending($resizedLogo, FALSE);
-      imagesavealpha($resizedLogo, TRUE);
+      if ($resizedLogo === false) {
+        throw new \RuntimeException('Failed to allocate resized logo canvas');
+      }
+      imagealphablending($resizedLogo, false);
+      imagesavealpha($resizedLogo, true);
+      $transparent = imagecolorallocatealpha($resizedLogo, 0, 0, 0, 127);
+      imagefill($resizedLogo, 0, 0, $transparent);

63-71: Avoid magic numbers for text width; center using GD font metrics.

Using 9 px per char is brittle and breaks with different fonts/strings. Use imagefontwidth()/imagefontheight().

-      $canvasWidth = $qrWidth + 100;
-      $canvasHeight = $newLogoHeight + $qrHeight + 160;
+      $font = 5;
+      $fw = \imagefontwidth($font);
+      $fh = \imagefontheight($font);
+      $canvasWidth  = $qrWidth + 100;
+      $canvasHeight = $newLogoHeight + 20 /*logo gap*/
+                    + $fh /*top text*/
+                    + 40 /*gap*/
+                    + $qrHeight
+                    + 30 /*gap*/
+                    + $fh /*bottom text*/
+                    + 10;
@@
-      $topY = $logoY + $newLogoHeight + 20;
-      $topX = (int) (($canvasWidth - (strlen($topText) * 9)) / 2);
-      imagestring($canvas, 5, $topX, $topY, $topText, $black);
+      $topY = $logoY + $newLogoHeight + 20;
+      $topX = (int)(($canvasWidth - ($fw * (function_exists('mb_strlen') ? mb_strlen($topText) : strlen($topText)))) / 2);
+      imagestring($canvas, $font, $topX, $topY, $topText, $black);
@@
-      $bottomY = $qrY + $qrHeight + 30;
-      $bottomX = (int) (($canvasWidth - (strlen($bottomText) * 9)) / 2);
-      imagestring($canvas, 5, $bottomX, $bottomY, $bottomText, $black);
+      $bottomY = $qrY + $qrHeight + 30;
+      $bottomX = (int)(($canvasWidth - ($fw * (function_exists('mb_strlen') ? mb_strlen($bottomText) : strlen($bottomText)))) / 2);
+      imagestring($canvas, $font, $bottomX, $bottomY, $bottomText, $black);

Optional: switch to imagettftext() for better typography.

Also applies to: 77-81, 87-91

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 7f664b4 and cc4d2b3.

📒 Files selected for processing (1)
  • wp-content/civi-extensions/goonjcustom/Civi/Traits/QrCodeable.php (1 hunks)
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2024-10-11T09:53:14.458Z
Learnt from: belwalshubham
PR: ColoredCow/goonj#284
File: wp-content/civi-extensions/goonjcustom/Civi/CollectionCampService.php:520-520
Timestamp: 2024-10-11T09:53:14.458Z
Learning: In `wp-content/civi-extensions/goonjcustom/Civi/CollectionCampService.php`, it's acceptable that the `reGenerateCollectionCampQr` method calls `generateQrCode` with only the `$collectionCampId` parameter, as QR codes are generated when the Dropping Center is authorized. We'll implement changes if any issues arise in the future.

Applied to files:

  • wp-content/civi-extensions/goonjcustom/Civi/Traits/QrCodeable.php
📚 Learning: 2024-10-09T15:24:42.859Z
Learnt from: belwalshubham
PR: ColoredCow/goonj#352
File: wp-content/civi-extensions/goonjcustom/Civi/QrCodeService.php:51-53
Timestamp: 2024-10-09T15:24:42.859Z
Learning: In `QrCodeService::generateQrCode`, when using `QRCode::OUTPUT_IMAGE_PNG`, the `render` method returns a data URI with base64-encoded image data. Therefore, it's necessary to remove the `data:image/png;base64,` prefix and base64-decode the data to obtain the raw binary image.

Applied to files:

  • wp-content/civi-extensions/goonjcustom/Civi/Traits/QrCodeable.php
🔇 Additional comments (1)
wp-content/civi-extensions/goonjcustom/Civi/Traits/QrCodeable.php (1)

97-100: Resource cleanup looks good.

Comment on lines +59 to +62
// Texts.
$topText = "Scan to Record Your Contribution";
$bottomText = "Venue: Gaur Saundaryam Society, Greater Noida";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Hard-coded, user-facing text; make it dynamic (and translatable if needed).

Venue and CTA should come from data/options, not hardcoded to a specific society.

-      $topText = "Scan to Record Your Contribution";
-      $bottomText = "Venue: Gaur Saundaryam Society, Greater Noida";
+      $topText    = $saveOptions['topText']    ?? 'Scan to Record Your Contribution';
+      $bottomText = $saveOptions['bottomText'] ?? ($saveOptions['venue'] ?? '');

If i18n applies, wrap defaults with the project’s translation helper.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Texts.
$topText = "Scan to Record Your Contribution";
$bottomText = "Venue: Gaur Saundaryam Society, Greater Noida";
// Texts.
- $topText = "Scan to Record Your Contribution";
// Allow overriding via saveOptions; fallback to default strings (wrap in __() for i18n if needed).
$topText = $saveOptions['topText'] ?? 'Scan to Record Your Contribution';
$bottomText = $saveOptions['bottomText'] ?? ($saveOptions['venue'] ?? '');
🤖 Prompt for AI Agents
In wp-content/civi-extensions/goonjcustom/Civi/Traits/QrCodeable.php around
lines 59-62, the topText and bottomText are hard-coded; change them to pull
values from the relevant contribution/options data or extension settings (e.g.,
QR label/venue fields) and only fall back to defaults when those are empty. Also
wrap the fallback default strings with the project’s translation helper (use the
CiviCRM translation helper) so they are translatable. Validate and sanitize the
retrieved values before use and ensure tests/defaults cover both populated and
empty-settings cases.

Comment on lines +92 to +96
// Save final.
ob_start();
imagepng($canvas);
$finalImage = ob_get_clean();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Validate PNG buffer before saving.

If imagepng/ob_get_clean fails, we’ll try to save an empty file and still return TRUE later.

       ob_start();
-      imagepng($canvas);
-      $finalImage = ob_get_clean();
+      $ok = imagepng($canvas);
+      $finalImage = ob_get_clean();
+      if (!$ok || $finalImage === '' || $finalImage === false) {
+        throw new \RuntimeException('Failed to render composite PNG');
+      }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Save final.
ob_start();
imagepng($canvas);
$finalImage = ob_get_clean();
// Save final.
ob_start();
$ok = imagepng($canvas);
$finalImage = ob_get_clean();
if (!$ok || $finalImage === '' || $finalImage === false) {
throw new \RuntimeException('Failed to render composite PNG');
}
🤖 Prompt for AI Agents
In wp-content/civi-extensions/goonjcustom/Civi/Traits/QrCodeable.php around
lines 92-96, the PNG data produced by imagepng is taken from the output buffer
without validating it; if imagepng or ob_get_clean fails we may write an empty
file and still return true. Fix by capturing the buffer with
ob_start/ob_get_contents and ob_end_clean, check the return value of imagepng
(it returns true/false) and ensure the buffer is a non-empty string before
calling file_put_contents; if either imagepng failed or the buffer is empty, do
not write the file and return/throw an error (or return false) so the caller
knows the save failed.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (6)
wp-content/civi-extensions/goonjcustom/Civi/Traits/QrCodeable.php (6)

23-28: Use raw PNG output; drop data-URI stripping and base64 decode

Set outputBase64 => false and use the renderer’s raw bytes. Removes unnecessary transform and avoids accidental double-decode.

       $options = new QROptions([
-        'version'    => 5,
-        'outputType' => QRCode::OUTPUT_IMAGE_PNG,
-        'eccLevel'   => QRCode::ECC_L,
+        'version'    => 0, // let library pick best version for data
+        'outputType' => QRCode::OUTPUT_IMAGE_PNG,
+        'outputBase64' => false,
+        'eccLevel'   => QRCode::ECC_M,
         'scale'      => 10,
       ]);
@@
-      $qrcode = (new QRCode($options))->render($data);
-      $qrcode = str_replace('data:image/png;base64,', '', $qrcode);
-      $qrcode = base64_decode($qrcode);
+      $qrcode = (new QRCode($options))->render($data); // raw PNG bytes

Run to confirm the library supports outputBase64 in your version:

#!/bin/bash
# Show installed chillerlan/php-qrcode version and whether QROptions has outputBase64
fd composer.lock -t f -x jq -r '.packages[]?|select(.name=="chillerlan/php-qrcode")|.version' {} 2>/dev/null
rg -n --no-heading 'class\s+QROptions|outputBase64' vendor 2>/dev/null | sed -n '1,50p'

Also applies to: 37-39


66-69: Hard-coded user-facing text; make dynamic (and translatable if applicable)

Let callers pass text/venue; default sensibly.

-      $topText = "Scan to Record Your Contribution";
-      $bottomText = "Venue: $collectionCampAddress";
+      $topText    = $saveOptions['topText']    ?? \CRM_Core_OptionGroup::getLabel('goonj_qr', 'top_text') ?? 'Scan to Record Your Contribution';
+      $bottomText = $saveOptions['bottomText'] ?? ($saveOptions['venue'] ?? ($collectionCampAddress ? "Venue: $collectionCampAddress" : ''));

40-42: Guard GD decode; avoid TypeError on imagesx/imagesy when decode fails

imagecreatefromstring() can return false; check before measuring.

-      $qrImage = imagecreatefromstring($qrcode);
-      $qrWidth = imagesx($qrImage);
-      $qrHeight = imagesy($qrImage);
+      $qrImage = imagecreatefromstring($qrcode);
+      if (!$qrImage) {
+        throw new \RuntimeException('Failed to create QR image from binary');
+      }
+      $qrWidth  = imagesx($qrImage);
+      $qrHeight = imagesy($qrImage);

44-51: Don’t hardcode uploads year/month or fetch logo over HTTP; use filesystem with fallback

Prefer basedir path (fast, reliable), make it configurable, and handle failures explicitly.

-      // Load logo (from uploads folder URL).
-      $upload_dir = wp_upload_dir();
-      $logoUrl = $upload_dir['baseurl'] . '/2024/09/Goonj-logo-10June20-300x193-1.png';
-      $logoData = file_get_contents($logoUrl);
-      $logoImage = imagecreatefromstring($logoData);
+      // Load logo (prefer filesystem; allow overrides).
+      $upload_dir = \wp_upload_dir();
+      $logoPath = $saveOptions['logoPath'] ?? ($upload_dir['basedir'] . '/brand/Goonj-logo.png');
+      if (\file_exists($logoPath) && \is_readable($logoPath)) {
+        $logoImage = \imagecreatefrompng($logoPath);
+      } else {
+        $logoUrl = $saveOptions['logoUrl'] ?? null;
+        if (!$logoUrl) {
+          throw new \RuntimeException("Logo not found at $logoPath and no logoUrl provided");
+        }
+        $resp = \wp_remote_get($logoUrl, ['timeout' => 5]);
+        $logoData = \wp_remote_retrieve_body($resp);
+        if (!$logoData) {
+          throw new \RuntimeException("Failed to download logo from $logoUrl");
+        }
+        $logoImage = \imagecreatefromstring($logoData);
+      }
+      if (!$logoImage) {
+        throw new \RuntimeException('Failed to create logo image resource');
+      }

99-103: Validate the PNG buffer before saving

Avoid writing empty/invalid files if GD or buffering fails.

       // Save final.
       ob_start();
-      imagepng($canvas);
-      $finalImage = ob_get_clean();
+      $ok = \imagepng($canvas);
+      $finalImage = ob_get_clean();
+      if (!$ok || $finalImage === '' || $finalImage === false) {
+        throw new \RuntimeException('Failed to render composite PNG');
+      }

109-113: Return the actual attachment (or FALSE) instead of always TRUE

Propagate saveQrCode() result so callers can inspect failures and metadata.

-      self::saveQrCode($finalImage, $saveOptions);
+      $attachment = self::saveQrCode($finalImage, $saveOptions);
+      if ($attachment === FALSE) {
+        return FALSE;
+      }
+      return $attachment;
@@
-    return TRUE;
+    return TRUE; // unreachable after early return above; remove if returning $attachment

If existing callers expect boolean, keep boolean but at least return FALSE on failure paths.

Also applies to: 120-120

🧹 Nitpick comments (3)
wp-content/civi-extensions/goonjcustom/Civi/Traits/QrCodeable.php (3)

52-64: Bounds-check resized logo; prevent 0/oversized dimensions

Handle tiny/huge logos gracefully and keep margins around the QR.

-      // Resize logo (smaller).
-      $scaleFactor = 0.5;
-      $newLogoWidth = (int) ($qrWidth * $scaleFactor);
-      $newLogoHeight = (int) ($logoHeight * ($newLogoWidth / $logoWidth));
+      // Resize logo (keep within 25% of QR width, min 32px, leave 40px margin).
+      $maxLogoWidth = max(32, min((int)($qrWidth * 0.25), max(32, $qrWidth - 40)));
+      $newLogoWidth  = (int)$maxLogoWidth;
+      $newLogoHeight = (int) max(16, ($logoWidth > 0 ? ($logoHeight * ($newLogoWidth / $logoWidth)) : 0));

104-107: Ensure resources are destroyed in a finally block

If an exception fires before these lines, resources leak for this request. Wrap cleanup in finally.

Example:

try {
  // ...
} catch (\Exception $e) {
  // ...
} finally {
  foreach ([$qrImage ?? null, $logoImage ?? null, $resizedLogo ?? null, $canvas ?? null] as $im) {
    if (\is_resource($im) || (is_object($im) && $im instanceof \GdImage)) { \imagedestroy($im); }
  }
}

45-46: Prefix global functions to skip namespace fallback

Minor perf/readability win; apply consistently (\wp_upload_dir, \file_put_contents, etc.).

-      $upload_dir = wp_upload_dir();
+      $upload_dir = \wp_upload_dir();
@@
-    $numBytes = file_put_contents($tempFilePath, $qrcode);
+    $numBytes = \file_put_contents($tempFilePath, $qrcode);

Also applies to: 135-136

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between cc4d2b3 and 41f2ef4.

📒 Files selected for processing (1)
  • wp-content/civi-extensions/goonjcustom/Civi/Traits/QrCodeable.php (2 hunks)
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2024-10-11T09:53:14.458Z
Learnt from: belwalshubham
PR: ColoredCow/goonj#284
File: wp-content/civi-extensions/goonjcustom/Civi/CollectionCampService.php:520-520
Timestamp: 2024-10-11T09:53:14.458Z
Learning: In `wp-content/civi-extensions/goonjcustom/Civi/CollectionCampService.php`, it's acceptable that the `reGenerateCollectionCampQr` method calls `generateQrCode` with only the `$collectionCampId` parameter, as QR codes are generated when the Dropping Center is authorized. We'll implement changes if any issues arise in the future.

Applied to files:

  • wp-content/civi-extensions/goonjcustom/Civi/Traits/QrCodeable.php
📚 Learning: 2024-10-09T15:24:42.859Z
Learnt from: belwalshubham
PR: ColoredCow/goonj#352
File: wp-content/civi-extensions/goonjcustom/Civi/QrCodeService.php:51-53
Timestamp: 2024-10-09T15:24:42.859Z
Learning: In `QrCodeService::generateQrCode`, when using `QRCode::OUTPUT_IMAGE_PNG`, the `render` method returns a data URI with base64-encoded image data. Therefore, it's necessary to remove the `data:image/png;base64,` prefix and base64-decode the data to obtain the raw binary image.

Applied to files:

  • wp-content/civi-extensions/goonjcustom/Civi/Traits/QrCodeable.php

Comment on lines +30 to +36
$collectionCamp = EckEntity::get('Collection_Camp', FALSE)
->addSelect('Collection_Camp_Intent_Details.Location_Area_of_camp')
->addWhere('id', '=', $entityId)
->execute()->first();

// Remove the base64 header and decode the image data.
$collectionCampAddress = $collectionCamp['Collection_Camp_Intent_Details.Location_Area_of_camp'];
// Generate QR.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Null-check the camp lookup to avoid runtime fatal on missing entity

first() may return null; indexing into it will throw. Bail out early with a clear log.

-      $collectionCamp = EckEntity::get('Collection_Camp', FALSE)
-        ->addSelect('Collection_Camp_Intent_Details.Location_Area_of_camp')
-        ->addWhere('id', '=', $entityId)
-        ->execute()->first();
-
-      $collectionCampAddress = $collectionCamp['Collection_Camp_Intent_Details.Location_Area_of_camp'];
+      $collectionCamp = EckEntity::get('Collection_Camp', FALSE)
+        ->addSelect('Collection_Camp_Intent_Details.Location_Area_of_camp')
+        ->addWhere('id', '=', $entityId)
+        ->execute()
+        ->first();
+      if (!$collectionCamp) {
+        \CRM_Core_Error::debug_log_message("Collection Camp not found for id {$entityId}");
+        return FALSE;
+      }
+      $collectionCampAddress = $collectionCamp['Collection_Camp_Intent_Details.Location_Area_of_camp'] ?? '';
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
$collectionCamp = EckEntity::get('Collection_Camp', FALSE)
->addSelect('Collection_Camp_Intent_Details.Location_Area_of_camp')
->addWhere('id', '=', $entityId)
->execute()->first();
// Remove the base64 header and decode the image data.
$collectionCampAddress = $collectionCamp['Collection_Camp_Intent_Details.Location_Area_of_camp'];
// Generate QR.
$collectionCamp = EckEntity::get('Collection_Camp', FALSE)
->addSelect('Collection_Camp_Intent_Details.Location_Area_of_camp')
->addWhere('id', '=', $entityId)
->execute()
->first();
if (!$collectionCamp) {
\CRM_Core_Error::debug_log_message("Collection Camp not found for id {$entityId}");
return FALSE;
}
$collectionCampAddress = $collectionCamp['Collection_Camp_Intent_Details.Location_Area_of_camp'] ?? '';
// Generate QR.
🤖 Prompt for AI Agents
In wp-content/civi-extensions/goonjcustom/Civi/Traits/QrCodeable.php around
lines 30 to 36, the result of ->first() may be null and the subsequent array
access on $collectionCamp will cause a fatal; add a null check after ->first()
and if null, log a clear message (including $entityId) and return/throw early to
avoid proceeding, otherwise continue to access the address field.

Comment thread wp-content/civi-extensions/goonjcustom/Civi/Traits/QrCodeable.php
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (8)
wp-content/civi-extensions/goonjcustom/Civi/Traits/QrCodeable.php (8)

30-35: Fix potential null pointer exception when collection camp is not found

The first() method can return null if no collection camp is found for the given ID, which will cause a fatal error when trying to access array keys on line 35.

Based on previous review feedback, this issue was already identified. Add a null check after ->first() and return early with proper error logging if the collection camp is not found.

      $collectionCamp = EckEntity::get('Collection_Camp', FALSE)
        ->addSelect('Collection_Camp_Intent_Details.Location_Area_of_camp')
        ->addWhere('id', '=', $entityId)
-        ->execute()->first();
+        ->execute()
+        ->first();
+      if (!$collectionCamp) {
+        \CRM_Core_Error::debug_log_message("Collection Camp not found for id {$entityId}");
+        return FALSE;
+      }

-      $collectionCampAddress = $collectionCamp['Collection_Camp_Intent_Details.Location_Area_of_camp'];
+      $collectionCampAddress = $collectionCamp['Collection_Camp_Intent_Details.Location_Area_of_camp'] ?? '';

37-42: Remove data URI processing since outputBase64 is not set to false

The code is still stripping the data URI prefix and base64-decoding the QR code output, but this will corrupt the PNG data when outputBase64 is not explicitly set to false.

-      // Generate QR.
-      $qrcode = (new QRCode($options))->render($data);
-      $qrcode = str_replace('data:image/png;base64,', '', $qrcode);
-      $qrcode = base64_decode($qrcode);
+      // Generate QR as raw PNG binary.
+      $options = new QROptions([
+        'version'      => 5,
+        'outputType'   => QRCode::OUTPUT_IMAGE_PNG,
+        'outputBase64' => false,
+        'eccLevel'     => QRCode::ECC_L,
+        'scale'        => 10,
+      ]);
+      $qrcode = (new QRCode($options))->render($data);
       $qrImage = imagecreatefromstring($qrcode);

40-42: Add validation for image creation to prevent fatal errors

imagecreatefromstring() can return false, which will cause imagesx() and imagesy() to throw TypeError exceptions.

       $qrImage = imagecreatefromstring($qrcode);
+      if (!$qrImage) {
+        throw new \RuntimeException('Failed to create QR image from binary data');
+      }
       $qrWidth = imagesx($qrImage);
       $qrHeight = imagesy($qrImage);

44-50: Replace hardcoded logo path and HTTP fetch with filesystem approach

The hardcoded path with year/month will break in future months, and fetching over HTTP adds unnecessary latency and potential failure points.

-      // Load logo (from uploads folder URL).
-      $upload_dir = wp_upload_dir();
-      $logoUrl = $upload_dir['baseurl'] . '/2024/09/Goonj-logo-10June20-300x193-1.png';
-      $logoData = file_get_contents($logoUrl);
-      $logoImage = imagecreatefromstring($logoData);
+      // Load logo from filesystem
+      $upload_dir = \wp_upload_dir();
+      $logoPath = $saveOptions['logoPath'] ?? ($upload_dir['basedir'] . '/brand/Goonj-logo.png');
+      
+      if (!\file_exists($logoPath)) {
+        $logoUrl = $saveOptions['logoUrl'] ?? null;
+        if (!$logoUrl) {
+          throw new \RuntimeException("Logo not found at $logoPath and no logoUrl provided");
+        }
+        $logoResponse = \wp_remote_get($logoUrl, ['timeout' => 5]);
+        if (\is_wp_error($logoResponse)) {
+          throw new \RuntimeException("Failed to download logo from $logoUrl: " . $logoResponse->get_error_message());
+        }
+        $logoData = \wp_remote_retrieve_body($logoResponse);
+        $logoImage = imagecreatefromstring($logoData);
+      } else {
+        $logoImage = imagecreatefrompng($logoPath);
+      }
+      
+      if (!$logoImage) {
+        throw new \RuntimeException('Failed to create logo image resource');
+      }

67-69: Make text content configurable instead of hardcoded

The text content should be configurable through parameters rather than hardcoded, especially the venue information which may vary.

-      // Texts.
-      // force a line break.
-      $topText = "Scan to Record Your\nContribution";
-      $bottomText = "Venue: $collectionCampAddress";
+      // Texts - make configurable
+      $topText = $saveOptions['topText'] ?? "Scan to Record Your\nContribution";
+      $bottomText = $saveOptions['bottomText'] ?? "Venue: $collectionCampAddress";

111-112: Fix inconsistent text centering approach

The bottom text still uses the brittle strlen() * 9 approach while the top text uses proper TTF measurement, creating inconsistent text positioning.

-      $bottomY = $qrY + $qrHeight + 30;
-      $bottomX = (int) (($canvasWidth - (strlen($bottomText) * 9)) / 2);
-      imagestring($canvas, 5, $bottomX, $bottomY, $bottomText, $black);
+      $bottomY = $qrY + $qrHeight + 30;
+      $bbox = imagettfbbox($fontSize, 0, $fontPath, $bottomText);
+      $textWidth = abs($bbox[2] - $bbox[0]);
+      $bottomX = (int) (($canvasWidth - $textWidth) / 2);
+      imagettftext($canvas, $fontSize, 0, $bottomX, $bottomY, $black, $fontPath, $bottomText);

115-117: Add validation for PNG rendering to prevent saving empty files

The code doesn't validate whether imagepng() succeeded or if the output buffer contains valid data before attempting to save.

       ob_start();
-      imagepng($canvas);
-      $finalImage = ob_get_clean();
+      $ok = imagepng($canvas);
+      $finalImage = ob_get_clean();
+      if (!$ok || $finalImage === '' || $finalImage === false) {
+        throw new \RuntimeException('Failed to render composite PNG');
+      }

127-127: Propagate save result instead of ignoring return value

The code ignores the return value from saveQrCode() and always returns TRUE, masking potential save failures from the caller.

-      self::saveQrCode($finalImage, $saveOptions);
+      $attachment = self::saveQrCode($finalImage, $saveOptions);
+      if ($attachment === FALSE) {
+        return FALSE;
+      }

And update the final return to propagate the actual result:

-    return TRUE;
+    return $attachment;
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 41f2ef4 and 2c263a1.

📒 Files selected for processing (1)
  • wp-content/civi-extensions/goonjcustom/Civi/Traits/QrCodeable.php (2 hunks)
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2024-10-11T09:53:14.458Z
Learnt from: belwalshubham
PR: ColoredCow/goonj#284
File: wp-content/civi-extensions/goonjcustom/Civi/CollectionCampService.php:520-520
Timestamp: 2024-10-11T09:53:14.458Z
Learning: In `wp-content/civi-extensions/goonjcustom/Civi/CollectionCampService.php`, it's acceptable that the `reGenerateCollectionCampQr` method calls `generateQrCode` with only the `$collectionCampId` parameter, as QR codes are generated when the Dropping Center is authorized. We'll implement changes if any issues arise in the future.

Applied to files:

  • wp-content/civi-extensions/goonjcustom/Civi/Traits/QrCodeable.php
📚 Learning: 2024-10-09T15:24:42.859Z
Learnt from: belwalshubham
PR: ColoredCow/goonj#352
File: wp-content/civi-extensions/goonjcustom/Civi/QrCodeService.php:51-53
Timestamp: 2024-10-09T15:24:42.859Z
Learning: In `QrCodeService::generateQrCode`, when using `QRCode::OUTPUT_IMAGE_PNG`, the `render` method returns a data URI with base64-encoded image data. Therefore, it's necessary to remove the `data:image/png;base64,` prefix and base64-decode the data to obtain the raw binary image.

Applied to files:

  • wp-content/civi-extensions/goonjcustom/Civi/Traits/QrCodeable.php

Comment thread wp-content/civi-extensions/goonjcustom/Civi/Traits/QrCodeable.php Outdated
Comment on lines +86 to +87
$fontPath = dirname(__DIR__, 2) . '/fonts/DejaVuSans-Bold.ttf';
$fontSize = 28;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Verify font file exists before using TTF functions

The code assumes the TTF font file exists at the hardcoded path, which could cause fatal errors if the file is missing.

       // --- Step 2: Place heading text below logo using TTF font
       $fontPath = dirname(__DIR__, 2) . '/fonts/DejaVuSans-Bold.ttf';
+      if (!file_exists($fontPath)) {
+        throw new \RuntimeException("TTF font file not found at: $fontPath");
+      }
       $fontSize = 28;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
$fontPath = dirname(__DIR__, 2) . '/fonts/DejaVuSans-Bold.ttf';
$fontSize = 28;
// --- Step 2: Place heading text below logo using TTF font
$fontPath = dirname(__DIR__, 2) . '/fonts/DejaVuSans-Bold.ttf';
if (!file_exists($fontPath)) {
throw new \RuntimeException("TTF font file not found at: $fontPath");
}
$fontSize = 28;
🤖 Prompt for AI Agents
In wp-content/civi-extensions/goonjcustom/Civi/Traits/QrCodeable.php around
lines 86-87, the code blindly assumes the TTF at dirname(__DIR__, 2) .
'/fonts/DejaVuSans-Bold.ttf' exists; add a pre-check using
file_exists/is_readable for that path and if it’s missing fall back to a safe
alternative (use a bundled default font path, use a non-TTF renderer, or skip
TTF-specific functions) and log or throw a controlled exception so the app
doesn’t fatally error; ensure any code that calls TTF functions only runs when
the font check passes and adjust fontSize or behavior appropriately in the
fallback branch.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
wp-content/civi-extensions/goonjcustom/Civi/Traits/QrCodeable.php (1)

187-197: Propagate save result; update callers
In Civi/Traits/QrCodeable.php (lines 187–197), capture the return value of self::saveQrCode($finalImage, $saveOptions) and return the created attachment (or FALSE on failure) instead of unconditionally returning TRUE. Then update all generateQrCode() call sites (e.g. in InstitutionCollectionCampService.php, InstitutionGoonjActivitiesService.php, GoonjInitiatedEventsService.php, etc.) to assign the return value and handle failures appropriately.

♻️ Duplicate comments (5)
wp-content/civi-extensions/goonjcustom/Civi/Traits/QrCodeable.php (5)

38-45: Stop stripping data URI and base64-decoding; use raw PNG bytes from render()

       // Generate QR.
       $qrcode = (new QRCode($options))->render($data);
-      $qrcode = str_replace('data:image/png;base64,', '', $qrcode);
-      $qrcode = base64_decode($qrcode);
       $qrImage = imagecreatefromstring($qrcode);

31-37: Null-check the EckEntity fetch to avoid fatal on missing record

->first() may return null; array access on $collectionCamp[...] will error. Bail out cleanly.

-      $collectionCamp = EckEntity::get('Collection_Camp', FALSE)
+      $collectionCamp = EckEntity::get('Collection_Camp', FALSE)
         ->addSelect('Collection_Camp_Intent_Details.Location_Area_of_camp')
         ->addWhere('id', '=', $entityId)
-        ->execute()->first();
+        ->execute()->first();
 
-      $collectionCampAddress = $collectionCamp['Collection_Camp_Intent_Details.Location_Area_of_camp'];
+      if (!$collectionCamp) {
+        \CRM_Core_Error::debug_log_message("Collection Camp not found for id {$entityId}");
+        return FALSE;
+      }
+      $collectionCampAddress = $collectionCamp['Collection_Camp_Intent_Details.Location_Area_of_camp'] ?? '';

42-45: Guard GD resource creation to prevent TypeError fatals

imagecreatefromstring() can return false; validate before imagesx()/imagesy().

-      $qrImage = imagecreatefromstring($qrcode);
-      $qrWidth = imagesx($qrImage);
-      $qrHeight = imagesy($qrImage);
+      $qrImage = imagecreatefromstring($qrcode);
+      if (!$qrImage) {
+        throw new \RuntimeException('Failed to create QR image from binary');
+      }
+      $qrWidth  = imagesx($qrImage);
+      $qrHeight = imagesy($qrImage);

176-180: Validate PNG buffer before saving

Avoid writing empty/corrupt files if imagepng() or buffering fails.

       // Save final.
       ob_start();
-      imagepng($canvas);
-      $finalImage = ob_get_clean();
+      $ok = imagepng($canvas);
+      $finalImage = ob_get_clean();
+      if (!$ok || $finalImage === '' || $finalImage === false) {
+        throw new \RuntimeException('Failed to render composite PNG');
+      }

62-69: Don’t fetch the logo over HTTP; read from filesystem, add fallbacks and checks

Hardcoded uploads Y/M and HTTP fetch are brittle; prefer basedir and validate GD resource.

-      $upload_dir = wp_upload_dir();
-      $logoUrl = $upload_dir['baseurl'] . '/2024/09/Goonj-logo-10June20-300x193-1.png';
-      $logoData = file_get_contents($logoUrl);
-      $logoImage = imagecreatefromstring($logoData);
+      $upload_dir = \wp_upload_dir();
+      $logoPath = $saveOptions['logoPath'] ?? ($upload_dir['basedir'] . '/brand/Goonj-logo.png');
+      if (\file_exists($logoPath)) {
+        $logoImage = imagecreatefrompng($logoPath);
+      } else {
+        $logoUrl = $saveOptions['logoUrl'] ?? null;
+        if (!$logoUrl) {
+          throw new \RuntimeException("Logo not found at $logoPath and no logoUrl provided");
+        }
+        $resp = \wp_remote_get($logoUrl, ['timeout' => 5]);
+        $logoData = \wp_remote_retrieve_body($resp);
+        $logoImage = $logoData ? imagecreatefromstring($logoData) : false;
+      }
+      if (!$logoImage) {
+        throw new \RuntimeException('Failed to create logo image resource');
+      }
       $logoWidth = imagesx($logoImage);
       $logoHeight = imagesy($logoImage);
🧹 Nitpick comments (5)
wp-content/civi-extensions/goonjcustom/Civi/Traits/QrCodeable.php (5)

23-29: Explicitly disable base64 output; consider stronger ECC

Set outputBase64 => false to ensure render() returns raw PNG; consider ECC_Q for resilience.

       $options = new QROptions([
         'version'    => 5,
         'outputType' => QRCode::OUTPUT_IMAGE_PNG,
-        'eccLevel'   => QRCode::ECC_L,
+        'eccLevel'   => QRCode::ECC_Q,
+        'outputBase64' => false,
         // High for good quality.
         'scale'      => 12,
       ]);

70-83: Use resampled scaling for better logo quality

imagecopyresized is jaggy; use imagecopyresampled.

-      imagecopyresized(
+      imagecopyresampled(
             $resizedLogo, $logoImage,
             0, 0, 0, 0,
             $newLogoWidth, $newLogoHeight,
             $logoWidth, $logoHeight
         );

84-88: Make user-facing text dynamic (and translatable)

Avoid hardcoded copy; read from $saveOptions, defaulting to translatable strings.

-      $topText = "Scan to Record Your\nContribution";
-      $venueLabel = "Venue: ";
-      $venueValue = $collectionCampAddress;
+      $topText    = $saveOptions['topText']    ?? \ts("Scan to Record Your\nContribution");
+      $venueLabel = $saveOptions['venueLabel'] ?? \ts('Venue: ');
+      $venueValue = $saveOptions['venueValue'] ?? $collectionCampAddress;

89-96: Canvas sizing may clip long text; size using measured TTF widths

Compute text widths first and use max of logo, heading, QR, and venue wrap width. If reordering is heavy, at minimum use $newQrWidth instead of $qrWidth.

-      // Canvas: logo + top text + QR + bottom text.
-      $canvasWidth = $qrWidth + 100;
-      $canvasHeight = $newLogoHeight + $qrHeight + 220;
+      // Canvas: logo + top text + QR + bottom text.
+      $canvasWidth = $newQrWidth + 100;
+      $canvasHeight = $newLogoHeight + $newQrHeight + 220;

Optional (recommended): pre-measure $topText and the wrapped venue to derive $canvasWidth = max($newQrWidth + 100, $maxTextWidth + 40, $newLogoWidth + 40).


126-154: Remove unused $currentWidth; tighten word-wrapping

Static analysis flags $currentWidth as unused; also ensure first line wraps cleanly.

-      $lines = [];
-      $currentLine = $venueLabel;
-      $currentWidth = $labelWidth;
+      $lines = [];
+      $currentLine = $venueLabel;
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 2c263a1 and 02c94aa.

📒 Files selected for processing (1)
  • wp-content/civi-extensions/goonjcustom/Civi/Traits/QrCodeable.php (2 hunks)
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2024-10-11T09:53:14.458Z
Learnt from: belwalshubham
PR: ColoredCow/goonj#284
File: wp-content/civi-extensions/goonjcustom/Civi/CollectionCampService.php:520-520
Timestamp: 2024-10-11T09:53:14.458Z
Learning: In `wp-content/civi-extensions/goonjcustom/Civi/CollectionCampService.php`, it's acceptable that the `reGenerateCollectionCampQr` method calls `generateQrCode` with only the `$collectionCampId` parameter, as QR codes are generated when the Dropping Center is authorized. We'll implement changes if any issues arise in the future.

Applied to files:

  • wp-content/civi-extensions/goonjcustom/Civi/Traits/QrCodeable.php
📚 Learning: 2024-10-09T15:24:42.859Z
Learnt from: belwalshubham
PR: ColoredCow/goonj#352
File: wp-content/civi-extensions/goonjcustom/Civi/QrCodeService.php:51-53
Timestamp: 2024-10-09T15:24:42.859Z
Learning: In `QrCodeService::generateQrCode`, when using `QRCode::OUTPUT_IMAGE_PNG`, the `render` method returns a data URI with base64-encoded image data. Therefore, it's necessary to remove the `data:image/png;base64,` prefix and base64-decode the data to obtain the raw binary image.

Applied to files:

  • wp-content/civi-extensions/goonjcustom/Civi/Traits/QrCodeable.php
🪛 PHPMD (2.15.0)
wp-content/civi-extensions/goonjcustom/Civi/Traits/QrCodeable.php

137-137: Avoid unused local variables such as '$currentWidth'. (Unused Code Rules)

(UnusedLocalVariable)

Comment on lines +98 to +101
// Fonts.
$fontPath = dirname(__DIR__, 2) . '/fonts/DejaVuSans-Bold.ttf';
$fontPathRegular = dirname(__DIR__, 2) . '/fonts/DejaVuSans.ttf';

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Verify TTF files exist before using imagettf APIs*

Missing font files will trigger warnings and broken rendering.

       // Fonts.
-      $fontPath = dirname(__DIR__, 2) . '/fonts/DejaVuSans-Bold.ttf';
-      $fontPathRegular = dirname(__DIR__, 2) . '/fonts/DejaVuSans.ttf';
+      $fontPath = dirname(__DIR__, 2) . '/fonts/DejaVuSans-Bold.ttf';
+      $fontPathRegular = dirname(__DIR__, 2) . '/fonts/DejaVuSans.ttf';
+      if (!\file_exists($fontPath) || !\file_exists($fontPathRegular)) {
+        throw new \RuntimeException("TTF font file(s) not found");
+      }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Fonts.
$fontPath = dirname(__DIR__, 2) . '/fonts/DejaVuSans-Bold.ttf';
$fontPathRegular = dirname(__DIR__, 2) . '/fonts/DejaVuSans.ttf';
// Fonts.
$fontPath = dirname(__DIR__, 2) . '/fonts/DejaVuSans-Bold.ttf';
$fontPathRegular = dirname(__DIR__, 2) . '/fonts/DejaVuSans.ttf';
if (!\file_exists($fontPath) || !\file_exists($fontPathRegular)) {
throw new \RuntimeException("TTF font file(s) not found");
}

Comment on lines +121 to +125
// --- Step 3: QR code
$qrY = $topY + 50;
$qrX = (int) (($canvasWidth - $newQrWidth) / 2);
imagecopy($canvas, $resizedQr, $qrX, $qrY, 0, 0, $newQrWidth, $newQrHeight);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Avoid QR overlap with the heading; position QR after measured text block

Use measured text height instead of a fixed 50px gap.

-      $fontSize = 35;
+      $fontSize = 35;
       $topY = $logoY + $newLogoHeight + 50;
       $lines = explode("\n", $topText);
       $lineHeight = $fontSize + 8;
 
       foreach ($lines as $i => $line) {
         $bbox = imagettfbbox($fontSize, 0, $fontPath, $line);
         $textWidth = abs($bbox[2] - $bbox[0]);
         $x = (int) (($canvasWidth - $textWidth) / 2);
         $y = $topY + ($i * $lineHeight);
         imagettftext($canvas, $fontSize, 0, $x, $y, $black, $fontPath, $line);
       }
 
       // --- Step 3: QR code
-      $qrY = $topY + 50;
+      $textBlockHeight = (count($lines) ?: 1) * $lineHeight;
+      $qrY = $topY + $textBlockHeight + 20;
       $qrX = (int) (($canvasWidth - $newQrWidth) / 2);
       imagecopy($canvas, $resizedQr, $qrX, $qrY, 0, 0, $newQrWidth, $newQrHeight);

Also applies to: 107-119

🤖 Prompt for AI Agents
In wp-content/civi-extensions/goonjcustom/Civi/Traits/QrCodeable.php around
lines 121-125 (and similarly adjust lines 107-119), the QR is placed using a
fixed 50px vertical offset which can overlap the heading; replace the hardcoded
value with the measured height of the rendered text block plus a small padding.
Use imagettfbbox (or your existing text measurement logic) to compute the total
text block height from topY (or the heading start) and set $qrY = $topY +
$textBlockHeight + $padding; keep the $qrX centering logic unchanged; apply the
same change to the other occurrence (lines 107-119) so both placements use
measured height rather than 50px.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (10)
wp-content/civi-extensions/goonjcustom/Civi/Traits/QrCodeable.php (10)

23-29: Stop base64-stripping; render() already returns raw PNG when not base64.

This corrupts the image. Explicitly disable base64 and use the raw bytes as-is.

   $options = new QROptions([
     'version'    => 5,
     'outputType' => QRCode::OUTPUT_IMAGE_PNG,
     'eccLevel'   => QRCode::ECC_L,
-  // High for good quality.
+    // High for good quality.
     'scale'      => 12,
+    // Ensure raw PNG bytes (not data URI).
+    'outputBase64' => false,
   ]);

-  // Generate QR.
-  $qrcode = (new QRCode($options))->render($data);
-  $qrcode = str_replace('data:image/png;base64,', '', $qrcode);
-  $qrcode = base64_decode($qrcode);
+  // Generate QR as raw PNG binary.
+  $qrcode = (new QRCode($options))->render($data);

Also applies to: 38-44


84-88: Externalize user-facing strings (and make translatable).

Hardcoding heading and label is brittle; pull from options and default.

-  $topText = "Scan to Record Your\nContribution";
-  $venueLabel = "Venue - ";
-  $venueValue = $collectionCampAddress;
+  $topText     = $saveOptions['topText']     ?? "Scan to Record Your\nContribution";
+  $venueLabel  = $saveOptions['venueLabel']  ?? 'Venue - ';
+  $venueValue  = $saveOptions['venue']       ?? $collectionCampAddress;

89-97: Canvas sizing and vertical spacing should use measured text metrics (avoid clipping/overlap).

Current fixed paddings can overlap the QR and clip long text. Use measured heights/widths.

-  // Canvas: logo + top text + QR + bottom text.
-  $canvasWidth = $qrWidth + 100;
-  $canvasHeight = $newLogoHeight + $qrHeight + 220;
+  // Precompute heading block height.
+  $fontSize = 47;
+  $lines = explode("\n", $topText);
+  $lineHeight = $fontSize + 10;
+  $textBlockHeight = (count($lines) ?: 1) * $lineHeight;
+  // Canvas: accommodate logo + heading + QR + venue text area.
+  $canvasWidth  = max($qrWidth + 100, 800); // widen as needed
+  $canvasHeight = $newLogoHeight + $textBlockHeight + $newQrHeight + 240;

   // --- Step 2: Heading text
-  $fontSize = 47;
-  $topY = $logoY + $newLogoHeight + 50;
-  $lines = explode("\n", $topText);
-  $lineHeight = $fontSize + 10;
+  $topY = $logoY + $newLogoHeight + 40;

   // --- Step 3: QR code
-  $qrY = $topY + 60;
+  $qrY = $topY + $textBlockHeight + 20;

You can further tighten by computing max text width via imagettfbbox and growing $canvasWidth accordingly.

Also applies to: 107-120, 121-124


136-138: Fix first-line wrap measurement; drop unused variable.

The first line mixes bold label + regular value but width is measured with a single font, causing mis-wraps. Also $currentWidth is unused.

-  $currentWidth = $labelWidth;
+  // (removed) $currentWidth was unused

   $words = explode(" ", $venueValue);
   foreach ($words as $word) {
     $testLine = ($currentLine === $venueLabel ? $currentLine : $currentLine . " ") . $word;
-    $bboxTest = imagettfbbox($bottomFontSize, 0, $fontPathRegular, $testLine);
-    $testWidth = abs($bboxTest[2] - $bboxTest[0]);
+    if ($currentLine === $venueLabel) {
+      // Measure bold label + regular value width correctly.
+      $valuePart = trim(str_replace($venueLabel, '', $testLine));
+      $bboxVal = imagettfbbox($bottomFontSize, 0, $fontPathRegular, $valuePart);
+      $testWidth = $labelWidth + abs($bboxVal[2] - $bboxVal[0]);
+    } else {
+      $bboxTest = imagettfbbox($bottomFontSize, 0, $fontPathRegular, $testLine);
+      $testWidth = abs($bboxTest[2] - $bboxTest[0]);
+    }

Also applies to: 139-146


187-197: Propagate the save result instead of always returning TRUE.

Return the attachment (or FALSE) so callers can react to failures.

-  self::saveQrCode($finalImage, $saveOptions);
+  $attachment = self::saveQrCode($finalImage, $saveOptions);
+  if ($attachment === FALSE) {
+    return FALSE;
+  }
+  return $attachment;
-  return TRUE;

Also applies to: 190-190


31-37: Null-check the camp lookup to avoid fatal on missing entity.

first() can return null; dereferencing will crash. Also cap to 1 row.

-  $collectionCamp = EckEntity::get('Collection_Camp', FALSE)
+  $collectionCamp = EckEntity::get('Collection_Camp', FALSE)
     ->addSelect('Collection_Camp_Intent_Details.Location_Area_of_camp')
     ->addWhere('id', '=', $entityId)
-    ->execute()->first();
+    ->setLimit(1)
+    ->execute()
+    ->first();
+
-  $collectionCampAddress = $collectionCamp['Collection_Camp_Intent_Details.Location_Area_of_camp'];
+  if (!$collectionCamp) {
+    \CRM_Core_Error::debug_log_message("Collection Camp not found for id {$entityId}");
+    return FALSE;
+  }
+  $collectionCampAddress = $collectionCamp['Collection_Camp_Intent_Details.Location_Area_of_camp'] ?? '';

42-44: Guard against GD image creation failures.

imagecreatefromstring() may return false; imagesx/y would then throw.

-  $qrImage = imagecreatefromstring($qrcode);
-  $qrWidth = imagesx($qrImage);
-  $qrHeight = imagesy($qrImage);
+  $qrImage = imagecreatefromstring($qrcode);
+  if (!$qrImage) {
+    throw new \RuntimeException('Failed to create QR image from binary');
+  }
+  $qrWidth  = imagesx($qrImage);
+  $qrHeight = imagesy($qrImage);

62-69: Don’t hardcode logo path or fetch over HTTP; prefer filesystem with fallbacks.

Hardcoded YYYY/MM and network fetch are brittle. Use uploads basedir and allow overrides.

-  $upload_dir = wp_upload_dir();
-  $logoUrl = $upload_dir['baseurl'] . '/2024/09/Goonj-logo-10June20-300x193-1.png';
-  $logoData = file_get_contents($logoUrl);
-  $logoImage = imagecreatefromstring($logoData);
+  $upload_dir = \wp_upload_dir();
+  $logoPath = $saveOptions['logoPath'] ?? ($upload_dir['basedir'] . '/brand/Goonj-logo.png');
+  if (\file_exists($logoPath)) {
+    $logoImage = imagecreatefrompng($logoPath);
+  } else {
+    $logoUrl = $saveOptions['logoUrl'] ?? null;
+    if (!$logoUrl) {
+      throw new \RuntimeException("Logo not found at {$logoPath} and no logoUrl provided");
+    }
+    $response = \wp_remote_get($logoUrl, ['timeout' => 5]);
+    $logoData = \wp_remote_retrieve_body($response);
+    if (!$logoData) {
+      throw new \RuntimeException("Failed to download logo from $logoUrl");
+    }
+    $logoImage = imagecreatefromstring($logoData);
+  }
+  if (!$logoImage) {
+    throw new \RuntimeException('Failed to create logo image resource');
+  }

98-101: Verify TTF font files exist before using imagettf.*

Avoid warnings/bad renders if fonts are missing.

   $fontPath = dirname(__DIR__, 2) . '/fonts/AbhayaLibre-Bold.ttf';
   $fontPathRegular = dirname(__DIR__, 2) . '/fonts/AbhayaLibre-Regular.ttf';
+  if (!\file_exists($fontPath) || !\file_exists($fontPathRegular)) {
+    throw new \RuntimeException('TTF font file(s) not found');
+  }

176-180: Validate PNG buffer before saving.

Avoid saving empty/corrupt files.

   ob_start();
-  imagepng($canvas);
-  $finalImage = ob_get_clean();
+  $ok = imagepng($canvas);
+  $finalImage = ob_get_clean();
+  if (!$ok || $finalImage === '' || $finalImage === false) {
+    throw new \RuntimeException('Failed to render composite PNG');
+  }
🧹 Nitpick comments (3)
wp-content/civi-extensions/goonjcustom/Civi/Traits/QrCodeable.php (3)

77-82: Use imagecopyresampled for better logo quality.

imagecopyresized is low quality for downscales.

-  imagecopyresized(
+  imagecopyresampled(
       $resizedLogo, $logoImage,
       0, 0, 0, 0,
       $newLogoWidth, $newLogoHeight,
       $logoWidth, $logoHeight
   );

203-217: Use strict check for file_put_contents result.

0 bytes shouldn’t be treated the same as FALSE; prefer strict comparison.

-  $numBytes = file_put_contents($tempFilePath, $qrcode);
-
-  if (!$numBytes) {
+  $numBytes = file_put_contents($tempFilePath, $qrcode);
+  if ($numBytes === false) {
     \CRM_Core_Error::debug_log_message('Failed to write QR code to temporary file for entity ID ' . $entityId);
     return FALSE;
   }

63-64: Namespace global WordPress functions for clarity/perf.

Prefix with backslash to avoid namespace resolution overhead.

-  $upload_dir = wp_upload_dir();
+  $upload_dir = \wp_upload_dir();
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 02c94aa and 673006a.

⛔ Files ignored due to path filters (5)
  • wp-content/civi-extensions/goonjcustom/fonts/AbhayaLibre-Bold.ttf is excluded by !**/*.ttf
  • wp-content/civi-extensions/goonjcustom/fonts/AbhayaLibre-ExtraBold.ttf is excluded by !**/*.ttf
  • wp-content/civi-extensions/goonjcustom/fonts/AbhayaLibre-Medium.ttf is excluded by !**/*.ttf
  • wp-content/civi-extensions/goonjcustom/fonts/AbhayaLibre-Regular.ttf is excluded by !**/*.ttf
  • wp-content/civi-extensions/goonjcustom/fonts/AbhayaLibre-SemiBold.ttf is excluded by !**/*.ttf
📒 Files selected for processing (1)
  • wp-content/civi-extensions/goonjcustom/Civi/Traits/QrCodeable.php (2 hunks)
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2024-10-11T09:53:14.458Z
Learnt from: belwalshubham
PR: ColoredCow/goonj#284
File: wp-content/civi-extensions/goonjcustom/Civi/CollectionCampService.php:520-520
Timestamp: 2024-10-11T09:53:14.458Z
Learning: In `wp-content/civi-extensions/goonjcustom/Civi/CollectionCampService.php`, it's acceptable that the `reGenerateCollectionCampQr` method calls `generateQrCode` with only the `$collectionCampId` parameter, as QR codes are generated when the Dropping Center is authorized. We'll implement changes if any issues arise in the future.

Applied to files:

  • wp-content/civi-extensions/goonjcustom/Civi/Traits/QrCodeable.php
📚 Learning: 2024-10-09T15:24:42.859Z
Learnt from: belwalshubham
PR: ColoredCow/goonj#352
File: wp-content/civi-extensions/goonjcustom/Civi/QrCodeService.php:51-53
Timestamp: 2024-10-09T15:24:42.859Z
Learning: In `QrCodeService::generateQrCode`, when using `QRCode::OUTPUT_IMAGE_PNG`, the `render` method returns a data URI with base64-encoded image data. Therefore, it's necessary to remove the `data:image/png;base64,` prefix and base64-decode the data to obtain the raw binary image.

Applied to files:

  • wp-content/civi-extensions/goonjcustom/Civi/Traits/QrCodeable.php
🪛 PHPMD (2.15.0)
wp-content/civi-extensions/goonjcustom/Civi/Traits/QrCodeable.php

137-137: Avoid unused local variables such as '$currentWidth'. (Unused Code Rules)

(UnusedLocalVariable)

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
wp-content/civi-extensions/goonjcustom/Civi/Traits/QrCodeable.php (1)

212-219: Propagate save result; don’t always return TRUE
If saving the attachment fails, the function still returns TRUE today.

-      self::saveQrCode($finalImage, $saveOptions);
+      $saved = self::saveQrCode($finalImage, $saveOptions);
+      if ($saved === FALSE) {
+        return FALSE;
+      }
@@
-    return TRUE;
+    return TRUE; // Optionally: return $saved to expose the attachment
♻️ Duplicate comments (7)
wp-content/civi-extensions/goonjcustom/Civi/Traits/QrCodeable.php (7)

106-110: Make user-facing strings dynamic (and translatable if needed)
Avoid hardcoding; allow overrides via $saveOptions.

-      $topText = "Scan to Record Your\nContribution";
-      $venueLabel = "Venue - ";
-      $venueValue = $address;
+      $topText    = $saveOptions['topText']    ?? __('Scan to Record Your', 'goonj') . "\n" . __('Contribution', 'goonj');
+      $venueLabel = $saveOptions['venueLabel'] ?? __('Venue - ', 'goonj');
+      $venueValue = $saveOptions['venue']      ?? $address;

31-35: Null-check the camp lookup to avoid fatals on missing entity

->first() can return null; subsequent array access will throw.

-      $campData = EckEntity::get('Collection_Camp', FALSE)
+      $campData = EckEntity::get('Collection_Camp', FALSE)
         ->addSelect('subtype:name', 'Collection_Camp_Intent_Details.Location_Area_of_camp', 'Dropping_Centre.Where_do_you_wish_to_open_dropping_center_Address_', 'Goonj_Activities.Where_do_you_wish_to_organise_the_activity_', 'Institution_Collection_Camp_Intent.Collection_Camp_Address', 'Institution_Dropping_Center_Intent.Dropping_Center_Address', 'Institution_Goonj_Activities.Where_do_you_wish_to_organise_the_activity_')
         ->addWhere('id', '=', $entityId)
-        ->execute()->first();
+        ->execute()
+        ->first();
+      if (!$campData) {
+        \CRM_Core_Error::debug_log_message("Collection Camp not found for id {$entityId}");
+        return FALSE;
+      }

64-66: Guard against GD image creation failures (QR)
imagecreatefromstring() can return false; imagesx/y() will then fatal.

-      $qrImage = imagecreatefromstring($qrcode);
-      $qrWidth = imagesx($qrImage);
-      $qrHeight = imagesy($qrImage);
+      $qrImage = imagecreatefromstring($qrcode);
+      if (!$qrImage) {
+        throw new \RuntimeException('Failed to create QR image from binary');
+      }
+      $qrWidth  = imagesx($qrImage);
+      $qrHeight = imagesy($qrImage);

84-91: Avoid hardcoded HTTP logo path; prefer filesystem + robust error handling
Year/month in URL will rot; HTTP adds latency and failures. Use uploads basedir or configurable path; validate and fallback.

-      $upload_dir = wp_upload_dir();
-      $logoUrl = $upload_dir['baseurl'] . '/2024/09/Goonj-logo-10June20-300x193-1.png';
-      $logoData = file_get_contents($logoUrl);
-      $logoImage = imagecreatefromstring($logoData);
+      $upload_dir = \wp_upload_dir();
+      $logoPath = $saveOptions['logoPath'] ?? ($upload_dir['basedir'] . '/brand/Goonj-logo.png');
+      if (\file_exists($logoPath)) {
+        $logoImage = imagecreatefrompng($logoPath);
+      } else {
+        $logoUrl = $saveOptions['logoUrl'] ?? null;
+        if (!$logoUrl) {
+          throw new \RuntimeException("Logo not found at $logoPath and no logoUrl provided");
+        }
+        $resp = \wp_remote_get($logoUrl, ['timeout' => 5]);
+        $logoData = \wp_remote_retrieve_body($resp);
+        if (!$logoData) {
+          throw new \RuntimeException("Failed to download logo from $logoUrl");
+        }
+        $logoImage = imagecreatefromstring($logoData);
+      }
+      if (!$logoImage) {
+        throw new \RuntimeException('Failed to create logo image resource');
+      }
       $logoWidth = imagesx($logoImage);
       $logoHeight = imagesy($logoImage);

120-123: Verify TTF font files exist before calling imagettf APIs*
Prevents warnings/fatals in misconfigured environments.

       // Fonts.
-      $fontPath = dirname(__DIR__, 2) . '/fonts/AbhayaLibre-Bold.ttf';
-      $fontPathRegular = dirname(__DIR__, 2) . '/fonts/AbhayaLibre-Regular.ttf';
+      $fontPath = dirname(__DIR__, 2) . '/fonts/AbhayaLibre-Bold.ttf';
+      $fontPathRegular = dirname(__DIR__, 2) . '/fonts/AbhayaLibre-Regular.ttf';
+      if (!\file_exists($fontPath) || !\file_exists($fontPathRegular)) {
+        throw new \RuntimeException('TTF font file(s) not found');
+      }

111-119: Canvas sizing and spacing don’t account for measured TTF text; risk of clipping/overlap
Compute text widths first; size canvas accordingly and place QR after measured text height.

-      // Canvas: logo + top text + QR + bottom text.
-      $canvasWidth = $qrWidth + 100;
-      $canvasHeight = $newLogoHeight + $qrHeight + 220;
-      $canvas = imagecreatetruecolor($canvasWidth, $canvasHeight);
+      // Measure heading block first
+      $fontSize = 47;
+      $lines = explode("\n", $topText);
+      $lineHeight = $fontSize + 10;
+      $maxTopW = 0;
+      foreach ($lines as $line) {
+        $bbox = imagettfbbox($fontSize, 0, $fontPath, $line);
+        $maxTopW = max($maxTopW, abs($bbox[2] - $bbox[0]));
+      }
+      // Canvas: logo + heading + QR + venue
+      $canvasWidth  = max($newQrWidth + 100, $newLogoWidth + 40, $maxTopW + 40);
+      $canvasHeight = $newLogoHeight + (count($lines) * $lineHeight) + $newQrHeight + 240;
+      $canvas = imagecreatetruecolor($canvasWidth, $canvasHeight);
@@
-      $fontSize = 47;
-      $topY = $logoY + $newLogoHeight + 60;
-      $lines = explode("\n", $topText);
-      $lineHeight = $fontSize + 10;
+      $topY = $logoY + $newLogoHeight + 60;
@@
-      $qrY = $topY + 70;
+      $textBlockHeight = max(1, count($lines)) * $lineHeight;
+      $qrY = $topY + $textBlockHeight + 20;

Also applies to: 129-141, 143-147


198-203: Validate PNG buffer before saving
Avoid writing empty/corrupt files.

-      ob_start();
-      imagepng($canvas);
-      $finalImage = ob_get_clean();
+      ob_start();
+      $ok = imagepng($canvas);
+      $finalImage = ob_get_clean();
+      if (!$ok || $finalImage === '' || $finalImage === false) {
+        throw new \RuntimeException('Failed to render composite PNG');
+      }
🧹 Nitpick comments (6)
wp-content/civi-extensions/goonjcustom/Civi/Traits/QrCodeable.php (6)

38-58: Replace long if/elseif with a map; also guard missing address field

Improves readability and avoids undefined index when a field is absent.

-      if ($campStatus == 'Collection_Camp') {
-        $address = $campData['Collection_Camp_Intent_Details.Location_Area_of_camp'];
-      }
-      elseif ($campStatus == 'Dropping_Center') {
-        $address = $campData['Dropping_Centre.Where_do_you_wish_to_open_dropping_center_Address_'];
-      }
-      elseif ($campStatus == 'Goonj_Activities') {
-        $address = $campData['Goonj_Activities.Where_do_you_wish_to_organise_the_activity_'];
-      }
-      elseif ($campStatus == 'Institution_Collection_Camp') {
-        $address = $campData['Institution_Collection_Camp_Intent.Collection_Camp_Address'];
-      }
-      elseif ($campStatus == 'Institution_Dropping_Center') {
-        $address = $campData['Institution_Dropping_Center_Intent.Dropping_Center_Address'];
-      }
-      elseif ($campStatus == 'Institution_Goonj_Activities') {
-        $address = $campData['Institution_Goonj_Activities.Where_do_you_wish_to_organise_the_activity_'];
-      }
-      else {
-        throw new \Exception('Invalid entity type for QR code generation.');
-      }
+      $addrFieldBySubtype = [
+        'Collection_Camp'              => 'Collection_Camp_Intent_Details.Location_Area_of_camp',
+        'Dropping_Center'              => 'Dropping_Centre.Where_do_you_wish_to_open_dropping_center_Address_',
+        'Goonj_Activities'             => 'Goonj_Activities.Where_do_you_wish_to_organise_the_activity_',
+        'Institution_Collection_Camp'  => 'Institution_Collection_Camp_Intent.Collection_Camp_Address',
+        'Institution_Dropping_Center'  => 'Institution_Dropping_Center_Intent.Dropping_Center_Address',
+        'Institution_Goonj_Activities' => 'Institution_Goonj_Activities.Where_do_you_wish_to_organise_the_activity_',
+      ];
+      if (!isset($addrFieldBySubtype[$campStatus])) {
+        throw new \Exception('Invalid entity type for QR code generation.');
+      }
+      $address = $campData[$addrFieldBySubtype[$campStatus]] ?? '';
+      if ($address === '') {
+        \CRM_Core_Error::debug_log_message("Empty address for {$campStatus} id {$entityId}");
+      }

23-29: Make QR output type explicit; don’t rely on library defaults

Per team learnings (PR #352), render() returns a data URI only when base64 mode is on. Be explicit: either request raw PNG and drop decoding, or keep base64 but set the option.

Option A (preferred: raw PNG, simpler/faster):

       $options = new QROptions([
         'version'    => 5,
         'outputType' => QRCode::OUTPUT_IMAGE_PNG,
         'eccLevel'   => QRCode::ECC_L,
+        'outputBase64' => false,
         // High for good quality.
         'scale'      => 12,
       ]);
-      $qrcode = (new QRCode($options))->render($data);
-      $qrcode = str_replace('data:image/png;base64,', '', $qrcode);
-      $qrcode = base64_decode($qrcode);
+      $qrcode = (new QRCode($options))->render($data);

Option B (keep base64 pipeline, but be explicit):

       $options = new QROptions([
         'version'    => 5,
         'outputType' => QRCode::OUTPUT_IMAGE_PNG,
         'eccLevel'   => QRCode::ECC_L,
+        'outputBase64' => true,

Also applies to: 60-66


99-104: Use resampling for higher-quality logo resize
imagecopyresampled yields better quality than imagecopyresized.

-      imagecopyresized(
+      imagecopyresampled(
             $resizedLogo, $logoImage,
             0, 0, 0, 0,
             $newLogoWidth, $newLogoHeight,
             $logoWidth, $logoHeight
         );

268-276: Prefix global functions in namespaces
Minor perf/readability: call global civicrm_api3() explicitly.

-    $result = civicrm_api3('Attachment', 'create', $params);
+    $result = \civicrm_api3('Attachment', 'create', $params);

225-239: Clean up temp file on failure paths
Prevent orphaned files when write/attach fails.

-    $numBytes = file_put_contents($tempFilePath, $qrcode);
+    $numBytes = file_put_contents($tempFilePath, $qrcode);
     if (!$numBytes) {
       \CRM_Core_Error::debug_log_message('Failed to write QR code to temporary file for entity ID ' . $entityId);
       return FALSE;
     }
@@
-    if (!empty($result['is_error'])) {
+    if (!empty($result['is_error'])) {
       \CRM_Core_Error::debug_log_message('Failed to create attachment for entity ID ' . $entityId);
+      @\unlink($tempFilePath);
       return FALSE;
     }

Also applies to: 268-273


23-29: Optional: increase ECC level for robustness
With compositing and scaling, ECC_M tends to scan more reliably than ECC_L.

-        'eccLevel'   => QRCode::ECC_L,
+        'eccLevel'   => QRCode::ECC_M,
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 673006a and 869f08b.

📒 Files selected for processing (1)
  • wp-content/civi-extensions/goonjcustom/Civi/Traits/QrCodeable.php (2 hunks)
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2024-10-11T09:53:14.458Z
Learnt from: belwalshubham
PR: ColoredCow/goonj#284
File: wp-content/civi-extensions/goonjcustom/Civi/CollectionCampService.php:520-520
Timestamp: 2024-10-11T09:53:14.458Z
Learning: In `wp-content/civi-extensions/goonjcustom/Civi/CollectionCampService.php`, it's acceptable that the `reGenerateCollectionCampQr` method calls `generateQrCode` with only the `$collectionCampId` parameter, as QR codes are generated when the Dropping Center is authorized. We'll implement changes if any issues arise in the future.

Applied to files:

  • wp-content/civi-extensions/goonjcustom/Civi/Traits/QrCodeable.php
📚 Learning: 2024-10-09T15:24:42.859Z
Learnt from: belwalshubham
PR: ColoredCow/goonj#352
File: wp-content/civi-extensions/goonjcustom/Civi/QrCodeService.php:51-53
Timestamp: 2024-10-09T15:24:42.859Z
Learning: In `QrCodeService::generateQrCode`, when using `QRCode::OUTPUT_IMAGE_PNG`, the `render` method returns a data URI with base64-encoded image data. Therefore, it's necessary to remove the `data:image/png;base64,` prefix and base64-decode the data to obtain the raw binary image.

Applied to files:

  • wp-content/civi-extensions/goonjcustom/Civi/Traits/QrCodeable.php
🪛 PHPMD (2.15.0)
wp-content/civi-extensions/goonjcustom/Civi/Traits/QrCodeable.php

159-159: Avoid unused local variables such as '$currentWidth'. (Unused Code Rules)

(UnusedLocalVariable)

🔇 Additional comments (1)
wp-content/civi-extensions/goonjcustom/Civi/Traits/QrCodeable.php (1)

73-83: Good: alpha-safe resampling pipeline for QR
Alpha handling + resampling is correct and readable.

Comment on lines +148 to +196
// --- Step 4: Venue text with wrapping
$bottomFontSize = 25;
$maxWidth = $canvasWidth - 80;
$bottomY = $qrY + $newQrHeight + 40;

// Start with bold "Venue:".
$bboxLabel = imagettfbbox($bottomFontSize, 0, $fontPath, $venueLabel);
$labelWidth = abs($bboxLabel[2] - $bboxLabel[0]);

$lines = [];
$currentLine = $venueLabel;
$currentWidth = $labelWidth;

$words = explode(" ", $venueValue);
foreach ($words as $word) {
$testLine = ($currentLine === $venueLabel ? $currentLine : $currentLine . " ") . $word;
$bboxTest = imagettfbbox($bottomFontSize, 0, $fontPathRegular, $testLine);
$testWidth = abs($bboxTest[2] - $bboxTest[0]);

if ($testWidth > $maxWidth) {
$lines[] = $currentLine;
$currentLine = $word;
}
else {
$currentLine = $testLine;
}
}
$lines[] = $currentLine;

$lineHeight = $bottomFontSize + 8;
foreach ($lines as $i => $line) {
$bbox = imagettfbbox($bottomFontSize, 0, $fontPathRegular, $line);
$lineWidth = abs($bbox[2] - $bbox[0]);
$x = (int) (($canvasWidth - $lineWidth) / 2);
$y = $bottomY + ($i * $lineHeight);

if ($i === 0) {
imagettftext($canvas, $bottomFontSize, 0, $x, $y, $black, $fontPath, $venueLabel);
$valuePart = trim(str_replace($venueLabel, "", $line));
if ($valuePart !== "") {
$bboxLabel = imagettfbbox($bottomFontSize, 0, $fontPath, $venueLabel);
$offset = abs($bboxLabel[2] - $bboxLabel[0]);
imagettftext($canvas, $bottomFontSize, 0, $x + $offset, $y, $black, $fontPathRegular, $valuePart);
}
}
else {
imagettftext($canvas, $bottomFontSize, 0, $x, $y, $black, $fontPathRegular, $line);
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Fix venue wrapping and centering: measure bold label separately; remove unused var
Current wrap measures the first line with regular font while drawing label in bold, causing off-center placement. $currentWidth is unused (PHPMD).

-      $bottomFontSize = 25;
-      $maxWidth = $canvasWidth - 80;
-      $bottomY = $qrY + $newQrHeight + 40;
+      $bottomFontSize = 25;
+      $maxWidth = $canvasWidth - 80;
+      $bottomY = $qrY + $newQrHeight + 40;
@@
-      $bboxLabel = imagettfbbox($bottomFontSize, 0, $fontPath, $venueLabel);
-      $labelWidth = abs($bboxLabel[2] - $bboxLabel[0]);
-
-      $lines = [];
-      $currentLine = $venueLabel;
-      $currentWidth = $labelWidth;
-
-      $words = explode(" ", $venueValue);
-      foreach ($words as $word) {
-        $testLine = ($currentLine === $venueLabel ? $currentLine : $currentLine . " ") . $word;
-        $bboxTest = imagettfbbox($bottomFontSize, 0, $fontPathRegular, $testLine);
-        $testWidth = abs($bboxTest[2] - $bboxTest[0]);
-
-        if ($testWidth > $maxWidth) {
-          $lines[] = $currentLine;
-          $currentLine = $word;
-        }
-        else {
-          $currentLine = $testLine;
-        }
-      }
-      $lines[] = $currentLine;
+      $bboxLabel  = imagettfbbox($bottomFontSize, 0, $fontPath, $venueLabel);
+      $labelWidth = abs($bboxLabel[2] - $bboxLabel[0]);
+      // Wrap only the value; first line has reduced width due to label
+      $words = explode(' ', trim($venueValue));
+      $valueLines = [];
+      $current = '';
+      foreach ($words as $word) {
+        $test = $current === '' ? $word : $current . ' ' . $word;
+        $bbox = imagettfbbox($bottomFontSize, 0, $fontPathRegular, $test);
+        $testW = abs($bbox[2] - $bbox[0]);
+        $allowed = empty($valueLines) ? ($maxWidth - $labelWidth) : $maxWidth;
+        if ($testW > $allowed && $current !== '') {
+          $valueLines[] = $current;
+          $current = $word;
+        } else {
+          $current = $test;
+        }
+      }
+      if ($current !== '') {
+        $valueLines[] = $current;
+      }
@@
-      foreach ($lines as $i => $line) {
-        $bbox = imagettfbbox($bottomFontSize, 0, $fontPathRegular, $line);
-        $lineWidth = abs($bbox[2] - $bbox[0]);
-        $x = (int) (($canvasWidth - $lineWidth) / 2);
-        $y = $bottomY + ($i * $lineHeight);
-
-        if ($i === 0) {
-          imagettftext($canvas, $bottomFontSize, 0, $x, $y, $black, $fontPath, $venueLabel);
-          $valuePart = trim(str_replace($venueLabel, "", $line));
-          if ($valuePart !== "") {
-            $bboxLabel = imagettfbbox($bottomFontSize, 0, $fontPath, $venueLabel);
-            $offset = abs($bboxLabel[2] - $bboxLabel[0]);
-            imagettftext($canvas, $bottomFontSize, 0, $x + $offset, $y, $black, $fontPathRegular, $valuePart);
-          }
-        }
-        else {
-          imagettftext($canvas, $bottomFontSize, 0, $x, $y, $black, $fontPathRegular, $line);
-        }
-      }
+      foreach ($valueLines as $i => $valLine) {
+        $bboxVal = imagettfbbox($bottomFontSize, 0, $fontPathRegular, $valLine);
+        $valW    = abs($bboxVal[2] - $bboxVal[0]);
+        $lineW   = ($i === 0) ? ($labelWidth + $valW) : $valW;
+        $x       = (int)(($canvasWidth - $lineW) / 2);
+        $y       = $bottomY + ($i * $lineHeight);
+        if ($i === 0) {
+          imagettftext($canvas, $bottomFontSize, 0, $x, $y, $black, $fontPath, $venueLabel);
+          imagettftext($canvas, $bottomFontSize, 0, $x + $labelWidth, $y, $black, $fontPathRegular, $valLine);
+        } else {
+          imagettftext($canvas, $bottomFontSize, 0, $x, $y, $black, $fontPathRegular, $valLine);
+        }
+      }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// --- Step 4: Venue text with wrapping
$bottomFontSize = 25;
$maxWidth = $canvasWidth - 80;
$bottomY = $qrY + $newQrHeight + 40;
// Start with bold "Venue:".
$bboxLabel = imagettfbbox($bottomFontSize, 0, $fontPath, $venueLabel);
$labelWidth = abs($bboxLabel[2] - $bboxLabel[0]);
$lines = [];
$currentLine = $venueLabel;
$currentWidth = $labelWidth;
$words = explode(" ", $venueValue);
foreach ($words as $word) {
$testLine = ($currentLine === $venueLabel ? $currentLine : $currentLine . " ") . $word;
$bboxTest = imagettfbbox($bottomFontSize, 0, $fontPathRegular, $testLine);
$testWidth = abs($bboxTest[2] - $bboxTest[0]);
if ($testWidth > $maxWidth) {
$lines[] = $currentLine;
$currentLine = $word;
}
else {
$currentLine = $testLine;
}
}
$lines[] = $currentLine;
$lineHeight = $bottomFontSize + 8;
foreach ($lines as $i => $line) {
$bbox = imagettfbbox($bottomFontSize, 0, $fontPathRegular, $line);
$lineWidth = abs($bbox[2] - $bbox[0]);
$x = (int) (($canvasWidth - $lineWidth) / 2);
$y = $bottomY + ($i * $lineHeight);
if ($i === 0) {
imagettftext($canvas, $bottomFontSize, 0, $x, $y, $black, $fontPath, $venueLabel);
$valuePart = trim(str_replace($venueLabel, "", $line));
if ($valuePart !== "") {
$bboxLabel = imagettfbbox($bottomFontSize, 0, $fontPath, $venueLabel);
$offset = abs($bboxLabel[2] - $bboxLabel[0]);
imagettftext($canvas, $bottomFontSize, 0, $x + $offset, $y, $black, $fontPathRegular, $valuePart);
}
}
else {
imagettftext($canvas, $bottomFontSize, 0, $x, $y, $black, $fontPathRegular, $line);
}
}
// --- Step 4: Venue text with wrapping
$bottomFontSize = 25;
$maxWidth = $canvasWidth - 80;
$bottomY = $qrY + $newQrHeight + 40;
// Measure bold "Venue:" label
$bboxLabel = imagettfbbox($bottomFontSize, 0, $fontPath, $venueLabel);
$labelWidth = abs($bboxLabel[2] - $bboxLabel[0]);
// Wrap only the venue value; the first line gets less width because of the label
$words = explode(' ', trim($venueValue));
$valueLines = [];
$current = '';
foreach ($words as $word) {
$test = $current === '' ? $word : $current . ' ' . $word;
$bbox = imagettfbbox($bottomFontSize, 0, $fontPathRegular, $test);
$testW = abs($bbox[2] - $bbox[0]);
$allowed = empty($valueLines) ? ($maxWidth - $labelWidth) : $maxWidth;
if ($testW > $allowed && $current !== '') {
$valueLines[] = $current;
$current = $word;
} else {
$current = $test;
}
}
if ($current !== '') {
$valueLines[] = $current;
}
// Render each line, centering the first line by combining label+value width
$lineHeight = $bottomFontSize + 8;
foreach ($valueLines as $i => $valLine) {
$bboxVal = imagettfbbox($bottomFontSize, 0, $fontPathRegular, $valLine);
$valW = abs($bboxVal[2] - $bboxVal[0]);
$lineW = ($i === 0) ? ($labelWidth + $valW) : $valW;
$x = (int)(($canvasWidth - $lineW) / 2);
$y = $bottomY + ($i * $lineHeight);
if ($i === 0) {
imagettftext($canvas, $bottomFontSize, 0, $x, $y, $black, $fontPath, $venueLabel);
imagettftext($canvas, $bottomFontSize, 0, $x + $labelWidth, $y, $black, $fontPathRegular, $valLine);
} else {
imagettftext($canvas, $bottomFontSize, 0, $x, $y, $black, $fontPathRegular, $valLine);
}
}
🧰 Tools
🪛 PHPMD (2.15.0)

159-159: Avoid unused local variables such as '$currentWidth'. (Unused Code Rules)

(UnusedLocalVariable)

🤖 Prompt for AI Agents
In wp-content/civi-extensions/goonjcustom/Civi/Traits/QrCodeable.php around
lines 148 to 196, the venue wrapping currently measures the first line with the
regular font while the label "Venue:" is drawn in bold and the variable
$currentWidth is unused; to fix: stop including the bold label in width
calculations for wrapped lines — keep $currentLine initially empty or only the
venue value, measure the bold label separately with imagettfbbox using $fontPath
to get its width, remove the unused $currentWidth variable, and when centering
the first rendered line compute x by measuring the text portions with their
actual fonts (label measured with $fontPath and value measured with
$fontPathRegular) so the combined drawn text is centered correctly; ensure
during the word-wrapping loop you measure test widths using $fontPathRegular for
the value-only test strings and when outputting the first line draw the bold
label at x and the remaining value at x + labelWidth.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (11)
wp-content/civi-extensions/goonjcustom/Civi/Traits/QrCodeable.php (11)

23-28: Stop base64-stripping; render raw PNG instead.

Set outputBase64=false and remove the decode. Current decode path can corrupt data when render() already returns raw bytes.

       $options = new QROptions([
         'version'    => 5,
         'outputType' => QRCode::OUTPUT_IMAGE_PNG,
-        'eccLevel'   => QRCode::ECC_L,
+        'eccLevel'   => QRCode::ECC_L,
+        'outputBase64' => false,
         'scale'      => 12,
       ]);
@@
-      $qrcode = (new QRCode($options))->render($data);
-      $qrcode = str_replace('data:image/png;base64,', '', $qrcode);
-      $qrcode = base64_decode($qrcode);
+      $qrcode = (new QRCode($options))->render($data);

Also applies to: 66-70


112-116: Hard-coded, user-facing text; make configurable.

Source from $saveOptions (and i18n if applicable).

-      $topText = "Scan to Record Your\nContribution";
-      $venueLabel = "Venue - ";
-      $venueValue = $address;
+      $topText    = $saveOptions['topText']    ?? "Scan to Record Your\nContribution";
+      $venueLabel = $saveOptions['venueLabel'] ?? "Venue - ";
+      $venueValue = $saveOptions['venueValue'] ?? $address;

149-153: Avoid QR overlapping the heading; position using measured text height.

Replace fixed 70px gap with actual heading block height.

-      $qrY = $topY + 70;
+      $headingBlockHeight = (count($lines) ?: 1) * $lineHeight;
+      $qrY = $topY + $headingBlockHeight + 20;

37-43: Null-check EckEntity::first() result before indexing.

first() may return null; subsequent array access will fatal. Bail out early with a clear log.

       $campData = EckEntity::get('Collection_Camp', FALSE)
         ->addSelect('subtype:name', 'Collection_Camp_Intent_Details.Location_Area_of_camp', 'Dropping_Centre.Where_do_you_wish_to_open_dropping_center_Address_', 'Goonj_Activities.Where_do_you_wish_to_organise_the_activity_', 'Institution_Collection_Camp_Intent.Collection_Camp_Address', 'Institution_Dropping_Center_Intent.Dropping_Center_Address', 'Institution_Goonj_Activities.Where_do_you_wish_to_organise_the_activity_')
         ->addWhere('id', '=', $entityId)
-        ->execute()->first();
+        ->execute()->first();
+
+      if (!$campData) {
+        \CRM_Core_Error::debug_log_message("Collection Camp not found for id {$entityId}");
+        return FALSE;
+      }

70-73: Guard GD creation failures to prevent TypeErrors.

imagecreatefromstring() can return false; check before imagesx()/imagesy().

-      $qrImage = imagecreatefromstring($qrcode);
-      $qrWidth = imagesx($qrImage);
-      $qrHeight = imagesy($qrImage);
+      $qrImage = imagecreatefromstring($qrcode);
+      if (!$qrImage) {
+        throw new \RuntimeException('Failed to create QR image from binary');
+      }
+      $qrWidth  = imagesx($qrImage);
+      $qrHeight = imagesy($qrImage);

90-97: Don’t fetch logo over HTTP or hardcode dated paths; prefer filesystem with fallback.

Use uploads basedir + configurable path; add timeouts and error handling.

-      $upload_dir = wp_upload_dir();
-      $logoUrl = $upload_dir['baseurl'] . '/2024/09/Goonj-logo-10June20-300x193-1.png';
-      $logoData = file_get_contents($logoUrl);
-      $logoImage = imagecreatefromstring($logoData);
+      $upload_dir = \wp_upload_dir();
+      $logoPath = $saveOptions['logoPath'] ?? ($upload_dir['basedir'] . '/brand/Goonj-logo.png');
+      if (\file_exists($logoPath) && \is_readable($logoPath)) {
+        $logoImage = \imagecreatefrompng($logoPath);
+      } else {
+        $logoUrl = $saveOptions['logoUrl'] ?? '';
+        if (!$logoUrl) {
+          throw new \RuntimeException("Logo not found at $logoPath and no logoUrl provided");
+        }
+        $resp = \wp_remote_get($logoUrl, ['timeout' => 5]);
+        $logoData = \wp_remote_retrieve_body($resp);
+        $logoImage = $logoData ? \imagecreatefromstring($logoData) : false;
+      }
+      if (!$logoImage) {
+        throw new \RuntimeException('Failed to create logo image');
+      }

126-129: Verify TTF font files exist before imagettf calls.*

Avoid warnings and broken rendering when fonts are missing.

-      $fontPath = dirname(__DIR__, 2) . '/fonts/AbhayaLibre-Bold.ttf';
-      $fontPathRegular = dirname(__DIR__, 2) . '/fonts/AbhayaLibre-Regular.ttf';
+      $fontPath        = dirname(__DIR__, 2) . '/fonts/AbhayaLibre-Bold.ttf';
+      $fontPathRegular = dirname(__DIR__, 2) . '/fonts/AbhayaLibre-Regular.ttf';
+      if (!\file_exists($fontPath) || !\file_exists($fontPathRegular)) {
+        throw new \RuntimeException('TTF font file(s) not found');
+      }

117-121: Canvas sizing should be based on measured TTF text, not fixed widths.

Current width/height risk clipping long headings/venues. Measure first, then allocate canvas.

-      // Canvas: logo + top text + QR + bottom text.
-      $canvasWidth = $qrWidth + 100;
-      $canvasHeight = $newLogoHeight + $qrHeight + 220;
-      $canvas = imagecreatetruecolor($canvasWidth, $canvasHeight);
+      // Canvas: logo + heading + QR + venue; compute dynamic dimensions.
+      $headingFontSize = 47;
+      $headingLines = explode("\n", $topText);
+      $maxTextWidth = 0;
+      foreach ($headingLines as $hl) {
+        $bbox = \imagettfbbox($headingFontSize, 0, $fontPath, $hl);
+        $maxTextWidth = max($maxTextWidth, abs($bbox[2] - $bbox[0]));
+      }
+      $canvasWidth  = max($newLogoWidth + 40, $newQrWidth + 100, $maxTextWidth + 40);
+      $canvasHeight = $newLogoHeight + (count($headingLines) * ($headingFontSize + 10)) + $newQrHeight + 200;
+      $canvas = \imagecreatetruecolor($canvasWidth, $canvasHeight);

Additional change (outside this hunk): move the “Fonts.” block (Lines 126–129) above this section so $fontPath is available for measurement.


163-202: Fix venue wrapping/centering and remove unused $currentWidth (PHPMD).

Measure bold label separately; wrap only value; center combined first line.

-      $lines = [];
-      $currentLine = $venueLabel;
-      $currentWidth = $labelWidth;
-
-      $words = explode(" ", $venueValue);
-      foreach ($words as $word) {
-        $testLine = ($currentLine === $venueLabel ? $currentLine : $currentLine . " ") . $word;
-        $bboxTest = imagettfbbox($bottomFontSize, 0, $fontPathRegular, $testLine);
-        $testWidth = abs($bboxTest[2] - $bboxTest[0]);
-
-        if ($testWidth > $maxWidth) {
-          $lines[] = $currentLine;
-          $currentLine = $word;
-        }
-        else {
-          $currentLine = $testLine;
-        }
-      }
-      $lines[] = $currentLine;
+      $words = explode(' ', trim($venueValue));
+      $valueLines = [];
+      $current = '';
+      foreach ($words as $word) {
+        $test = $current === '' ? $word : $current . ' ' . $word;
+        $bbox = imagettfbbox($bottomFontSize, 0, $fontPathRegular, $test);
+        $testW = abs($bbox[2] - $bbox[0]);
+        $allowed = empty($valueLines) ? ($maxWidth - $labelWidth) : $maxWidth;
+        if ($testW > $allowed && $current !== '') {
+          $valueLines[] = $current;
+          $current = $word;
+        } else {
+          $current = $test;
+        }
+      }
+      if ($current !== '') {
+        $valueLines[] = $current;
+      }
@@
-      foreach ($lines as $i => $line) {
-        $bbox = imagettfbbox($bottomFontSize, 0, $fontPathRegular, $line);
-        $lineWidth = abs($bbox[2] - $bbox[0]);
-        $x = (int) (($canvasWidth - $lineWidth) / 2);
-        $y = $bottomY + ($i * $lineHeight);
-
-        if ($i === 0) {
-          imagettftext($canvas, $bottomFontSize, 0, $x, $y, $black, $fontPath, $venueLabel);
-          $valuePart = trim(str_replace($venueLabel, "", $line));
-          if ($valuePart !== "") {
-            $bboxLabel = imagettfbbox($bottomFontSize, 0, $fontPath, $venueLabel);
-            $offset = abs($bboxLabel[2] - $bboxLabel[0]);
-            imagettftext($canvas, $bottomFontSize, 0, $x + $offset, $y, $black, $fontPathRegular, $valuePart);
-          }
-        }
-        else {
-          imagettftext($canvas, $bottomFontSize, 0, $x, $y, $black, $fontPathRegular, $line);
-        }
-      }
+      foreach ($valueLines as $i => $valLine) {
+        $bboxVal = imagettfbbox($bottomFontSize, 0, $fontPathRegular, $valLine);
+        $valW    = abs($bboxVal[2] - $bboxVal[0]);
+        $lineW   = ($i === 0) ? ($labelWidth + $valW) : $valW;
+        $x       = (int)(($canvasWidth - $lineW) / 2);
+        $y       = $bottomY + ($i * $lineHeight);
+        if ($i === 0) {
+          imagettftext($canvas, $bottomFontSize, 0, $x, $y, $black, $fontPath, $venueLabel);
+          imagettftext($canvas, $bottomFontSize, 0, $x + $labelWidth, $y, $black, $fontPathRegular, $valLine);
+        } else {
+          imagettftext($canvas, $bottomFontSize, 0, $x, $y, $black, $fontPathRegular, $valLine);
+        }
+      }

204-208: Validate PNG buffer before saving.

If imagepng/ob_get_clean fails, you’ll write an empty file and still report success.

-      ob_start();
-      imagepng($canvas);
-      $finalImage = ob_get_clean();
+      ob_start();
+      $ok = \imagepng($canvas);
+      $finalImage = ob_get_clean();
+      if (!$ok || $finalImage === '' || $finalImage === false) {
+        throw new \RuntimeException('Failed to render composite PNG');
+      }

215-219: Propagate save result instead of always returning TRUE.

Return the attachment or FALSE; don’t swallow failures.

-      self::saveQrCode($finalImage, $saveOptions);
+      $attachment = self::saveQrCode($finalImage, $saveOptions);
+      if ($attachment === FALSE) {
+        return FALSE;
+      }
+      return $attachment;
@@
-    return TRUE;
+    // return TRUE; // handled above by returning $attachment

Also applies to: 225-225

🧹 Nitpick comments (3)
wp-content/civi-extensions/goonjcustom/Civi/Traits/QrCodeable.php (3)

105-110: Use resampling for better logo quality.

imagecopyresampled() yields smoother results than imagecopyresized().

-      imagecopyresized(
+      imagecopyresampled(
             $resizedLogo, $logoImage,
             0, 0, 0, 0,
             $newLogoWidth, $newLogoHeight,
             $logoWidth, $logoHeight
         );

23-28: Optional: increase ECC level for better scan resilience.

ECC_L is minimal; consider ECC_M for real-world usage.

-        'eccLevel'   => QRCode::ECC_L,
+        'eccLevel'   => QRCode::ECC_M,

204-214: Optional: ensure GD resources are freed on exceptions.

Move imagedestroy(...) calls to a finally block to avoid leaks on early errors.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 869f08b and b426060.

⛔ Files ignored due to path filters (2)
  • wp-content/civi-extensions/goonjcustom/fonts/Fontspring-DEMO-proximanova-bold.otf is excluded by !**/*.otf
  • wp-content/civi-extensions/goonjcustom/fonts/Fontspring-DEMO-proximanova-regular.otf is excluded by !**/*.otf
📒 Files selected for processing (1)
  • wp-content/civi-extensions/goonjcustom/Civi/Traits/QrCodeable.php (2 hunks)
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2024-10-11T09:53:14.458Z
Learnt from: belwalshubham
PR: ColoredCow/goonj#284
File: wp-content/civi-extensions/goonjcustom/Civi/CollectionCampService.php:520-520
Timestamp: 2024-10-11T09:53:14.458Z
Learning: In `wp-content/civi-extensions/goonjcustom/Civi/CollectionCampService.php`, it's acceptable that the `reGenerateCollectionCampQr` method calls `generateQrCode` with only the `$collectionCampId` parameter, as QR codes are generated when the Dropping Center is authorized. We'll implement changes if any issues arise in the future.

Applied to files:

  • wp-content/civi-extensions/goonjcustom/Civi/Traits/QrCodeable.php
📚 Learning: 2024-10-09T15:24:42.859Z
Learnt from: belwalshubham
PR: ColoredCow/goonj#352
File: wp-content/civi-extensions/goonjcustom/Civi/QrCodeService.php:51-53
Timestamp: 2024-10-09T15:24:42.859Z
Learning: In `QrCodeService::generateQrCode`, when using `QRCode::OUTPUT_IMAGE_PNG`, the `render` method returns a data URI with base64-encoded image data. Therefore, it's necessary to remove the `data:image/png;base64,` prefix and base64-decode the data to obtain the raw binary image.

Applied to files:

  • wp-content/civi-extensions/goonjcustom/Civi/Traits/QrCodeable.php
🪛 PHPMD (2.15.0)
wp-content/civi-extensions/goonjcustom/Civi/Traits/QrCodeable.php

165-165: Avoid unused local variables such as '$currentWidth'. (Unused Code Rules)

(UnusedLocalVariable)

Comment thread wp-content/civi-extensions/goonjcustom/Civi/Traits/QrCodeable.php
Comment thread wp-content/civi-extensions/goonjcustom/Civi/Traits/QrCodeable.php Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (9)
wp-content/civi-extensions/goonjcustom/Civi/Traits/QrCodeable.php (9)

127-131: Canvas sizing ignores TTF text dimensions; may clip long text

Measure text first (heading and venue) and size canvas accordingly. Also verify fonts exist.

Apply:

-      // Canvas: logo + top text + QR + bottom text.
-      $canvasWidth = $qrWidth + 100;
-      $canvasHeight = $newLogoHeight + $qrHeight + 220;
-      $canvas = imagecreatetruecolor($canvasWidth, $canvasHeight);
+      // Fonts.
+      $fontPath = dirname(__DIR__, 2) . '/fonts/AbhayaLibre-Bold.ttf';
+      $fontPathRegular = dirname(__DIR__, 2) . '/fonts/AbhayaLibre-Regular.ttf';
+      if (!\file_exists($fontPath) || !\file_exists($fontPathRegular)) {
+        throw new \RuntimeException('TTF font file(s) not found');
+      }
+
+      // Measure text to derive canvas size.
+      $headingSize = 47;
+      $headingLines = explode("\n", $topText);
+      $maxHeadingW = 0;
+      foreach ($headingLines as $line) {
+        $bbox = imagettfbbox($headingSize, 0, $fontPath, $line);
+        $maxHeadingW = max($maxHeadingW, abs($bbox[2] - $bbox[0]));
+      }
+      $venueSize = 25;
+      $bboxLabel = imagettfbbox($venueSize, 0, $fontPath, $venueLabel);
+      $labelW = abs($bboxLabel[2] - $bboxLabel[0]);
+      $bboxVal = imagettfbbox($venueSize, 0, $fontPathRegular, (string)$address);
+      $valW = abs($bboxVal[2] - $bboxVal[0]);
+      $maxTextW = max($maxHeadingW, $labelW + $valW);
+
+      // Canvas: accommodate widest element.
+      $canvasWidth = max($newQrWidth + 100, $newLogoWidth + 40, $maxTextW + 80);
+      $canvasHeight = $newLogoHeight + (count($headingLines) * ($headingSize + 10)) + $newQrHeight + ($venueSize + 8) * 2 + 200;
+      $canvas = imagecreatetruecolor($canvasWidth, $canvasHeight);

Also applies to: 136-139


145-157: Place QR after measured heading to avoid overlap

Use measured heading height instead of fixed 70px gap.

Apply:

-      $fontSize = 47;
-      $topY = $logoY + $newLogoHeight + 60;
-      $lines = explode("\n", $topText);
-      $lineHeight = $fontSize + 10;
+      $fontSize = 47;
+      $topY = $logoY + $newLogoHeight + 60;
+      $lines = explode("\n", $topText);
+      $lineHeight = $fontSize + 10;
@@
-      // --- Step 3: QR code
-      $qrY = $topY + 70;
+      // --- Step 3: QR code
+      $textBlockHeight = (count($lines) ?: 1) * $lineHeight;
+      $qrY = $topY + $textBlockHeight + 20;
       $qrX = (int) (($canvasWidth - $newQrWidth) / 2);

Also applies to: 159-163


169-212: Fix venue wrapping/centering; remove unused var

Measure bold label separately; wrap only the value; drop $currentWidth.

Apply:

-      $lines = [];
-      $currentLine = $venueLabel;
-      $currentWidth = $labelWidth;
-
-      $words = explode(" ", $venueValue);
-      foreach ($words as $word) {
-        $testLine = ($currentLine === $venueLabel ? $currentLine : $currentLine . " ") . $word;
-        $bboxTest = imagettfbbox($bottomFontSize, 0, $fontPathRegular, $testLine);
-        $testWidth = abs($bboxTest[2] - $bboxTest[0]);
-
-        if ($testWidth > $maxWidth) {
-          $lines[] = $currentLine;
-          $currentLine = $word;
-        }
-        else {
-          $currentLine = $testLine;
-        }
-      }
-      $lines[] = $currentLine;
+      $words = explode(' ', trim((string)$venueValue));
+      $valueLines = [];
+      $current = '';
+      foreach ($words as $word) {
+        $test = $current === '' ? $word : $current . ' ' . $word;
+        $bboxTest = imagettfbbox($bottomFontSize, 0, $fontPathRegular, $test);
+        $testWidth = abs($bboxTest[2] - $bboxTest[0]);
+        $allowed = empty($valueLines) ? ($maxWidth - $labelWidth) : $maxWidth;
+        if ($testWidth > $allowed && $current !== '') {
+          $valueLines[] = $current;
+          $current = $word;
+        } else {
+          $current = $test;
+        }
+      }
+      if ($current !== '') {
+        $valueLines[] = $current;
+      }
@@
-      foreach ($lines as $i => $line) {
-        $bbox = imagettfbbox($bottomFontSize, 0, $fontPathRegular, $line);
-        $lineWidth = abs($bbox[2] - $bbox[0]);
-        $x = (int) (($canvasWidth - $lineWidth) / 2);
-        $y = $bottomY + ($i * $lineHeight);
-
-        if ($i === 0) {
-          imagettftext($canvas, $bottomFontSize, 0, $x, $y, $black, $fontPath, $venueLabel);
-          $valuePart = trim(str_replace($venueLabel, "", $line));
-          if ($valuePart !== "") {
-            $bboxLabel = imagettfbbox($bottomFontSize, 0, $fontPath, $venueLabel);
-            $offset = abs($bboxLabel[2] - $bboxLabel[0]);
-            imagettftext($canvas, $bottomFontSize, 0, $x + $offset, $y, $black, $fontPathRegular, $valuePart);
-          }
-        }
-        else {
-          imagettftext($canvas, $bottomFontSize, 0, $x, $y, $black, $fontPathRegular, $line);
-        }
-      }
+      foreach ($valueLines as $i => $valLine) {
+        $bboxVal = imagettfbbox($bottomFontSize, 0, $fontPathRegular, $valLine);
+        $valW    = abs($bboxVal[2] - $bboxVal[0]);
+        $lineW   = ($i === 0) ? ($labelWidth + $valW) : $valW;
+        $x       = (int)(($canvasWidth - $lineW) / 2);
+        $y       = $bottomY + ($i * $lineHeight);
+        if ($i === 0) {
+          imagettftext($canvas, $bottomFontSize, 0, $x, $y, $black, $fontPath, $venueLabel);
+          imagettftext($canvas, $bottomFontSize, 0, $x + $labelWidth, $y, $black, $fontPathRegular, $valLine);
+        } else {
+          imagettftext($canvas, $bottomFontSize, 0, $x, $y, $black, $fontPathRegular, $valLine);
+        }
+      }

122-126: Make user-facing text configurable

Hardcoded heading/label/venue reduces reuse; read from options with sensible defaults.

Apply:

-      $topText = "Scan to Record Your\nContribution";
-      $venueLabel = "Venue - ";
-      $venueValue = $address;
+      $topText    = $saveOptions['topText']    ?? "Scan to Record Your\nContribution";
+      $venueLabel = $saveOptions['venueLabel'] ?? "Venue - ";
+      $venueValue = $saveOptions['venue']      ?? ($address ?? '');

225-235: Propagate save result; don’t always return TRUE

Return FALSE on save failure; optionally return attachment.

Apply:

-      self::saveQrCode($finalImage, $saveOptions);
+      $attachment = self::saveQrCode($finalImage, $saveOptions);
+      if ($attachment === FALSE) {
+        return FALSE;
+      }
@@
-    return TRUE;
+    return TRUE; // or `return $attachment;` if callers expect the attachment

Also applies to: 228-228


45-74: Null-check camp data and guard missing keys

$campData may be null; individual keys may be absent. Prevent undefined index/fatals.

Apply:

-        $campData = EckEntity::get('Collection_Camp', FALSE)
+        $campData = EckEntity::get('Collection_Camp', FALSE)
           ->addSelect('subtype:name', 'Collection_Camp_Intent_Details.Location_Area_of_camp', 'Dropping_Centre.Where_do_you_wish_to_open_dropping_center_Address_', 'Goonj_Activities.Where_do_you_wish_to_organise_the_activity_', 'Institution_Collection_Camp_Intent.Collection_Camp_Address', 'Institution_Dropping_Center_Intent.Dropping_Center_Address', 'Institution_Goonj_Activities.Where_do_you_wish_to_organise_the_activity_')
           ->addWhere('id', '=', $entityId)
           ->execute()->first();
-
-        $campStatus = $campData['subtype:name'];
+        if (!$campData) {
+          \CRM_Core_Error::debug_log_message("Collection Camp not found for id {$entityId}");
+          return FALSE;
+        }
+        $campStatus = $campData['subtype:name'] ?? null;
+        if (!$campStatus) {
+          \CRM_Core_Error::debug_log_message("Missing subtype for camp id {$entityId}");
+          return FALSE;
+        }
@@
-        if ($campStatus == 'Collection_Camp') {
-          $address = $campData['Collection_Camp_Intent_Details.Location_Area_of_camp'];
+        if ($campStatus == 'Collection_Camp') {
+          $address = $campData['Collection_Camp_Intent_Details.Location_Area_of_camp'] ?? '';
         }
         elseif ($campStatus == 'Dropping_Center') {
-          $address = $campData['Dropping_Centre.Where_do_you_wish_to_open_dropping_center_Address_'];
+          $address = $campData['Dropping_Centre.Where_do_you_wish_to_open_dropping_center_Address_'] ?? '';
         }
         elseif ($campStatus == 'Goonj_Activities') {
-          $address = $campData['Goonj_Activities.Where_do_you_wish_to_organise_the_activity_'];
+          $address = $campData['Goonj_Activities.Where_do_you_wish_to_organise_the_activity_'] ?? '';
         }
         elseif ($campStatus == 'Institution_Collection_Camp') {
-          $address = $campData['Institution_Collection_Camp_Intent.Collection_Camp_Address'];
+          $address = $campData['Institution_Collection_Camp_Intent.Collection_Camp_Address'] ?? '';
         }
         elseif ($campStatus == 'Institution_Dropping_Center') {
-          $address = $campData['Institution_Dropping_Center_Intent.Dropping_Center_Address'];
+          $address = $campData['Institution_Dropping_Center_Intent.Dropping_Center_Address'] ?? '';
         }
         elseif ($campStatus == 'Institution_Goonj_Activities') {
-          $address = $campData['Institution_Goonj_Activities.Where_do_you_wish_to_organise_the_activity_'];
+          $address = $campData['Institution_Goonj_Activities.Where_do_you_wish_to_organise_the_activity_'] ?? '';
         }

80-83: Check QR image creation result

imagecreatefromstring() can return false; guard before imagesx/y.

Apply:

-      $qrImage = imagecreatefromstring($qrcode);
-      $qrWidth = imagesx($qrImage);
-      $qrHeight = imagesy($qrImage);
+      $qrImage = imagecreatefromstring($qrcode);
+      if (!$qrImage) {
+        throw new \RuntimeException('Failed to create QR image from binary');
+      }
+      $qrWidth  = imagesx($qrImage);
+      $qrHeight = imagesy($qrImage);

100-121: Don’t hardcode HTTP logo path; use filesystem and validate

Current code fetches over HTTP with a dated path and no error checks; brittle and slower.

Apply:

-      $upload_dir = wp_upload_dir();
-      $logoUrl = $upload_dir['baseurl'] . '/2024/09/Goonj-logo-10June20-300x193-1.png';
-      $logoData = file_get_contents($logoUrl);
-      $logoImage = imagecreatefromstring($logoData);
+      $upload_dir = \wp_upload_dir();
+      $logoPath = $saveOptions['logoPath'] ?? ($upload_dir['basedir'] . '/brand/Goonj-logo.png');
+      if (\file_exists($logoPath)) {
+        $logoImage = imagecreatefrompng($logoPath);
+      } else {
+        $logoUrl = $saveOptions['logoUrl'] ?? null;
+        if (!$logoUrl) {
+          throw new \RuntimeException("Logo not found at $logoPath and no logoUrl provided");
+        }
+        $resp = \wp_remote_get($logoUrl, ['timeout' => 5]);
+        $logoData = \wp_remote_retrieve_body($resp);
+        if (!$logoData) {
+          throw new \RuntimeException("Failed to download logo from $logoUrl");
+        }
+        $logoImage = imagecreatefromstring($logoData);
+      }
+      if (!$logoImage) {
+        throw new \RuntimeException('Failed to create logo image resource');
+      }

214-218: Validate PNG buffer before saving

Avoid writing empty buffer on failure.

Apply:

       // Save final.
       ob_start();
-      imagepng($canvas);
-      $finalImage = ob_get_clean();
+      $ok = imagepng($canvas);
+      $finalImage = ob_get_clean();
+      if (!$ok || $finalImage === '' || $finalImage === false) {
+        throw new \RuntimeException('Failed to render composite PNG');
+      }
🧹 Nitpick comments (2)
wp-content/civi-extensions/goonjcustom/Civi/Traits/QrCodeable.php (2)

25-30: Avoid base64 round-trip; render raw PNG

Set outputBase64=false and remove stripping/decoding to cut CPU/memory.

Apply:

       $options = new QROptions([
         'version'    => 5,
         'outputType' => QRCode::OUTPUT_IMAGE_PNG,
         'eccLevel'   => QRCode::ECC_L,
-        'scale'      => 12,
+        'scale'      => 12,
+        'outputBase64' => false,
       ]);
@@
-      $qrcode = str_replace('data:image/png;base64,', '', $qrcode);
-      $qrcode = base64_decode($qrcode);
+      // $qrcode is raw PNG bytes when outputBase64=false

Also applies to: 78-79


115-120: Use resampling for logo quality

imagecopyresized produces jaggies; prefer imagecopyresampled.

Apply:

-      imagecopyresized(
+      imagecopyresampled(
             $resizedLogo, $logoImage,
             0, 0, 0, 0,
             $newLogoWidth, $newLogoHeight,
             $logoWidth, $logoHeight
         );
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between b426060 and 6aaa7d4.

📒 Files selected for processing (1)
  • wp-content/civi-extensions/goonjcustom/Civi/Traits/QrCodeable.php (2 hunks)
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2024-10-11T09:53:14.458Z
Learnt from: belwalshubham
PR: ColoredCow/goonj#284
File: wp-content/civi-extensions/goonjcustom/Civi/CollectionCampService.php:520-520
Timestamp: 2024-10-11T09:53:14.458Z
Learning: In `wp-content/civi-extensions/goonjcustom/Civi/CollectionCampService.php`, it's acceptable that the `reGenerateCollectionCampQr` method calls `generateQrCode` with only the `$collectionCampId` parameter, as QR codes are generated when the Dropping Center is authorized. We'll implement changes if any issues arise in the future.

Applied to files:

  • wp-content/civi-extensions/goonjcustom/Civi/Traits/QrCodeable.php
📚 Learning: 2024-10-09T15:24:42.859Z
Learnt from: belwalshubham
PR: ColoredCow/goonj#352
File: wp-content/civi-extensions/goonjcustom/Civi/QrCodeService.php:51-53
Timestamp: 2024-10-09T15:24:42.859Z
Learning: In `QrCodeService::generateQrCode`, when using `QRCode::OUTPUT_IMAGE_PNG`, the `render` method returns a data URI with base64-encoded image data. Therefore, it's necessary to remove the `data:image/png;base64,` prefix and base64-decode the data to obtain the raw binary image.

Applied to files:

  • wp-content/civi-extensions/goonjcustom/Civi/Traits/QrCodeable.php
🪛 PHPMD (2.15.0)
wp-content/civi-extensions/goonjcustom/Civi/Traits/QrCodeable.php

175-175: Avoid unused local variables such as '$currentWidth'. (Unused Code Rules)

(UnusedLocalVariable)

Comment on lines +32 to +43
if (!empty($saveOptions['customGroupName']) && $saveOptions['customGroupName'] === 'Event_QR') {
$event = Event::get(TRUE)
->addSelect('loc_block_id.address_id')
->addWhere('id', '=', $entityId)
->execute()->first();

$addresses = Address::get(FALSE)
->addWhere('id', '=', $event['loc_block_id.address_id'])
->setLimit(1)
->execute()->first();
$address = \CRM_Utils_Address::format($addresses);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Guard Event/address lookups to avoid fatals

->first() may return null; accessing array offsets would fatal. Also handle missing loc_block/address rows.

Apply:

-        $event = Event::get(TRUE)
+        $event = Event::get(TRUE)
           ->addSelect('loc_block_id.address_id')
           ->addWhere('id', '=', $entityId)
-          ->execute()->first();
+          ->execute()->first();
+        if (!$event || empty($event['loc_block_id.address_id'])) {
+          \CRM_Core_Error::debug_log_message("Event or address not found for id {$entityId}");
+          return FALSE;
+        }
-
-        $addresses = Address::get(FALSE)
+        $addresses = Address::get(FALSE)
           ->addWhere('id', '=', $event['loc_block_id.address_id'])
           ->setLimit(1)
-          ->execute()->first();
-        $address = \CRM_Utils_Address::format($addresses);
+          ->execute()->first();
+        if (!$addresses) {
+          \CRM_Core_Error::debug_log_message("Address not found for event id {$entityId}");
+          return FALSE;
+        }
+        $address = \CRM_Utils_Address::format($addresses);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (!empty($saveOptions['customGroupName']) && $saveOptions['customGroupName'] === 'Event_QR') {
$event = Event::get(TRUE)
->addSelect('loc_block_id.address_id')
->addWhere('id', '=', $entityId)
->execute()->first();
$addresses = Address::get(FALSE)
->addWhere('id', '=', $event['loc_block_id.address_id'])
->setLimit(1)
->execute()->first();
$address = \CRM_Utils_Address::format($addresses);
}
if (!empty($saveOptions['customGroupName']) && $saveOptions['customGroupName'] === 'Event_QR') {
$event = Event::get(TRUE)
->addSelect('loc_block_id.address_id')
->addWhere('id', '=', $entityId)
->execute()->first();
if (!$event || empty($event['loc_block_id.address_id'])) {
\CRM_Core_Error::debug_log_message("Event or address not found for id {$entityId}");
return FALSE;
}
$addresses = Address::get(FALSE)
->addWhere('id', '=', $event['loc_block_id.address_id'])
->setLimit(1)
->execute()->first();
if (!$addresses) {
\CRM_Core_Error::debug_log_message("Address not found for event id {$entityId}");
return FALSE;
}
$address = \CRM_Utils_Address::format($addresses);
}
🤖 Prompt for AI Agents
In wp-content/civi-extensions/goonjcustom/Civi/Traits/QrCodeable.php around
lines 32 to 43, the code assumes Event::get(...)->execute()->first() and
Address::get(...)->execute()->first() always return arrays and directly indexes
them which can cause fatal errors; update the code to guard both lookups: check
the event result is non-null/array before accessing 'loc_block_id.address_id',
verify that the retrieved address row exists before passing it to
CRM_Utils_Address::format (or provide a sensible default/empty string if
missing), and bail out or skip QR/address logic when required IDs/rows are
absent to avoid notices/fatals.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (12)
wp-content/civi-extensions/goonjcustom/Civi/Traits/QrCodeable.php (12)

25-30: Render raw PNG: set outputBase64=false and remove manual base64 handling

Explicitly disable base64 output to avoid brittle string ops and potential binary mangling; let render() return raw PNG bytes and drop the decode logic. Also prefer auto version and higher ECC for scan reliability.

       $options = new QROptions([
-        'version'    => 5,
+        'version'    => 0, // auto
         'outputType' => QRCode::OUTPUT_IMAGE_PNG,
-        'eccLevel'   => QRCode::ECC_L,
+        'eccLevel'   => QRCode::ECC_M,
         'scale'      => 12,
+        'outputBase64' => false,
       ]);
@@
-      $qrcode = str_replace('data:image/png;base64,', '', $qrcode);
-      $qrcode = base64_decode($qrcode);

Also applies to: 76-80


122-126: Make heading and venue text configurable (and sanitize address)

Avoid hardcoded strings; pull from $saveOptions and strip tags from $address.

-      $topText = "Scan to Record Your\nContribution";
-      $venueLabel = "Venue: ";
-      $venueValue = $address;
+      $topText    = $saveOptions['topText']    ?? "Scan to Record Your\nContribution";
+      $venueLabel = $saveOptions['venueLabel'] ?? "Venue: ";
+      $venueValue = isset($address) ? \wp_strip_all_tags((string)$address) : '';

127-135: Canvas sizing should consider resized QR and measured TTF text

Using $qrWidth and fixed paddings risks clipping/overwide canvases. Compute widths from imagettfbbox and use $newQrWidth.

-      $canvasWidth = $qrWidth + 100;
-      $canvasHeight = $newLogoHeight + $qrHeight + 220;
+      // After measuring text (see font block), recompute to fit content.
+      $canvasWidth  = $newQrWidth + 100; // will be max'd against text widths later
+      $canvasHeight = $newLogoHeight + $newQrHeight + 220;

Optionally: pre-measure top/bottom text with imagettfbbox and set canvasWidth = max(newQrWidth+100, maxTextWidth+40, newLogoWidth+40).


159-163: Avoid hardcoded QR Y-offset; place after measured text block

Compute text block height to prevent overlaps across font/line changes.

-      $qrY = $topY + 70;
+      $textBlockHeight = (count($lines) ?: 1) * $lineHeight;
+      $qrY = $topY + $textBlockHeight + 20;

225-235: Propagate save result instead of always returning TRUE

Return the attachment (or FALSE) so callers can react to failures.

       $saveOptions['baseFileName'] = $baseFileName;
       $saveOptions['entityId'] = $entityId;
-      self::saveQrCode($finalImage, $saveOptions);
+      $attachment = self::saveQrCode($finalImage, $saveOptions);
+      if ($attachment === FALSE) {
+        return FALSE;
+      }
@@
-    return TRUE;
+    return $attachment;

32-43: Null-guard Event and Address fetches to prevent fatals

->first() may return null; indexing into it will fatal. Guard both lookups and bail with a clear log.

         $event = Event::get(TRUE)
           ->addSelect('loc_block_id.address_id')
           ->addWhere('id', '=', $entityId)
-          ->execute()->first();
+          ->execute()->first();
+        if (!$event || empty($event['loc_block_id.address_id'])) {
+          \CRM_Core_Error::debug_log_message("Event or address not found for id {$entityId}");
+          return FALSE;
+        }
 
         $addresses = Address::get(FALSE)
           ->addWhere('id', '=', $event['loc_block_id.address_id'])
           ->setLimit(1)
-          ->execute()->first();
-        $address = \CRM_Utils_Address::format($addresses);
+          ->execute()->first();
+        if (!$addresses) {
+          \CRM_Core_Error::debug_log_message("Address not found for event id {$entityId}");
+          return FALSE;
+        }
+        $address = \CRM_Utils_Address::format($addresses);

45-74: Guard Collection_Camp lookup and missing keys

$campData can be null and individual fields can be absent; current code will fatal or raise notices. Add null checks and use null-coalescing on reads.

-        $campData = EckEntity::get('Collection_Camp', FALSE)
+        $campData = EckEntity::get('Collection_Camp', FALSE)
           ->addSelect('subtype:name', 'Collection_Camp_Intent_Details.Location_Area_of_camp', 'Dropping_Centre.Where_do_you_wish_to_open_dropping_center_Address_', 'Goonj_Activities.Where_do_you_wish_to_organise_the_activity_', 'Institution_Collection_Camp_Intent.Collection_Camp_Address', 'Institution_Dropping_Center_Intent.Dropping_Center_Address', 'Institution_Goonj_Activities.Where_do_you_wish_to_organise_the_activity_')
           ->addWhere('id', '=', $entityId)
           ->execute()->first();
+        if (!$campData) {
+          \CRM_Core_Error::debug_log_message("Collection Camp not found for id {$entityId}");
+          return FALSE;
+        }
 
-        $campStatus = $campData['subtype:name'];
+        $campStatus = $campData['subtype:name'] ?? null;
+        if (!$campStatus) {
+          \CRM_Core_Error::debug_log_message("Missing subtype for Collection Camp id {$entityId}");
+          return FALSE;
+        }
@@
-          $address = $campData['Collection_Camp_Intent_Details.Location_Area_of_camp'];
+          $address = $campData['Collection_Camp_Intent_Details.Location_Area_of_camp'] ?? '';
@@
-          $address = $campData['Dropping_Centre.Where_do_you_wish_to_open_dropping_center_Address_'];
+          $address = $campData['Dropping_Centre.Where_do_you_wish_to_open_dropping_center_Address_'] ?? '';
@@
-          $address = $campData['Goonj_Activities.Where_do_you_wish_to_organise_the_activity_'];
+          $address = $campData['Goonj_Activities.Where_do_you_wish_to_organise_the_activity_'] ?? '';
@@
-          $address = $campData['Institution_Collection_Camp_Intent.Collection_Camp_Address'];
+          $address = $campData['Institution_Collection_Camp_Intent.Collection_Camp_Address'] ?? '';
@@
-          $address = $campData['Institution_Dropping_Center_Intent.Dropping_Center_Address'];
+          $address = $campData['Institution_Dropping_Center_Intent.Dropping_Center_Address'] ?? '';
@@
-          $address = $campData['Institution_Goonj_Activities.Where_do_you_wish_to_organise_the_activity_'];
+          $address = $campData['Institution_Goonj_Activities.Where_do_you_wish_to_organise_the_activity_'] ?? '';

80-82: Guard GD image creation for QR to avoid TypeError on imagesx/y

imagecreatefromstring() can return false; check before measuring.

-      $qrImage = imagecreatefromstring($qrcode);
-      $qrWidth = imagesx($qrImage);
-      $qrHeight = imagesy($qrImage);
+      $qrImage = imagecreatefromstring($qrcode);
+      if (!$qrImage) {
+        throw new \RuntimeException('Failed to create QR image from binary');
+      }
+      $qrWidth  = imagesx($qrImage);
+      $qrHeight = imagesy($qrImage);

100-107: Don’t hardcode dated logo URL or fetch via HTTP; prefer filesystem with fallbacks

Use uploads basedir and allow overrides via $saveOptions; add failure checks.

-      $upload_dir = wp_upload_dir();
-      $logoUrl = $upload_dir['baseurl'] . '/2024/09/Goonj-logo-10June20-300x193-1.png';
-      $logoData = file_get_contents($logoUrl);
-      $logoImage = imagecreatefromstring($logoData);
+      $upload_dir = \wp_upload_dir();
+      $logoPath = $saveOptions['logoPath'] ?? ($upload_dir['basedir'] . '/brand/Goonj-logo.png');
+      if (\file_exists($logoPath)) {
+        $logoImage = \imagecreatefrompng($logoPath);
+      } else {
+        $logoUrl = $saveOptions['logoUrl'] ?? null;
+        if (!$logoUrl) {
+          throw new \RuntimeException("Logo not found at $logoPath and no logoUrl provided");
+        }
+        $resp = \wp_remote_get($logoUrl, ['timeout' => 5]);
+        $logoData = \wp_remote_retrieve_body($resp);
+        if (!$logoData) {
+          throw new \RuntimeException("Failed to download logo from $logoUrl");
+        }
+        $logoImage = \imagecreatefromstring($logoData);
+      }
+      if (!$logoImage) {
+        throw new \RuntimeException('Failed to create logo image resource');
+      }

136-139: Verify font files exist before calling imagettf APIs*

Missing OTFs will emit warnings and break rendering; add explicit checks.

       $fontPath = dirname(__DIR__, 2) . '/fonts/Fontspring-proximanova-bold.otf';
       $fontPathRegular = dirname(__DIR__, 2) . '/fonts/Fontspring-proximanova-regular.otf';
+      if (!\file_exists($fontPath) || !\file_exists($fontPathRegular)) {
+        throw new \RuntimeException('TTF/OTF font file(s) not found');
+      }

164-212: Fix venue wrapping/centering; remove unused var and measure bold label separately

Current wrap mixes fonts during measurement and leaves $currentWidth unused. Center the combined first line (bold label + regular value) and wrap value-only lines.

       $bottomFontSize = 25;
       $maxWidth = $canvasWidth - 80;
       $bottomY = $qrY + $newQrHeight + 40;
@@
-      $lines = [];
-      $currentLine = $venueLabel;
-      $currentWidth = $labelWidth;
-
-      $words = explode(" ", $venueValue);
-      foreach ($words as $word) {
-        $testLine = ($currentLine === $venueLabel ? $currentLine : $currentLine . " ") . $word;
-        $bboxTest = imagettfbbox($bottomFontSize, 0, $fontPathRegular, $testLine);
-        $testWidth = abs($bboxTest[2] - $bboxTest[0]);
-
-        if ($testWidth > $maxWidth) {
-          $lines[] = $currentLine;
-          $currentLine = $word;
-        }
-        else {
-          $currentLine = $testLine;
-        }
-      }
-      $lines[] = $currentLine;
+      $words = explode(' ', trim($venueValue));
+      $valueLines = [];
+      $current = '';
+      foreach ($words as $word) {
+        $test = $current === '' ? $word : $current . ' ' . $word;
+        $bbox = imagettfbbox($bottomFontSize, 0, $fontPathRegular, $test);
+        $testW = abs($bbox[2] - $bbox[0]);
+        $allowed = empty($valueLines) ? ($maxWidth - $labelWidth) : $maxWidth;
+        if ($testW > $allowed && $current !== '') {
+          $valueLines[] = $current;
+          $current = $word;
+        } else {
+          $current = $test;
+        }
+      }
+      if ($current !== '') {
+        $valueLines[] = $current;
+      }
@@
-      foreach ($lines as $i => $line) {
-        $bbox = imagettfbbox($bottomFontSize, 0, $fontPathRegular, $line);
-        $lineWidth = abs($bbox[2] - $bbox[0]);
-        $x = (int) (($canvasWidth - $lineWidth) / 2);
-        $y = $bottomY + ($i * $lineHeight);
-
-        if ($i === 0) {
-          imagettftext($canvas, $bottomFontSize, 0, $x, $y, $black, $fontPath, $venueLabel);
-          $valuePart = trim(str_replace($venueLabel, "", $line));
-          if ($valuePart !== "") {
-            $bboxLabel = imagettfbbox($bottomFontSize, 0, $fontPath, $venueLabel);
-            $offset = abs($bboxLabel[2] - $bboxLabel[0]);
-            imagettftext($canvas, $bottomFontSize, 0, $x + $offset, $y, $black, $fontPathRegular, $valuePart);
-          }
-        }
-        else {
-          imagettftext($canvas, $bottomFontSize, 0, $x, $y, $black, $fontPathRegular, $line);
-        }
-      }
+      foreach ($valueLines as $i => $valLine) {
+        $bboxVal = imagettfbbox($bottomFontSize, 0, $fontPathRegular, $valLine);
+        $valW    = abs($bboxVal[2] - $bboxVal[0]);
+        $lineW   = ($i === 0) ? ($labelWidth + $valW) : $valW;
+        $x       = (int)(($canvasWidth - $lineW) / 2);
+        $y       = $bottomY + ($i * $lineHeight);
+        if ($i === 0) {
+          imagettftext($canvas, $bottomFontSize, 0, $x, $y, $black, $fontPath, $venueLabel);
+          imagettftext($canvas, $bottomFontSize, 0, $x + $labelWidth, $y, $black, $fontPathRegular, $valLine);
+        } else {
+          imagettftext($canvas, $bottomFontSize, 0, $x, $y, $black, $fontPathRegular, $valLine);
+        }
+      }

214-218: Validate PNG buffer before saving

Check imagepng return and ensure buffer is non-empty to avoid writing corrupt files.

       ob_start();
-      imagepng($canvas);
-      $finalImage = ob_get_clean();
+      $ok = imagepng($canvas);
+      $finalImage = ob_get_clean();
+      if (!$ok || $finalImage === '' || $finalImage === false) {
+        throw new \RuntimeException('Failed to render composite PNG');
+      }
🧹 Nitpick comments (3)
wp-content/civi-extensions/goonjcustom/Civi/Traits/QrCodeable.php (3)

115-120: Use imagecopyresampled for logo too (better quality)

Resampling avoids jagged edges on scaling.

-      imagecopyresized(
+      imagecopyresampled(
             $resizedLogo, $logoImage,
             0, 0, 0, 0,
             $newLogoWidth, $newLogoHeight,
             $logoWidth, $logoHeight
         );

284-289: Prefix global civicrm_api3 for clarity and minor perf

Avoid namespaced function lookup overhead.

-    $result = civicrm_api3('Attachment', 'create', $params);
+    $result = \civicrm_api3('Attachment', 'create', $params);

25-30: Optional: bump ECC and let version auto-size for scan robustness

ECC_M/H improves resilience; version=0 lets library pick minimal size.

No code sample beyond the earlier QROptions diff needed.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 6aaa7d4 and e6e081b.

⛔ Files ignored due to path filters (2)
  • wp-content/civi-extensions/goonjcustom/fonts/Fontspring-proximanova-bold.otf is excluded by !**/*.otf
  • wp-content/civi-extensions/goonjcustom/fonts/Fontspring-proximanova-regular.otf is excluded by !**/*.otf
📒 Files selected for processing (1)
  • wp-content/civi-extensions/goonjcustom/Civi/Traits/QrCodeable.php (2 hunks)
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2024-10-11T09:53:14.458Z
Learnt from: belwalshubham
PR: ColoredCow/goonj#284
File: wp-content/civi-extensions/goonjcustom/Civi/CollectionCampService.php:520-520
Timestamp: 2024-10-11T09:53:14.458Z
Learning: In `wp-content/civi-extensions/goonjcustom/Civi/CollectionCampService.php`, it's acceptable that the `reGenerateCollectionCampQr` method calls `generateQrCode` with only the `$collectionCampId` parameter, as QR codes are generated when the Dropping Center is authorized. We'll implement changes if any issues arise in the future.

Applied to files:

  • wp-content/civi-extensions/goonjcustom/Civi/Traits/QrCodeable.php
📚 Learning: 2024-10-09T15:24:42.859Z
Learnt from: belwalshubham
PR: ColoredCow/goonj#352
File: wp-content/civi-extensions/goonjcustom/Civi/QrCodeService.php:51-53
Timestamp: 2024-10-09T15:24:42.859Z
Learning: In `QrCodeService::generateQrCode`, when using `QRCode::OUTPUT_IMAGE_PNG`, the `render` method returns a data URI with base64-encoded image data. Therefore, it's necessary to remove the `data:image/png;base64,` prefix and base64-decode the data to obtain the raw binary image.

Applied to files:

  • wp-content/civi-extensions/goonjcustom/Civi/Traits/QrCodeable.php
🪛 PHPMD (2.15.0)
wp-content/civi-extensions/goonjcustom/Civi/Traits/QrCodeable.php

175-175: Avoid unused local variables such as '$currentWidth'. (Unused Code Rules)

(UnusedLocalVariable)

🔇 Additional comments (2)
wp-content/civi-extensions/goonjcustom/Civi/Traits/QrCodeable.php (2)

89-98: Alpha-safe resample for QR looks good

Preserving alpha and using imagecopyresampled is correct for QR quality.


146-157: Heading rendering is accurate

Multi-line TTF with bbox centering looks good.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (10)
wp-content/civi-extensions/goonjcustom/Civi/Traits/QrCodeable.php (10)

25-31: Stop base64-stripping; render raw PNG and validate GD resource

With raw PNG output, stripping/decoding corrupts data. Also guard imagecreatefromstring.

       $options = new QROptions([
         'version'    => 5,
         'outputType' => QRCode::OUTPUT_IMAGE_PNG,
         'eccLevel'   => QRCode::ECC_L,
-        'scale'      => 12,
+        'scale'      => 12,
+        // Ensure render() returns raw PNG, not a data URI.
+        'outputBase64' => false,
       ]);
@@
-      $qrcode = str_replace('data:image/png;base64,', '', $qrcode);
-      $qrcode = base64_decode($qrcode);
-      $qrImage = imagecreatefromstring($qrcode);
+      // $qrcode already is raw PNG bytes.
+      $qrImage = imagecreatefromstring($qrcode);
+      if (!$qrImage) {
+        throw new \RuntimeException('Failed to create QR image from binary');
+      }
       $qrWidth = imagesx($qrImage);
       $qrHeight = imagesy($qrImage);

Also applies to: 76-83


122-126: Hard-coded user-facing text; make configurable (and translatable if needed)

Allow callers to pass headline and venue label/value via $saveOptions.

-      // Texts.
-      $topText = "Scan to Record Your\nContribution";
-      $venueLabel = "Venue: ";
-      $venueValue = $address;
+      // Texts.
+      $topText    = $saveOptions['topText']    ?? "Scan to Record Your\nContribution";
+      $venueLabel = $saveOptions['venueLabel'] ?? "Venue: ";
+      $venueValue = $saveOptions['venue']      ?? ($address ?? '');

225-235: Propagate save result (attachment) instead of always TRUE

Return FALSE when save fails; otherwise return the created attachment for downstream use.

       $baseFileName = "qr_code_{$entityId}.png";
       $saveOptions['baseFileName'] = $baseFileName;
       $saveOptions['entityId'] = $entityId;
-      self::saveQrCode($finalImage, $saveOptions);
+      $attachment = self::saveQrCode($finalImage, $saveOptions);
+      if ($attachment === FALSE) {
+        return FALSE;
+      }
+      return $attachment;
-    }
+    }
     catch (\Exception $e) {
       \CRM_Core_Error::debug_log_message('Error generating QR code: ' . $e->getMessage());
       return FALSE;
     }
 
-    return TRUE;
+    // Unreachable after early returns above.
+    return TRUE;

100-107: Don’t fetch logo over HTTP; use filesystem, validate, and resample

Avoid hardcoded month path and network fetch. Prefer uploads basedir and handle errors. Also use imagecopyresampled for quality.

-      // Load logo.
-      $upload_dir = wp_upload_dir();
-      $logoUrl = $upload_dir['baseurl'] . '/2024/09/Goonj-logo-10June20-300x193-1.png';
-      $logoData = file_get_contents($logoUrl);
-      $logoImage = imagecreatefromstring($logoData);
+      // Load logo (filesystem preferred; allow override).
+      $upload_dir = \wp_upload_dir();
+      $logoPath = $saveOptions['logoPath'] ?? ($upload_dir['basedir'] . '/brand/Goonj-logo.png');
+      if (\file_exists($logoPath)) {
+        $logoImage = imagecreatefrompng($logoPath);
+      } else {
+        $logoUrl = $saveOptions['logoUrl'] ?? null;
+        if (!$logoUrl) {
+          throw new \RuntimeException("Logo not found at $logoPath and no logoUrl provided");
+        }
+        $resp = \wp_remote_get($logoUrl, ['timeout' => 5]);
+        $logoData = \wp_remote_retrieve_body($resp);
+        $logoImage = $logoData ? imagecreatefromstring($logoData) : false;
+      }
+      if (!$logoImage) {
+        throw new \RuntimeException('Failed to create logo image resource');
+      }
@@
-      imagecopyresized(
+      imagecopyresampled(
             $resizedLogo, $logoImage,
             0, 0, 0, 0,
             $newLogoWidth, $newLogoHeight,
             $logoWidth, $logoHeight
         );

Also applies to: 115-120


127-131: Fix canvas sizing and QR placement (use resized dims and measured text height)

Avoid clipping/overlap; base width/height on resized QR and heading block height.

-      // Canvas: logo + top text + QR + bottom text.
-      $canvasWidth = $qrWidth + 100;
-      $canvasHeight = $newLogoHeight + $qrHeight + 220;
+      // Canvas: logo + heading + QR + venue.
+      $textBlockHeight = max(1, count($lines)) * ($lineHeight);
+      $canvasWidth  = max($newQrWidth + 100, $newLogoWidth + 60);
+      $canvasHeight = $newLogoHeight + 40 + $textBlockHeight + 20 + $newQrHeight + 180;
@@
-      $qrY = $topY + 70;
+      $qrY = $topY + $textBlockHeight + 20;
       $qrX = (int) (($canvasWidth - $newQrWidth) / 2);

Also applies to: 159-163


136-139: Verify font files exist; fail fast with clear error or fallback

Avoid warnings from missing OTF/TTF files.

       // Fonts.
       $fontPath = dirname(__DIR__, 2) . '/fonts/Fontspring-proximanova-bold.otf';
       $fontPathRegular = dirname(__DIR__, 2) . '/fonts/Fontspring-proximanova-regular.otf';
+      if (!\file_exists($fontPath) || !\file_exists($fontPathRegular)) {
+        throw new \RuntimeException('Required font file(s) not found');
+      }

164-212: Fix venue wrapping/centering; remove unused var

Measure bold label separately; wrap and center value correctly; drop $currentWidth.

       // --- Step 4: Venue text with wrapping
       $bottomFontSize = 25;
       $maxWidth = $canvasWidth - 80;
       $bottomY = $qrY + $newQrHeight + 40;
 
       // Start with bold "Venue:".
       $bboxLabel = imagettfbbox($bottomFontSize, 0, $fontPath, $venueLabel);
       $labelWidth = abs($bboxLabel[2] - $bboxLabel[0]);
 
-      $lines = [];
-      $currentLine = $venueLabel;
-      $currentWidth = $labelWidth;
-
-      $words = explode(" ", $venueValue);
-      foreach ($words as $word) {
-        $testLine = ($currentLine === $venueLabel ? $currentLine : $currentLine . " ") . $word;
-        $bboxTest = imagettfbbox($bottomFontSize, 0, $fontPathRegular, $testLine);
-        $testWidth = abs($bboxTest[2] - $bboxTest[0]);
-
-        if ($testWidth > $maxWidth) {
-          $lines[] = $currentLine;
-          $currentLine = $word;
-        }
-        else {
-          $currentLine = $testLine;
-        }
-      }
-      $lines[] = $currentLine;
+      $words = explode(' ', trim($venueValue ?? ''));
+      $valueLines = [];
+      $current = '';
+      foreach ($words as $word) {
+        $test = $current === '' ? $word : $current . ' ' . $word;
+        $bbox = imagettfbbox($bottomFontSize, 0, $fontPathRegular, $test);
+        $testW = abs($bbox[2] - $bbox[0]);
+        $allowed = empty($valueLines) ? ($maxWidth - $labelWidth) : $maxWidth;
+        if ($testW > $allowed && $current !== '') {
+          $valueLines[] = $current;
+          $current = $word;
+        } else {
+          $current = $test;
+        }
+      }
+      if ($current !== '') {
+        $valueLines[] = $current;
+      }
 
       $lineHeight = $bottomFontSize + 8;
-      foreach ($lines as $i => $line) {
-        $bbox = imagettfbbox($bottomFontSize, 0, $fontPathRegular, $line);
-        $lineWidth = abs($bbox[2] - $bbox[0]);
-        $x = (int) (($canvasWidth - $lineWidth) / 2);
-        $y = $bottomY + ($i * $lineHeight);
-
-        if ($i === 0) {
-          imagettftext($canvas, $bottomFontSize, 0, $x, $y, $black, $fontPath, $venueLabel);
-          $valuePart = trim(str_replace($venueLabel, "", $line));
-          if ($valuePart !== "") {
-            $bboxLabel = imagettfbbox($bottomFontSize, 0, $fontPath, $venueLabel);
-            $offset = abs($bboxLabel[2] - $bboxLabel[0]);
-            imagettftext($canvas, $bottomFontSize, 0, $x + $offset, $y, $black, $fontPathRegular, $valuePart);
-          }
-        }
-        else {
-          imagettftext($canvas, $bottomFontSize, 0, $x, $y, $black, $fontPathRegular, $line);
-        }
-      }
+      foreach ($valueLines as $i => $valLine) {
+        $bboxVal = imagettfbbox($bottomFontSize, 0, $fontPathRegular, $valLine);
+        $valW    = abs($bboxVal[2] - $bboxVal[0]);
+        $lineW   = ($i === 0) ? ($labelWidth + $valW) : $valW;
+        $x       = (int) (($canvasWidth - $lineW) / 2);
+        $y       = $bottomY + ($i * $lineHeight);
+        if ($i === 0) {
+          imagettftext($canvas, $bottomFontSize, 0, $x, $y, $black, $fontPath, $venueLabel);
+          imagettftext($canvas, $bottomFontSize, 0, $x + $labelWidth, $y, $black, $fontPathRegular, $valLine);
+        } else {
+          imagettftext($canvas, $bottomFontSize, 0, $x, $y, $black, $fontPathRegular, $valLine);
+        }
+      }

214-218: Validate PNG buffer before saving to avoid empty writes

Check imagepng() result and buffer content.

       // Save final.
       ob_start();
-      imagepng($canvas);
-      $finalImage = ob_get_clean();
+      $ok = imagepng($canvas);
+      $finalImage = ob_get_clean();
+      if (!$ok || $finalImage === '' || $finalImage === false) {
+        throw new \RuntimeException('Failed to render composite PNG');
+      }

32-43: Guard Event/address lookups to avoid fatals; bail out cleanly

Both ->first() calls can return null. Accessing offsets would fatal. Return FALSE with logs when missing.

       if (!empty($saveOptions['customGroupName']) && $saveOptions['customGroupName'] === 'Event_QR') {
-        $event = Event::get(FALSE)
+        $event = Event::get(FALSE)
           ->addSelect('loc_block_id.address_id')
           ->addWhere('id', '=', $entityId)
-          ->execute()->first();
+          ->execute()->first();
+        if (!$event || empty($event['loc_block_id.address_id'])) {
+          \CRM_Core_Error::debug_log_message("Event or address not found for id {$entityId}");
+          return FALSE;
+        }
 
-        $addresses = Address::get(FALSE)
+        $addresses = Address::get(FALSE)
           ->addWhere('id', '=', $event['loc_block_id.address_id'])
           ->setLimit(1)
-          ->execute()->first();
-        $address = \CRM_Utils_Address::format($addresses);
+          ->execute()->first();
+        if (!$addresses) {
+          \CRM_Core_Error::debug_log_message("Address row missing for event id {$entityId}");
+          return FALSE;
+        }
+        $address = \CRM_Utils_Address::format($addresses);
       }

45-74: Null-check camp fetch and guard missing keys

->first() may be null; individual keys may be absent. Prevent notices and undefined $address.

-      if (!empty($saveOptions['customGroupName']) && $saveOptions['customGroupName'] != 'Event_QR') {
+      if (!empty($saveOptions['customGroupName']) && $saveOptions['customGroupName'] != 'Event_QR') {
         $campData = EckEntity::get('Collection_Camp', FALSE)
           ->addSelect('subtype:name', 'Collection_Camp_Intent_Details.Location_Area_of_camp', 'Dropping_Centre.Where_do_you_wish_to_open_dropping_center_Address_', 'Goonj_Activities.Where_do_you_wish_to_organise_the_activity_', 'Institution_Collection_Camp_Intent.Collection_Camp_Address', 'Institution_Dropping_Center_Intent.Dropping_Center_Address', 'Institution_Goonj_Activities.Where_do_you_wish_to_organise_the_activity_')
           ->addWhere('id', '=', $entityId)
           ->execute()->first();
+        if (!$campData) {
+          \CRM_Core_Error::debug_log_message("Collection_Camp not found for id {$entityId}");
+          return FALSE;
+        }
 
-        $campStatus = $campData['subtype:name'];
+        $campStatus = $campData['subtype:name'] ?? null;
+        if (!$campStatus) {
+          \CRM_Core_Error::debug_log_message("Missing subtype for Collection_Camp id {$entityId}");
+          return FALSE;
+        }
 
         if ($campStatus == 'Collection_Camp') {
-          $address = $campData['Collection_Camp_Intent_Details.Location_Area_of_camp'];
+          $address = $campData['Collection_Camp_Intent_Details.Location_Area_of_camp'] ?? '';
         }
         elseif ($campStatus == 'Dropping_Center') {
-          $address = $campData['Dropping_Centre.Where_do_you_wish_to_open_dropping_center_Address_'];
+          $address = $campData['Dropping_Centre.Where_do_you_wish_to_open_dropping_center_Address_'] ?? '';
         }
         elseif ($campStatus == 'Goonj_Activities') {
-          $address = $campData['Goonj_Activities.Where_do_you_wish_to_organise_the_activity_'];
+          $address = $campData['Goonj_Activities.Where_do_you_wish_to_organise_the_activity_'] ?? '';
         }
         elseif ($campStatus == 'Institution_Collection_Camp') {
-          $address = $campData['Institution_Collection_Camp_Intent.Collection_Camp_Address'];
+          $address = $campData['Institution_Collection_Camp_Intent.Collection_Camp_Address'] ?? '';
         }
         elseif ($campStatus == 'Institution_Dropping_Center') {
-          $address = $campData['Institution_Dropping_Center_Intent.Dropping_Center_Address'];
+          $address = $campData['Institution_Dropping_Center_Intent.Dropping_Center_Address'] ?? '';
         }
         elseif ($campStatus == 'Institution_Goonj_Activities') {
-          $address = $campData['Institution_Goonj_Activities.Where_do_you_wish_to_organise_the_activity_'];
+          $address = $campData['Institution_Goonj_Activities.Where_do_you_wish_to_organise_the_activity_'] ?? '';
         }
         else {
           throw new \Exception('Invalid entity type for QR code generation.');
         }
       }
🧹 Nitpick comments (1)
wp-content/civi-extensions/goonjcustom/Civi/Traits/QrCodeable.php (1)

25-31: Consider higher ECC for field scanning reliability

ECC_L is the lowest. ECC_M/Q/H are more robust against logo overlays and print defects.

-        'eccLevel'   => QRCode::ECC_L,
+        'eccLevel'   => QRCode::ECC_Q,
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between e6e081b and ec9aba6.

📒 Files selected for processing (1)
  • wp-content/civi-extensions/goonjcustom/Civi/Traits/QrCodeable.php (2 hunks)
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2024-10-11T09:53:14.458Z
Learnt from: belwalshubham
PR: ColoredCow/goonj#284
File: wp-content/civi-extensions/goonjcustom/Civi/CollectionCampService.php:520-520
Timestamp: 2024-10-11T09:53:14.458Z
Learning: In `wp-content/civi-extensions/goonjcustom/Civi/CollectionCampService.php`, it's acceptable that the `reGenerateCollectionCampQr` method calls `generateQrCode` with only the `$collectionCampId` parameter, as QR codes are generated when the Dropping Center is authorized. We'll implement changes if any issues arise in the future.

Applied to files:

  • wp-content/civi-extensions/goonjcustom/Civi/Traits/QrCodeable.php
📚 Learning: 2024-10-09T15:24:42.859Z
Learnt from: belwalshubham
PR: ColoredCow/goonj#352
File: wp-content/civi-extensions/goonjcustom/Civi/QrCodeService.php:51-53
Timestamp: 2024-10-09T15:24:42.859Z
Learning: In `QrCodeService::generateQrCode`, when using `QRCode::OUTPUT_IMAGE_PNG`, the `render` method returns a data URI with base64-encoded image data. Therefore, it's necessary to remove the `data:image/png;base64,` prefix and base64-decode the data to obtain the raw binary image.

Applied to files:

  • wp-content/civi-extensions/goonjcustom/Civi/Traits/QrCodeable.php
🪛 PHPMD (2.15.0)
wp-content/civi-extensions/goonjcustom/Civi/Traits/QrCodeable.php

175-175: Avoid unused local variables such as '$currentWidth'. (Unused Code Rules)

(UnusedLocalVariable)

@tarunnjoshi tarunnjoshi merged commit 0158c00 into dev Sep 3, 2025
@tarunnjoshi tarunnjoshi deleted the dynamic-qr-code-with-some-more-details branch September 3, 2025 12:12
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
wp-content/civi-extensions/goonjcustom/Civi/Traits/QrCodeable.php (1)

25-31: Render raw PNG; drop base64 handling

Set outputBase64=false and stop stripping/decoding. Prevents corrupted data and simplifies IO.

@@
-      $options = new QROptions([
+      $options = new QROptions([
         'version'    => 5,
         'outputType' => QRCode::OUTPUT_IMAGE_PNG,
         'eccLevel'   => QRCode::ECC_L,
-        'scale'      => 12,
+        'scale'      => 12,
+        'outputBase64' => false,
       ]);
@@
-      $qrcode = (new QRCode($options))->render($data);
-      $qrcode = str_replace('data:image/png;base64,', '', $qrcode);
-      $qrcode = base64_decode($qrcode);
+      $qrcode = (new QRCode($options))->render($data);
@@
-      // Remove the base64 header and decode the image data.
-      $qrcode = str_replace('data:image/png;base64,', '', $qrcode);
-      $qrcode = base64_decode($qrcode);
+      // Render already returns raw PNG bytes when outputBase64=false.
+      // $qrcode is raw PNG binary.

Also applies to: 78-80, 306-311

♻️ Duplicate comments (8)
wp-content/civi-extensions/goonjcustom/Civi/Traits/QrCodeable.php (8)

122-126: Make heading/venue text dynamic (and translatable)

Don’t hardcode user-facing strings; pull from options with safe defaults.

-      $topText = "Scan to Record Your\nContribution";
-      $venueLabel = "Venue: ";
-      $venueValue = $address;
+      $topText    = $saveOptions['topText']    ?? "Scan to Record Your\nContribution";
+      $venueLabel = $saveOptions['venueLabel'] ?? "Venue: ";
+      $venueValue = $saveOptions['venue']      ?? ($address ?? '');

159-163: Position QR after measured heading; avoid overlap

Use measured text height instead of a fixed 70px gap.

-      $qrY = $topY + 70;
+      $textBlockHeight = (count($lines) ?: 1) * ($lineHeight);
+      $qrY = $topY + $textBlockHeight + 20;

228-235: Propagate save result instead of always returning TRUE

Return the attachment (or FALSE) so callers can act on failures.

-      self::saveQrCode($finalImage, $saveOptions);
+      $attachment = self::saveQrCode($finalImage, $saveOptions);
+      if ($attachment === FALSE) {
+        return FALSE;
+      }
+      return $attachment;
@@
-    return TRUE;
+    return TRUE; // Unreachable if returning above; keep if you prefer boolean API.

80-83: Guard GD image creation to prevent TypeError

imagecreatefromstring() may return false; check before imagesx/y.

-      $qrImage = imagecreatefromstring($qrcode);
-      $qrWidth = imagesx($qrImage);
-      $qrHeight = imagesy($qrImage);
+      $qrImage = imagecreatefromstring($qrcode);
+      if (!$qrImage) {
+        throw new \RuntimeException('Failed to create QR image from binary');
+      }
+      $qrWidth  = imagesx($qrImage);
+      $qrHeight = imagesy($qrImage);

100-107: Don’t hardcode logo URL; prefer filesystem and handle errors

Avoid HTTP fetch, year/month coupling, and missing-file fatals.

-      $upload_dir = wp_upload_dir();
-      $logoUrl = $upload_dir['baseurl'] . '/2024/09/Goonj-logo-10June20-300x193-1.png';
-      $logoData = file_get_contents($logoUrl);
-      $logoImage = imagecreatefromstring($logoData);
+      $upload_dir = \wp_upload_dir();
+      $logoPath = $saveOptions['logoPath'] ?? ($upload_dir['basedir'] . '/brand/Goonj-logo.png');
+      if (\file_exists($logoPath)) {
+        $logoImage = imagecreatefrompng($logoPath);
+      }
+      else {
+        $logoUrl = $saveOptions['logoUrl'] ?? null;
+        if (!$logoUrl) {
+          throw new \RuntimeException("Logo not found at $logoPath and no logoUrl provided");
+        }
+        $resp = \wp_remote_get($logoUrl, ['timeout' => 5]);
+        $logoData = \wp_remote_retrieve_body($resp);
+        if (!$logoData) {
+          throw new \RuntimeException("Failed to download logo from $logoUrl");
+        }
+        $logoImage = imagecreatefromstring($logoData);
+      }
+      if (!$logoImage) {
+        throw new \RuntimeException('Failed to create logo image resource');
+      }

136-139: Verify TTF files exist before rendering

Avoid warnings and broken text when fonts are missing.

       $fontPath = dirname(__DIR__, 2) . '/fonts/Fontspring-proximanova-bold.otf';
       $fontPathRegular = dirname(__DIR__, 2) . '/fonts/Fontspring-proximanova-regular.otf';
+      if (!\file_exists($fontPath) || !\file_exists($fontPathRegular)) {
+        throw new \RuntimeException('TTF font file(s) not found');
+      }

214-218: Validate PNG buffer before saving

Protect against empty/failed renders.

       ob_start();
-      imagepng($canvas);
-      $finalImage = ob_get_clean();
+      $ok = imagepng($canvas);
+      $finalImage = ob_get_clean();
+      if (!$ok || $finalImage === '' || $finalImage === false) {
+        throw new \RuntimeException('Failed to render composite PNG');
+      }

32-43: Guard Event/address lookups to avoid fatals

first() can return null; bail out cleanly if missing.

         $event = Event::get(FALSE)
           ->addSelect('loc_block_id.address_id')
           ->addWhere('id', '=', $entityId)
-          ->execute()->first();
+          ->execute()->first();
+        if (!$event || empty($event['loc_block_id.address_id'])) {
+          \CRM_Core_Error::debug_log_message("Event/address missing for id {$entityId}");
+          return FALSE;
+        }
 
         $addresses = Address::get(FALSE)
           ->addWhere('id', '=', $event['loc_block_id.address_id'])
           ->setLimit(1)
-          ->execute()->first();
+          ->execute()->first();
+        if (!$addresses) {
+          \CRM_Core_Error::debug_log_message("Address row not found for id {$entityId}");
+          return FALSE;
+        }
         $address = \CRM_Utils_Address::format($addresses);
🧹 Nitpick comments (7)
wp-content/civi-extensions/goonjcustom/Civi/DroppingCenterService.php (1)

298-304: Check poster QR save result and log on failure

Handle failures from generateQrCodeForPoster to avoid silent drops.

-    self::generateQrCodeForPoster($data, $id, $saveOptionsForPoster);
+    $poster = self::generateQrCodeForPoster($data, $id, $saveOptionsForPoster);
+    if ($poster === FALSE) {
+      \Civi::log()->warning("Poster QR save failed for Dropping Center {$id}");
+    }
wp-content/civi-extensions/goonjcustom/Civi/InstitutionDroppingCenterService.php (1)

616-623: Guard poster QR generation result

Same handling as main QR; improves debuggability.

-    self::generateQrCodeForPoster($data, $id, $saveOptionsForPoster);
+    $poster = self::generateQrCodeForPoster($data, $id, $saveOptionsForPoster);
+    if ($poster === FALSE) {
+      \Civi::log()->warning("Poster QR save failed for Institution Dropping Center {$id}");
+    }
wp-content/civi-extensions/goonjcustom/Civi/InstitutionCollectionCampService.php (1)

536-542: Guard poster QR generation result

Mirror main QR handling.

-    self::generateQrCodeForPoster($data, $id, $saveOptionsForPoster);
+    $poster = self::generateQrCodeForPoster($data, $id, $saveOptionsForPoster);
+    if ($poster === FALSE) {
+      \Civi::log()->warning("Poster QR save failed for Institution Collection Camp {$id}");
+    }
wp-content/civi-extensions/goonjcustom/Civi/CollectionCampService.php (1)

533-540: Standardize call order across services

Other services call poster first, then main; this one calls main first. Pick one order for consistency (prefer poster then main).

-    self::generateQrCode($data, $id, $saveOptions);
-    self::generateQrCodeForPoster($data, $id, $saveOptionsForPoster);
+    self::generateQrCodeForPoster($data, $id, $saveOptionsForPoster);
+    self::generateQrCode($data, $id, $saveOptions);
wp-content/civi-extensions/goonjcustom/Civi/GoonjInitiatedEventsService.php (1)

584-589: Don’t skip poster generation when the main Event QR already exists

Early return is based only on Event_QR.QR_Code. If legacy events already have the main QR but lack the poster field, the poster won’t be generated. Consider fetching Event_QR.QR_Code_For_Poster as well and only return when both exist.

wp-content/civi-extensions/goonjcustom/Civi/InstitutionGoonjActivitiesService.php (1)

493-499: Poster QR path looks good; consider idempotency and constants

  • LGTM to generate poster first, then main QR.
  • Optional: check whether Collection_Camp_QR_Code.QR_Code_For_Poster already exists to avoid redundant writes and churn.
  • Extract 'Collection_Camp_QR_Code' and 'QR_Code_For_Poster' as shared constants (or centralize in QrCodeable) to remove duplication across services.
wp-content/civi-extensions/goonjcustom/Civi/GoonjActivitiesService.php (1)

606-612: Consistent poster QR generation; minor polish

  • Implementation matches the Collection Camp schema; poster first, then main QR.
  • As above, consider an existence check to keep writes idempotent and lift group/field strings into shared constants.
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between ec9aba6 and eda2e97.

📒 Files selected for processing (8)
  • wp-content/civi-extensions/goonjcustom/Civi/CollectionCampService.php (1 hunks)
  • wp-content/civi-extensions/goonjcustom/Civi/DroppingCenterService.php (1 hunks)
  • wp-content/civi-extensions/goonjcustom/Civi/GoonjActivitiesService.php (1 hunks)
  • wp-content/civi-extensions/goonjcustom/Civi/GoonjInitiatedEventsService.php (1 hunks)
  • wp-content/civi-extensions/goonjcustom/Civi/InstitutionCollectionCampService.php (1 hunks)
  • wp-content/civi-extensions/goonjcustom/Civi/InstitutionDroppingCenterService.php (1 hunks)
  • wp-content/civi-extensions/goonjcustom/Civi/InstitutionGoonjActivitiesService.php (1 hunks)
  • wp-content/civi-extensions/goonjcustom/Civi/Traits/QrCodeable.php (3 hunks)
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2024-10-11T09:53:14.458Z
Learnt from: belwalshubham
PR: ColoredCow/goonj#284
File: wp-content/civi-extensions/goonjcustom/Civi/CollectionCampService.php:520-520
Timestamp: 2024-10-11T09:53:14.458Z
Learning: In `wp-content/civi-extensions/goonjcustom/Civi/CollectionCampService.php`, it's acceptable that the `reGenerateCollectionCampQr` method calls `generateQrCode` with only the `$collectionCampId` parameter, as QR codes are generated when the Dropping Center is authorized. We'll implement changes if any issues arise in the future.

Applied to files:

  • wp-content/civi-extensions/goonjcustom/Civi/InstitutionGoonjActivitiesService.php
  • wp-content/civi-extensions/goonjcustom/Civi/InstitutionDroppingCenterService.php
  • wp-content/civi-extensions/goonjcustom/Civi/DroppingCenterService.php
  • wp-content/civi-extensions/goonjcustom/Civi/GoonjActivitiesService.php
  • wp-content/civi-extensions/goonjcustom/Civi/CollectionCampService.php
  • wp-content/civi-extensions/goonjcustom/Civi/InstitutionCollectionCampService.php
  • wp-content/civi-extensions/goonjcustom/Civi/GoonjInitiatedEventsService.php
  • wp-content/civi-extensions/goonjcustom/Civi/Traits/QrCodeable.php
📚 Learning: 2024-10-09T15:24:42.859Z
Learnt from: belwalshubham
PR: ColoredCow/goonj#352
File: wp-content/civi-extensions/goonjcustom/Civi/QrCodeService.php:51-53
Timestamp: 2024-10-09T15:24:42.859Z
Learning: In `QrCodeService::generateQrCode`, when using `QRCode::OUTPUT_IMAGE_PNG`, the `render` method returns a data URI with base64-encoded image data. Therefore, it's necessary to remove the `data:image/png;base64,` prefix and base64-decode the data to obtain the raw binary image.

Applied to files:

  • wp-content/civi-extensions/goonjcustom/Civi/Traits/QrCodeable.php
🧬 Code graph analysis (7)
wp-content/civi-extensions/goonjcustom/Civi/InstitutionGoonjActivitiesService.php (1)
wp-content/civi-extensions/goonjcustom/Civi/Traits/QrCodeable.php (1)
  • generateQrCodeForPoster (297-325)
wp-content/civi-extensions/goonjcustom/Civi/InstitutionDroppingCenterService.php (1)
wp-content/civi-extensions/goonjcustom/Civi/Traits/QrCodeable.php (1)
  • generateQrCodeForPoster (297-325)
wp-content/civi-extensions/goonjcustom/Civi/DroppingCenterService.php (1)
wp-content/civi-extensions/goonjcustom/Civi/Traits/QrCodeable.php (1)
  • generateQrCodeForPoster (297-325)
wp-content/civi-extensions/goonjcustom/Civi/GoonjActivitiesService.php (1)
wp-content/civi-extensions/goonjcustom/Civi/Traits/QrCodeable.php (1)
  • generateQrCodeForPoster (297-325)
wp-content/civi-extensions/goonjcustom/Civi/CollectionCampService.php (1)
wp-content/civi-extensions/goonjcustom/Civi/Traits/QrCodeable.php (2)
  • generateQrCode (23-236)
  • generateQrCodeForPoster (297-325)
wp-content/civi-extensions/goonjcustom/Civi/InstitutionCollectionCampService.php (1)
wp-content/civi-extensions/goonjcustom/Civi/Traits/QrCodeable.php (1)
  • generateQrCodeForPoster (297-325)
wp-content/civi-extensions/goonjcustom/Civi/GoonjInitiatedEventsService.php (1)
wp-content/civi-extensions/goonjcustom/Civi/Traits/QrCodeable.php (1)
  • generateQrCodeForPoster (297-325)
🪛 PHPMD (2.15.0)
wp-content/civi-extensions/goonjcustom/Civi/GoonjInitiatedEventsService.php

589-589: Avoid unused local variables such as '$data'. (Unused Code Rules)

(UnusedLocalVariable)


589-589: Avoid unused local variables such as '$id'. (Unused Code Rules)

(UnusedLocalVariable)

wp-content/civi-extensions/goonjcustom/Civi/Traits/QrCodeable.php

175-175: Avoid unused local variables such as '$currentWidth'. (Unused Code Rules)

(UnusedLocalVariable)

Comment on lines +584 to +587
$saveOptionsForPoster = [
'customGroupName' => 'Collection_Camp_QR_Code',
'customFieldName' => 'QR_Code_For_Poster',
];
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Poster QR saved under Collection_Camp_QR_Code for an Event — likely wrong custom group

The standard QR for events uses customGroupName 'Event_QR'. Saving the poster QR into 'Collection_Camp_QR_Code' will either fail to resolve the field or write to the wrong entity. Use the Event group for consistency unless there’s a schema difference you intend.

Suggested fix:

-      $saveOptionsForPoster = [
-        'customGroupName' => 'Collection_Camp_QR_Code',
-        'customFieldName' => 'QR_Code_For_Poster',
-      ];
+      $saveOptionsForPoster = [
+        'customGroupName' => 'Event_QR',
+        'customFieldName' => 'QR_Code_For_Poster',
+      ];

Verification script:


🏁 Script executed:

#!/bin/bash
# Verify where QR_Code_For_Poster is defined and used.
rg -n -C2 -S "QR_Code_For_Poster|Event_QR|Collection_Camp_QR_Code" wp-content/civi-extensions

# Check other services for the expected group/field pairing for Event vs Collection_Camp.
rg -n -C2 -S "customGroupName' => 'Event_QR'|customGroupName' => 'Collection_Camp_QR_Code'" wp-content/civi-extensions

Length of output: 19392


Use Event_QR as the custom group for event poster QR
In GoonjInitiatedEventsService.php (lines 584–587) $saveOptionsForPoster uses 'Collection_Camp_QR_Code', which routes poster QR saving through the camp branch. Change to 'Event_QR' so the poster QR is saved under the event group.

-      $saveOptionsForPoster = [
-        'customGroupName' => 'Collection_Camp_QR_Code',
-        'customFieldName' => 'QR_Code_For_Poster',
-      ];
+      $saveOptionsForPoster = [
+        'customGroupName' => 'Event_QR',
+        'customFieldName' => 'QR_Code_For_Poster',
+      ];
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
$saveOptionsForPoster = [
'customGroupName' => 'Collection_Camp_QR_Code',
'customFieldName' => 'QR_Code_For_Poster',
];
$saveOptionsForPoster = [
'customGroupName' => 'Event_QR',
'customFieldName' => 'QR_Code_For_Poster',
];
🤖 Prompt for AI Agents
In wp-content/civi-extensions/goonjcustom/Civi/GoonjInitiatedEventsService.php
around lines 584 to 587, the $saveOptionsForPoster array sets 'customGroupName'
to 'Collection_Camp_QR_Code' which incorrectly routes poster QR saves to the
camp group; change the 'customGroupName' value to 'Event_QR' so poster QR
entries are saved under the Event_QR custom group (leave the 'customFieldName'
as 'QR_Code_For_Poster').

'customFieldName' => 'QR_Code_For_Poster',
];

self::generateQrCodeForPoster($data, $id, $saveOptionsForPoster);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Undefined variables passed to generateQrCodeForPoster() — runtime fatal

$data and $id are not defined in this scope. You likely meant $qrCodeData and $eventId.

Apply:

-      self::generateQrCodeForPoster($data, $id, $saveOptionsForPoster);
+      self::generateQrCodeForPoster($qrCodeData, $eventId, $saveOptionsForPoster);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
self::generateQrCodeForPoster($data, $id, $saveOptionsForPoster);
self::generateQrCodeForPoster($qrCodeData, $eventId, $saveOptionsForPoster);
🧰 Tools
🪛 PHPMD (2.15.0)

589-589: Avoid unused local variables such as '$data'. (Unused Code Rules)

(UnusedLocalVariable)


589-589: Avoid unused local variables such as '$id'. (Unused Code Rules)

(UnusedLocalVariable)

🤖 Prompt for AI Agents
In wp-content/civi-extensions/goonjcustom/Civi/GoonjInitiatedEventsService.php
around line 589, the call uses undefined variables $data and $id; replace them
with the correct variables $qrCodeData and $eventId (i.e., call
self::generateQrCodeForPoster($qrCodeData, $eventId, $saveOptionsForPoster));
ensure those variables are in scope and populated before the call, or if
differently named earlier, use the existing variable names consistently and run
static analysis to confirm no other mismatches.

Comment on lines +45 to +74
if (!empty($saveOptions['customGroupName']) && $saveOptions['customGroupName'] != 'Event_QR') {
$campData = EckEntity::get('Collection_Camp', FALSE)
->addSelect('subtype:name', 'Collection_Camp_Intent_Details.Location_Area_of_camp', 'Dropping_Centre.Where_do_you_wish_to_open_dropping_center_Address_', 'Goonj_Activities.Where_do_you_wish_to_organise_the_activity_', 'Institution_Collection_Camp_Intent.Collection_Camp_Address', 'Institution_Dropping_Center_Intent.Dropping_Center_Address', 'Institution_Goonj_Activities.Where_do_you_wish_to_organise_the_activity_')
->addWhere('id', '=', $entityId)
->execute()->first();

$campStatus = $campData['subtype:name'];

if ($campStatus == 'Collection_Camp') {
$address = $campData['Collection_Camp_Intent_Details.Location_Area_of_camp'];
}
elseif ($campStatus == 'Dropping_Center') {
$address = $campData['Dropping_Centre.Where_do_you_wish_to_open_dropping_center_Address_'];
}
elseif ($campStatus == 'Goonj_Activities') {
$address = $campData['Goonj_Activities.Where_do_you_wish_to_organise_the_activity_'];
}
elseif ($campStatus == 'Institution_Collection_Camp') {
$address = $campData['Institution_Collection_Camp_Intent.Collection_Camp_Address'];
}
elseif ($campStatus == 'Institution_Dropping_Center') {
$address = $campData['Institution_Dropping_Center_Intent.Dropping_Center_Address'];
}
elseif ($campStatus == 'Institution_Goonj_Activities') {
$address = $campData['Institution_Goonj_Activities.Where_do_you_wish_to_organise_the_activity_'];
}
else {
throw new \Exception('Invalid entity type for QR code generation.');
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Null-check camp data and use safe indexing

Avoid undefined index notices and handle unknown subtype.

-        $campData = EckEntity::get('Collection_Camp', FALSE)
+        $campData = EckEntity::get('Collection_Camp', FALSE)
           ->addSelect('subtype:name', 'Collection_Camp_Intent_Details.Location_Area_of_camp', 'Dropping_Centre.Where_do_you_wish_to_open_dropping_center_Address_', 'Goonj_Activities.Where_do_you_wish_to_organise_the_activity_', 'Institution_Collection_Camp_Intent.Collection_Camp_Address', 'Institution_Dropping_Center_Intent.Dropping_Center_Address', 'Institution_Goonj_Activities.Where_do_you_wish_to_organise_the_activity_')
           ->addWhere('id', '=', $entityId)
           ->execute()->first();
+        if (!$campData) {
+          \CRM_Core_Error::debug_log_message("Camp not found for id {$entityId}");
+          return FALSE;
+        }
 
-        $campStatus = $campData['subtype:name'];
+        $campStatus = $campData['subtype:name'] ?? null;
+        if (!$campStatus) {
+          throw new \RuntimeException('Missing subtype for QR generation');
+        }
@@
-          $address = $campData['Collection_Camp_Intent_Details.Location_Area_of_camp'];
+          $address = $campData['Collection_Camp_Intent_Details.Location_Area_of_camp'] ?? '';
@@
-          $address = $campData['Dropping_Centre.Where_do_you_wish_to_open_dropping_center_Address_'];
+          $address = $campData['Dropping_Centre.Where_do_you_wish_to_open_dropping_center_Address_'] ?? '';
@@
-          $address = $campData['Goonj_Activities.Where_do_you_wish_to_organise_the_activity_'];
+          $address = $campData['Goonj_Activities.Where_do_you_wish_to_organise_the_activity_'] ?? '';
@@
-          $address = $campData['Institution_Collection_Camp_Intent.Collection_Camp_Address'];
+          $address = $campData['Institution_Collection_Camp_Intent.Collection_Camp_Address'] ?? '';
@@
-          $address = $campData['Institution_Dropping_Center_Intent.Dropping_Center_Address'];
+          $address = $campData['Institution_Dropping_Center_Intent.Dropping_Center_Address'] ?? '';
@@
-          $address = $campData['Institution_Goonj_Activities.Where_do_you_wish_to_organise_the_activity_'];
+          $address = $campData['Institution_Goonj_Activities.Where_do_you_wish_to_organise_the_activity_'] ?? '';
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (!empty($saveOptions['customGroupName']) && $saveOptions['customGroupName'] != 'Event_QR') {
$campData = EckEntity::get('Collection_Camp', FALSE)
->addSelect('subtype:name', 'Collection_Camp_Intent_Details.Location_Area_of_camp', 'Dropping_Centre.Where_do_you_wish_to_open_dropping_center_Address_', 'Goonj_Activities.Where_do_you_wish_to_organise_the_activity_', 'Institution_Collection_Camp_Intent.Collection_Camp_Address', 'Institution_Dropping_Center_Intent.Dropping_Center_Address', 'Institution_Goonj_Activities.Where_do_you_wish_to_organise_the_activity_')
->addWhere('id', '=', $entityId)
->execute()->first();
$campStatus = $campData['subtype:name'];
if ($campStatus == 'Collection_Camp') {
$address = $campData['Collection_Camp_Intent_Details.Location_Area_of_camp'];
}
elseif ($campStatus == 'Dropping_Center') {
$address = $campData['Dropping_Centre.Where_do_you_wish_to_open_dropping_center_Address_'];
}
elseif ($campStatus == 'Goonj_Activities') {
$address = $campData['Goonj_Activities.Where_do_you_wish_to_organise_the_activity_'];
}
elseif ($campStatus == 'Institution_Collection_Camp') {
$address = $campData['Institution_Collection_Camp_Intent.Collection_Camp_Address'];
}
elseif ($campStatus == 'Institution_Dropping_Center') {
$address = $campData['Institution_Dropping_Center_Intent.Dropping_Center_Address'];
}
elseif ($campStatus == 'Institution_Goonj_Activities') {
$address = $campData['Institution_Goonj_Activities.Where_do_you_wish_to_organise_the_activity_'];
}
else {
throw new \Exception('Invalid entity type for QR code generation.');
}
}
if (!empty($saveOptions['customGroupName']) && $saveOptions['customGroupName'] != 'Event_QR') {
$campData = EckEntity::get('Collection_Camp', FALSE)
->addSelect(
'subtype:name',
'Collection_Camp_Intent_Details.Location_Area_of_camp',
'Dropping_Centre.Where_do_you_wish_to_open_dropping_center_Address_',
'Goonj_Activities.Where_do_you_wish_to_organise_the_activity_',
'Institution_Collection_Camp_Intent.Collection_Camp_Address',
'Institution_Dropping_Center_Intent.Dropping_Center_Address',
'Institution_Goonj_Activities.Where_do_you_wish_to_organise_the_activity_'
)
->addWhere('id', '=', $entityId)
->execute()
->first();
if (!$campData) {
\CRM_Core_Error::debug_log_message("Camp not found for id {$entityId}");
return FALSE;
}
$campStatus = $campData['subtype:name'] ?? null;
if (!$campStatus) {
throw new \RuntimeException('Missing subtype for QR generation');
}
if ($campStatus == 'Collection_Camp') {
$address = $campData['Collection_Camp_Intent_Details.Location_Area_of_camp'] ?? '';
}
elseif ($campStatus == 'Dropping_Center') {
$address = $campData['Dropping_Centre.Where_do_you_wish_to_open_dropping_center_Address_'] ?? '';
}
elseif ($campStatus == 'Goonj_Activities') {
$address = $campData['Goonj_Activities.Where_do_you_wish_to_organise_the_activity_'] ?? '';
}
elseif ($campStatus == 'Institution_Collection_Camp') {
$address = $campData['Institution_Collection_Camp_Intent.Collection_Camp_Address'] ?? '';
}
elseif ($campStatus == 'Institution_Dropping_Center') {
$address = $campData['Institution_Dropping_Center_Intent.Dropping_Center_Address'] ?? '';
}
elseif ($campStatus == 'Institution_Goonj_Activities') {
$address = $campData['Institution_Goonj_Activities.Where_do_you_wish_to_organise_the_activity_'] ?? '';
}
else {
throw new \Exception('Invalid entity type for QR code generation.');
}
}

Comment on lines +164 to +176
// --- Step 4: Venue text with wrapping
$bottomFontSize = 25;
$maxWidth = $canvasWidth - 80;
$bottomY = $qrY + $newQrHeight + 40;

// Start with bold "Venue:".
$bboxLabel = imagettfbbox($bottomFontSize, 0, $fontPath, $venueLabel);
$labelWidth = abs($bboxLabel[2] - $bboxLabel[0]);

$lines = [];
$currentLine = $venueLabel;
$currentWidth = $labelWidth;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Fix venue wrapping/centering and remove unused variable

Measure bold label separately, wrap only the value, and center combined line. Drop $currentWidth (PHPMD).

-      $lines = [];
-      $currentLine = $venueLabel;
-      $currentWidth = $labelWidth;
-
-      $words = explode(" ", $venueValue);
-      foreach ($words as $word) {
-        $testLine = ($currentLine === $venueLabel ? $currentLine : $currentLine . " ") . $word;
-        $bboxTest = imagettfbbox($bottomFontSize, 0, $fontPathRegular, $testLine);
-        $testWidth = abs($bboxTest[2] - $bboxTest[0]);
-
-        if ($testWidth > $maxWidth) {
-          $lines[] = $currentLine;
-          $currentLine = $word;
-        }
-        else {
-          $currentLine = $testLine;
-        }
-      }
-      $lines[] = $currentLine;
+      $words = explode(' ', trim($venueValue));
+      $valueLines = [];
+      $current = '';
+      foreach ($words as $word) {
+        $test = $current === '' ? $word : $current . ' ' . $word;
+        $bbox = imagettfbbox($bottomFontSize, 0, $fontPathRegular, $test);
+        $testW = abs($bbox[2] - $bbox[0]);
+        $allowed = empty($valueLines) ? ($maxWidth - $labelWidth) : $maxWidth;
+        if ($testW > $allowed && $current !== '') {
+          $valueLines[] = $current;
+          $current = $word;
+        } else {
+          $current = $test;
+        }
+      }
+      if ($current !== '') {
+        $valueLines[] = $current;
+      }
@@
-      foreach ($lines as $i => $line) {
-        $bbox = imagettfbbox($bottomFontSize, 0, $fontPathRegular, $line);
-        $lineWidth = abs($bbox[2] - $bbox[0]);
-        $x = (int) (($canvasWidth - $lineWidth) / 2);
-        $y = $bottomY + ($i * $lineHeight);
-
-        if ($i === 0) {
-          imagettftext($canvas, $bottomFontSize, 0, $x, $y, $black, $fontPath, $venueLabel);
-          $valuePart = trim(str_replace($venueLabel, "", $line));
-          if ($valuePart !== "") {
-            $bboxLabel = imagettfbbox($bottomFontSize, 0, $fontPath, $venueLabel);
-            $offset = abs($bboxLabel[2] - $bboxLabel[0]);
-            imagettftext($canvas, $bottomFontSize, 0, $x + $offset, $y, $black, $fontPathRegular, $valuePart);
-          }
-        }
-        else {
-          imagettftext($canvas, $bottomFontSize, 0, $x, $y, $black, $fontPathRegular, $line);
-        }
-      }
+      foreach ($valueLines as $i => $valLine) {
+        $bboxVal = imagettfbbox($bottomFontSize, 0, $fontPathRegular, $valLine);
+        $valW    = abs($bboxVal[2] - $bboxVal[0]);
+        $lineW   = ($i === 0) ? ($labelWidth + $valW) : $valW;
+        $x       = (int) (($canvasWidth - $lineW) / 2);
+        $y       = $bottomY + ($i * $lineHeight);
+        if ($i === 0) {
+          imagettftext($canvas, $bottomFontSize, 0, $x, $y, $black, $fontPath, $venueLabel);
+          imagettftext($canvas, $bottomFontSize, 0, $x + $labelWidth, $y, $black, $fontPathRegular, $valLine);
+        } else {
+          imagettftext($canvas, $bottomFontSize, 0, $x, $y, $black, $fontPathRegular, $valLine);
+        }
+      }

Also applies to: 193-212

🧰 Tools
🪛 PHPMD (2.15.0)

175-175: Avoid unused local variables such as '$currentWidth'. (Unused Code Rules)

(UnusedLocalVariable)

🤖 Prompt for AI Agents
In wp-content/civi-extensions/goonjcustom/Civi/Traits/QrCodeable.php around
lines 164 to 176, the venue text wrapping and centering logic is incorrect and
an unused variable $currentWidth is present; to fix, compute the bounding box
and width for the bold "Venue:" label alone, then perform word-wrapping only on
the venue value using the remaining width (maxWidth minus labelWidth), create
combined lines by prepending the label to the first wrapped line only, center
the combined text block horizontally by calculating total combined line widths
per line, and remove the unused $currentWidth variable; apply the same changes
to the corresponding block at lines 193-212.

Comment on lines +297 to +305
public static function generateQrCodeForPoster($data, $entityId, $saveOptions) {
try {
$options = new QROptions([
'version' => 5,
'outputType' => QRCode::OUTPUT_IMAGE_PNG,
'eccLevel' => QRCode::ECC_L,
'scale' => 10,
]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Unify poster path with main QR flow: raw PNG + return attachment

Mirror the raw-bytes rendering and return value.

       $options = new QROptions([
         'version'    => 5,
         'outputType' => QRCode::OUTPUT_IMAGE_PNG,
         'eccLevel'   => QRCode::ECC_L,
-        'scale'      => 10,
+        'scale'      => 10,
+        'outputBase64' => false,
       ]);
 
-      $qrcode = (new QRCode($options))->render($data);
-
-      // Remove the base64 header and decode the image data.
-      $qrcode = str_replace('data:image/png;base64,', '', $qrcode);
-      $qrcode = base64_decode($qrcode);
+      $qrcode = (new QRCode($options))->render($data);
@@
-      self::saveQrCode($qrcode, $saveOptions);
+      $attachment = self::saveQrCode($qrcode, $saveOptions);
+      if ($attachment === FALSE) {
+        return FALSE;
+      }
+      return $attachment;
@@
-    return TRUE;
+    return TRUE; // If keeping boolean API.

Also applies to: 312-325

🤖 Prompt for AI Agents
In wp-content/civi-extensions/goonjcustom/Civi/Traits/QrCodeable.php around
lines 297-305 (and similarly 312-325), the poster-specific flow currently
diverges from the main QR flow by not returning raw PNG bytes and an attachment
object; change it to mirror the main flow: render the QR as raw PNG bytes (not
only saving to disk), create and return the same attachment structure used
elsewhere (filename, mimetype image/png, raw data/blob), and remove any
alternate return types so both paths consistently produce the raw PNG +
attachment object; ensure options, output type, and ECC match the main flow and
reuse the same attachment creation logic.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant